Compare commits

..

309 commits

Author SHA1 Message Date
6c1f444943
Move api git urls to the new domain
Signed-off-by: baalajimaestro <baalajimaestro@ptr.moe>
2024-12-01 23:28:13 +05:30
98fb07771f
Merge branch 'master' of https://github.com/TeamNewPipe/NewPipe into sponsorblock
Signed-off-by: baalajimaestro <baalajimaestro@ptr.moe>
2024-12-01 23:24:50 +05:30
Stypox
3847b32c11
Release v0.27.4 (1001) 2024-11-30 15:11:23 +01:00
Stypox
9054575f6c
Add changelog for v0.27.4 (1001) 2024-11-30 15:10:38 +01:00
Stypox
0dca92dd59
Merge branch 'master' into dev 2024-11-30 14:55:31 +01:00
Hosted Weblate
b19cd00dba
Translated using Weblate (Malay)
Currently translated at 9.8% (8 of 81 strings)

Translated using Weblate (Malay)

Currently translated at 57.9% (429 of 740 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 96.2% (78 of 81 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 64.1% (52 of 81 strings)

Translated using Weblate (Hungarian)

Currently translated at 50.6% (41 of 81 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.4% (736 of 740 strings)

Translated using Weblate (Arabic (Libya))

Currently translated at 4.9% (4 of 81 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Hungarian)

Currently translated at 32.0% (26 of 81 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Turkish)

Currently translated at 46.9% (38 of 81 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (German)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Japanese)

Currently translated at 12.3% (10 of 81 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (740 of 740 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (740 of 740 strings)

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

Translated using Weblate (Polish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hungarian)

Currently translated at 28.3% (23 of 81 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.4% (735 of 739 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Hungarian)

Currently translated at 23.4% (19 of 81 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Hungarian)

Currently translated at 23.4% (19 of 81 strings)

Translated using Weblate (Punjabi (Pakistan))

Currently translated at 16.9% (125 of 739 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 66.6% (54 of 81 strings)

Translated using Weblate (Albanian)

Currently translated at 1.2% (1 of 81 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Polish)

Currently translated at 60.4% (49 of 81 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Hungarian)

Currently translated at 19.7% (16 of 81 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (German)

Currently translated at 100.0% (81 of 81 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (German)

Currently translated at 100.0% (739 of 739 strings)

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

* Update backup and restore explanation in hindi README

* add_matrix_link to hindi and punjabi README

also translate Warning in hindi & punjabi language Readme's

* improve hindi and punjabi readme

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

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

* update backup and restore explanation in assamese README

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

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

* Update Matrix room URL to new link

oh! I missed this one earlier

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

* more improvements in punjabi README

* fix CONTRIBUTING.md link in punjabi readme

* fix CONTRIBUTING.md link in assamese readme

* add missing paragraphs in hindi translation for hi readme

* revert localisation of app name NewPipe as it stands out

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

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

* fix formatting issues in assamese readme

* fix link to releases in punjabi readme

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

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

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

Translated using Weblate (Icelandic)

Currently translated at 99.3% (734 of 739 strings)

Translated using Weblate (Welsh)

Currently translated at 3.7% (3 of 80 strings)

Translated using Weblate (Bulgarian)

Currently translated at 5.0% (4 of 80 strings)

Added translation using Weblate (Welsh)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 99.7% (737 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 99.4% (735 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 98.1% (725 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 97.8% (723 of 739 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Galician)

Currently translated at 98.5% (728 of 739 strings)

Translated using Weblate (Burmese)

Currently translated at 2.9% (22 of 739 strings)

Translated using Weblate (Tagalog)

Currently translated at 8.1% (60 of 739 strings)

Translated using Weblate (French)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Tamil)

Currently translated at 23.7% (19 of 80 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Dutch)

Currently translated at 62.5% (50 of 80 strings)

Translated using Weblate (Persian)

Currently translated at 92.9% (687 of 739 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Catalan)

Currently translated at 87.1% (644 of 739 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 66.2% (53 of 80 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Albanian)

Currently translated at 79.8% (590 of 739 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (French)

Currently translated at 90.0% (72 of 80 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Telugu)

Currently translated at 58.5% (433 of 739 strings)

Translated using Weblate (Esperanto)

Currently translated at 70.2% (519 of 739 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 97.8% (723 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 17.5% (14 of 80 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 65.0% (52 of 80 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Tamil)

Currently translated at 25.0% (20 of 80 strings)

Translated using Weblate (Hungarian)

Currently translated at 18.7% (15 of 80 strings)

Translated using Weblate (Galician)

Currently translated at 98.3% (727 of 739 strings)

Translated using Weblate (Finnish)

Currently translated at 98.3% (727 of 739 strings)

Translated using Weblate (German)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Bulgarian)

Currently translated at 80.2% (593 of 739 strings)

Translated using Weblate (Hungarian)

Currently translated at 18.7% (15 of 80 strings)

Translated using Weblate (Tamil)

Currently translated at 47.0% (348 of 739 strings)

Translated using Weblate (Tatar)

Currently translated at 6.4% (48 of 739 strings)

Added translation using Weblate (Tatar)

Translated using Weblate (Slovak)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Hungarian)

Currently translated at 16.2% (13 of 80 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Icelandic)

Currently translated at 97.5% (721 of 739 strings)

Translated using Weblate (Tamil)

Currently translated at 21.2% (17 of 80 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (German)

Currently translated at 100.0% (80 of 80 strings)

Translated using Weblate (French)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 97.1% (718 of 739 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (79 of 79 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 11.3% (9 of 79 strings)

Translated using Weblate (Finnish)

Currently translated at 11.3% (9 of 79 strings)

Translated using Weblate (German)

Currently translated at 100.0% (79 of 79 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Finnish)

Currently translated at 97.4% (720 of 739 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (79 of 79 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.8% (738 of 739 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 96.6% (714 of 739 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (79 of 79 strings)

Translated using Weblate (Latvian)

Currently translated at 92.1% (681 of 739 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Latvian)

Currently translated at 91.0% (673 of 739 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Vietnamese)

Currently translated at 76.9% (60 of 78 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Tagalog)

Currently translated at 1.2% (1 of 78 strings)

Translated using Weblate (Latvian)

Currently translated at 87.1% (644 of 739 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Bulgarian)

Currently translated at 80.1% (592 of 739 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Lithuanian)

Currently translated at 98.3% (727 of 739 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Basque)

Currently translated at 42.3% (33 of 78 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Marathi)

Currently translated at 31.9% (236 of 739 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Bulgarian)

Currently translated at 66.0% (488 of 739 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (French)

Currently translated at 99.8% (738 of 739 strings)

Translated using Weblate (Interlingua)

Currently translated at 32.4% (240 of 739 strings)

Translated using Weblate (Mongolian)

Currently translated at 5.5% (41 of 739 strings)

Added translation using Weblate (Mongolian)

Translated using Weblate (Interlingua)

Currently translated at 32.0% (237 of 739 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.8% (738 of 739 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.8% (738 of 739 strings)

Translated using Weblate (Tigrinya)

Currently translated at 9.4% (70 of 739 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.8% (738 of 739 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Icelandic)

Currently translated at 3.8% (3 of 78 strings)

Translated using Weblate (Icelandic)

Currently translated at 96.0% (710 of 739 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Turkish)

Currently translated at 44.8% (35 of 78 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 94.3% (697 of 739 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Vietnamese)

Currently translated at 75.6% (59 of 78 strings)

Translated using Weblate (Albanian)

Currently translated at 78.7% (582 of 739 strings)

Translated using Weblate (Dutch)

Currently translated at 61.5% (48 of 78 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Armenian)

Currently translated at 27.8% (206 of 739 strings)

Translated using Weblate (German)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Burmese)

Currently translated at 2.5% (19 of 739 strings)

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

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Tigrinya)

Currently translated at 9.3% (69 of 739 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Tamil)

Currently translated at 46.5% (344 of 739 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (German)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Turkish)

Currently translated at 44.8% (35 of 78 strings)

Translated using Weblate (Belarusian)

Currently translated at 98.9% (731 of 739 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (738 of 739 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Romanian)

Currently translated at 99.8% (738 of 739 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (739 of 739 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (739 of 739 strings)

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

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

---------

Co-authored-by: Tobi <TobiGr@users.noreply.github.com>
2024-08-30 16:32:42 +02:00
Tobi
fad3120b00
Merge pull request #11428 from Two-Ai/remove-returnActivity-test
Remove outdated returnActivity test code
2024-08-15 01:53:28 +02:00
TwoAi
38c823a042 Remove outdated returnActivity test code
returnActivity was removed in 463dd8e
2024-08-10 23:09:54 -04:00
5277c9e1fe
Merge branch 'master' of https://github.com/TeamNewPipe/NewPipe into sponsorblock 2024-07-26 08:54:19 +05:30
Stypox
51ee2f8d1e
Merge branch 'master' into dev 2024-07-25 21:20:44 +02:00
Stypox
d442b45836
Remove code committed accidentally 2024-07-25 20:58:29 +02:00
Stypox
dbcb721dc2
Don't warn about rhino class in proguard
Likely related to 01a7b20655 but I am not completely sure.
I tested the app and it works well, so I think that org.mozilla.javascript.JavaToJSONConverters is not used really.

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

* Update Matrix room link in CONTRIBUTING.md

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

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

Translated using Weblate (Croatian)

Currently translated at 99.5% (735 of 738 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (738 of 738 strings)

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

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (French (Louisiana))

Currently translated at 0.2% (2 of 738 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (738 of 738 strings)

Added translation using Weblate (French (Louisiana))

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (French)

Currently translated at 99.7% (736 of 738 strings)

Added translation using Weblate (Arabic (Tunisian))

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

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

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Kannada)

Currently translated at 5.8% (43 of 738 strings)

Translated using Weblate (Kannada)

Currently translated at 5.1% (4 of 78 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (737 of 738 strings)

Translated using Weblate (German)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (German)

Currently translated at 100.0% (738 of 738 strings)

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

Currently translated at 99.0% (731 of 738 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (736 of 738 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (736 of 738 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 50.0% (39 of 78 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Japanese)

Currently translated at 99.4% (734 of 738 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Slovak)

Currently translated at 21.7% (17 of 78 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 62.8% (49 of 78 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Polish)

Currently translated at 61.5% (48 of 78 strings)

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

Currently translated at 23.0% (18 of 78 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 46.7% (36 of 77 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (736 of 738 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (German)

Currently translated at 99.7% (736 of 738 strings)

Translated using Weblate (Korean)

Currently translated at 31.1% (24 of 77 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Korean)

Currently translated at 98.9% (726 of 734 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (German)

Currently translated at 99.7% (732 of 734 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (German)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Hungarian)

Currently translated at 16.8% (13 of 77 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (730 of 730 strings)

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

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (730 of 730 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Apious <apious@kakao.com>
Co-authored-by: Eduardo Malaspina <vaio0@swismail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: GET100PERCENT <eraofphysics@yahoo.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Michael Moroni <michaelmoroni@disroot.org>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Pi-Cla <pirateclip@protonmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Tim Trek <T.Trek@byom.de>
Co-authored-by: TobiGr <TobiGr@users.noreply.github.com>
Co-authored-by: Tấn Lực Trương <september122022ios16@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kuragehime <kuragehime641@gmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hu/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ko/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/vi/
Translation: NewPipe/Metadata
2024-04-01 13:38:40 +02:00
Stypox
2756ef6d2f
Show notification when failing to import settings 2024-03-30 18:53:45 +01:00
Stypox
7da1d30010
Expose all import/export errors to the user 2024-03-30 18:47:20 +01:00
Stypox
8e192acb63
Add test zips and extensive tests for ImportExportManager
Now all possible combinations of files in the zip (present or not) are checked at the same time
2024-03-30 18:42:11 +01:00
Stypox
d8423499dc
Use JSON for settings imports/exports 2024-03-30 16:58:12 +01:00
TobiGr
974167fcb8 Add comment that empty constructors are needed for IcePick
See 5e7ad6ffd1 and https://github.com/TeamNewPipe/NewPipe/pull/10781#discussion_r1545351144
2024-03-30 16:19:02 +01:00
Stypox
6afdbd6fd3
Add test: vulnerable settings should fail importing 2024-03-30 16:12:41 +01:00
Stypox
d8668ed226
Show snackbar error when settings import fails 2024-03-30 16:12:41 +01:00
Stypox
d75a6eaa41
Fix vulnerability with whitelist-aware ObjectInputStream
Only a few specific classes are now allowed.
2024-03-30 16:12:35 +01:00
Stypox
235fb92638
Make checkstyle accept javadocs with long links 2024-03-30 15:49:06 +01:00
Stypox
ea18b4ea1f
Move import export manager to separate folder 2024-03-30 15:49:05 +01:00
Stypox
58f5ec0181
Merge pull request #9580 from pratyaksh1610/branch-8232
Moved player notification setting to notification section
2024-03-30 15:38:33 +01:00
pratyaksh1610
e42c9abdde
moved player notification to notification section 2024-03-30 15:23:46 +01:00
Stypox
5e7ad6ffd1
Fix fragments without empty constructor 2024-03-30 15:15:31 +01:00
Stypox
4c8238874e
Merge pull request #8221 from GGAutomaton/feature-7870
Sort bookmarked playlists
2024-03-30 15:02:37 +01:00
Stypox
38d4887901
Undo some unneeded changes to LocalPlaylistManager 2024-03-30 14:46:13 +01:00
Stypox
c9051d33c1
Fix warnings and allow moving only up and down even in grid 2024-03-30 14:39:40 +01:00
Stypox
3cc0205def
Fix inconsistencies when removing playlist
Remove checkDisplayIndexModified because it was causing more problems than it solved. Now when adding new playlists they won't necessarily appear at the top, but will get sorted alphabetically along with the other playlists with index -1. This will be the case until any playlist is sorted, at which point all indices are assigned and newly added playlists will appear at the top again.
2024-03-30 14:14:31 +01:00
Stypox
90979e2a81
Fix PlaylistLocalItemTest 2024-03-29 20:58:07 +01:00
Stypox
e66e1b542c
Also sort playlist duplicates by display index 2024-03-29 20:55:24 +01:00
Stypox
92e9c3e42e
Fix DatabaseMigrationTest
Complete removal of unneeded index, and remove default value for `remote_playlists.display_index`.
2024-03-29 20:43:55 +01:00
Stypox
4591c09637
Apply review 2024-03-29 18:08:37 +01:00
Stypox
e1ce3fef1b
Merge branch 'dev' into pr8221 2024-03-29 18:08:31 +01:00
Stypox
3c0a200f7b
Merge pull request #6045 from bg172/showOverallDurationInPlaylist
show overall duration of videos in playlist
2024-03-29 14:32:29 +01:00
bg1722
bef5907ec3
show OverallDuration in Playlist
earlier only overall amount of videos was shown. Now overall duration is shown there too - as formatted by existing Localization.concatenateStrings() and Localization.getDurationString().

show all videos OverallDuration in local Playlist too

refactor to make implementation in LocalPlaylistFragment and PlaylistFragment more obviously similar

unfortunately could not refactor upto BaseLocalListFragment

revert the changes for online Playlists

because they are paginated and may be infinite i.e. correct count may come only from the service->extractor chain which unfortunately does not give overall duration yet

next try to improve user-experience with online Playlist

just show that duration is longer (">") than the calculated value in case there is more page(s)

even more improve user-experience for online Playlist

by adding the duration of next items as soon as they are made visible

make showing of playlists duration configurable, disabled by default

adjusted duration to be handled as long because it comes as long from extractor

no idea why I handled it as int earlier

Revert "make showing of playlists duration configurable, disabled by default", refactor

This reverts commit bc1ba17a20d3dd1763210f81d7ca67c5f1734a3d.

Fix rebase

Apply review

Rename video -> stream

Remove unused settings keys
2024-03-29 14:11:27 +01:00
Stypox
f0beb662aa
Merge pull request #10790 from TeamNewPipe/update-check-consent
Ask for consent before checking for updates
2024-03-29 11:42:57 +01:00
Stypox
92402685f8
Improve new version checks before running 2024-03-29 11:14:30 +01:00
Stypox
3703fed1a5
update_app_key default value should be false 2024-03-29 11:08:33 +01:00
Stypox
f4fb960c62
Migrate to non-transitive R classes 2024-03-29 00:17:13 +01:00
Tobi
a3bbbf03b4 Ask for consent before starting update checks
NewPipe is contacting its servers without asking for the users' consent. This is categorized as "tracking" by F-Droid (see https://github.com/TeamNewPipe/NewPipe/discussions/10785).

This commit disables checking for udpates by default and adds a dialog asking for the user's consent to automatically check for updates if the app version is eligible for them. After upgrading to a version containing this commit the user is asked directly on the first app start. On fresh installs however, showing it on the first app start contributes to a bad onboarding an welcoming experience. Therefore, the dialog is shown at the second app start.

Co-authored-by: Stypox <stypox@pm.me>
2024-03-28 23:42:00 +01:00
TobiGr
1d3a69a29f Move text from manual_update_title to check_for_updates 2024-03-28 23:08:02 +01:00
Stypox
10c57b15da
Merge pull request #10781 from Profpatsch/BaseDescriptionFragment-assert-member-is-initialized
BaseDescriptionFragment: Assert member is initialized
2024-03-28 22:48:00 +01:00
Stypox
b85f7a6747
Some more slight improvements 2024-03-28 22:46:30 +01:00
Stypox
3f94e7b638
Merge pull request #10912 from Stypox/download-fixes
Download fixes
2024-03-28 19:14:20 +01:00
Stypox
2af95cc1d4
Merge pull request #9236 from vincetzr/Option-to-reset-settings
Option to reset settings
2024-03-28 19:00:54 +01:00
Stypox
cefdefdfd2
11111th commit 2024-03-28 18:51:36 +01:00
Stypox
37f7fa7ef4
Merge branch 'dev' into pr9236 2024-03-28 18:43:29 +01:00
Stypox
e687eb5631
Merge pull request #8242 from dtcxzyw/trim-search-string
Trim search string and remove duplicate records from the database
2024-03-28 18:34:59 +01:00
Stypox
88c3af7647
Merge pull request #9975 from Marius1501/landscape_card_mode_improve
Changed the landscape layout of list card item
2024-03-28 15:01:41 +01:00
ge78fug
ddd6c8cbf1
Changed the landscape layout of list card item
Make layout-land/list_stream_card_item a symlink to layout/list_stream_item
2024-03-28 14:46:18 +01:00
Stypox
81220f90d6
Merge pull request #10909 from Stypox/fix-getAudioTrackType-null
Fix not considering nullability when comparing getAudioTrackType
2024-03-28 13:47:12 +01:00
Stypox
e0268a91ad
Merge pull request #10717 from Stypox/cache-key-type
Calculate cache key based on info type instead of item type
2024-03-28 13:34:51 +01:00
Stypox
29e4135aaa
Try to fix PR labeler
Reference: https://github.com/actions/labeler?tab=readme-ov-file#permissions
2024-03-28 12:03:10 +01:00
Stypox
5d9adce40d
Fix NPE, since dismissing a dialog still calls onViewCreated() 2024-03-28 11:35:21 +01:00
Stypox
d3afde8789
Remove unused DownloadDialog.onDismissListener 2024-03-28 11:21:33 +01:00
Stypox
d8a5d5545d
Fix choosing audio format to mux with video-only download 2024-03-28 11:09:56 +01:00
Stypox
bed3516687
Fix non-desugared method being used
Search for "Not supported at all minSDK levels" here: https://developer.android.com/studio/write/java8-support-table
2024-03-27 17:30:23 +01:00
Stypox
3a014d8d46
Fix not considering nullability when comparing getAudioTrackType 2024-03-27 16:05:17 +01:00
Stypox
58ae7fbccb
Merge pull request #10724 from Roshanjossey/patch-1
use GitHub markdown to emphasise warning in Readme
2024-03-27 10:27:50 +01:00
Stypox
b06a9618d4
use GitHub markdown to emphasise warning in all READMEs 2024-03-27 10:22:52 +01:00
Stypox
434c4a5cbc
Merge pull request #10908 from TeamNewPipe/node-20
Update workflows to use Node 20
2024-03-27 09:47:48 +01:00
TobiGr
c34d30dc17 [CI] Update sonar job to use cache@v4
Updates deprecated Node 16 to 20
2024-03-26 22:48:47 +01:00
TobiGr
0d4c1bee3f [CI] Update gradle/wrapper-validation-action to v2
Updates deprecated Node 16 to 20
2024-03-26 22:32:29 +01:00
Tobi
34a25d0be3
Merge pull request #10907 from TeamNewPipe/weblate
Update translations
2024-03-26 21:42:40 +01:00
Mohammed Anas
3134f5e747
Don't add "question" label to question discussions (#10906)
They already have a "Questions" category of their own anyway.
2024-03-26 20:28:28 +00:00
Hosted Weblate
1732584e5e Translated using Weblate (Danish)
Currently translated at 100.0% (729 of 729 strings)

Co-authored-by: cat <158170307+cultcats@users.noreply.github.com>
2024-03-26 21:19:18 +01:00
Tobi
f50cafbac1
Merge pull request #10905 from mhmdanas/fix-question-discussion-form
Fix GitHub question discussion form
2024-03-26 21:04:02 +01:00
Mohammed Anas
bc7c3f48ad
Fix GitHub question discussion form
The name and description fields are both invalid here.
2024-03-26 19:53:45 +00:00
Tobi
b760419fd5
Merge pull request #10896 from TeamNewPipe/storage-message
Add separate message when download is rejected due to insufficient storage
2024-03-26 18:56:18 +01:00
Tobi
5cf3c58d0e
Merge pull request #10732 from Profpatsch/dont-write-media-format
Don't write defaultFormat setting, use default value
2024-03-25 10:36:35 +01:00
TobiGr
206d1b6db4 Add separate message when download is rejected due to insufficient storage 2024-03-21 11:56:42 +01:00
CloudyRowly
2e318b8b03
Added "free memory" check before downloading [Android N / API 24+] (#10505)
Added "free space" check before downloading eliminating bugs related to out-of-memory on Android N / API 24+
2024-03-21 09:18:55 +01:00
Isira Seneviratne
5bdb6f18d6 Use hexToByteArray() extension 2024-03-20 14:00:13 +01:00
Isira Seneviratne
2e53a99361 Convert isReleaseApk to lazy value 2024-03-20 14:00:13 +01:00
Isira Seneviratne
bec18e13d3 Improve app signature check 2024-03-20 14:00:13 +01:00
Tobi
7edd471ec5
Merge pull request #10890 from TeamNewPipe/weblate
Update translations
2024-03-18 10:58:28 +01:00
Hosted Weblate
e6a4a3fa4f
Translated using Weblate (Danish)
Currently translated at 96.4% (703 of 729 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Danish)

Currently translated at 96.4% (703 of 729 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Danish)

Currently translated at 88.3% (644 of 729 strings)

Translated using Weblate (Danish)

Currently translated at 88.3% (644 of 729 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Georgian)

Currently translated at 92.2% (71 of 77 strings)

Translated using Weblate (Uzbek (latin))

Currently translated at 62.6% (457 of 729 strings)

Translated using Weblate (Santali)

Currently translated at 12.6% (92 of 729 strings)

Translated using Weblate (French)

Currently translated at 89.6% (69 of 77 strings)

Translated using Weblate (Japanese)

Currently translated at 11.6% (9 of 77 strings)

Translated using Weblate (Bulgarian)

Currently translated at 5.1% (4 of 77 strings)

Translated using Weblate (Bengali)

Currently translated at 20.7% (16 of 77 strings)

Translated using Weblate (German)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Bengali (India))

Currently translated at 40.7% (297 of 729 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 85.5% (624 of 729 strings)

Translated using Weblate (Tamil)

Currently translated at 46.6% (340 of 729 strings)

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 55.1% (402 of 729 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (German)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 98.7% (76 of 77 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.4% (725 of 729 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Croatian)

Currently translated at 99.4% (725 of 729 strings)

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

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Croatian)

Currently translated at 2.5% (2 of 77 strings)

Translated using Weblate (Malay)

Currently translated at 48.6% (355 of 729 strings)

Translated using Weblate (Croatian)

Currently translated at 99.3% (724 of 729 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Finnish)

Currently translated at 98.3% (717 of 729 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (729 of 729 strings)

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

Currently translated at 22.0% (17 of 77 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 95.4% (696 of 729 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Vietnamese)

Currently translated at 42.8% (33 of 77 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Esperanto)

Currently translated at 70.7% (516 of 729 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 94.6% (690 of 729 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (French)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Santali)

Currently translated at 10.0% (73 of 729 strings)

Translated using Weblate (Turkish)

Currently translated at 42.8% (33 of 77 strings)

Translated using Weblate (German)

Currently translated at 81.8% (63 of 77 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (729 of 729 strings)

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

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Kannada)

Currently translated at 5.4% (40 of 729 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (729 of 729 strings)

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

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Croatian)

Currently translated at 88.7% (647 of 729 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Slovak)

Currently translated at 20.7% (16 of 77 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (French)

Currently translated at 99.8% (728 of 729 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Catalan)

Currently translated at 86.5% (631 of 729 strings)

Translated using Weblate (Telugu)

Currently translated at 58.9% (430 of 729 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Tigrinya)

Currently translated at 8.9% (65 of 729 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (76 of 77 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Slovak)

Currently translated at 99.7% (727 of 729 strings)

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

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 85.5% (624 of 729 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (728 of 729 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (German)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.8% (728 of 729 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Swedish)

Currently translated at 99.8% (728 of 729 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (728 of 729 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (German)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (German)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (729 of 729 strings)

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

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Serbian)

Currently translated at 18.1% (14 of 77 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Greek)

Currently translated at 24.6% (19 of 77 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (German)

Currently translated at 100.0% (728 of 728 strings)

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

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Kannada)

Currently translated at 5.5% (40 of 726 strings)

Translated using Weblate (Sinhala)

Currently translated at 2.6% (2 of 76 strings)

Translated using Weblate (Sinhala)

Currently translated at 4.1% (30 of 726 strings)

Translated using Weblate (Bulgarian)

Currently translated at 5.2% (4 of 76 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Turkish)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Slovak)

Currently translated at 98.4% (715 of 726 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (726 of 726 strings)

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

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Italian)

Currently translated at 99.5% (723 of 726 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (German)

Currently translated at 100.0% (726 of 726 strings)

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

Currently translated at 21.0% (16 of 76 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Romanian)

Currently translated at 99.8% (724 of 725 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (725 of 725 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Alexthegib <traducoes@skiff.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Andrey F <firsan777@mail.ru>
Co-authored-by: Angelk90 <angelo.k90@hotmail.it>
Co-authored-by: Chethan <76928501+ch3thanhs@users.noreply.github.com>
Co-authored-by: Danr <mdp43140@gmail.com>
Co-authored-by: David Svane <davidcygnus@users.noreply.hosted.weblate.org>
Co-authored-by: Deleted User <noreply+77891@weblate.org>
Co-authored-by: DuninduH <mateyh37@gmail.com>
Co-authored-by: Eric <zxmegaxqug@hldrive.com>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: GET100PERCENT <eraofphysics@yahoo.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Heidhou chazanouvha <Heidhou@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Rückert <igorruckert@yahoo.com.br>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihfandi <ihfandicahyo@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jan Layola <gilajan@protonmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Juan Martinez <jjml.nipon@gmail.com>
Co-authored-by: KarmaKat <lloydwestbury@gmail.com>
Co-authored-by: Kuko <kuko7@protonmail.ch>
Co-authored-by: LiJu09 <lisojuraj@gmail.com>
Co-authored-by: Martin Constantino–Bodin <martin.bodin@ens-lyon.org>
Co-authored-by: Mehmet <mehmetyalcin.0103@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Nils Van Zuijlen <nils.van-zuijlen@mailo.com>
Co-authored-by: Nista <42772160+Nista11@users.noreply.github.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: P.O <rasmusson.mikael@protonmail.com>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Pi-Cla <pirateclip@protonmail.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Ray <ray@users.noreply.hosted.weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Co-authored-by: Sergio Marques <so.boston.android@gmail.com>
Co-authored-by: ShareASmile <aapshergill@gmail.com>
Co-authored-by: Subbarayudu <raidu.g6@gmail.com>
Co-authored-by: Subham Jena <subhamjena8465@gmail.com>
Co-authored-by: T1z3n <info@njbraun.de>
Co-authored-by: Terry Louwers <t.louwers@gmail.com>
Co-authored-by: TobiGr <TobiGr@users.noreply.github.com>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: VfBFan <VfBFan@users.noreply.hosted.weblate.org>
Co-authored-by: Xəyyam Qocayev <xxmn77@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: cat <158170307+cultcats@users.noreply.github.com>
Co-authored-by: ds-z <drazen.sostaric01@gmail.com>
Co-authored-by: dyare darbani <darbanidyare@gmail.com>
Co-authored-by: fsbat0 <fsbat0@users.noreply.hosted.weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: hshbuk <hsh.bukchin@gmail.com>
Co-authored-by: jspast <joao.pastorello@protonmail.com>
Co-authored-by: kuragehime <kuragehime641@gmail.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: trunars <trunars@gmail.com>
Co-authored-by: v1s7 <v1s7@users.noreply.hosted.weblate.org>
Co-authored-by: Åzze <laitinen.jere222@gmail.com>
Co-authored-by: Çağla Pickaxe <caglapickaxe@gmail.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bg/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bn/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/el/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ja/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ka/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_PT/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/si/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/vi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2024-03-18 09:59:34 +01:00
Tobi
de2a139340
Merge pull request #10889 from eltociear/patch-1
Fix typo in TextLinkifier.java
2024-03-18 08:38:16 +01:00
Ikko Eltociear Ashimine
9d6ac67c46
Update TextLinkifier.java
minor fix
2024-03-18 14:43:16 +09:00
Audric V
6f7b905983
Merge pull request #10740 from Goooler/gha
Update GitHub action dependencies in workflows
2024-02-05 23:44:45 +01:00
Audric V
bcd4626008
Merge pull request #10817 from Isira-Seneviratne/Jsoup
Update jsoup to 1.17.2
2024-02-05 14:37:43 +01:00
Isira Seneviratne
27730a20d6 Update Jsoup to 1.17.2 2024-02-05 10:52:08 +05:30
Audric V
4aa0190175
Merge pull request #10795 from TeamNewPipe/matrix_room_URL_change
Update Matrix chat URL to new link
2024-01-28 14:14:30 +01:00
opusforlife2
6dd62335e9
Update Matrix room URL to new link 2024-01-27 16:36:13 +00:00
Profpatsch
32d2606a65 BaseDescriptionFragment: Assert member is initialized
`streamInfo` and `channelInfo` have to be initialized, since the only
way to construct the class it to pass them. So we can remove the null
check boilerplate and make some of the accessors `NonNull`.
2024-01-23 14:28:37 +01:00
Zongle Wang
2051334bba
Bump GH actions
Old ones are deprecated.
2024-01-08 11:55:57 +08:00
Profpatsch
575e809004 Don't write defaultFormat setting, use default value
Nowhere else does this (write a setting if it’s not set).

It took me a while to see that this code does not do what it intends,
because `defaultFormat` is already the default value in the first
`context.getString`, so calling `getMediaFormatFromKey` again is the
exact same call (“do you know the definition of insanity…”) and will
return `null` again …

So let’s drop the setting write and just rely on the default values.
2024-01-06 17:24:53 +01:00
d207d47ce9
Rebrand the fork
Its not ideal to keep using poly's branding, when his fork of the app is not anymore maintained. It rather would cause confusion among users on whether they should report me or poly

Signed-off-by: baalajimaestro <me@baalajimaestro.me>
2024-01-02 17:18:31 +05:30
Roshan Jossy
66e8e2a696
use GitHub markdown to emphasise warning in Readme 2024-01-01 15:07:37 +01:00
Stypox
55373c95d9
Update NewPipeExtractor to include MediaCCC channel fix 2023-12-30 23:49:09 +01:00
Stypox
04bdc1cc0b
Base cache key on info type instead of item type
It didn't really made sense to consider two cache keys as equal based on the type of items contained within that list.
2023-12-30 23:46:16 +01:00
Stypox
1d8850d1b2
Merge pull request #10712 from Stypox/notification-actions-api-33-2
[Android 13+] Restore support of custom notification actions
2023-12-30 21:55:44 +01:00
Stypox
f98548698a
Android 33 -> Android 13
Co-authored-by: Tobi <TobiGr@users.noreply.github.com>
2023-12-30 21:55:32 +01:00
Stypox
4b1824e8c1
Allow play/pausing from notification when buffering
This change is in line with a recent change in how the play/pause button behaves in the player ui: if the buffering indicator is shown, it's still possible to toggle play/pause, to allow e.g. pausing videos before they even start.
This change was needed because on Android 13+ notification actions can't be null, and thus the buffering hourglass action wasn't shown.
2023-12-29 16:18:26 +01:00
Stypox
17e88f1749
Do not update notification actions if nothing changed
This should avoid costly updates of the media session.
2023-12-29 16:16:45 +01:00
Stypox
5edafca05a
Implement notification actions via MediaSessionConnector on Android 13+ 2023-12-29 15:54:15 +01:00
Stypox
2c4c283099
Extract NotificationActionData from NotificationUtil 2023-12-29 15:54:15 +01:00
Stypox
9fb8125655
Allow each notification slot to contain any possible action 2023-12-29 15:54:15 +01:00
Stypox
aab6580195
Extract NotificationSlot from NotificationActionsPreference 2023-12-29 12:31:59 +01:00
Stypox
30f0db1d28
Customize only 2 notification actions on Android 13+ 2023-12-29 12:13:08 +01:00
Stypox
5a4dae2070
Fix settings_notification.xml indentation 2023-12-29 11:37:17 +01:00
Stypox
8345f348f6
Merge pull request #10091 from TeamNewPipe/feat/playlist_description
Add playlist description to playlist fragment
2023-12-29 10:58:13 +01:00
Stypox
9220e32463
Fix FeedDAOTest 2023-12-29 10:54:31 +01:00
Stypox
845e72bf4a
Merge branch 'master' into dev 2023-12-29 10:48:37 +01:00
ef238a8b81
Merge branch 'master' of https://github.com/TeamNewPipe/NewPipe into sponsorblock 2023-12-28 08:37:10 +05:30
Tobi
49429ff40a
Merge pull request #10700 from TeamNewPipe/newpipe_0.26.1
Newpipe 0.26.1
2023-12-26 18:26:48 +01:00
TobiGr
3df21ad25e Bump version to 0.26.1 (996) 2023-12-26 16:59:02 +01:00
TobiGr
d0f4600be4 Add changelog for NewPipe 0.26.1 2023-12-26 16:58:49 +01:00
TobiGr
0fa2e76c3e Fix NPE when ChannelTabLHFactory not implemented for a service
Fixes #10698
2023-12-26 16:55:52 +01:00
Stypox
9ff1b5230f
Improve TextEllipsizer class 2023-12-23 18:04:05 +01:00
TobiGr
65eb631711
Ellipsize playlist description if it is longer than 5 lines
The description can be expanded / collapsed via a "show more" / "show less" button.
2023-12-23 12:33:52 +01:00
TobiGr
6c99557553
Add playlist description to PlaylistFragment 2023-12-23 12:13:34 +01:00
TobiGr
df2e0be08d Add summary to reset preference 2023-09-21 16:01:07 +02:00
TobiGr
ff1aca272e Remove strange change 2023-09-21 16:01:07 +02:00
TobiGr
f2e352832a Create new settings category: Backup and restore
Following settings have been move to the new category:
- import database (from ContenttSettings)
- export database (from ContenttSettings)
- reset settings (from DebugSettings)
2023-09-21 16:01:07 +02:00
vincetzr
ad0855ac83 Update app/src/main/res/xml/debug_settings.xml
Ensuring title to be fully displayed on small devices.

Co-authored-by: Tobi <TobiGr@users.noreply.github.com>
2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
d7ef9b1f0c Added strings to strings.xml to allow translation. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
40a3e1b18a Revert committed file change of settings key. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
25a73090f5 Revert changes made to dev. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
a239a26b17 Revert changes made to dev. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
06d256294f Revert changes made to dev. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
81ad50e82a Added delete xml method inside the yes dialogue. 2023-09-21 16:01:07 +02:00
Zhidong Piao
23de9bf93e clear shared preference xmls 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
5c46412faa Changed alert dialogue. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
076e9eee01 Added alert dialogue and restarts the app when resetting settings. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
2103a04092 Cleaned up xml files. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
58517d1d27 Added reset button but working as intended for theme. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
aa1847189b Added reset button but slightly working as intended. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
5d101e7b88 Added reset button but not working as intended. 2023-09-21 16:01:07 +02:00
TobiGr
9118ecd68f Remove unnecessary debug warning and use JDoc instead 2023-08-17 16:51:31 +02:00
TobiGr
15fd47c7f2 Apply review 2023-08-16 22:18:53 +02:00
Yingwei Zheng
ef40ac7bb3 Fix a typo 2023-08-16 22:02:12 +02:00
Yingwei Zheng
881d04ba1e Refactor database migration test and string trimming 2023-08-16 22:02:12 +02:00
TobiGr
4af5b5f6f2 Fix database migration and string trimming
Co-authored-by:  Yingwei Zheng <dtcxzyw@qq.com>
2023-08-16 22:02:12 +02:00
TobiGr
90f0809029 Trim search string and remove duplicate records from the database
Co-authored-by:  Yingwei Zheng <dtcxzyw@qq.com>
2023-08-16 21:26:35 +02:00
GGAutomaton
8ad7bf60d7 Delete saveImmediate warnings & add comments 2022-06-23 23:31:56 +08:00
GGAutomaton
898a936064 Update index modification logic & redo sorting in the merge algorithm 2022-06-23 23:19:59 +08:00
GGAutomaton
4e401bc059 Update playlists in parallel 2022-06-23 20:36:21 +08:00
GGAutomaton
9ecef6f011 Add abstract methods in PlaylistLocalItem & rename setIsModified 2022-06-23 19:20:16 +08:00
GGAutomaton
ba394a7ab4 Update test and Javadoc 2022-05-11 18:08:14 +08:00
GGAutomaton
d32490a4be Create sub-package and default interval for DebounceSaver & sort playlists in db 2022-05-11 16:47:34 +08:00
GGAutomaton
6526ff1612 Add tests 2022-04-17 20:20:20 +08:00
GGAutomaton
bb5390d63a Reuse DebounceSaver 2022-04-17 14:53:02 +08:00
GGAutomaton
bd1aae8d66 Fix sonar warning 2022-04-16 12:44:24 +08:00
GGAutomaton
c24aed054f Fix sonar warning and typo 2022-04-16 12:00:02 +08:00
GGAutomaton
0aa08a5e40 Use new item holder 2022-04-15 23:19:24 +08:00
GGAutomaton
3c48825699 Debounced saver & bugfix & clean code 2022-04-15 20:44:54 +08:00
GGAutomaton
bfb56b4144 UI design and behavior 2022-04-14 16:59:52 +08:00
GGAutomaton
ba8370bcfd Save changes to the database and bugfix 2022-04-14 12:13:42 +08:00
GGAutomaton
813f55152a
Merge branch 'TeamNewPipe:dev' into feature-7870 2022-04-13 22:48:26 +08:00
GGAutomaton
270a541a7c Implement algorithm to merge playlists 2022-04-13 22:46:24 +08:00
GGAutomaton
c34549a47d Update database migrations and getter/setter 2022-04-13 21:35:38 +08:00
GGAutomaton
96d6b309ec Migrate database 2022-04-13 19:41:07 +08:00
774 changed files with 15619 additions and 4319 deletions

View file

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

View file

@ -1,6 +1,3 @@
name: Question
description: Ask about anything NewPipe-related
labels: [question]
body:
- type: markdown
attributes:

View file

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

View file

@ -13,7 +13,7 @@ body:
attributes:
label: "Checklist"
options:
- label: "I made sure that there are *no existing issues* - [open](https://github.com/polymorphicshade/NewPipe/issues) or [closed](https://github.com/polymorphicshade/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
- label: "I made sure that there are *no existing issues* - [open](https://git.baalajimaestro.me/baalajimaesstro/NewPipe/issues) or [closed](https://github.com/polymorphicshade/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my problem isn't listed."
required: true
@ -23,7 +23,7 @@ body:
required: true
- label: "This issue contains only one feature request."
required: true
- label: "I have read and understood the [contribution guidelines](https://github.com/polymorphicshade/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
- label: "I have read and understood the [contribution guidelines](https://git.baalajimaestro.me/baalajimaesstro/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
required: true

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

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

View file

@ -6,6 +6,7 @@ on:
branches:
- dev
- master
- refactor
- release**
paths-ignore:
- 'README.md'
@ -36,8 +37,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v2
- name: create and checkout branch
# push events already checked out the branch
@ -46,10 +47,10 @@ jobs:
BRANCH: ${{ github.head_ref }}
run: git checkout -B "$BRANCH"
- name: set up JDK 17
uses: actions/setup-java@v3
- name: set up JDK
uses: actions/setup-java@v4
with:
java-version: 17
java-version: 21
distribution: "temurin"
cache: 'gradle'
@ -57,14 +58,13 @@ jobs:
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
- name: Upload APK
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: app
path: app/build/outputs/apk/debug/*.apk
test-android:
# macos has hardware acceleration. See android-emulator-runner action
runs-on: macos-latest
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
matrix:
@ -80,12 +80,18 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: set up JDK 17
uses: actions/setup-java@v3
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: set up JDK
uses: actions/setup-java@v4
with:
java-version: 17
java-version: 21
distribution: "temurin"
cache: 'gradle'
@ -98,7 +104,7 @@ jobs:
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
uses: actions/upload-artifact@v4
if: failure()
with:
name: android-test-report-api${{ matrix.api-level }}
@ -111,19 +117,19 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up JDK 17
uses: actions/setup-java@v3
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: 17
java-version: 21
distribution: "temurin"
cache: 'gradle'
- name: Cache SonarCloud packages
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar

View file

@ -17,9 +17,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: 16
@ -27,7 +27,7 @@ jobs:
run: npm i probe-image-size@7.2.3 --ignore-scripts
- name: Minimize simple images
uses: actions/github-script@v6
uses: actions/github-script@v7
timeout-minutes: 3
with:
script: |

View file

@ -1,5 +1,5 @@
name: "PR size labeler"
on: [pull_request]
on: [pull_request_target]
permissions:
contents: read
pull-requests: write

21
.idea/icon.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 850 B

View file

@ -7,17 +7,18 @@ A fork of [NewPipe](https://github.com/TeamNewPipe/NewPipe) with [SponsorBlock](
The implementation is still a bit basic but it generally works pretty well.
## How can I get this?
[<img alt="Get it on IzzyOnDroid" height="80" src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png">](https://apt.izzysoft.de/fdroid/index/apk/org.polymorphicshade.newpipe)
Builds will be uploaded in the [Releases](https://github.com/polymorphicshade/NewPipe/releases) section. Please download the APK from the newest release and install it on your device.
Builds will be uploaded in the [Releases](https://git.ptr.moe/baalajimaestro/NewPipe/releases) section. Please download the APK from the newest release and install it on your device.
## Why isn't this in upstream NewPipe?
[The developer team](https://github.com/TeamNewPipe) behind the official NewPipe decided that they do not want to include these kinds of functionality in their app. See https://newpipe.schabi.org/blog/pinned/newpipe-and-online-advertising/, https://github.com/TeamNewPipe/NewPipe/pull/3205, and https://github.com/TeamNewPipe/NewPipe/issues/7469 for more information and discussion.
We obviously disagree but we respect their decision and continue to offer SponsorBlock and Return YouTube Dislike in NewPipe via this fork.
This fork, attempts to keep the [work by polymorphicshade](https://github.com/polymorphicshade/NewPipe) updated with latest NewPipe releases, with almost no changes to the SponsorBlock code, since it is feature-complete.
## Bugs
Please do not report bugs encountered while using this fork to the upstream developers. Either try to reproduce the bug in vanilla NewPipe and then report it (preferred) or [create a bug report in our repo](https://github.com/polymorphicshade/NewPipe/issues/new?assignees=&labels=bug&template=bug_report.md).
Please do not report bugs encountered while using this fork to the upstream developers. Either try to reproduce the bug in vanilla NewPipe and then report it (preferred) or [create a bug report in polymorphicshade's repo](https://github.com/polymorphicshade/NewPipe/issues/new?assignees=&labels=bug&template=bug_report.md).
## License
[![GNU GPLv3](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)

View file

@ -16,12 +16,19 @@ android {
namespace 'org.schabi.newpipe'
defaultConfig {
applicationId "org.polymorphicshade.newpipe"
applicationId "org.baalajimaestro.newpipe"
resValue "string", "app_name", "NewPipe SponsorBlock"
minSdk 21
targetSdk 33
versionCode 995
versionName "0.26.0"
if (System.properties.containsKey('versionCodeOverride')) {
versionCode System.getProperty('versionCodeOverride') as Integer
} else {
versionCode 1001
}
versionName "0.27.4"
if (System.properties.containsKey('versionNameSuffix')) {
versionNameSuffix System.getProperty('versionNameSuffix')
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -92,6 +99,7 @@ android {
buildFeatures {
viewBinding true
buildConfig true
}
packagingOptions {
@ -112,7 +120,7 @@ ext {
androidxRoomVersion = '2.6.1'
androidxWorkVersion = '2.8.1'
icepickVersion = '3.2.0'
stateSaverVersion = '1.4.1'
exoPlayerVersion = '2.18.7'
googleAutoServiceVersion = '1.1.1'
groupieVersion = '2.10.1'
@ -198,7 +206,9 @@ 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.23.1'
// WORKAROUND: if you get errors with the NewPipeExtractor dependency, replace `v0.24.3` with
// the corresponding commit hash, since JitPack is sometimes buggy
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.3'
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
/** Checkstyle **/
@ -234,11 +244,12 @@ dependencies {
/** Third-party libraries **/
// Instance state boilerplate elimination
implementation "frankiesardo:icepick:${icepickVersion}"
kapt "frankiesardo:icepick-processor:${icepickVersion}"
implementation 'com.github.livefront:bridge:v2.0.2'
implementation "com.evernote:android-state:$stateSaverVersion"
kapt "com.evernote:android-state-processor:$stateSaverVersion"
// HTML parser
implementation "org.jsoup:jsoup:1.16.2"
implementation "org.jsoup:jsoup:1.17.2"
// HTTP client
implementation "com.squareup.okhttp3:okhttp:4.12.0"
@ -282,7 +293,7 @@ dependencies {
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
// Date and time formatting
implementation "org.ocpsoft.prettytime:prettytime:5.0.7.Final"
implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final"
/** Debugging **/
// Memory leak detection

View file

@ -7,20 +7,12 @@
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
-keep class org.mozilla.javascript.** { *; }
-keep class org.mozilla.classfile.ClassFileWriter
-dontwarn org.mozilla.javascript.JavaToJSONConverters
-dontwarn org.mozilla.javascript.tools.**
## Rules for ExoPlayer
-keep class com.google.android.exoplayer2.** { *; }
## Rules for Icepick. Copy pasted from https://github.com/frankiesardo/icepick
-dontwarn icepick.**
-keep class icepick.** { *; }
-keep class **$$Icepick { *; }
-keepclasseswithmembernames class * {
@icepick.* <fields>;
}
-keepnames class * { @icepick.State *;}
## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp
-dontwarn okhttp3.**
-dontwarn okio.**

View file

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

View file

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

View file

@ -8,10 +8,14 @@ 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.assertNotEquals
import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.stream.StreamType
@RunWith(AndroidJUnit4::class)
@ -20,13 +24,17 @@ class DatabaseMigrationTest {
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 const val DEFAULT_NAME = "Test Name"
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_SERVICE_ID = 1
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
private const val DEFAULT_THIRD_SERVICE_ID = 2
private const val DEFAULT_THIRD_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
@get:Rule
@ -106,6 +114,20 @@ class DatabaseMigrationTest {
Migrations.MIGRATION_6_7
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_8,
true,
Migrations.MIGRATION_7_8
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
)
val migratedDatabaseV3 = getMigratedDatabase()
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
@ -140,6 +162,157 @@ class DatabaseMigrationTest {
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
}
@Test
fun migrateDatabaseFrom7to8() {
val databaseInV7 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_7)
val defaultSearch1 = " abc "
val defaultSearch2 = " abc"
val serviceId = DEFAULT_SERVICE_ID // YouTube
// Use id different to YouTube because two searches with the same query
// but different service are considered not equal.
val otherServiceId = ServiceList.SoundCloud.serviceId
databaseInV7.run {
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", serviceId)
put("search", defaultSearch1)
}
)
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", serviceId)
put("search", defaultSearch2)
}
)
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", otherServiceId)
put("search", defaultSearch1)
}
)
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", otherServiceId)
put("search", defaultSearch2)
}
)
close()
}
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_8,
true, Migrations.MIGRATION_7_8
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_9,
true, Migrations.MIGRATION_8_9
)
val migratedDatabaseV8 = getMigratedDatabase()
val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
assertEquals(2, listFromDB.size)
assertEquals("abc", listFromDB[0].search)
assertEquals("abc", listFromDB[1].search)
assertNotEquals(listFromDB[0].serviceId, listFromDB[1].serviceId)
}
@Test
fun migrateDatabaseFrom8to9() {
val databaseInV8 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_8)
val localUid1: Long
val localUid2: Long
val remoteUid1: Long
val remoteUid2: Long
databaseInV8.run {
localUid1 = insert(
"playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("name", DEFAULT_NAME + "1")
put("is_thumbnail_permanent", false)
put("thumbnail_stream_id", -1)
}
)
localUid2 = insert(
"playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("name", DEFAULT_NAME + "2")
put("is_thumbnail_permanent", false)
put("thumbnail_stream_id", -1)
}
)
delete(
"playlists", "uid = ?",
Array(1) { localUid1 }
)
remoteUid1 = insert(
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SERVICE_ID)
put("url", DEFAULT_URL)
}
)
remoteUid2 = insert(
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SECOND_SERVICE_ID)
put("url", DEFAULT_SECOND_URL)
}
)
delete(
"remote_playlists", "uid = ?",
Array(1) { remoteUid2 }
)
close()
}
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
)
val migratedDatabaseV9 = getMigratedDatabase()
var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
assertEquals(1, localListFromDB.size)
assertEquals(localUid2, localListFromDB[0].uid)
assertEquals(-1, localListFromDB[0].displayIndex)
assertEquals(1, remoteListFromDB.size)
assertEquals(remoteUid1, remoteListFromDB[0].uid)
assertEquals(-1, remoteListFromDB[0].displayIndex)
val localUid3 = migratedDatabaseV9.playlistDAO().insert(
PlaylistEntity(DEFAULT_NAME + "3", false, -1, -1)
)
val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert(
PlaylistRemoteEntity(
DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL,
DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10
)
)
localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
assertEquals(2, localListFromDB.size)
assertEquals(localUid3, localListFromDB[1].uid)
assertEquals(-1, localListFromDB[1].displayIndex)
assertEquals(2, remoteListFromDB.size)
assertEquals(remoteUid3, remoteListFromDB[1].uid)
assertEquals(-1, remoteListFromDB[1].displayIndex)
}
private fun getMigratedDatabase(): AppDatabase {
val database: AppDatabase = Room.databaseBuilder(
ApplicationProvider.getApplicationContext(),

View file

@ -85,7 +85,13 @@ class FeedDAOTest {
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
assertNotNull(streams)
assertEquals(allowedStreams, streams!!.stream().map { it.stream }.toList().sortedBy { it.uid })
assertEquals(
allowedStreams,
streams!!
.map { it.stream }
.sortedBy { it.uid }
.toList()
)
}
private fun setupUnlinkDelete(time: String) {

View file

@ -364,6 +364,7 @@
<data android:host="tilvids.com" />
<data android:host="video.lqdn.fr" />
<data android:host="video.ploud.fr" />
<data android:host="subscribeto.me" />
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
<data android:pathPrefix="/w/" /> <!-- short video URLs -->

View file

@ -19,11 +19,12 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.BridgeStateSaverInitializer;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import java.io.IOException;
@ -60,6 +61,8 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
public class App extends Application {
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
private static final String TAG = App.class.toString();
private boolean isFirstRun = false;
private static App app;
@NonNull
@ -85,7 +88,13 @@ public class App extends Application {
return;
}
// Initialize settings first because others inits can use its values
// check if the last used preference version is set
// to determine whether this is the first app run
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this)
.getInt(getString(R.string.last_used_preferences_version), -1);
isFirstRun = lastUsedPrefVersion == -1;
// Initialize settings first because other initializations can use its values
NewPipeSettings.initSettings(this);
NewPipe.init(getDownloader(),
@ -93,6 +102,7 @@ public class App extends Application {
Localization.getPreferredContentCountry(this));
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
BridgeStateSaverInitializer.init(this);
StateSaver.init(this);
initNotificationChannels();
@ -255,4 +265,7 @@ public class App extends Application {
return false;
}
public boolean isFirstRun() {
return isFirstRun;
}
}

View file

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

View file

@ -29,7 +29,7 @@ import okhttp3.ResponseBody;
public final class DownloaderImpl extends Downloader {
public static final String USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.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

@ -79,6 +79,7 @@ import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.settings.UpdateSettingsFragment;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.KioskTranslator;
@ -86,6 +87,7 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PeertubeHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ReleaseVersionUtil;
import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
@ -167,6 +169,11 @@ public class MainActivity extends AppCompatActivity {
// if this is enabled by the user.
NotificationWorker.initialize(this);
}
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
&& !App.getApp().isFirstRun()
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
}
}
@Override
@ -176,7 +183,8 @@ public class MainActivity extends AppCompatActivity {
final App app = App.getApp();
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
&& prefs.getBoolean(app.getString(R.string.update_check_consent_key), false)) {
// Start the worker which is checking all conditions
// and eventually searching for a new version.
NewVersionWorker.enqueueNewVersionCheckingWork(app, false);

View file

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

View file

@ -20,8 +20,8 @@ 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
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
import org.schabi.newpipe.util.Version
import java.io.IOException
@ -86,7 +86,7 @@ class NewVersionWorker(
@Throws(IOException::class, ReCaptchaException::class)
private fun checkNewVersion() {
// Check if the current apk is a github one or not.
if (!isReleaseApk()) {
if (!ReleaseVersionUtil.isReleaseApk) {
return
}
@ -95,7 +95,7 @@ class NewVersionWorker(
// 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)) {
if (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) {
return
}
}
@ -110,7 +110,7 @@ class NewVersionWorker(
try {
// Store a timestamp which needs to be exceeded,
// before a new request to the API is made.
val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires"))
val newExpiry = ReleaseVersionUtil.coerceUpdateCheckExpiry(response.getHeader("expires"))
prefs.edit {
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
}
@ -155,7 +155,7 @@ class NewVersionWorker(
private val DEBUG = MainActivity.DEBUG
private val TAG = NewVersionWorker::class.java.simpleName
private const val API_URL =
"https://api.github.com/repos/polymorphicshade/NewPipe/releases/latest"
"https://git.ptr.moe/api/v1/repos/baalajimaestro/NewPipe/releases/latest"
private const val IS_MANUAL = "isManual"
/**

View file

@ -41,6 +41,9 @@ import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceManager;
import com.evernote.android.state.State;
import com.livefront.bridge.Bridge;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
@ -98,8 +101,6 @@ import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import icepick.Icepick;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
@ -152,7 +153,7 @@ public class RouterActivity extends AppCompatActivity {
getWindow().setAttributes(params);
super.onCreate(savedInstanceState);
Icepick.restoreInstanceState(this, savedInstanceState);
Bridge.restoreInstanceState(this, savedInstanceState);
// FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates
// We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments
@ -197,7 +198,7 @@ public class RouterActivity extends AppCompatActivity {
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
Bridge.saveInstanceState(this, outState);
}
@Override

View file

@ -138,8 +138,12 @@ class AboutActivity : AppCompatActivity() {
"https://github.com/lisawray/groupie", StandardLicenses.MIT
),
SoftwareComponent(
"Icepick", "2015", "Frankie Sardo",
"https://github.com/frankiesardo/icepick", StandardLicenses.EPL1
"Android-State", "2018", "Evernote",
"https://github.com/Evernote/android-state", StandardLicenses.EPL1
),
SoftwareComponent(
"Bridge", "2021", "Livefront",
"https://github.com/livefront/bridge", StandardLicenses.APACHE2
),
SoftwareComponent(
"Jsoup", "2009 - 2020", "Jonathan Hedley",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,8 +8,6 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
@ -17,6 +15,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.provider.Settings;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@ -41,6 +40,8 @@ import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.DialogFragment;
import androidx.preference.PreferenceManager;
import com.evernote.android.state.State;
import com.livefront.bridge.Bridge;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.MainActivity;
@ -61,6 +62,8 @@ 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.AudioTrackAdapter;
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.FilenameUtils;
import org.schabi.newpipe.util.ListHelper;
@ -70,8 +73,6 @@ import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
import org.schabi.newpipe.util.SponsorBlockUtils;
import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
import org.schabi.newpipe.util.AudioTrackAdapter;
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.VideoSegment;
@ -83,8 +84,6 @@ import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import icepick.Icepick;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
@ -118,14 +117,11 @@ public class DownloadDialog extends DialogFragment
@State
int selectedSubtitleIndex = 0; // default to the first item
@Nullable
private OnDismissListener onDismissListener = null;
private StoredDirectoryHelper mainStorageAudio = null;
private StoredDirectoryHelper mainStorageVideo = null;
private DownloadManager downloadManager = null;
private ActionMenuItemView okButton = null;
private Context context;
private Context context = null;
private boolean askForSavePath;
private AudioTrackAdapter audioTrackAdapter;
@ -155,7 +151,6 @@ public class DownloadDialog extends DialogFragment
registerForActivityResult(
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
/*//////////////////////////////////////////////////////////////////////////
// Instance creation
//////////////////////////////////////////////////////////////////////////*/
@ -207,13 +202,6 @@ public class DownloadDialog extends DialogFragment
this.segments = seg;
}
/**
* @param onDismissListener the listener to call in {@link #onDismiss(DialogInterface)}
*/
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
this.onDismissListener = onDismissListener;
}
/*//////////////////////////////////////////////////////////////////////////
// Android lifecycle
@ -233,10 +221,12 @@ public class DownloadDialog extends DialogFragment
return;
}
// context will remain null if dismiss() was called above, allowing to check whether the
// dialog is being dismissed in onViewCreated()
context = getContext();
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
Icepick.restoreInstanceState(this, savedInstanceState);
Bridge.restoreInstanceState(this, savedInstanceState);
this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks);
this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
@ -317,6 +307,9 @@ public class DownloadDialog extends DialogFragment
@Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
dialogBinding = DownloadDialogBinding.bind(view);
if (context == null) {
return; // the dialog is being dismissed, see the call to dismiss() in onCreate()
}
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
currentInfo.getName()));
@ -378,14 +371,6 @@ public class DownloadDialog extends DialogFragment
});
}
@Override
public void onDismiss(@NonNull final DialogInterface dialog) {
super.onDismiss(dialog);
if (onDismissListener != null) {
onDismissListener.onDismiss(dialog);
}
}
@Override
public void onDestroy() {
super.onDestroy();
@ -401,7 +386,7 @@ public class DownloadDialog extends DialogFragment
@Override
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
Bridge.saveInstanceState(this, outState);
}
@ -579,7 +564,6 @@ public class DownloadDialog extends DialogFragment
}
}
/*//////////////////////////////////////////////////////////////////////////
// Listeners
//////////////////////////////////////////////////////////////////////////*/
@ -798,6 +782,7 @@ public class DownloadDialog extends DialogFragment
final StoredDirectoryHelper mainStorage;
final MediaFormat format;
final String selectedMediaType;
final long size;
// first, build the filename and get the output folder (if possible)
// later, run a very very very large file checking logic
@ -809,6 +794,7 @@ public class DownloadDialog extends DialogFragment
selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg";
filenameTmp += "opus";
@ -821,6 +807,7 @@ public class DownloadDialog extends DialogFragment
selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
@ -830,6 +817,7 @@ public class DownloadDialog extends DialogFragment
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
if (format != null) {
mimeTmp = format.mimeType;
}
@ -885,6 +873,21 @@ public class DownloadDialog extends DialogFragment
return;
}
// Check for free storage space
final long freeSpace = mainStorage.getFreeStorageSpace();
if (freeSpace <= size) {
Toast.makeText(context, getString(R.
string.error_insufficient_storage), Toast.LENGTH_LONG).show();
// move the user to storage setting tab
final Intent storageSettingsIntent = new Intent(Settings.
ACTION_INTERNAL_STORAGE_SETTINGS);
if (storageSettingsIntent.resolveActivity(context.getPackageManager())
!= null) {
startActivity(storageSettingsIntent);
}
return;
}
// check for existing file with the same name
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
mimeTmp);
@ -1149,7 +1152,8 @@ public class DownloadDialog extends DialogFragment
}
DownloadManagerService.startMission(context, urls, storage, kind, threads,
currentInfo.getUrl(), psName, psArgs, nearLength, new ArrayList<>(recoveryInfo), segments);
currentInfo.getUrl(), psName, psArgs, nearLength,
new ArrayList<>(recoveryInfo), segments);
Toast.makeText(context, getString(R.string.download_has_started),
Toast.LENGTH_SHORT).show();

View file

@ -2,7 +2,6 @@ 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.net.Uri;
@ -13,7 +12,6 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
@ -22,7 +20,6 @@ import androidx.core.content.IntentCompat;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityErrorBinding;
import org.schabi.newpipe.util.Localization;
@ -64,11 +61,11 @@ public class ErrorActivity extends AppCompatActivity {
// BUNDLE TAGS
public static final String ERROR_INFO = "error_info";
public static final String ERROR_EMAIL_ADDRESS = "polymorphicshade@gmail.com";
public static final String ERROR_EMAIL_ADDRESS = "newpipesponsorblock@aol.com";
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
public static final String ERROR_GITHUB_ISSUE_URL =
"https://github.com/polymorphicshade/NewPipe/issues";
"https://git.ptr.moe/baalajimaestro/NewPipe";
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
@ -187,25 +184,6 @@ public class ErrorActivity extends AppCompatActivity {
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
}
/**
* Get the checked activity.
*
* @param returnActivity the activity to return to
* @return the casted return activity or null
*/
@Nullable
static Class<? extends Activity> getReturnActivity(final Class<?> returnActivity) {
Class<? extends Activity> checkedReturnActivity = null;
if (returnActivity != null) {
if (Activity.class.isAssignableFrom(returnActivity)) {
checkedReturnActivity = returnActivity.asSubclass(Activity.class);
} else {
checkedReturnActivity = MainActivity.class;
}
}
return checkedReturnActivity;
}
private void buildInfo(final ErrorInfo info) {
String text = "";

View file

@ -60,7 +60,7 @@ class ErrorUtil {
*/
@JvmStatic
fun showSnackbar(context: Context, errorInfo: ErrorInfo) {
val rootView = if (context is Activity) context.findViewById<View>(R.id.content) else null
val rootView = (context as? Activity)?.findViewById<View>(android.R.id.content)
showSnackbar(context, rootView, errorInfo)
}
@ -77,7 +77,7 @@ class ErrorUtil {
fun showSnackbar(fragment: Fragment, errorInfo: ErrorInfo) {
var rootView = fragment.view
if (rootView == null && fragment.activity != null) {
rootView = fragment.requireActivity().findViewById(R.id.content)
rootView = fragment.requireActivity().findViewById(android.R.id.content)
}
showSnackbar(fragment.requireContext(), rootView, errorInfo)
}

View file

@ -27,8 +27,6 @@ import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.UnsupportedEncodingException;
/*
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
*
@ -187,14 +185,11 @@ public class ReCaptchaActivity extends AppCompatActivity {
final int abuseEnd = url.indexOf("+path");
try {
String abuseCookie = url.substring(abuseStart + 13, abuseEnd);
abuseCookie = Utils.decodeUrlUtf8(abuseCookie);
handleCookies(abuseCookie);
} catch (UnsupportedEncodingException | StringIndexOutOfBoundsException e) {
handleCookies(Utils.decodeUrlUtf8(url.substring(abuseStart + 13, abuseEnd)));
} catch (final StringIndexOutOfBoundsException e) {
if (MainActivity.DEBUG) {
e.printStackTrace();
Log.d(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
+ abuseStart + " and ending at " + abuseEnd + " for url " + url);
Log.e(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
+ abuseStart + " and ending at " + abuseEnd + " for url " + url, e);
}
}
}

View file

@ -6,6 +6,7 @@ package org.schabi.newpipe.error;
public enum UserAction {
USER_REPORT("user report"),
UI_ERROR("ui error"),
DATABASE_IMPORT_EXPORT("database import or export"),
SUBSCRIPTION_CHANGE("subscription change"),
SUBSCRIPTION_UPDATE("subscription update"),
SUBSCRIPTION_GET("get subscription"),

View file

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

View file

@ -220,7 +220,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
public void commitPlaylistTabs() {
pagerAdapter.getLocalPlaylistFragments()
.stream()
.forEach(LocalPlaylistFragment::commitChanges);
.forEach(LocalPlaylistFragment::saveImmediate);
}
private void updateTabLayoutPosition() {
@ -245,10 +245,10 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
// change the background and icon color of the tab layout:
// service-colored at the top, app-background-colored at the bottom
tabLayout.setBackgroundColor(ThemeHelper.resolveColorFromAttr(requireContext(),
bottom ? R.attr.colorSecondary : R.attr.colorPrimary));
bottom ? android.R.attr.windowBackground : R.attr.colorPrimary));
@ColorInt final int iconColor = bottom
? ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent)
? ThemeHelper.resolveColorFromAttr(requireContext(), android.R.attr.colorAccent)
: Color.WHITE;
tabLayout.setTabRippleColor(ColorStateList.valueOf(iconColor).withAlpha(32));
tabLayout.setTabIconTint(ColorStateList.valueOf(iconColor));
@ -282,7 +282,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
* during runtime and changes are not committed immediately. However, in some cases,
* the changes need to be committed immediately by calling
* {@link LocalPlaylistFragment#commitChanges()}.
* {@link LocalPlaylistFragment#saveImmediate()}.
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
*/
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();

View file

@ -64,7 +64,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
/**
* Get the description to display.
* @return description object
* @return description object, if available
*/
@Nullable
protected abstract Description getDescription();
@ -73,7 +73,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
* Get the streaming service. Used for generating description links.
* @return streaming service
*/
@Nullable
@NonNull
protected abstract StreamingService getService();
/**
@ -93,7 +93,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
* Get the list of tags to display below the description.
* @return tag list
*/
@Nullable
@NonNull
public abstract List<String> getTags();
/**
@ -158,7 +158,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
final LinearLayout layout,
final boolean linkifyContent,
@StringRes final int type,
@Nullable final String content) {
@NonNull final String content) {
if (isBlank(content)) {
return;
}
@ -221,16 +221,12 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
urls.append(imageSizeToText(image.getWidth()));
} else {
switch (image.getEstimatedResolutionLevel()) {
case LOW:
urls.append(getString(R.string.image_quality_low));
break;
default: // unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
case MEDIUM:
urls.append(getString(R.string.image_quality_medium));
break;
case HIGH:
urls.append(getString(R.string.image_quality_high));
break;
case LOW -> urls.append(getString(R.string.image_quality_low));
case MEDIUM -> urls.append(getString(R.string.image_quality_medium));
case HIGH -> urls.append(getString(R.string.image_quality_high));
default -> {
// unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
}
}
}
@ -255,7 +251,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
final List<String> tags = getTags();
if (tags != null && !tags.isEmpty()) {
if (!tags.isEmpty()) {
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {

View file

@ -7,9 +7,12 @@ import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import com.evernote.android.state.State;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.Description;
@ -18,61 +21,46 @@ import org.schabi.newpipe.util.Localization;
import java.util.List;
import icepick.State;
public class DescriptionFragment extends BaseDescriptionFragment {
@State
StreamInfo streamInfo = null;
public DescriptionFragment() {
}
StreamInfo streamInfo;
public DescriptionFragment(final StreamInfo streamInfo) {
this.streamInfo = streamInfo;
}
@Nullable
@Override
protected Description getDescription() {
if (streamInfo == null) {
return null;
}
return streamInfo.getDescription();
public DescriptionFragment() {
// keep empty constructor for State when resuming fragment from memory
}
@Nullable
@Override
protected Description getDescription() {
return streamInfo.getDescription();
}
@NonNull
@Override
protected StreamingService getService() {
if (streamInfo == null) {
return null;
}
return streamInfo.getService();
}
@Override
protected int getServiceId() {
if (streamInfo == null) {
return -1;
}
return streamInfo.getServiceId();
}
@Nullable
@NonNull
@Override
protected String getStreamUrl() {
if (streamInfo == null) {
return null;
}
return streamInfo.getUrl();
}
@Nullable
@NonNull
@Override
public List<String> getTags() {
if (streamInfo == null) {
return null;
}
return streamInfo.getTags();
}

View file

@ -56,6 +56,7 @@ import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
import com.evernote.android.state.State;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.material.appbar.AppBarLayout;
@ -72,7 +73,6 @@ import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
@ -109,15 +109,16 @@ import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ReturnYouTubeDislikeUtils;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.InfoCache;
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.image.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.ArrayList;
import java.util.Iterator;
@ -128,7 +129,6 @@ import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
@ -483,7 +483,7 @@ public final class VideoDetailFragment
// commit previous pending changes to database
if (fragment instanceof LocalPlaylistFragment) {
((LocalPlaylistFragment) fragment).commitChanges();
((LocalPlaylistFragment) fragment).saveImmediate();
} else if (fragment instanceof MainFragment) {
((MainFragment) fragment).commitPlaylistTabs();
}
@ -1446,7 +1446,7 @@ public final class VideoDetailFragment
super.showLoading();
//if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required
if (!ExtractorHelper.isCached(serviceId, url, InfoItem.InfoType.STREAM)) {
if (!ExtractorHelper.isCached(serviceId, url, InfoCache.Type.STREAM)) {
binding.detailContentRootHiding.setVisibility(View.INVISIBLE);
}

View file

@ -9,6 +9,8 @@ import android.view.View;
import androidx.annotation.NonNull;
import com.evernote.android.state.State;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
@ -24,7 +26,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable;
@ -143,7 +144,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
currentWorker = loadResult(forceLoad)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((@NonNull L result) -> {
.subscribe((@NonNull final L result) -> {
isLoading.set(false);
currentInfo = result;
currentNextPage = result.getNextPage();

View file

@ -2,14 +2,16 @@ package org.schabi.newpipe.fragments.list.channel;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.evernote.android.state.State;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
@ -20,20 +22,16 @@ import org.schabi.newpipe.util.Localization;
import java.util.List;
import icepick.State;
public class ChannelAboutFragment extends BaseDescriptionFragment {
@State
protected ChannelInfo channelInfo;
public static ChannelAboutFragment getInstance(final ChannelInfo channelInfo) {
final ChannelAboutFragment fragment = new ChannelAboutFragment();
fragment.channelInfo = channelInfo;
return fragment;
ChannelAboutFragment(@NonNull final ChannelInfo channelInfo) {
this.channelInfo = channelInfo;
}
public ChannelAboutFragment() {
super();
// keep empty constructor for State when resuming fragment from memory
}
@Override
@ -45,26 +43,17 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
@Nullable
@Override
protected Description getDescription() {
if (channelInfo == null) {
return null;
}
return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT);
}
@Nullable
@NonNull
@Override
protected StreamingService getService() {
if (channelInfo == null) {
return null;
}
return channelInfo.getService();
}
@Override
protected int getServiceId() {
if (channelInfo == null) {
return -1;
}
return channelInfo.getServiceId();
}
@ -74,12 +63,9 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
return null;
}
@Nullable
@NonNull
@Override
public List<String> getTags() {
if (channelInfo == null) {
return null;
}
return channelInfo.getTags();
}
@ -93,10 +79,11 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
return;
}
final Context context = getContext();
if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) {
addMetadataItem(inflater, layout, false, R.string.metadata_subscribers,
Localization.localizeNumber(context, channelInfo.getSubscriberCount()));
Localization.localizeNumber(
requireContext(),
channelInfo.getSubscriberCount()));
}
addImagesMetadataItem(inflater, layout, R.string.metadata_avatars,

View file

@ -22,8 +22,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.core.view.MenuProvider;
import androidx.preference.PreferenceManager;
import com.evernote.android.state.State;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.tabs.TabLayout;
import com.jakewharton.rxbinding4.view.RxView;
@ -49,16 +51,15 @@ import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
@ -99,6 +100,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
private MenuItem menuRssButton;
private MenuItem menuNotifyButton;
private SubscriptionEntity channelSubscription;
private MenuProvider menuProvider;
public static ChannelFragment getInstance(final int serviceId, final String url,
final String name) {
@ -121,7 +123,62 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
menuProvider = new MenuProvider() {
@Override
public void onCreateMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
inflater.inflate(R.menu.menu_channel, menu);
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
}
@Override
public void onPrepareMenu(@NonNull final Menu menu) {
menuRssButton = menu.findItem(R.id.menu_item_rss);
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
updateRssButton();
updateNotifyButton(channelSubscription);
}
@Override
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_notify:
final boolean value = !item.isChecked();
item.setEnabled(false);
setNotify(value);
break;
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_rss:
if (currentInfo != null) {
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
}
break;
case R.id.menu_item_openInBrowser:
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(requireContext(),
currentInfo.getOriginalUrl());
}
break;
case R.id.menu_item_share:
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name,
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
}
break;
default:
return false;
}
return true;
}
};
activity.addMenuProvider(menuProvider);
}
@Override
@ -183,73 +240,16 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
}
disposables.clear();
binding = null;
activity.removeMenuProvider(menuProvider);
menuProvider = null;
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_channel, menu);
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
}
@Override
public void onPrepareOptionsMenu(@NonNull final Menu menu) {
super.onPrepareOptionsMenu(menu);
menuRssButton = menu.findItem(R.id.menu_item_rss);
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
updateNotifyButton(channelSubscription);
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_notify:
final boolean value = !item.isChecked();
item.setEnabled(false);
setNotify(value);
break;
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_rss:
if (currentInfo != null) {
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
}
break;
case R.id.menu_item_openInBrowser:
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl());
}
break;
case R.id.menu_item_share:
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
currentInfo.getAvatars());
}
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Channel Subscription
//////////////////////////////////////////////////////////////////////////*/
private void monitorSubscription(final ChannelInfo info) {
final Consumer<Throwable> onError = (Throwable throwable) -> {
final Consumer<Throwable> onError = (final Throwable throwable) -> {
animate(binding.channelSubscribeButton, false, 100);
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
"Get subscription status", currentInfo));
@ -284,14 +284,14 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
}
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
return (@NonNull Object o) -> {
return (@NonNull final Object o) -> {
subscriptionManager.insertSubscription(subscription);
return o;
};
}
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
return (@NonNull Object o) -> {
return (@NonNull final Object o) -> {
subscriptionManager.deleteSubscription(subscription);
return o;
};
@ -318,7 +318,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
}
private Disposable monitorSubscribeButton(final Function<Object, Object> action) {
final Consumer<Object> onNext = (@NonNull Object o) -> {
final Consumer<Object> onNext = (@NonNull final Object o) -> {
if (DEBUG) {
Log.d(TAG, "Changed subscription status to this channel!");
}
@ -338,7 +338,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
}
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
return (List<SubscriptionEntity> subscriptionEntities) -> {
return (final List<SubscriptionEntity> subscriptionEntities) -> {
if (DEBUG) {
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
+ "subscriptionEntities = [" + subscriptionEntities + "]");
@ -408,6 +408,13 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
animate(binding.channelSubscribeButton, true, 100, AnimationType.LIGHT_SCALE_AND_ALPHA);
}
private void updateRssButton() {
if (menuRssButton == null || currentInfo == null) {
return;
}
menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl()));
}
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
if (menuNotifyButton == null) {
return;
@ -474,7 +481,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
if (ChannelTabHelper.showChannelTab(
context, preferences, R.string.show_channel_tabs_about)) {
tabAdapter.addFragment(
ChannelAboutFragment.getInstance(currentInfo),
new ChannelAboutFragment(currentInfo),
context.getString(R.string.channel_tab_about));
}
}
@ -610,9 +617,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
binding.subChannelAvatarView.setVisibility(View.VISIBLE);
}
if (menuRssButton != null) {
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
}
updateRssButton();
channelContentNotSupported = false;
for (final Throwable throwable : result.getErrors()) {

View file

@ -9,6 +9,8 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.evernote.android.state.State;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.error.UserAction;
@ -17,6 +19,7 @@ import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.linkhandler.ReadyChannelTabListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
@ -31,13 +34,12 @@ import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import icepick.State;
import io.reactivex.rxjava3.core.Single;
public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo>
implements PlaylistControlViewHolder {
// states must be protected and not private for IcePick being able to access them
// states must be protected and not private for State being able to access them
@State
protected ListLinkHandler tabHandler;
@State
@ -128,10 +130,13 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
// once `handleResult` is called, the parsed data was already saved to cache, so
// we can discard any raw data in ReadyChannelTabListLinkHandler and create a
// link handler with identical properties, but without any raw data
tabHandler = result.getService()
.getChannelTabLHFactory()
.fromQuery(tabHandler.getId(), tabHandler.getContentFilters(),
tabHandler.getSortFilter());
final ListLinkHandlerFactory channelTabLHFactory = result.getService()
.getChannelTabLHFactory();
if (channelTabLHFactory != null) {
// some services do not not have a ChannelTabLHFactory
tabHandler = channelTabLHFactory.fromQuery(tabHandler.getId(),
tabHandler.getContentFilters(), tabHandler.getSortFilter());
}
} catch (final ParsingException e) {
// silently ignore the error, as the app can continue to function normally
Log.w(TAG, "Could not recreate channel tab handler", e);
@ -152,6 +157,7 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
}
}
@Override
public PlayQueue getPlayQueue() {
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
.filter(StreamInfoItem.class::isInstance)

View file

@ -12,6 +12,8 @@ import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.text.HtmlCompat;
import com.evernote.android.state.State;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.CommentRepliesHeaderBinding;
import org.schabi.newpipe.error.UserAction;
@ -38,7 +40,8 @@ public final class CommentRepliesFragment
public static final String TAG = CommentRepliesFragment.class.getSimpleName();
private CommentsInfoItem commentsInfoItem; // the comment to show replies of
@State
CommentsInfoItem commentsInfoItem; // the comment to show replies of
private final CompositeDisposable disposables = new CompositeDisposable();

View file

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

View file

@ -1,7 +1,9 @@
package org.schabi.newpipe.fragments.list.playlist;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
import android.content.Context;
import android.os.Bundle;
@ -37,6 +39,7 @@ import org.schabi.newpipe.extractor.ListExtractor;
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.Description;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
@ -48,9 +51,10 @@ import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextEllipsizer;
import java.util.ArrayList;
import java.util.List;
@ -85,6 +89,9 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
private MenuItem playlistBookmarkButton;
private long streamCount;
private long playlistOverallDurationSeconds;
public static PlaylistFragment getInstance(final int serviceId, final String url,
final String name) {
final PlaylistFragment instance = new PlaylistFragment();
@ -273,6 +280,12 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
animate(headerBinding.uploaderLayout, false, 200);
}
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
setStreamCountAndOverallDuration(result.getItems(), !result.hasNextPage());
}
@Override
public void handleResult(@NonNull final PlaylistInfo result) {
super.handleResult(result);
@ -318,8 +331,32 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
.into(headerBinding.uploaderAvatarView);
}
headerBinding.playlistStreamCount.setText(Localization
.localizeStreamCount(getContext(), result.getStreamCount()));
streamCount = result.getStreamCount();
setStreamCountAndOverallDuration(result.getRelatedItems(), !result.hasNextPage());
final Description description = result.getDescription();
if (description != null && description != Description.EMPTY_DESCRIPTION
&& !isBlank(description.getContent())) {
final TextEllipsizer ellipsizer = new TextEllipsizer(
headerBinding.playlistDescription, 5, getServiceById(result.getServiceId()));
ellipsizer.setStateChangeListener(isEllipsized ->
headerBinding.playlistDescriptionReadMore.setText(
Boolean.TRUE.equals(isEllipsized) ? R.string.show_more : R.string.show_less
));
ellipsizer.setOnContentChanged(canBeEllipsized -> {
headerBinding.playlistDescriptionReadMore.setVisibility(
Boolean.TRUE.equals(canBeEllipsized) ? View.VISIBLE : View.GONE);
if (Boolean.TRUE.equals(canBeEllipsized)) {
ellipsizer.ellipsize();
}
});
ellipsizer.setContent(description);
headerBinding.playlistDescriptionReadMore.setOnClickListener(v -> ellipsizer.toggle());
headerBinding.playlistDescription.setOnClickListener(v -> ellipsizer.toggle());
} else {
headerBinding.playlistDescription.setVisibility(View.GONE);
headerBinding.playlistDescriptionReadMore.setVisibility(View.GONE);
}
if (!result.getErrors().isEmpty()) {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
@ -459,4 +496,20 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
playlistBookmarkButton.setIcon(drawable);
playlistBookmarkButton.setTitle(titleRes);
}
private void setStreamCountAndOverallDuration(final List<StreamInfoItem> list,
final boolean isDurationComplete) {
if (activity != null && headerBinding != null) {
playlistOverallDurationSeconds += list.stream()
.mapToLong(x -> x.getDuration())
.sum();
headerBinding.playlistStreamCount.setText(
Localization.concatenateStrings(
Localization.localizeStreamCount(activity, streamCount),
Localization.getDurationString(playlistOverallDurationSeconds,
isDurationComplete, true))
);
}
}
}

View file

@ -1,6 +1,7 @@
package org.schabi.newpipe.fragments.list.search;
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
import static java.util.Arrays.asList;
@ -39,6 +40,8 @@ import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.evernote.android.state.State;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentSearchBinding;
import org.schabi.newpipe.error.ErrorInfo;
@ -76,7 +79,6 @@ import java.util.Queue;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
@ -389,7 +391,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override
public void onSaveInstanceState(@NonNull final Bundle bundle) {
searchString = searchEditText != null
? searchEditText.getText().toString()
? getSearchEditString().trim()
: searchString;
super.onSaveInstanceState(bundle);
}
@ -400,11 +402,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override
public void reloadContent() {
if (!TextUtils.isEmpty(searchString)
|| (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
if (!TextUtils.isEmpty(searchString) || (searchEditText != null
&& !isSearchEditBlank())) {
search(!TextUtils.isEmpty(searchString)
? searchString
: searchEditText.getText().toString(), this.contentFilter, "");
: getSearchEditString(), this.contentFilter, "");
} else {
if (searchEditText != null) {
searchEditText.setText("");
@ -498,7 +500,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
}
searchEditText.setText(searchString);
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
if (TextUtils.isEmpty(searchString)
|| isSearchEditBlank()) {
searchToolbarContainer.setTranslationX(100);
searchToolbarContainer.setAlpha(0.0f);
searchToolbarContainer.setVisibility(View.VISIBLE);
@ -522,7 +525,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (DEBUG) {
Log.d(TAG, "onClick() called with: v = [" + v + "]");
}
if (TextUtils.isEmpty(searchEditText.getText())) {
if (isSearchEditBlank()) {
NavigationHelper.gotoMainFragment(getFM());
return;
}
@ -548,7 +551,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
}
});
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
searchEditText.setOnFocusChangeListener((final View v, final boolean hasFocus) -> {
if (DEBUG) {
Log.d(TAG, "onFocusChange() called with: "
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
@ -603,13 +606,13 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
s.removeSpan(span);
}
final String newText = searchEditText.getText().toString();
final String newText = getSearchEditString().trim();
suggestionPublisher.onNext(newText);
}
};
searchEditText.addTextChangedListener(textWatcher);
searchEditText.setOnEditorActionListener(
(TextView v, int actionId, KeyEvent event) -> {
(final TextView v, final int actionId, final KeyEvent event) -> {
if (DEBUG) {
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], "
+ "actionId = [" + actionId + "], event = [" + event + "]");
@ -619,7 +622,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
} else if (event != null
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
search(searchEditText.getText().toString(), new String[0], "");
searchEditText.setText(getSearchEditString().trim());
search(getSearchEditString(), new String[0], "");
return true;
}
return false;
@ -694,7 +698,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> suggestionPublisher
.onNext(searchEditText.getText().toString()),
.onNext(getSearchEditString()),
throwable -> showSnackBarError(new ErrorInfo(throwable,
UserAction.DELETE_FROM_HISTORY,
"Deleting item failed")));
@ -723,9 +727,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.getRelatedSearches(query, similarQueryLimit, 25)
.toObservable()
.map(searchHistoryEntries ->
searchHistoryEntries.stream()
.map(entry -> new SuggestionItem(true, entry))
.collect(Collectors.toList()));
searchHistoryEntries.stream()
.map(entry -> new SuggestionItem(true, entry))
.collect(Collectors.toList()));
}
private Observable<List<SuggestionItem>> getRemoteSuggestionsObservable(final String query) {
@ -792,12 +796,12 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
} else if (listNotification.isOnError()
&& listNotification.getError() != null
&& !ExceptionUtils.isInterruptedCaused(
listNotification.getError())) {
listNotification.getError())) {
showSnackBarError(new ErrorInfo(listNotification.getError(),
UserAction.GET_SUGGESTIONS, searchString, serviceId));
}
}, throwable -> showSnackBarError(new ErrorInfo(
throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId)));
throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId)));
}
@Override
@ -805,7 +809,13 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
// no-op
}
private void search(final String theSearchString,
/**
* Perform a search.
* @param theSearchString the trimmed search string
* @param theContentFilter the content filter to use. FIXME: unused param
* @param theSortFilter FIXME: unused param
*/
private void search(@NonNull final String theSearchString,
final String[] theContentFilter,
final String theSortFilter) {
if (DEBUG) {
@ -815,25 +825,26 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
return;
}
// Check if theSearchString is a URL which can be opened by NewPipe directly
// and open it if possible.
try {
final StreamingService streamingService = NewPipe.getServiceByUrl(theSearchString);
if (streamingService != null) {
showLoading();
disposables.add(Observable
.fromCallable(() -> NavigationHelper.getIntentByLink(activity,
streamingService, theSearchString))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(intent -> {
getFM().popBackStackImmediate();
activity.startActivity(intent);
}, throwable -> showTextError(getString(R.string.unsupported_url))));
return;
}
showLoading();
disposables.add(Observable
.fromCallable(() -> NavigationHelper.getIntentByLink(activity,
streamingService, theSearchString))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(intent -> {
getFM().popBackStackImmediate();
activity.startActivity(intent);
}, throwable -> showTextError(getString(R.string.unsupported_url))));
return;
} catch (final Exception ignored) {
// Exception occurred, it's not a url
}
// prepare search
lastSearchedString = this.searchString;
this.searchString = theSearchString;
infoListAdapter.clearStreamItemList();
@ -842,13 +853,17 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
searchBinding.searchMetaInfoSeparator, disposables);
hideKeyboardSearch();
// store search query if search history is enabled
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
ignored -> { },
ignored -> {
},
throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED,
theSearchString, serviceId))
));
// load search results
suggestionPublisher.onNext(theSearchString);
startLoading(false);
}
@ -938,6 +953,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
sortFilter = theSortFilter;
}
private String getSearchEditString() {
return searchEditText.getText().toString();
}
private boolean isSearchEditBlank() {
return isBlank(getSearchEditString());
}
/*//////////////////////////////////////////////////////////////////////////
// Suggestion Results
//////////////////////////////////////////////////////////////////////////*/
@ -979,6 +1002,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
}
searchSuggestion = result.getSearchSuggestion();
if (searchSuggestion != null) {
searchSuggestion = searchSuggestion.trim();
}
isCorrectedSearch = result.isCorrectedSearch();
// List<MetaInfo> cannot be bundled without creating some containers
@ -1080,7 +1106,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> suggestionPublisher
.onNext(searchEditText.getText().toString()),
.onNext(getSearchEditString()),
throwable -> showSnackBarError(new ErrorInfo(throwable,
UserAction.DELETE_FROM_HISTORY, "Deleting item failed")));
disposables.add(onDelete);

View file

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

View file

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

View file

@ -1,12 +1,13 @@
package org.schabi.newpipe.info_list.holder;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine;
import android.graphics.Paint;
import android.text.Layout;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
@ -15,42 +16,27 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
import androidx.fragment.app.FragmentActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
import org.schabi.newpipe.util.text.TextLinkifier;
import java.util.function.Consumer;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import org.schabi.newpipe.util.text.TextEllipsizer;
public class CommentInfoItemHolder extends InfoItemHolder {
private static final String ELLIPSIS = "";
private static final int COMMENT_DEFAULT_LINES = 2;
private static final int COMMENT_EXPANDED_LINES = 1000;
private final int commentHorizontalPadding;
private final int commentVerticalPadding;
private final Paint paintAtContentSize;
private final float ellipsisWidthPx;
private final RelativeLayout itemRoot;
private final ImageView itemThumbnailView;
private final TextView itemContentView;
@ -61,13 +47,8 @@ public class CommentInfoItemHolder extends InfoItemHolder {
private final ImageView itemPinnedView;
private final Button repliesButton;
private final CompositeDisposable disposables = new CompositeDisposable();
@Nullable
private Description commentText;
@Nullable
private StreamingService streamService;
@Nullable
private String streamUrl;
@NonNull
private final TextEllipsizer textEllipsizer;
public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
final ViewGroup parent) {
@ -88,9 +69,14 @@ public class CommentInfoItemHolder extends InfoItemHolder {
commentVerticalPadding = (int) infoItemBuilder.getContext()
.getResources().getDimension(R.dimen.comments_vertical_padding);
paintAtContentSize = new Paint();
paintAtContentSize.setTextSize(itemContentView.getTextSize());
ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS);
textEllipsizer = new TextEllipsizer(itemContentView, COMMENT_DEFAULT_LINES, null);
textEllipsizer.setStateChangeListener(isEllipsized -> {
if (Boolean.TRUE.equals(isEllipsized)) {
denyLinkFocus();
} else {
determineMovementMethod();
}
});
}
@Override
@ -139,16 +125,35 @@ public class CommentInfoItemHolder extends InfoItemHolder {
// setup comment content and click listeners to expand/ellipsize it
streamService = getServiceById(item.getServiceId());
streamUrl = item.getUrl();
commentText = item.getCommentText();
ellipsize();
textEllipsizer.setStreamingService(getServiceById(item.getServiceId()));
textEllipsizer.setStreamUrl(item.getUrl());
textEllipsizer.setContent(item.getCommentText());
textEllipsizer.ellipsize();
//noinspection ClickableViewAccessibility
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
itemContentView.setOnTouchListener((v, event) -> {
final CharSequence text = itemContentView.getText();
if (text instanceof Spanned buffer) {
final int action = event.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
final int offset = getOffsetForHorizontalLine(itemContentView, event);
final var links = buffer.getSpans(offset, offset, ClickableSpan.class);
if (links.length != 0) {
if (action == MotionEvent.ACTION_UP) {
links[0].onClick(itemContentView);
}
// we handle events that intersect links, so return true
return true;
}
}
}
return false;
});
itemView.setOnClickListener(view -> {
toggleEllipsize();
textEllipsizer.toggle();
if (itemBuilder.getOnCommentsSelectedListener() != null) {
itemBuilder.getOnCommentsSelectedListener().selected(item);
}
@ -202,76 +207,4 @@ public class CommentInfoItemHolder extends InfoItemHolder {
denyLinkFocus();
}
}
private void ellipsize() {
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
linkifyCommentContentView(v -> {
boolean hasEllipsis = false;
final CharSequence charSeqText = itemContentView.getText();
if (charSeqText != null && itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
// Note that converting to String removes spans (i.e. links), but that's something
// we actually want since when the text is ellipsized we want all clicks on the
// comment to expand the comment, not to open links.
final String text = charSeqText.toString();
final Layout layout = itemContentView.getLayout();
final float lineWidth = layout.getLineWidth(COMMENT_DEFAULT_LINES - 1);
final float layoutWidth = layout.getWidth();
final int lineStart = layout.getLineStart(COMMENT_DEFAULT_LINES - 1);
final int lineEnd = layout.getLineEnd(COMMENT_DEFAULT_LINES - 1);
// remove characters up until there is enough space for the ellipsis
// (also summing 2 more pixels, just to be sure to avoid float rounding errors)
int end = lineEnd;
float removedCharactersWidth = 0.0f;
while (lineWidth - removedCharactersWidth + ellipsisWidthPx + 2.0f > layoutWidth
&& end >= lineStart) {
end -= 1;
// recalculate each time to account for ligatures or other similar things
removedCharactersWidth = paintAtContentSize.measureText(
text.substring(end, lineEnd));
}
// remove trailing spaces and newlines
while (end > 0 && Character.isWhitespace(text.charAt(end - 1))) {
end -= 1;
}
final String newVal = text.substring(0, end) + ELLIPSIS;
itemContentView.setText(newVal);
hasEllipsis = true;
}
itemContentView.setMaxLines(COMMENT_DEFAULT_LINES);
if (hasEllipsis) {
denyLinkFocus();
} else {
determineMovementMethod();
}
});
}
private void toggleEllipsize() {
final CharSequence text = itemContentView.getText();
if (!isEmpty(text) && text.charAt(text.length() - 1) == ELLIPSIS.charAt(0)) {
expand();
} else if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
ellipsize();
}
}
private void expand() {
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
linkifyCommentContentView(v -> determineMovementMethod());
}
private void linkifyCommentContentView(@Nullable final Consumer<TextView> onCompletion) {
disposables.clear();
if (commentText != null) {
TextLinkifier.fromDescription(itemContentView, commentText,
HtmlCompat.FROM_HTML_MODE_LEGACY, streamService, streamUrl, disposables,
onCompletion);
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -44,11 +44,11 @@ import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.evernote.android.state.State
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Item
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.OnItemLongClickListener
import icepick.State
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
@ -549,7 +549,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
var typeface = Typeface.DEFAULT
var backgroundSupplier = { ctx: Context ->
resolveDrawable(ctx, R.attr.selectableItemBackground)
resolveDrawable(ctx, android.R.attr.selectableItemBackground)
}
if (doCheck) {
// If the uploadDate is null or true we should highlight the item
@ -562,7 +562,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
LayerDrawable(
arrayOf(
resolveDrawable(ctx, R.attr.dashed_border),
resolveDrawable(ctx, R.attr.selectableItemBackground)
resolveDrawable(ctx, android.R.attr.selectableItemBackground)
)
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,6 +26,8 @@ import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewbinding.ViewBinding;
import com.evernote.android.state.State;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
@ -53,27 +55,25 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.debounce.DebounceSavable;
import org.schabi.newpipe.util.debounce.DebounceSaver;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
implements PlaylistControlViewHolder {
/** Save the list 10 seconds after the last change occurred. */
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
implements PlaylistControlViewHolder, DebounceSavable {
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
@State
protected Long playlistId;
@ -90,13 +90,12 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
private LocalPlaylistManager playlistManager;
private Subscription databaseSubscription;
private PublishSubject<Long> debouncedSaveSignal;
private CompositeDisposable disposables;
/** Whether the playlist has been fully loaded from db. */
private AtomicBoolean isLoadingComplete;
/** Whether the playlist has been modified (e.g. items reordered or deleted) */
private AtomicBoolean isModified;
/** Used to debounce saving playlist edits to disk. */
private DebounceSaver debounceSaver;
/** Flag to prevent simultaneous rewrites of the playlist. */
private boolean isRewritingPlaylist = false;
@ -121,12 +120,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
debouncedSaveSignal = PublishSubject.create();
disposables = new CompositeDisposable();
isLoadingComplete = new AtomicBoolean();
isModified = new AtomicBoolean();
debounceSaver = new DebounceSaver(this);
}
@Override
@ -166,17 +164,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
return headerBinding;
}
/**
* <p>Commit changes immediately if the playlist has been modified.</p>
* Delete operations and other modifications will be committed to ensure that the database
* is up to date, e.g. when the user adds the just deleted stream from another fragment.
*/
public void commitChanges() {
if (isModified != null && isModified.get()) {
saveImmediate();
}
}
@Override
protected void initListeners() {
super.initListeners();
@ -243,10 +230,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
if (disposables != null) {
disposables.clear();
}
disposables.add(getDebouncedSaver());
if (debounceSaver != null) {
disposables.add(debounceSaver.getDebouncedSaver());
debounceSaver.setNoChangesToSave();
}
isLoadingComplete.set(false);
isModified.set(false);
playlistManager.getPlaylistStreams(playlistId)
.onBackpressureLatest()
@ -304,8 +294,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public void onDestroy() {
super.onDestroy();
if (debouncedSaveSignal != null) {
debouncedSaveSignal.onComplete();
if (debounceSaver != null) {
debounceSaver.getDebouncedSaveSignal().onComplete();
}
if (disposables != null) {
disposables.dispose();
@ -314,12 +304,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
tabsPagerAdapter.getLocalPlaylistFragments().remove(this);
}
debouncedSaveSignal = null;
debounceSaver = null;
playlistManager = null;
disposables = null;
isLoadingComplete = null;
isModified = null;
}
///////////////////////////////////////////////////////////////////////////
@ -343,7 +332,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public void onNext(final List<PlaylistStreamEntry> streams) {
// Skip handling the result after it has been modified
if (isModified == null || !isModified.get()) {
if (debounceSaver == null || !debounceSaver.getIsModified()) {
handleResult(streams);
isLoadingComplete.set(true);
}
@ -495,14 +484,14 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
itemListAdapter.clearStreamItemList();
itemListAdapter.addItems(itemsToKeep);
saveChanges();
debounceSaver.setHasChangesToSave();
if (thumbnailVideoRemoved) {
updateThumbnailUrl();
}
final long videoCount = itemListAdapter.getItemsList().size();
setVideoCount(videoCount);
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
if (videoCount == 0) {
showEmptyState();
}
@ -532,7 +521,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
setVideoCount(itemListAdapter.getItemsList().size());
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
@ -665,8 +654,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
.subscribe(itemsToKeep -> {
itemListAdapter.clearStreamItemList();
itemListAdapter.addItems(itemsToKeep);
setVideoCount(itemListAdapter.getItemsList().size());
saveChanges();
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
debounceSaver.setHasChangesToSave();
hideLoading();
isRewritingPlaylist = false;
@ -684,42 +673,24 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
updateThumbnailUrl();
}
setVideoCount(itemListAdapter.getItemsList().size());
saveChanges();
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
debounceSaver.setHasChangesToSave();
}
private void saveChanges() {
if (isModified == null || debouncedSaveSignal == null) {
return;
}
isModified.set(true);
debouncedSaveSignal.onNext(System.currentTimeMillis());
}
private Disposable getDebouncedSaver() {
if (debouncedSaveSignal == null) {
return Disposable.empty();
}
return debouncedSaveSignal
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> saveImmediate(), throwable ->
showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE,
"Debounced saver")));
}
private void saveImmediate() {
/**
* <p>Commit changes immediately if the playlist has been modified.</p>
* Delete operations and other modifications will be committed to ensure that the database
* is up to date, e.g. when the user adds the just deleted stream from another fragment.
*/
@Override
public void saveImmediate() {
if (playlistManager == null || itemListAdapter == null) {
return;
}
// List must be loaded and modified in order to save
if (isLoadingComplete == null || isModified == null
|| !isLoadingComplete.get() || !isModified.get()) {
Log.w(TAG, "Attempting to save playlist when local playlist "
+ "is not loaded or not modified: playlist id=[" + playlistId + "]");
if (isLoadingComplete == null || debounceSaver == null
|| !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
return;
}
@ -740,8 +711,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
() -> {
if (isModified != null) {
isModified.set(false);
if (debounceSaver != null) {
debounceSaver.setNoChangesToSave();
}
},
throwable -> showError(new ErrorInfo(throwable,
@ -784,7 +755,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
final int targetIndex = target.getBindingAdapterPosition();
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
if (isSwapped) {
saveChanges();
debounceSaver.setHasChangesToSave();
}
return isSwapped;
}
@ -855,13 +826,25 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
this.name = !TextUtils.isEmpty(title) ? title : "";
}
private void setVideoCount(final long count) {
private void setStreamCountAndOverallDuration(final ArrayList<LocalItem> itemsList) {
if (activity != null && headerBinding != null) {
headerBinding.playlistStreamCount.setText(Localization
.localizeStreamCount(activity, count));
final long streamCount = itemsList.size();
final long playlistOverallDurationSeconds = itemsList.stream()
.filter(PlaylistStreamEntry.class::isInstance)
.map(PlaylistStreamEntry.class::cast)
.map(PlaylistStreamEntry::getStreamEntity)
.mapToLong(StreamEntity::getDuration)
.sum();
headerBinding.playlistStreamCount.setText(
Localization.concatenateStrings(
Localization.localizeStreamCount(activity, streamCount),
Localization.getDurationString(playlistOverallDurationSeconds,
true, true))
);
}
}
@Override
public PlayQueue getPlayQueue() {
return getPlayQueue(0);
}

View file

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

View file

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

View file

@ -1,5 +1,7 @@
package org.schabi.newpipe.local.subscription;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
@ -10,13 +12,11 @@ import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import com.evernote.android.state.State;
import com.livefront.bridge.Bridge;
import org.schabi.newpipe.R;
import icepick.Icepick;
import icepick.State;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class ImportConfirmationDialog extends DialogFragment {
@State
protected Intent resultServiceIntent;
@ -57,12 +57,12 @@ public class ImportConfirmationDialog extends DialogFragment {
throw new IllegalStateException("Result intent is null");
}
Icepick.restoreInstanceState(this, savedInstanceState);
Bridge.restoreInstanceState(this, savedInstanceState);
}
@Override
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
Bridge.saveInstanceState(this, outState);
}
}

View file

@ -20,11 +20,11 @@ import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import com.evernote.android.state.State
import com.xwray.groupie.Group
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.GroupieViewHolder
import icepick.State
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID

View file

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

View file

@ -27,6 +27,8 @@ import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.core.text.util.LinkifyCompat;
import com.evernote.android.state.State;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
@ -44,8 +46,6 @@ import org.schabi.newpipe.util.ServiceHelper;
import java.util.Collections;
import java.util.List;
import icepick.State;
public class SubscriptionsImportFragment extends BaseFragment {
@State
int currentServiceId = Constants.NO_SERVICE_ID;

View file

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

View file

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

View file

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

View file

@ -580,16 +580,16 @@ public final class PlayQueueActivity extends AppCompatActivity
private void onPlayModeChanged(final int repeatMode, final boolean shuffled) {
switch (repeatMode) {
case com.google.android.exoplayer2.Player.REPEAT_MODE_OFF:
queueControlBinding.controlRepeat
.setImageResource(R.drawable.exo_controls_repeat_off);
queueControlBinding.controlRepeat.setImageResource(
com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_off);
break;
case com.google.android.exoplayer2.Player.REPEAT_MODE_ONE:
queueControlBinding.controlRepeat
.setImageResource(R.drawable.exo_controls_repeat_one);
queueControlBinding.controlRepeat.setImageResource(
com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_one);
break;
case com.google.android.exoplayer2.Player.REPEAT_MODE_ALL:
queueControlBinding.controlRepeat
.setImageResource(R.drawable.exo_controls_repeat_all);
queueControlBinding.controlRepeat.setImageResource(
com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_all);
break;
}

View file

@ -473,7 +473,6 @@ public final class Player implements PlaybackListener, Listener {
if (oldPlayerType != playerType && playQueue != null) {
// If playerType changes from one to another we should reload the player
// (to disable/enable video stream or to set quality)
setRecovery();
reloadPlayQueueManager();
stopProgressLoop();
startProgressLoop();

View file

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

View file

@ -1,10 +1,12 @@
package org.schabi.newpipe.player.mediasession;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.os.Build;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log;
@ -14,26 +16,40 @@ import androidx.annotation.Nullable;
import androidx.media.session.MediaButtonReceiver;
import com.google.android.exoplayer2.ForwardingPlayer;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.notification.NotificationActionData;
import org.schabi.newpipe.player.notification.NotificationConstants;
import org.schabi.newpipe.player.ui.PlayerUi;
import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.StreamTypeUtil;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class MediaSessionPlayerUi extends PlayerUi
implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "MediaSessUi";
@Nullable
private MediaSessionCompat mediaSession;
@Nullable
private MediaSessionConnector sessionConnector;
private final String ignoreHardwareMediaButtonsKey;
private boolean shouldIgnoreHardwareMediaButtons = false;
// used to check whether any notification action changed, before sending costly updates
private List<NotificationActionData> prevNotificationActions = List.of();
public MediaSessionPlayerUi(@NonNull final Player player) {
super(player);
ignoreHardwareMediaButtonsKey =
@ -63,6 +79,10 @@ public class MediaSessionPlayerUi extends PlayerUi
sessionConnector.setMetadataDeduplicationEnabled(true);
sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata());
// force updating media session actions by resetting the previous ones
prevNotificationActions = List.of();
updateMediaSessionActions();
}
@Override
@ -80,6 +100,7 @@ public class MediaSessionPlayerUi extends PlayerUi
mediaSession.release();
mediaSession = null;
}
prevNotificationActions = List.of();
}
@Override
@ -163,4 +184,114 @@ public class MediaSessionPlayerUi extends PlayerUi
return builder.build();
}
private void updateMediaSessionActions() {
// On Android 13+ (or Android T or API 33+) the actions in the player notification can't be
// controlled directly anymore, but are instead derived from custom media session actions.
// However the system allows customizing only two of these actions, since the other three
// are fixed to play-pause-buffering, previous, next.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// Although setting media session actions on older android versions doesn't seem to
// cause any trouble, it also doesn't seem to do anything, so we don't do anything to
// save battery. Check out NotificationUtil.updateActions() to see what happens on
// older android versions.
return;
}
if (sessionConnector == null) {
// sessionConnector will be null after destroyPlayer is called
return;
}
// only use the fourth and fifth actions (the settings page also shows only the last 2 on
// Android 13+)
final List<NotificationActionData> newNotificationActions = IntStream.of(3, 4)
.map(i -> player.getPrefs().getInt(
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
NotificationConstants.SLOT_DEFAULTS[i]))
.mapToObj(action -> NotificationActionData
.fromNotificationActionEnum(player, action))
.filter(Objects::nonNull)
.collect(Collectors.toList());
// avoid costly notification actions update, if nothing changed from last time
if (!newNotificationActions.equals(prevNotificationActions)) {
prevNotificationActions = newNotificationActions;
sessionConnector.setCustomActionProviders(
newNotificationActions.stream()
.map(data -> new SessionConnectorActionProvider(data, context))
.toArray(SessionConnectorActionProvider[]::new));
}
}
@Override
public void onBlocked() {
super.onBlocked();
updateMediaSessionActions();
}
@Override
public void onPlaying() {
super.onPlaying();
updateMediaSessionActions();
}
@Override
public void onBuffering() {
super.onBuffering();
updateMediaSessionActions();
}
@Override
public void onPaused() {
super.onPaused();
updateMediaSessionActions();
}
@Override
public void onPausedSeek() {
super.onPausedSeek();
updateMediaSessionActions();
}
@Override
public void onCompleted() {
super.onCompleted();
updateMediaSessionActions();
}
@Override
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
super.onRepeatModeChanged(repeatMode);
updateMediaSessionActions();
}
@Override
public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
super.onShuffleModeEnabledChanged(shuffleModeEnabled);
updateMediaSessionActions();
}
@Override
public void onBroadcastReceived(final Intent intent) {
super.onBroadcastReceived(intent);
if (ACTION_RECREATE_NOTIFICATION.equals(intent.getAction())) {
// the notification actions changed
updateMediaSessionActions();
}
}
@Override
public void onMetadataChanged(@NonNull final StreamInfo info) {
super.onMetadataChanged(info);
updateMediaSessionActions();
}
@Override
public void onPlayQueueEdited() {
super.onPlayQueueEdited();
updateMediaSessionActions();
}
}

View file

@ -0,0 +1,47 @@
package org.schabi.newpipe.player.mediasession;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.media.session.PlaybackStateCompat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.player.notification.NotificationActionData;
import java.lang.ref.WeakReference;
public class SessionConnectorActionProvider implements MediaSessionConnector.CustomActionProvider {
private final NotificationActionData data;
@NonNull
private final WeakReference<Context> context;
public SessionConnectorActionProvider(final NotificationActionData notificationActionData,
@NonNull final Context context) {
this.data = notificationActionData;
this.context = new WeakReference<>(context);
}
@Override
public void onCustomAction(@NonNull final Player player,
@NonNull final String action,
@Nullable final Bundle extras) {
final Context actualContext = context.get();
if (actualContext != null) {
actualContext.sendBroadcast(new Intent(action));
}
}
@Nullable
@Override
public PlaybackStateCompat.CustomAction getCustomAction(@NonNull final Player player) {
return new PlaybackStateCompat.CustomAction.Builder(
data.action(), data.name(), data.icon()
).build();
}
}

View file

@ -0,0 +1,205 @@
package org.schabi.newpipe.player.notification;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.Player;
import java.util.Objects;
public final class NotificationActionData {
@NonNull
private final String action;
@NonNull
private final String name;
@DrawableRes
private final int icon;
public NotificationActionData(@NonNull final String action, @NonNull final String name,
@DrawableRes final int icon) {
this.action = action;
this.name = name;
this.icon = icon;
}
@NonNull
public String action() {
return action;
}
@NonNull
public String name() {
return name;
}
@DrawableRes
public int icon() {
return icon;
}
@SuppressLint("PrivateResource") // we currently use Exoplayer's internal strings and icons
@Nullable
public static NotificationActionData fromNotificationActionEnum(
@NonNull final Player player,
@NotificationConstants.Action final int selectedAction
) {
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
final Context ctx = player.getContext();
switch (selectedAction) {
case NotificationConstants.PREVIOUS:
return new NotificationActionData(ACTION_PLAY_PREVIOUS,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_previous_description), baseActionIcon);
case NotificationConstants.NEXT:
return new NotificationActionData(ACTION_PLAY_NEXT,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_next_description), baseActionIcon);
case NotificationConstants.REWIND:
return new NotificationActionData(ACTION_FAST_REWIND,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_rewind_description), baseActionIcon);
case NotificationConstants.FORWARD:
return new NotificationActionData(ACTION_FAST_FORWARD,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_fastforward_description), baseActionIcon);
case NotificationConstants.SMART_REWIND_PREVIOUS:
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
return new NotificationActionData(ACTION_PLAY_PREVIOUS,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_previous_description),
com.google.android.exoplayer2.ui.R.drawable.exo_notification_previous);
} else {
return new NotificationActionData(ACTION_FAST_REWIND,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_rewind_description),
com.google.android.exoplayer2.ui.R.drawable.exo_controls_rewind);
}
case NotificationConstants.SMART_FORWARD_NEXT:
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
return new NotificationActionData(ACTION_PLAY_NEXT,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_next_description),
com.google.android.exoplayer2.ui.R.drawable.exo_notification_next);
} else {
return new NotificationActionData(ACTION_FAST_FORWARD,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_fastforward_description),
com.google.android.exoplayer2.ui.R.drawable.exo_controls_fastforward);
}
case NotificationConstants.PLAY_PAUSE_BUFFERING:
if (player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == Player.STATE_BLOCKED
|| player.getCurrentState() == Player.STATE_BUFFERING) {
return new NotificationActionData(ACTION_PLAY_PAUSE,
ctx.getString(R.string.notification_action_buffering),
R.drawable.ic_hourglass_top);
}
// fallthrough
case NotificationConstants.PLAY_PAUSE:
if (player.getCurrentState() == Player.STATE_COMPLETED) {
return new NotificationActionData(ACTION_PLAY_PAUSE,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_pause_description),
R.drawable.ic_replay);
} else if (player.isPlaying()
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == Player.STATE_BLOCKED
|| player.getCurrentState() == Player.STATE_BUFFERING) {
return new NotificationActionData(ACTION_PLAY_PAUSE,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_pause_description),
com.google.android.exoplayer2.ui.R.drawable.exo_notification_pause);
} else {
return new NotificationActionData(ACTION_PLAY_PAUSE,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_play_description),
com.google.android.exoplayer2.ui.R.drawable.exo_notification_play);
}
case NotificationConstants.REPEAT:
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
return new NotificationActionData(ACTION_REPEAT,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_repeat_all_description),
com.google.android.exoplayer2.ext.mediasession.R.drawable
.exo_media_action_repeat_all);
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
return new NotificationActionData(ACTION_REPEAT,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_repeat_one_description),
com.google.android.exoplayer2.ext.mediasession.R.drawable
.exo_media_action_repeat_one);
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
return new NotificationActionData(ACTION_REPEAT,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_repeat_off_description),
com.google.android.exoplayer2.ext.mediasession.R.drawable
.exo_media_action_repeat_off);
}
case NotificationConstants.SHUFFLE:
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
return new NotificationActionData(ACTION_SHUFFLE,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_shuffle_on_description),
com.google.android.exoplayer2.ui.R.drawable.exo_controls_shuffle_on);
} else {
return new NotificationActionData(ACTION_SHUFFLE,
ctx.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_shuffle_off_description),
com.google.android.exoplayer2.ui.R.drawable.exo_controls_shuffle_off);
}
case NotificationConstants.CLOSE:
return new NotificationActionData(ACTION_CLOSE, ctx.getString(R.string.close),
R.drawable.ic_close);
case NotificationConstants.NOTHING:
default:
// do nothing
return null;
}
}
@Override
public boolean equals(@Nullable final Object obj) {
return (obj instanceof NotificationActionData other)
&& this.action.equals(other.action)
&& this.name.equals(other.name)
&& this.icon == other.icon;
}
@Override
public int hashCode() {
return Objects.hash(action, name, icon);
}
}

View file

@ -13,7 +13,7 @@ import org.schabi.newpipe.util.Localization;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
@ -65,23 +65,29 @@ public final class NotificationConstants {
public static final int CLOSE = 11;
@Retention(RetentionPolicy.SOURCE)
@IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT,
PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT, SHUFFLE, CLOSE})
@IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD,
SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT,
SHUFFLE, CLOSE})
public @interface Action { }
@Action
public static final int[] ALL_ACTIONS = {NOTHING, PREVIOUS, NEXT, REWIND, FORWARD,
SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT,
SHUFFLE, CLOSE};
@DrawableRes
public static final int[] ACTION_ICONS = {
0,
R.drawable.exo_icon_previous,
R.drawable.exo_icon_next,
R.drawable.exo_icon_rewind,
R.drawable.exo_icon_fastforward,
R.drawable.exo_icon_previous,
R.drawable.exo_icon_next,
com.google.android.exoplayer2.ui.R.drawable.exo_icon_previous,
com.google.android.exoplayer2.ui.R.drawable.exo_icon_next,
com.google.android.exoplayer2.ui.R.drawable.exo_icon_rewind,
com.google.android.exoplayer2.ui.R.drawable.exo_icon_fastforward,
com.google.android.exoplayer2.ui.R.drawable.exo_icon_previous,
com.google.android.exoplayer2.ui.R.drawable.exo_icon_next,
R.drawable.ic_pause,
R.drawable.ic_hourglass_top,
R.drawable.exo_icon_repeat_all,
R.drawable.exo_icon_shuffle_on,
com.google.android.exoplayer2.ui.R.drawable.exo_icon_repeat_all,
com.google.android.exoplayer2.ui.R.drawable.exo_icon_shuffle_on,
R.drawable.ic_close,
};
@ -95,16 +101,6 @@ public final class NotificationConstants {
CLOSE,
};
@Action
public static final int[][] SLOT_ALLOWED_ACTIONS = {
new int[] {PREVIOUS, REWIND, SMART_REWIND_PREVIOUS},
new int[] {REWIND, PLAY_PAUSE, PLAY_PAUSE_BUFFERING},
new int[] {NEXT, FORWARD, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING},
new int[] {NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS,
SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE},
new int[] {NOTHING, NEXT, FORWARD, SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE},
};
public static final int[] SLOT_PREF_KEYS = {
R.string.notification_slot_0_key,
R.string.notification_slot_1_key,
@ -126,29 +122,41 @@ public final class NotificationConstants {
public static String getActionName(@NonNull final Context context, @Action final int action) {
switch (action) {
case PREVIOUS:
return context.getString(R.string.exo_controls_previous_description);
return context.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_previous_description);
case NEXT:
return context.getString(R.string.exo_controls_next_description);
return context.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_next_description);
case REWIND:
return context.getString(R.string.exo_controls_rewind_description);
return context.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_rewind_description);
case FORWARD:
return context.getString(R.string.exo_controls_fastforward_description);
return context.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_fastforward_description);
case SMART_REWIND_PREVIOUS:
return Localization.concatenateStrings(
context.getString(R.string.exo_controls_rewind_description),
context.getString(R.string.exo_controls_previous_description));
context.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_rewind_description),
context.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_previous_description));
case SMART_FORWARD_NEXT:
return Localization.concatenateStrings(
context.getString(R.string.exo_controls_fastforward_description),
context.getString(R.string.exo_controls_next_description));
context.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_fastforward_description),
context.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_next_description));
case PLAY_PAUSE:
return Localization.concatenateStrings(
context.getString(R.string.exo_controls_play_description),
context.getString(R.string.exo_controls_pause_description));
context.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_play_description),
context.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_pause_description));
case PLAY_PAUSE_BUFFERING:
return Localization.concatenateStrings(
context.getString(R.string.exo_controls_play_description),
context.getString(R.string.exo_controls_pause_description),
context.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_play_description),
context.getString(com.google.android.exoplayer2.ui.R.string
.exo_controls_pause_description),
context.getString(R.string.notification_action_buffering));
case REPEAT:
return context.getString(R.string.notification_action_repeat);
@ -165,14 +173,11 @@ public final class NotificationConstants {
/**
* @param context the context to use
* @param sharedPreferences the shared preferences to query values from
* @param slotCount remove indices >= than this value (set to {@code 5} to do nothing, or make
* it lower if there are slots with empty actions)
* @return a sorted list of the indices of the slots to use as compact slots
*/
public static List<Integer> getCompactSlotsFromPreferences(
public static Collection<Integer> getCompactSlotsFromPreferences(
@NonNull final Context context,
final SharedPreferences sharedPreferences,
final int slotCount) {
final SharedPreferences sharedPreferences) {
final SortedSet<Integer> compactSlots = new TreeSet<>();
for (int i = 0; i < 3; i++) {
final int compactSlot = sharedPreferences.getInt(
@ -180,14 +185,14 @@ public final class NotificationConstants {
if (compactSlot == Integer.MAX_VALUE) {
// settings not yet populated, return default values
return new ArrayList<>(SLOT_COMPACT_DEFAULTS);
return SLOT_COMPACT_DEFAULTS;
}
// a negative value (-1) is set when the user does not want a particular compact slot
if (compactSlot >= 0 && compactSlot < slotCount) {
if (compactSlot >= 0) {
// compact slot is < 0 if there are less than 3 checked checkboxes
compactSlots.add(compactSlot);
}
}
return new ArrayList<>(compactSlots);
return compactSlots;
}
}

View file

@ -1,16 +1,19 @@
package org.schabi.newpipe.player.notification;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static androidx.media.app.NotificationCompat.MediaStyle;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.graphics.Bitmap;
import android.os.Build;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.PendingIntentCompat;
@ -23,23 +26,12 @@ import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static androidx.media.app.NotificationCompat.MediaStyle;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
/**
* This is a utility class for player notifications.
*/
@ -100,29 +92,21 @@ public final class NotificationUtil {
final NotificationCompat.Builder builder =
new NotificationCompat.Builder(player.getContext(),
player.getContext().getString(R.string.notification_channel_id));
final MediaStyle mediaStyle = new MediaStyle();
initializeNotificationSlots();
// count the number of real slots, to make sure compact slots indices are not out of bound
int nonNothingSlotCount = 5;
if (notificationSlots[3] == NotificationConstants.NOTHING) {
--nonNothingSlotCount;
// setup media style (compact notification slots and media session)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// notification actions are ignored on Android 13+, and are replaced by code in
// MediaSessionPlayerUi
final int[] compactSlots = initializeNotificationSlots();
mediaStyle.setShowActionsInCompactView(compactSlots);
}
if (notificationSlots[4] == NotificationConstants.NOTHING) {
--nonNothingSlotCount;
}
// build the compact slot indices array (need code to convert from Integer... because Java)
final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
player.getContext(), player.getPrefs(), nonNothingSlotCount);
final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray();
final MediaStyle mediaStyle = new MediaStyle().setShowActionsInCompactView(compactSlots);
player.UIs()
.get(MediaSessionPlayerUi.class)
.flatMap(MediaSessionPlayerUi::getSessionToken)
.ifPresent(mediaStyle::setMediaSession);
// setup notification builder
builder.setStyle(mediaStyle)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
@ -157,7 +141,11 @@ public final class NotificationUtil {
notificationBuilder.setContentText(player.getUploaderName());
notificationBuilder.setTicker(player.getVideoTitle());
updateActions(notificationBuilder);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// notification actions are ignored on Android 13+, and are replaced by code in
// MediaSessionPlayerUi
updateActions(notificationBuilder);
}
}
@ -209,12 +197,35 @@ public final class NotificationUtil {
// ACTIONS
/////////////////////////////////////////////////////
private void initializeNotificationSlots() {
/**
* The compact slots array from settings contains indices from 0 to 4, each referring to one of
* the five actions configurable by the user. However, if the user sets an action to "Nothing",
* then all of the actions coming after will have a "settings index" different than the index
* of the corresponding action when sent to the system.
*
* @return the indices of compact slots referred to the list of non-nothing actions that will be
* sent to the system
*/
private int[] initializeNotificationSlots() {
final Collection<Integer> settingsCompactSlots = NotificationConstants
.getCompactSlotsFromPreferences(player.getContext(), player.getPrefs());
final List<Integer> adjustedCompactSlots = new ArrayList<>();
int nonNothingIndex = 0;
for (int i = 0; i < 5; ++i) {
notificationSlots[i] = player.getPrefs().getInt(
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
NotificationConstants.SLOT_DEFAULTS[i]);
if (notificationSlots[i] != NotificationConstants.NOTHING) {
if (settingsCompactSlots.contains(i)) {
adjustedCompactSlots.add(nonNothingIndex);
}
nonNothingIndex += 1;
}
}
return adjustedCompactSlots.stream().mapToInt(Integer::intValue).toArray();
}
@SuppressLint("RestrictedApi")
@ -227,115 +238,15 @@ public final class NotificationUtil {
private void addAction(final NotificationCompat.Builder builder,
@NotificationConstants.Action final int slot) {
final NotificationCompat.Action action = getAction(slot);
if (action != null) {
builder.addAction(action);
@Nullable final NotificationActionData data =
NotificationActionData.fromNotificationActionEnum(player, slot);
if (data == null) {
return;
}
}
@Nullable
private NotificationCompat.Action getAction(
@NotificationConstants.Action final int selectedAction) {
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
switch (selectedAction) {
case NotificationConstants.PREVIOUS:
return getAction(baseActionIcon,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
case NotificationConstants.NEXT:
return getAction(baseActionIcon,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
case NotificationConstants.REWIND:
return getAction(baseActionIcon,
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
case NotificationConstants.FORWARD:
return getAction(baseActionIcon,
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
case NotificationConstants.SMART_REWIND_PREVIOUS:
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
return getAction(R.drawable.exo_notification_previous,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
} else {
return getAction(R.drawable.exo_controls_rewind,
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
}
case NotificationConstants.SMART_FORWARD_NEXT:
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
return getAction(R.drawable.exo_notification_next,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
} else {
return getAction(R.drawable.exo_controls_fastforward,
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
}
case NotificationConstants.PLAY_PAUSE_BUFFERING:
if (player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == Player.STATE_BLOCKED
|| player.getCurrentState() == Player.STATE_BUFFERING) {
// null intent -> show hourglass icon that does nothing when clicked
return new NotificationCompat.Action(R.drawable.ic_hourglass_top,
player.getContext().getString(R.string.notification_action_buffering),
null);
}
// fallthrough
case NotificationConstants.PLAY_PAUSE:
if (player.getCurrentState() == Player.STATE_COMPLETED) {
return getAction(R.drawable.ic_replay,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else if (player.isPlaying()
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == Player.STATE_BLOCKED
|| player.getCurrentState() == Player.STATE_BUFFERING) {
return getAction(R.drawable.exo_notification_pause,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else {
return getAction(R.drawable.exo_notification_play,
R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
}
case NotificationConstants.REPEAT:
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
return getAction(R.drawable.exo_media_action_repeat_all,
R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
return getAction(R.drawable.exo_media_action_repeat_one,
R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
return getAction(R.drawable.exo_media_action_repeat_off,
R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
}
case NotificationConstants.SHUFFLE:
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
return getAction(R.drawable.exo_controls_shuffle_on,
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
} else {
return getAction(R.drawable.exo_controls_shuffle_off,
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
}
case NotificationConstants.CLOSE:
return getAction(R.drawable.ic_close,
R.string.close, ACTION_CLOSE);
case NotificationConstants.NOTHING:
default:
// do nothing
return null;
}
}
private NotificationCompat.Action getAction(@DrawableRes final int drawable,
@StringRes final int title,
final String intentAction) {
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
PendingIntentCompat.getBroadcast(player.getContext(), NOTIFICATION_ID,
new Intent(intentAction), FLAG_UPDATE_CURRENT, false));
final PendingIntent intent = PendingIntentCompat.getBroadcast(player.getContext(),
NOTIFICATION_ID, new Intent(data.action()), FLAG_UPDATE_CURRENT, false);
builder.addAction(new NotificationCompat.Action(data.icon(), data.name(), intent));
}
private Intent getIntentForNotification() {

View file

@ -132,17 +132,8 @@ public class SeekbarPreviewThumbnailHolder {
// Get the bounds where the frame is found
final int[] bounds = frameset.getFrameBoundsAt(currentPosMs);
generatedDataForUrl.put(currentPosMs, () -> {
// It can happen, that the original bitmap could not be downloaded
// In such a case - we don't want a NullPointer - simply return null
if (srcBitMap == null) {
return null;
}
// Cut out the corresponding bitmap form the "srcBitMap"
return Bitmap.createBitmap(srcBitMap, bounds[1], bounds[2],
frameset.getFrameWidth(), frameset.getFrameHeight());
});
generatedDataForUrl.put(currentPosMs,
createBitmapSupplier(srcBitMap, bounds, frameset));
currentPosMs += frameset.getDurationPerFrame();
pos++;
@ -165,6 +156,45 @@ public class SeekbarPreviewThumbnailHolder {
}
}
private Supplier<Bitmap> createBitmapSupplier(final Bitmap srcBitMap,
final int[] bounds,
final Frameset frameset) {
return () -> {
// It can happen, that the original bitmap could not be downloaded
// (or it was recycled though that should not happen)
// In such a case - we don't want a NullPointer/
// "cannot use a recycled source in createBitmap" Exception -> simply return null
if (srcBitMap == null || srcBitMap.isRecycled()) {
return null;
}
// Under some rare circumstances the YouTube API returns slightly too small storyboards,
// (or not the matching frame width/height)
// This would lead to createBitmap cutting out a bitmap that is out of bounds,
// so we need to adjust the bounds accordingly
if (srcBitMap.getWidth() < bounds[1] + frameset.getFrameWidth()) {
bounds[1] = srcBitMap.getWidth() - frameset.getFrameWidth();
}
if (srcBitMap.getHeight() < bounds[2] + frameset.getFrameHeight()) {
bounds[2] = srcBitMap.getHeight() - frameset.getFrameHeight();
}
// Cut out the corresponding bitmap form the "srcBitMap"
final Bitmap cutOutBitmap = Bitmap.createBitmap(srcBitMap, bounds[1], bounds[2],
frameset.getFrameWidth(), frameset.getFrameHeight());
// If the cut out bitmap is identical to its source,
// we need to copy the bitmap to create a new instance.
// createBitmap allows itself to return the original object that is was created with
// this leads to recycled bitmaps being returned (if they are identical)
// Reference: https://stackoverflow.com/a/23683075 + first comment
// Fixes: https://github.com/TeamNewPipe/NewPipe/issues/11461
return cutOutBitmap == srcBitMap
? cutOutBitmap.copy(cutOutBitmap.getConfig(), true) : cutOutBitmap;
};
}
@Nullable
private Bitmap getBitMapFrom(final String url) {
if (url == null) {

View file

@ -970,11 +970,14 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
super.onRepeatModeChanged(repeatMode);
if (repeatMode == REPEAT_MODE_ALL) {
binding.repeatButton.setImageResource(R.drawable.exo_controls_repeat_all);
binding.repeatButton.setImageResource(
com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_all);
} else if (repeatMode == REPEAT_MODE_ONE) {
binding.repeatButton.setImageResource(R.drawable.exo_controls_repeat_one);
binding.repeatButton.setImageResource(
com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_one);
} else /* repeatMode == REPEAT_MODE_OFF */ {
binding.repeatButton.setImageResource(R.drawable.exo_controls_repeat_off);
binding.repeatButton.setImageResource(
com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_off);
}
}

View file

@ -0,0 +1,301 @@
package org.schabi.newpipe.settings;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.NewPipeDatabase;
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.settings.export.BackupFileLocator;
import org.schabi.newpipe.settings.export.ImportExportManager;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ZipHelper;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
private static final String ZIP_MIME_TYPE = "application/zip";
private final SimpleDateFormat exportDateFormat =
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
private ImportExportManager manager;
private String importExportDataPathKey;
private final ActivityResultLauncher<Intent> requestImportPathLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
this::requestImportPathResult);
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
this::requestExportPathResult);
@Override
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
@Nullable final String rootKey) {
final File homeDir = ContextCompat.getDataDir(requireContext());
Objects.requireNonNull(homeDir);
manager = new ImportExportManager(new BackupFileLocator(homeDir));
importExportDataPathKey = getString(R.string.import_export_data_path);
addPreferencesFromResourceRegistry();
final Preference importDataPreference = requirePreference(R.string.import_data);
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
NoFileManagerSafeGuard.launchSafe(
requestImportPathLauncher,
StoredFileHelper.getPicker(requireContext(),
ZIP_MIME_TYPE, getImportExportDataUri()),
TAG,
getContext()
);
return true;
});
final Preference exportDataPreference = requirePreference(R.string.export_data);
exportDataPreference.setOnPreferenceClickListener((final Preference p) -> {
NoFileManagerSafeGuard.launchSafe(
requestExportPathLauncher,
StoredFileHelper.getNewPicker(requireContext(),
"NewPipeData-" + exportDateFormat.format(new Date()) + ".zip",
ZIP_MIME_TYPE, getImportExportDataUri()),
TAG,
getContext()
);
return true;
});
final Preference resetSettings = findPreference(getString(R.string.reset_settings));
// Resets all settings by deleting shared preference and restarting the app
// A dialogue will pop up to confirm if user intends to reset all settings
assert resetSettings != null;
resetSettings.setOnPreferenceClickListener(preference -> {
// Show Alert Dialogue
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setMessage(R.string.reset_all_settings);
builder.setCancelable(true);
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
// Deletes all shared preferences xml files.
final SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(requireContext());
sharedPreferences.edit().clear().apply();
// Restarts the app
if (getActivity() == null) {
return;
}
NavigationHelper.restartApp(getActivity());
});
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> {
});
final AlertDialog alertDialog = builder.create();
alertDialog.show();
return true;
});
}
private void requestExportPathResult(final ActivityResult result) {
assureCorrectAppLanguage(requireContext());
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
// will be saved only on success
final Uri lastExportDataUri = result.getData().getData();
final StoredFileHelper file = new StoredFileHelper(
requireContext(), result.getData().getData(), ZIP_MIME_TYPE);
exportDatabase(file, lastExportDataUri);
}
}
private void requestImportPathResult(final ActivityResult result) {
assureCorrectAppLanguage(requireContext());
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
// will be saved only on success
final Uri lastImportDataUri = result.getData().getData();
final StoredFileHelper file = new StoredFileHelper(
requireContext(), result.getData().getData(), ZIP_MIME_TYPE);
new androidx.appcompat.app.AlertDialog.Builder(requireActivity())
.setMessage(R.string.override_current_data)
.setPositiveButton(R.string.ok, (d, id) ->
importDatabase(file, lastImportDataUri))
.setNegativeButton(R.string.cancel, (d, id) ->
d.cancel())
.show();
}
}
private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) {
try {
//checkpoint before export
NewPipeDatabase.checkpoint();
final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(requireContext());
manager.exportDatabase(preferences, file);
saveLastImportExportDataUri(exportDataUri); // save export path only on success
Toast.makeText(requireContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT)
.show();
} catch (final Exception e) {
showErrorSnackbar(e, "Exporting database and settings");
}
}
private void importDatabase(final StoredFileHelper file, final Uri importDataUri) {
// check if file is supported
if (!ZipHelper.isValidZipFile(file)) {
Toast.makeText(requireContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
.show();
return;
}
try {
if (!manager.ensureDbDirectoryExists()) {
throw new IOException("Could not create databases dir");
}
// replace the current database
if (!manager.extractDb(file)) {
Toast.makeText(requireContext(), R.string.could_not_import_all_files,
Toast.LENGTH_LONG)
.show();
}
// if settings file exist, ask if it should be imported.
final boolean hasJsonPrefs = manager.exportHasJsonPrefs(file);
if (hasJsonPrefs || manager.exportHasSerializedPrefs(file)) {
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
.setTitle(R.string.import_settings)
.setMessage(hasJsonPrefs ? null : requireContext()
.getString(R.string.import_settings_vulnerable_format))
.setOnDismissListener(dialog -> finishImport(importDataUri))
.setNegativeButton(R.string.cancel, (dialog, which) -> {
dialog.dismiss();
finishImport(importDataUri);
})
.setPositiveButton(R.string.ok, (dialog, which) -> {
dialog.dismiss();
final Context context = requireContext();
final SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(context);
try {
if (hasJsonPrefs) {
manager.loadJsonPrefs(file, prefs);
} else {
manager.loadSerializedPrefs(file, prefs);
}
} catch (IOException | ClassNotFoundException | JsonParserException e) {
createErrorNotification(e, "Importing preferences");
return;
}
cleanImport(context, prefs);
finishImport(importDataUri);
})
.show();
} else {
finishImport(importDataUri);
}
} catch (final Exception e) {
showErrorSnackbar(e, "Importing database and settings");
}
}
/**
* Remove settings that are not supposed to be imported on different devices
* and reset them to default values.
* @param context the context used for the import
* @param prefs the preferences used while running the import
*/
private void cleanImport(@NonNull final Context context,
@NonNull final SharedPreferences prefs) {
// Check if media tunnelling needs to be disabled automatically,
// if it was disabled automatically in the imported preferences.
final String tunnelingKey = context.getString(R.string.disable_media_tunneling_key);
final String automaticTunnelingKey =
context.getString(R.string.disabled_media_tunneling_automatically_key);
// R.string.disable_media_tunneling_key should always be true
// if R.string.disabled_media_tunneling_automatically_key equals 1,
// but we double check here just to be sure and to avoid regressions
// caused by possible later modification of the media tunneling functionality.
// R.string.disabled_media_tunneling_automatically_key == 0:
// automatic value overridden by user in settings
// R.string.disabled_media_tunneling_automatically_key == -1: not set
final boolean wasMediaTunnelingDisabledAutomatically =
prefs.getInt(automaticTunnelingKey, -1) == 1
&& prefs.getBoolean(tunnelingKey, false);
if (wasMediaTunnelingDisabledAutomatically) {
prefs.edit()
.putInt(automaticTunnelingKey, -1)
.putBoolean(tunnelingKey, false)
.apply();
NewPipeSettings.setMediaTunneling(context);
}
}
/**
* Save import path and restart app.
*
* @param importDataUri The import path to save
*/
private void finishImport(final Uri importDataUri) {
// save import path only on success
saveLastImportExportDataUri(importDataUri);
// restart app to properly load db
NavigationHelper.restartApp(requireActivity());
}
private Uri getImportExportDataUri() {
final String path = defaultPreferences.getString(importExportDataPathKey, null);
return isBlank(path) ? null : Uri.parse(path);
}
private void saveLastImportExportDataUri(final Uri importExportDataUri) {
final SharedPreferences.Editor editor = defaultPreferences.edit()
.putString(importExportDataPathKey, importExportDataUri.toString());
editor.apply();
}
private void showErrorSnackbar(final Throwable e, final String request) {
ErrorUtil.showSnackbar(this, new ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request));
}
private void createErrorNotification(final Throwable e, final String request) {
ErrorUtil.createNotification(
requireContext(),
new ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request)
);
}
}

View file

@ -1,106 +1,36 @@
package org.schabi.newpipe.settings;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ZipHelper;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
public class ContentSettingsFragment extends BasePreferenceFragment {
private static final String ZIP_MIME_TYPE = "application/zip";
private final SimpleDateFormat exportDateFormat =
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
private ContentSettingsManager manager;
private String importExportDataPathKey;
private String youtubeRestrictedModeEnabledKey;
private Localization initialSelectedLocalization;
private ContentCountry initialSelectedContentCountry;
private String initialLanguage;
private final ActivityResultLauncher<Intent> requestImportPathLauncher =
registerForActivityResult(new StartActivityForResult(), this::requestImportPathResult);
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
registerForActivityResult(new StartActivityForResult(), this::requestExportPathResult);
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
final File homeDir = ContextCompat.getDataDir(requireContext());
Objects.requireNonNull(homeDir);
manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir));
manager.deleteSettingsFile();
importExportDataPathKey = getString(R.string.import_export_data_path);
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
addPreferencesFromResourceRegistry();
final Preference importDataPreference = requirePreference(R.string.import_data);
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
NoFileManagerSafeGuard.launchSafe(
requestImportPathLauncher,
StoredFileHelper.getPicker(requireContext(),
ZIP_MIME_TYPE, getImportExportDataUri()),
TAG,
getContext()
);
return true;
});
final Preference exportDataPreference = requirePreference(R.string.export_data);
exportDataPreference.setOnPreferenceClickListener((final Preference p) -> {
NoFileManagerSafeGuard.launchSafe(
requestExportPathLauncher,
StoredFileHelper.getNewPicker(requireContext(),
"NewPipeData-" + exportDateFormat.format(new Date()) + ".zip",
ZIP_MIME_TYPE, getImportExportDataUri()),
TAG,
getContext()
);
return true;
});
initialSelectedLocalization = org.schabi.newpipe.util.Localization
.getPreferredLocalization(requireContext());
initialSelectedContentCountry = org.schabi.newpipe.util.Localization
@ -158,151 +88,4 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
NewPipe.setupLocalization(selectedLocalization, selectedContentCountry);
}
}
private void requestExportPathResult(final ActivityResult result) {
assureCorrectAppLanguage(getContext());
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
// will be saved only on success
final Uri lastExportDataUri = result.getData().getData();
final StoredFileHelper file =
new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE);
exportDatabase(file, lastExportDataUri);
}
}
private void requestImportPathResult(final ActivityResult result) {
assureCorrectAppLanguage(getContext());
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
// will be saved only on success
final Uri lastImportDataUri = result.getData().getData();
final StoredFileHelper file =
new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE);
new AlertDialog.Builder(requireActivity())
.setMessage(R.string.override_current_data)
.setPositiveButton(R.string.ok, (d, id) ->
importDatabase(file, lastImportDataUri))
.setNegativeButton(R.string.cancel, (d, id) ->
d.cancel())
.show();
}
}
private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) {
try {
//checkpoint before export
NewPipeDatabase.checkpoint();
final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(requireContext());
manager.exportDatabase(preferences, file);
saveLastImportExportDataUri(exportDataUri); // save export path only on success
Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show();
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Exporting database", e);
}
}
private void importDatabase(final StoredFileHelper file, final Uri importDataUri) {
// check if file is supported
if (!ZipHelper.isValidZipFile(file)) {
Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
.show();
return;
}
try {
if (!manager.ensureDbDirectoryExists()) {
throw new IOException("Could not create databases dir");
}
if (!manager.extractDb(file)) {
Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG)
.show();
}
// if settings file exist, ask if it should be imported.
if (manager.extractSettings(file)) {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.import_settings)
.setNegativeButton(R.string.cancel, (dialog, which) -> {
dialog.dismiss();
finishImport(importDataUri);
})
.setPositiveButton(R.string.ok, (dialog, which) -> {
dialog.dismiss();
final Context context = requireContext();
final SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(context);
manager.loadSharedPreferences(prefs);
cleanImport(context, prefs);
finishImport(importDataUri);
})
.show();
} else {
finishImport(importDataUri);
}
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Importing database", e);
}
}
/**
* Remove settings that are not supposed to be imported on different devices
* and reset them to default values.
* @param context the context used for the import
* @param prefs the preferences used while running the import
*/
private void cleanImport(@NonNull final Context context,
@NonNull final SharedPreferences prefs) {
// Check if media tunnelling needs to be disabled automatically,
// if it was disabled automatically in the imported preferences.
final String tunnelingKey = context.getString(R.string.disable_media_tunneling_key);
final String automaticTunnelingKey =
context.getString(R.string.disabled_media_tunneling_automatically_key);
// R.string.disable_media_tunneling_key should always be true
// if R.string.disabled_media_tunneling_automatically_key equals 1,
// but we double check here just to be sure and to avoid regressions
// caused by possible later modification of the media tunneling functionality.
// R.string.disabled_media_tunneling_automatically_key == 0:
// automatic value overridden by user in settings
// R.string.disabled_media_tunneling_automatically_key == -1: not set
final boolean wasMediaTunnelingDisabledAutomatically =
prefs.getInt(automaticTunnelingKey, -1) == 1
&& prefs.getBoolean(tunnelingKey, false);
if (wasMediaTunnelingDisabledAutomatically) {
prefs.edit()
.putInt(automaticTunnelingKey, -1)
.putBoolean(tunnelingKey, false)
.apply();
NewPipeSettings.setMediaTunneling(context);
}
}
/**
* Save import path and restart system.
*
* @param importDataUri The import path to save
*/
private void finishImport(final Uri importDataUri) {
// save import path only on success
saveLastImportExportDataUri(importDataUri);
// restart app to properly load db
NavigationHelper.restartApp(requireActivity());
}
private Uri getImportExportDataUri() {
final String path = defaultPreferences.getString(importExportDataPathKey, null);
return isBlank(path) ? null : Uri.parse(path);
}
private void saveLastImportExportDataUri(final Uri importExportDataUri) {
final SharedPreferences.Editor editor = defaultPreferences.edit()
.putString(importExportDataPathKey, importExportDataUri.toString());
editor.apply();
}
}

View file

@ -1,120 +0,0 @@
package org.schabi.newpipe.settings
import android.content.SharedPreferences
import android.util.Log
import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.streams.io.SharpOutputStream
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.ZipHelper
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.util.zip.ZipOutputStream
class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
companion object {
const val TAG = "ContentSetManager"
}
/**
* Exports given [SharedPreferences] to the file in given outputPath.
* It also creates the file.
*/
@Throws(Exception::class)
fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) {
file.create()
ZipOutputStream(SharpOutputStream(file.stream).buffered())
.use { outZip ->
ZipHelper.addFileToZip(outZip, fileLocator.db.path, "newpipe.db")
try {
ObjectOutputStream(fileLocator.settings.outputStream()).use { output ->
output.writeObject(preferences.all)
output.flush()
}
} catch (e: IOException) {
if (DEBUG) {
Log.e(TAG, "Unable to exportDatabase", e)
}
}
ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings")
}
}
fun deleteSettingsFile() {
fileLocator.settings.delete()
}
/**
* Tries to create database directory if it does not exist.
*
* @return Whether the directory exists afterwards.
*/
fun ensureDbDirectoryExists(): Boolean {
return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir()
}
fun extractDb(file: StoredFileHelper): Boolean {
val success = ZipHelper.extractFileFromZip(file, fileLocator.db.path, "newpipe.db")
if (success) {
fileLocator.dbJournal.delete()
fileLocator.dbWal.delete()
fileLocator.dbShm.delete()
}
return success
}
fun extractSettings(file: StoredFileHelper): Boolean {
return ZipHelper.extractFileFromZip(file, fileLocator.settings.path, "newpipe.settings")
}
/**
* Remove all shared preferences from the app and load the preferences supplied to the manager.
*/
fun loadSharedPreferences(preferences: SharedPreferences) {
try {
val preferenceEditor = preferences.edit()
ObjectInputStream(fileLocator.settings.inputStream()).use { input ->
preferenceEditor.clear()
@Suppress("UNCHECKED_CAST")
val entries = input.readObject() as Map<String, *>
for ((key, value) in entries) {
when (value) {
is Boolean -> {
preferenceEditor.putBoolean(key, value)
}
is Float -> {
preferenceEditor.putFloat(key, value)
}
is Int -> {
preferenceEditor.putInt(key, value)
}
is Long -> {
preferenceEditor.putLong(key, value)
}
is String -> {
preferenceEditor.putString(key, value)
}
is Set<*> -> {
// There are currently only Sets with type String possible
@Suppress("UNCHECKED_CAST")
preferenceEditor.putStringSet(key, value as Set<String>?)
}
}
}
preferenceEditor.commit()
}
} catch (e: IOException) {
if (DEBUG) {
Log.e(TAG, "Unable to loadSharedPreferences", e)
}
} catch (e: ClassNotFoundException) {
if (DEBUG) {
Log.e(TAG, "Unable to loadSharedPreferences", e)
}
}
}
}

View file

@ -1,6 +1,5 @@
package org.schabi.newpipe.settings;
import static org.schabi.newpipe.extractor.utils.Utils.decodeUrlUtf8;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity;
@ -30,8 +29,6 @@ import org.schabi.newpipe.util.FilePickerActivityHelper;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
public class DownloadSettingsFragment extends BasePreferenceFragment {
public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true;
@ -108,28 +105,15 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
private void showPathInSummary(final String prefKey, @StringRes final int defaultString,
final Preference target) {
String rawUri = defaultPreferences.getString(prefKey, null);
if (rawUri == null || rawUri.isEmpty()) {
final Uri uri = Uri.parse(defaultPreferences.getString(prefKey, ""));
if (uri.equals(Uri.EMPTY)) {
target.setSummary(getString(defaultString));
return;
}
if (rawUri.charAt(0) == File.separatorChar) {
target.setSummary(rawUri);
return;
}
if (rawUri.startsWith(ContentResolver.SCHEME_FILE)) {
target.setSummary(new File(URI.create(rawUri)).getPath());
return;
}
try {
rawUri = decodeUrlUtf8(rawUri);
} catch (final UnsupportedEncodingException e) {
// nothing to do
}
target.setSummary(rawUri);
final String summary = ContentResolver.SCHEME_FILE.equals(uri.getScheme())
? uri.getPath() : uri.toString();
target.setSummary(summary);
}
private boolean isFileUri(final String path) {

View file

@ -23,7 +23,7 @@ public class MainSettingsFragment extends BasePreferenceFragment {
setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called
// Check if the app is updatable
if (!ReleaseVersionUtil.isReleaseApk()) {
if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
getPreferenceScreen().removePreference(
findPreference(getString(R.string.update_pref_screen_key)));

View file

@ -1,21 +0,0 @@
package org.schabi.newpipe.settings
import java.io.File
/**
* Locates specific files of NewPipe based on the home directory of the app.
*/
class NewPipeFileLocator(private val homeDir: File) {
val dbDir by lazy { File(homeDir, "/databases") }
val db by lazy { File(homeDir, "/databases/newpipe.db") }
val dbJournal by lazy { File(homeDir, "/databases/newpipe.db-journal") }
val dbShm by lazy { File(homeDir, "/databases/newpipe.db-shm") }
val dbWal by lazy { File(homeDir, "/databases/newpipe.db-wal") }
val settings by lazy { File(homeDir, "/databases/newpipe.settings") }
}

View file

@ -11,6 +11,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.DeviceUtils;
@ -44,14 +45,8 @@ public final class NewPipeSettings {
private NewPipeSettings() { }
public static void initSettings(final Context context) {
// check if the last used preference version is set
// to determine whether this is the first app run
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(context)
.getInt(context.getString(R.string.last_used_preferences_version), -1);
final boolean isFirstRun = lastUsedPrefVersion == -1;
// first run migrations, then setDefaultValues, since the latter requires the correct types
SettingMigrations.runMigrationsIfNeeded(context, isFirstRun);
SettingMigrations.runMigrationsIfNeeded(context);
// readAgain is true so that if new settings are added their default value is set
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
@ -64,11 +59,12 @@ public final class NewPipeSettings {
PreferenceManager.setDefaultValues(context, R.xml.update_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.sponsor_block_category_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.backup_restore_settings, true);
saveDefaultVideoDownloadDirectory(context);
saveDefaultAudioDownloadDirectory(context);
disableMediaTunnelingIfNecessary(context, isFirstRun);
disableMediaTunnelingIfNecessary(context);
}
static void saveDefaultVideoDownloadDirectory(final Context context) {
@ -146,8 +142,7 @@ public final class NewPipeSettings {
R.string.show_remote_search_suggestions_key);
}
private static void disableMediaTunnelingIfNecessary(@NonNull final Context context,
final boolean isFirstRun) {
private static void disableMediaTunnelingIfNecessary(@NonNull final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String disabledTunnelingKey = context.getString(R.string.disable_media_tunneling_key);
final String disabledTunnelingAutomaticallyKey =
@ -162,7 +157,7 @@ public final class NewPipeSettings {
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
&& !prefs.getBoolean(disabledTunnelingKey, false);
if (Boolean.TRUE.equals(isFirstRun)
if (App.getApp().isFirstRun()
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
setMediaTunneling(context);
}

View file

@ -3,8 +3,10 @@ package org.schabi.newpipe.settings
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.SwitchPreference
import com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
@ -21,15 +23,17 @@ import org.schabi.newpipe.local.subscription.SubscriptionManager
class NotificationsSettingsFragment : BasePreferenceFragment(), OnSharedPreferenceChangeListener {
private var streamsNotificationsPreference: SwitchPreference? = null
private var notificationWarningSnackbar: Snackbar? = null
private var loader: Disposable? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.notifications_settings)
streamsNotificationsPreference =
findPreference(getString(R.string.enable_streams_notifications))
// main check is done in onResume, but also do it here to prevent flickering
preferenceScreen.isEnabled =
NotificationHelper.areNotificationsEnabledOnDevice(requireContext())
updateEnabledState(NotificationHelper.areNotificationsEnabledOnDevice(requireContext()))
}
override fun onStart() {
@ -68,7 +72,7 @@ class NotificationsSettingsFragment : BasePreferenceFragment(), OnSharedPreferen
// If they are disabled, show a snackbar informing the user about that
// while allowing them to open the device's app settings.
val enabled = NotificationHelper.areNotificationsEnabledOnDevice(requireContext())
preferenceScreen.isEnabled = enabled // it is disabled by default, see the xml
updateEnabledState(enabled)
if (!enabled) {
if (notificationWarningSnackbar == null) {
notificationWarningSnackbar = Snackbar.make(
@ -109,6 +113,16 @@ class NotificationsSettingsFragment : BasePreferenceFragment(), OnSharedPreferen
super.onPause()
}
private fun updateEnabledState(enabled: Boolean) {
// On Android 13 player notifications are exempt from notification settings
// so the preferences in app should always be available.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
streamsNotificationsPreference?.isEnabled = enabled
} else {
preferenceScreen.isEnabled = enabled
}
}
private fun updateSubscriptions(subscriptions: List<SubscriptionEntity>) {
val notified = subscriptions.count { it.notificationMode != NotificationMode.DISABLED }
val preference = findPreference<Preference>(getString(R.string.streams_notifications_channels_key))

View file

@ -1,5 +1,7 @@
package org.schabi.newpipe.settings;
import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@ -31,7 +33,6 @@ import java.util.List;
import java.util.Vector;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.Disposable;
public class SelectPlaylistFragment extends DialogFragment {
@ -90,8 +91,7 @@ public class SelectPlaylistFragment extends DialogFragment {
final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database);
final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database);
disposable = Flowable.combineLatest(localPlaylistManager.getPlaylists(),
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
disposable = getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::displayPlaylists, this::onError);
}
@ -118,7 +118,7 @@ public class SelectPlaylistFragment extends DialogFragment {
if (selectedItem instanceof PlaylistMetadataEntry) {
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
onSelectedListener.onLocalPlaylistSelected(entry.uid, entry.name);
onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.name);
} else if (selectedItem instanceof PlaylistRemoteEntity) {
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);

View file

@ -7,6 +7,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
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;
@ -163,15 +164,14 @@ public final class SettingMigrations {
private static final int VERSION = 6;
public static void runMigrationsIfNeeded(@NonNull final Context context,
final boolean isFirstRun) {
public static void runMigrationsIfNeeded(@NonNull final Context context) {
// setup migrations and check if there is something to do
sp = PreferenceManager.getDefaultSharedPreferences(context);
final String lastPrefVersionKey = context.getString(R.string.last_used_preferences_version);
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
// no migration to run, already up to date
if (isFirstRun) {
if (App.getApp().isFirstRun()) {
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
return;
} else if (lastPrefVersion == VERSION) {

View file

@ -21,7 +21,9 @@ import androidx.fragment.app.FragmentManager;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.evernote.android.state.State;
import com.jakewharton.rxbinding4.widget.RxTextView;
import com.livefront.bridge.Bridge;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
@ -41,9 +43,6 @@ import org.schabi.newpipe.views.FocusOverlayView;
import java.util.concurrent.TimeUnit;
import icepick.Icepick;
import icepick.State;
/*
* Created by Christian Schabesberger on 31.08.15.
*
@ -93,7 +92,7 @@ public class SettingsActivity extends AppCompatActivity implements
assureCorrectAppLanguage(this);
super.onCreate(savedInstanceBundle);
Icepick.restoreInstanceState(this, savedInstanceBundle);
Bridge.restoreInstanceState(this, savedInstanceBundle);
final boolean restored = savedInstanceBundle != null;
final SettingsLayoutBinding settingsLayoutBinding =
@ -125,7 +124,7 @@ public class SettingsActivity extends AppCompatActivity implements
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
Bridge.saveInstanceState(this, outState);
}
@Override
@ -266,7 +265,7 @@ public class SettingsActivity extends AppCompatActivity implements
*/
private void ensureSearchRepresentsApplicationState() {
// Check if the update settings are available
if (!ReleaseVersionUtil.isReleaseApk()) {
if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
SettingsResourceRegistry.getInstance()
.getEntryByPreferencesResId(R.xml.update_settings)
.setSearchable(false);

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