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

Compare commits

..

305 Commits

Author SHA1 Message Date
Tobi
a95a5ed13e Merge pull request #9290 from TeamNewPipe/release/0.24.1
Release v0.24.1
2022-11-05 21:25:52 +01:00
Tobi
da61c9f915 Merge pull request #9298 from Stypox/fix-inconsistent-channel-groups
Fix inconsistent channel group list and items view mode
2022-11-05 21:11:51 +01:00
Stypox
9472c36cbd Merge pull request #9109 from TeamNewPipe/fix/overlayPlayQueueButton
Hide play queue button in VideoDetailsFragment when queue is empty
2022-11-05 20:28:14 +01:00
Stypox
49c12a31e9 Fix wrongly calculated channel groups span count 2022-11-05 20:05:59 +01:00
Stypox
fc061599f8 Fix inconsistent channel group list and item view mode 2022-11-04 18:21:35 +01:00
TobiGr
b066457ccf Update NewPipe to 0.24.1 (991) 2022-11-04 17:07:13 +01:00
Hosted Weblate
2c5c7dfe3a Translated using Weblate (Punjabi)
Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Punjabi (Pakistan))

Currently translated at 18.4% (119 of 646 strings)

Translated using Weblate (Slovak)

Currently translated at 9.7% (7 of 72 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 97.2% (70 of 72 strings)

Translated using Weblate (Polish)

Currently translated at 59.7% (43 of 72 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (72 of 72 strings)

Translated using Weblate (Turkish)

Currently translated at 99.8% (645 of 646 strings)

Translated using Weblate (German)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Hungarian)

Currently translated at 9.8% (7 of 71 strings)

Translated using Weblate (Punjabi)

Currently translated at 5.6% (4 of 71 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.6% (644 of 646 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Asturian)

Currently translated at 2.8% (2 of 71 strings)

Translated using Weblate (Punjabi)

Currently translated at 99.6% (644 of 646 strings)

Translated using Weblate (Vietnamese)

Currently translated at 98.9% (639 of 646 strings)

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

Currently translated at 99.5% (643 of 646 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (646 of 646 strings)

Added translation using Weblate (English (Middle))

Added translation using Weblate (English (Old))

Added translation using Weblate (Sicilian)

Added translation using Weblate (Arabic (Najdi))

Added translation using Weblate (Kashmiri)

Added translation using Weblate (German (Low))

Added translation using Weblate (Aymará)

Added translation using Weblate (Kazakh)

Translated using Weblate (Bulgarian)

Currently translated at 2.8% (2 of 71 strings)

Translated using Weblate (Bengali)

Currently translated at 90.5% (585 of 646 strings)

Translated using Weblate (Punjabi)

Currently translated at 99.6% (644 of 646 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (646 of 646 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Balázs Meskó <meskobalazs@mailbox.org>
Co-authored-by: Enol P <enolp@softastur.org>
Co-authored-by: Ergün Can Taş <erguntas1968@gmail.com>
Co-authored-by: Gediminas Murauskas <muziejusinfo@gmail.com>
Co-authored-by: Giovanni Donisi <giovannidonisi0701@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Lyudmil Borisov <lyuskoborisov@abv.bg>
Co-authored-by: Marian Hanzel <marulinko@gmail.com>
Co-authored-by: Nizami <nizamismidov4@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Ray <ray.cfu@protonmail.com>
Co-authored-by: Rohan Deb Sarkar <rohandebsarkar+git@gmail.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: ShareASmile <aapshergill@gmail.com>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.com>
Co-authored-by: Thọ Bùi Nguyễn Hoàng <buitho061997@gmail.com>
Co-authored-by: Vri 🌈 <weblate@vrifox.cc>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: bgo-eiu <huyaqoob+toolforge@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: zaioti <zaioti@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ast/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/az/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bg/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hu/
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/uk/
Translation: NewPipe/Metadata
2022-11-04 17:06:26 +01:00
Tobi
4573407fc7 Merge pull request #9291 from AudricV/support-yt-handles-and-update-extractor
Support YouTube handles and update NewPipe Extractor
2022-11-04 12:48:54 +01:00
AudricV
9912c11043 Update NewPipe Extractor to support handles 2022-11-04 12:26:42 +01:00
AudricV
231c5e515f [YouTube] Support opening handles from external apps 2022-11-04 12:26:42 +01:00
Tobi
e9870d9e1d Merge pull request #9286 from TeamNewPipe/changelog/0.24.1
Add changelog for 0.24.1 (991)
2022-11-03 19:56:43 +01:00
TobiGr
c274ee9873 Add changelog for 0.24.1 (991) 2022-11-03 17:28:22 +01:00
Tobi
c8caf48cda Merge pull request #9230 from Stypox/duplicate-feed-videos
Fix duplicate videos in feed group "All"
2022-11-03 17:23:05 +01:00
Tobi
1de662f779 Merge pull request #9272 from TeamNewPipe/prettytime
Update PrettyTime from 5.0.3 to 5.0.6 to include new localizations
2022-11-03 16:17:02 +01:00
Isira Seneviratne
84887395f8 Merge pull request #8655 from Isira-Seneviratne/Use_TextViewCompat_setCompoundDrawableTIntList
Use TextViewCompat.setCompoundDrawableTintList().
2022-11-03 20:13:17 +05:30
TobiGr
bf766f1670 Update PrettyTime from 5.0.3 to 5.0.6 to include new localizations 2022-11-01 19:32:31 +01:00
Isira Seneviratne
51bdc30ed0 Use TextViewCompat.setCompoundDrawableTintList(). 2022-11-01 06:31:35 +05:30
Isira Seneviratne
4b892e2b30 Update AppCompat to 1.5.1. 2022-11-01 06:29:08 +05:30
Tobi
43b2176956 Merge pull request #9256 from TeamNewPipe/weblate
Update translations and remove empty localizations
2022-10-31 13:00:53 +01:00
TobiGr
00283fac30 Remove Kazakh from language picker 2022-10-31 12:20:40 +01:00
Hosted Weblate
78f6a86645 Translated using Weblate (Dutch (Middle))
Currently translated at 4.6% (30 of 646 strings)

Deleted translation using Weblate (Sicilian)

Deleted translation using Weblate (Kazakh)

Deleted translation using Weblate (Kashmiri)

Deleted translation using Weblate (German (Low))

Deleted translation using Weblate (English (Old))

Deleted translation using Weblate (English (Middle))

Deleted translation using Weblate (Aymará)

Deleted translation using Weblate (Arabic (Najdi))

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (French)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (German)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Hebrew)

Currently translated at 53.5% (38 of 71 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.5% (643 of 646 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Portuguese)

Currently translated at 69.0% (49 of 71 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Croatian)

Currently translated at 99.5% (642 of 645 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (French)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (German)

Currently translated at 100.0% (645 of 645 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Eric <hamburger1024@mailbox.org>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Gediminas Murauskas <muziejusinfo@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Rückert <igorruckert@yahoo.com.br>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nizami <nizamismidov4@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
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: SC <lalocas@protonmail.com>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Terry Louwers <t.louwers@gmail.com>
Co-authored-by: TobiGr <tobigr@mail.de>
Co-authored-by: Translator <kvb@tuta.io>
Co-authored-by: William <eduardo.957@hotmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: bomzhellino <adm.bomzh@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: sonix-github <sonix.internet@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt/
Translation: NewPipe/Metadata
2022-10-31 12:10:21 +01:00
Stypox
9d2ab61993 Merge pull request #9203 from Callisto404/add-chapter-timestamp-share
Added timestamped link sharing from the start of a chapter with a long hold press
2022-10-30 22:31:09 +01:00
Stypox
8fdd828de4 Merge pull request #8739 from Isira-Seneviratne/Stream_average
Calculate search score using streams.
2022-10-30 22:09:55 +01:00
Stypox
25795c3a96 Merge pull request #8706 from Isira-Seneviratne/Improve_LocalPlaylistFragment
Refactor removeWatchedStreams() in LocalPlaylistFragment.
2022-10-30 22:03:39 +01:00
Daniel M
7f3da04fee Added an "isYouTube" check to start of long click handler 2022-10-30 13:48:03 +11:00
Stypox
7864521cb4 Merge pull request #8767 from Isira-Seneviratne/Use_ByteString
Use Okio's ByteString for download checksums.
2022-10-29 11:16:44 +02:00
Tobi
31b83ba47a Add info on date + time localization to contribution guidelines (#9229)
Dates and times are localized using PrettyTime. Added a note to guide translators to also localize that library to create a fully localized experience.

Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com>
2022-10-28 17:35:56 +02:00
Stypox
9524c6245d Merge pull request #8747 from Isira-Seneviratne/Range_limit
Use range-limiting methods in more places.
2022-10-28 10:34:04 +02:00
Stypox
57d2fe113a Fix duplicate videos in feed "All" 2022-10-27 23:43:39 +02:00
Stypox
2f6cb87bba Use GROUP_ALL_ID instead of hardcoded -1 2022-10-27 23:32:19 +02:00
Stypox
3cef7f3201 Merge pull request #9207 from cern1710/list-view-alt-alt-implementation
undefined
2022-10-27 22:48:03 +02:00
Tobi
2225933946 Merge pull request #9179 from OneGuitars/9094-add-translations
Added Icelandic, Latvian, Malayalam to language selector
2022-10-27 20:57:28 +02:00
Zhuojun Xiao
47259ef152 Added Icelandic, Latvian, Malayalam to language selector in alphabetical order 2022-10-27 20:26:37 +02:00
Zhuojun Xiao
b2eb631a97 Added Icelandic,Latvian,Malayalam to language selector 2022-10-27 20:26:37 +02:00
Hosted Weblate
9e0f37a2de Translated using Weblate (Greek)
Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (French)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (French)

Currently translated at 99.5% (642 of 645 strings)

Translated using Weblate (Greek)

Currently translated at 99.6% (643 of 645 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.0% (636 of 642 strings)

Translated using Weblate (Polish)

Currently translated at 59.1% (42 of 71 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Icelandic)

Currently translated at 83.1% (534 of 642 strings)

Translated using Weblate (Punjabi (Pakistan))

Currently translated at 18.3% (118 of 642 strings)

Translated using Weblate (Punjabi (Pakistan))

Currently translated at 1.4% (1 of 71 strings)

Translated using Weblate (Icelandic)

Currently translated at 83.0% (533 of 642 strings)

Translated using Weblate (Aymara (Southern))

Currently translated at 0.1% (1 of 642 strings)

Translated using Weblate (Swedish)

Currently translated at 60.5% (43 of 71 strings)

Translated using Weblate (Russian)

Currently translated at 30.9% (22 of 71 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (French)

Currently translated at 100.0% (642 of 642 strings)

Added translation using Weblate (Punjabi (Pakistan))

Translated using Weblate (Icelandic)

Currently translated at 70.0% (450 of 642 strings)

Translated using Weblate (Undetermined)

Currently translated at 21.4% (138 of 642 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Undetermined)

Currently translated at 18.3% (118 of 642 strings)

Translated using Weblate (Icelandic)

Currently translated at 2.8% (2 of 71 strings)

Translated using Weblate (Icelandic)

Currently translated at 51.5% (331 of 642 strings)

Translated using Weblate (French)

Currently translated at 91.5% (65 of 71 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (71 of 71 strings)

Translated using Weblate (Italian)

Currently translated at 43.6% (31 of 71 strings)

Translated using Weblate (Basque)

Currently translated at 45.0% (32 of 71 strings)

Translated using Weblate (Punjabi)

Currently translated at 84.7% (544 of 642 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Tamil)

Currently translated at 54.8% (352 of 642 strings)

Translated using Weblate (Tamil)

Currently translated at 54.8% (352 of 642 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (642 of 642 strings)

Added translation using Weblate (Undetermined)

Translated using Weblate (Icelandic)

Currently translated at 7.3% (47 of 642 strings)

Translated using Weblate (Icelandic)

Currently translated at 4.6% (30 of 642 strings)

Added translation using Weblate (Icelandic)

Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Chitraarasu <chitraarasu@kirshi.co>
Co-authored-by: Chitraarasu.k <kchitraarasu@gmail.com>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Florian <flo.site@zaclys.net>
Co-authored-by: Gediminas Murauskas <muziejusinfo@gmail.com>
Co-authored-by: GnuPGを使うべきだ <dieeeazpnnqbpddh@cock.email>
Co-authored-by: Gontzal Manuel Pujana Onaindia <thadahdenyse@gmail.com>
Co-authored-by: Hoseok Seo <ddinghoya@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Mehmet Ali <2045uuttb@relay.firefox.com>
Co-authored-by: Nizami <nizamismidov4@gmail.com>
Co-authored-by: OneGuitars <xiaozhuojun1125@gmail.com>
Co-authored-by: Retrial <giwrgosmant@gmail.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Translator <kvb@tuta.io>
Co-authored-by: Trendyne <eiko@chiru.no>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: atilluF <atilluf@outlook.com>
Co-authored-by: bgo-eiu <huyaqoob+toolforge@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: julboudin <boudin.julie@orange.fr>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: Артём Нефедов <artem10397g@gmail.com>
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/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/is/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa_PK/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translation: NewPipe/Metadata
2022-10-27 20:25:47 +02:00
Samuel Wu
f712ea34e0 Merge remote-tracking branch 'origin/list-view-alt-alt-implementation' into list-view-alt-alt-implementation 2022-10-28 04:54:42 +11:00
Samuel Wu
a44b7c9c9e Disabled animations for subscription fragment 2022-10-28 04:54:33 +11:00
Stypox
4b32890b5f Fix random crash in SubscriptionFragment 2022-10-27 18:45:06 +02:00
Stypox
a41aa01461 Solve two SonarCloud smells 2022-10-27 17:52:17 +02:00
Stypox
2ed6819e2c Make channel groups button sizes larger 2022-10-27 17:35:55 +02:00
Stypox
ea875c59af Deduplicate isGridLayout calls 2022-10-27 17:25:06 +02:00
YonghaoDeng
a22162ffac Add an Open in browser button on error panel (#9180)
* add a open in browser button

* Corrected a few things that needed to be changed

* Remove unneeded changes.

* Remove unneeded changes.

* Add showAndSetOpenInBrowserButtonAction function

* modify some codes
2022-10-27 14:38:08 +02:00
Stypox
83d16dc656 Fix flickering in channel groups list 2022-10-27 14:01:04 +02:00
Stypox
8ceefee1e3 Put "New feed group" item at the top 2022-10-27 13:51:56 +02:00
Samuel Wu
8f157be7e0 Revert changes 2022-10-27 12:15:36 +11:00
Stypox
38579e9a29 Merge pull request #9214 from Stypox/update-extractor
Update extractor to fix Jitpack failures in CI
2022-10-26 23:47:36 +02:00
Stypox
30a91f59ae Update extractor to fix Jitpack failures in CI
Jitpack seems to have deleted the previous commit form their servers (5c710da160f488bb40ab2cf4469bec9bd4cefd38)
2022-10-26 23:38:23 +02:00
Stypox
0e169951f7 Fix grid/list toggle implementation of feed 2022-10-26 23:20:32 +02:00
Samuel Wu
8b9db369f6 Resized add new item button 2022-10-26 21:23:50 +11:00
Samuel Wu
f7e10eb094 Fully working card and list view 2022-10-26 21:05:55 +11:00
Jfax510
0d73d193ad Added Toast Notification "Hold to enqueue" (#9196)
* Added Toast Notification "Hold to enqueue"

* Check if enqueue tips are enabled

* created function showHoldToAppendTipIfNeeded() for toast message
2022-10-26 11:35:03 +02:00
plasticanu
40815086ad Fix crash when the user clicks download then quits the history fragment (#9143)
* Fix crash when the user clicks download then quits the history fragment

* add a nonnull annotation to the context parameter in the DownloadDialog constructor.

* Revert "Merge branch 'TeamNewPipe:dev' into fix/HistoryFragmentDownloadDialogCrash"

This reverts commit 968d7a7603.

* Revert "Merge branch 'TeamNewPipe:dev' into fix/HistoryFragmentDownloadDialogCrash"

This reverts commit 968d7a7603, reversing
changes made to 52963ba37d.

Reverted merge

jlhzxc

* update project to the latest dev branch

* Revert "update project to the latest dev branch"

This reverts commit fb3ed83d51.

revert changes to build files

* Revert "Revert "Merge branch 'TeamNewPipe:dev' into fix/HistoryFragmentDownloadDialogCrash""

This reverts commit f9e1835e71.
2022-10-26 11:22:32 +02:00
Yuuu2990
16860603fd Add Link to FAQ in the app (#9164)
* Link to FAQ in the app #4447

* remove redundant comments produced by me.

* Update app/src/main/res/values/strings.xml

Update FAQ description

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

* Format the CodeStyle and readjust the layout.

* Update app/src/main/res/layout/fragment_about.xml

Remove redundant id.

Co-authored-by: Stypox <stypox@pm.me>

* Update app/src/main/res/layout/fragment_about.xml

Remove redundant id.

Co-authored-by: Stypox <stypox@pm.me>

* Update app/src/main/res/values/strings.xml

Keep the uppercase for consistency.

Co-authored-by: Stypox <stypox@pm.me>

* Update app/src/main/res/values/strings.xml

Modify the description of FAQ.

Co-authored-by: Stypox <stypox@pm.me>

Co-authored-by: opusforlife2 <53176348+opusforlife2@users.noreply.github.com>
Co-authored-by: Stypox <stypox@pm.me>
2022-10-26 09:59:51 +02:00
Samuel Wu
c607089cbb Altered grid view similar to Youtube app layout 2022-10-26 00:06:48 +11:00
Samuel Wu
28464344c1 Finalized design for vertical card view and removed unneeded variables in SubscriptionFragment.kt 2022-10-25 11:43:25 +11:00
Samuel Wu
ed68e3bd46 Fully working toggle button that change between vertical and horizontal view 2022-10-25 10:54:27 +11:00
Samuel Wu
082d7a3f18 Added working binding for a "new" button that works in the list layout. 2022-10-25 02:38:31 +11:00
Samuel Wu
6eddaa0d38 Added boolean to handle feed groups. May need a better solution for this 2022-10-25 02:20:14 +11:00
Samuel Wu
1aa1a0287e Could toggle between list view and grid view...once. Requires bug fixing on refreshing 2022-10-25 02:01:57 +11:00
Samuel Wu
3bfcb16f9a Bug: SubscriptionViewModel.kt did not map values for FeedGroupCardVerticalItem in line 26 2022-10-25 00:32:21 +11:00
Samuel Wu
f37d869ea2 Button can be toggled but not all strings have been fed 2022-10-24 23:01:02 +11:00
Samuel Wu
78547b4fa4 Created a list view for channel group. 2022-10-24 18:55:08 +11:00
Samuel Wu
29e56b9f2d Created a button in SubscriptionFragment.kt that reads whether button is clicked 2022-10-24 16:55:12 +11:00
Daniel M
83357ca67e Added sharing a link with timestamp from a chapter into the long click for each chapter 2022-10-24 14:14:41 +11:00
Samuel Wu
8482bf9fed Created a non-functional button in HeaderWithMenuItem.kt 2022-10-23 23:43:52 +11:00
Tobi
2a98cca801 Merge pull request #8986 from Isira-Seneviratne/AGP_7.3
Update Android Gradle Plugin to 7.3.0.
2022-10-18 23:03:01 +02:00
Isira Seneviratne
6277d4981c Update Android Gradle Plugin to 7.3.0. 2022-10-15 04:09:08 +05:30
opusforlife2
02deaa0f1a Update label to 'feature request' 2022-10-14 19:40:04 +02:00
TobiGr
4a278ef102 Hide play queue button in VideoDetailsFragment mini player when the play queue is empty
Related PR introducing the button: #8946
2022-10-11 21:27:04 +02:00
Tobi
7ab8f9f112 Merge pull request #9079 from devlearner/fix-screen-rotate
Fix `DownloadDialog` crash on screen rotation
2022-10-10 11:18:50 +02:00
Tobi
7fca0e0786 Merge pull request #9065 from devlearner/fix-spannable-cast
Fix potential cast exception in comments text
2022-10-10 10:51:32 +02:00
Tobi
0b0dfd0a37 Merge pull request #9092 from TeamNewPipe/weblate
Update translations
2022-10-09 20:09:14 +02:00
Hosted Weblate
dd07bd91a4 Translated using Weblate (Korean)
Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 67.6% (48 of 71 strings)

Translated using Weblate (Portuguese)

Currently translated at 67.6% (48 of 71 strings)

Translated using Weblate (Bengali)

Currently translated at 90.9% (584 of 642 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.8% (641 of 642 strings)

Translated using Weblate (Danish)

Currently translated at 91.4% (587 of 642 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 64.4% (414 of 642 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (641 of 642 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (642 of 642 strings)

Added translation using Weblate (Aymara (Southern))

Added translation using Weblate (Aymará)

Translated using Weblate (Bengali)

Currently translated at 88.9% (571 of 642 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.6% (640 of 642 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (71 of 71 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 28.1% (20 of 71 strings)

Translated using Weblate (Turkish)

Currently translated at 32.3% (23 of 71 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Hindi)

Currently translated at 70.2% (451 of 642 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (642 of 642 strings)

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

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (640 of 642 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Dutch)

Currently translated at 98.9% (635 of 642 strings)

Translated using Weblate (French)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (640 of 642 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (640 of 642 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (642 of 642 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (640 of 642 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (640 of 642 strings)

Translated using Weblate (German)

Currently translated at 100.0% (642 of 642 strings)

Merge branch 'origin/dev' into Weblate.

Translated using Weblate (Tamil)

Currently translated at 52.6% (337 of 640 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.5% (637 of 640 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Portuguese)

Currently translated at 60.5% (43 of 71 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Tamil)

Currently translated at 52.5% (336 of 640 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Slovak)

Currently translated at 8.4% (6 of 71 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 98.1% (628 of 640 strings)

Translated using Weblate (Galician)

Currently translated at 99.6% (638 of 640 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (German)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Spanish)

Currently translated at 88.7% (63 of 71 strings)

Translated using Weblate (Hindi)

Currently translated at 4.2% (3 of 71 strings)

Translated using Weblate (Portuguese)

Currently translated at 60.5% (43 of 71 strings)

Translated using Weblate (Hindi)

Currently translated at 68.7% (440 of 640 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (640 of 640 strings)

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

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Dutch)

Currently translated at 99.3% (636 of 640 strings)

Translated using Weblate (English)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (71 of 71 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 63.3% (45 of 71 strings)

Translated using Weblate (Swedish)

Currently translated at 47.8% (34 of 71 strings)

Translated using Weblate (French)

Currently translated at 90.1% (64 of 71 strings)

Translated using Weblate (Spanish)

Currently translated at 57.7% (41 of 71 strings)

Translated using Weblate (Polish)

Currently translated at 57.7% (41 of 71 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (71 of 71 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (71 of 71 strings)

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

Currently translated at 15.4% (11 of 71 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (640 of 640 strings)

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

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (French)

Currently translated at 100.0% (640 of 640 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: AioiLight <info@aioilight.space>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Allan Kimmer Jensen <mail@akj.io>
Co-authored-by: Andrew Boonin <catassasin331@gmail.com>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: Denys Nykula <vegan@libre.net.ua>
Co-authored-by: Eduardo Malaspina <vaio0@swismail.com>
Co-authored-by: Elias Quispe Chura <ilaies_2012@hotmail.com>
Co-authored-by: Eric <hamburger1024@mailbox.org>
Co-authored-by: Error Specialist <errorspecialist02@gmail.com>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Florin Voicu <florin.bkk@gmail.com>
Co-authored-by: GnuPGを使うべきだ <dieeeazpnnqbpddh@cock.email>
Co-authored-by: Gontzal Manuel Pujana Onaindia <thadahdenyse@gmail.com>
Co-authored-by: Hasan <hasanyildiz0@yaani.com>
Co-authored-by: Hoseok Seo <ddinghoya@gmail.com>
Co-authored-by: Hossain Rizbi <rsajib387@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Nedoboy <i.nedoboy@mail.ru>
Co-authored-by: Igor Rückert <igorruckert@yahoo.com.br>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: JS Ahn <freirepublik@gmail.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Leonardo Brauna <leonardo_brauna@hotmail.com.br>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Maday <royalcoolness7898@gmail.com>
Co-authored-by: Marc Barten <mwbarten@hotmail.com>
Co-authored-by: Marian Hanzel <marulinko@gmail.com>
Co-authored-by: Max Xie <monyxie@gmail.com>
Co-authored-by: MohammedSR Vevo <mohammednajmidin@gmail.com>
Co-authored-by: NTFSynergy <ntfsynergy@gmail.com>
Co-authored-by: Nico Guo <fewis64883@herrain.com>
Co-authored-by: Oymate <dhruboadittya96@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Ray <ray.cfu@protonmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: S3aBreeze <paperwork@evilcorp.ltd>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: SEENUVASAN T <seenuthiruvpm@gmail.com>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.com>
Co-authored-by: Terry Louwers <t.louwers@gmail.com>
Co-authored-by: TiA4f8R <avdivers84@gmail.com>
Co-authored-by: Tom Sawyer <weblate@grymkoll.se>
Co-authored-by: Translator <kvb@tuta.io>
Co-authored-by: Vas R <mrkomododragon1234@gmail.com>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: atilluF <atilluf@outlook.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gonzalo <misctrashy@gmail.com>
Co-authored-by: komiratsu19273240ad76c354986 <2011945@naver.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: pitachips <hjkim3323@gmail.com>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: variable virus <variablevirus@gmail.com>
Co-authored-by: weughgh <ahmedhuntingpro@proton.me>
Co-authored-by: zaioti <zaioti@tuta.io>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: 이정희 <daemul72@gmail.com>
Co-authored-by: 정주찬 <ju1801@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
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/pl/
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/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2022-10-09 19:53:02 +02:00
Tobi
ed4eb124e4 Merge pull request #8734 from Stypox/feed-group-factory
Improve `FeedGroupDialogViewModel` factory
2022-10-09 19:35:47 +02:00
Tobi
4070007c93 Merge pull request #9011 from TacoTheDank/useSimpleSummaryProvider
Utilize useSimpleSummaryProvider attribute
2022-10-09 19:23:26 +02:00
Tobi
5b213a19e4 Merge pull request #8934 from Isira-Seneviratne/LinkifyCompat
Use LinkifyCompat.
2022-10-09 12:07:50 +02:00
Tobi
34d81d3bf2 Merge pull request #8987 from Sandelinos/themed-icons
Add monochrome icon
2022-10-09 11:40:20 +02:00
Tobi
8bc8355b68 Merge pull request #8946 from HybridAU/add_play_queue_button_to_video_details_fragment
Add play queue button to video details fragment
2022-10-06 18:55:50 +02:00
devlearner
ab99c14fd2 Fix crash on screen rotation 2022-10-06 18:15:36 +08:00
devlearner
1047158a66 Fix potential cast exception
when casting to `Spannable` in `CommentTextOnTouchListener`
2022-10-04 17:31:35 +08:00
Stypox
0c63950429 Merge pull request #8889 from TeamNewPipe/release-0.24.0
Release v0.24.0 (990)
2022-09-25 13:56:20 +02:00
Stypox
aa9cd8c88f Update NewPipeExtractor again 2022-09-25 13:33:49 +02:00
Hosted Weblate
3110b08988 Translated using Weblate (Tamil)
Currently translated at 52.6% (337 of 640 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.5% (637 of 640 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Portuguese)

Currently translated at 60.5% (43 of 71 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Tamil)

Currently translated at 52.5% (336 of 640 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Slovak)

Currently translated at 8.4% (6 of 71 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 98.1% (628 of 640 strings)

Translated using Weblate (Galician)

Currently translated at 99.6% (638 of 640 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (German)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Spanish)

Currently translated at 88.7% (63 of 71 strings)

Translated using Weblate (Hindi)

Currently translated at 4.2% (3 of 71 strings)

Translated using Weblate (Portuguese)

Currently translated at 60.5% (43 of 71 strings)

Translated using Weblate (Hindi)

Currently translated at 68.7% (440 of 640 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (640 of 640 strings)

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

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Dutch)

Currently translated at 99.3% (636 of 640 strings)

Translated using Weblate (English)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (71 of 71 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 63.3% (45 of 71 strings)

Translated using Weblate (Swedish)

Currently translated at 47.8% (34 of 71 strings)

Translated using Weblate (French)

Currently translated at 90.1% (64 of 71 strings)

Translated using Weblate (Spanish)

Currently translated at 57.7% (41 of 71 strings)

Translated using Weblate (Polish)

Currently translated at 57.7% (41 of 71 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (71 of 71 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (71 of 71 strings)

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

Currently translated at 15.4% (11 of 71 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (640 of 640 strings)

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

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (French)

Currently translated at 100.0% (640 of 640 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Denys Nykula <vegan@libre.net.ua>
Co-authored-by: Eduardo Malaspina <vaio0@swismail.com>
Co-authored-by: Eric <hamburger1024@mailbox.org>
Co-authored-by: Error Specialist <errorspecialist02@gmail.com>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Hoseok Seo <ddinghoya@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Nedoboy <i.nedoboy@mail.ru>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Leonardo Brauna <leonardo_brauna@hotmail.com.br>
Co-authored-by: Maday <royalcoolness7898@gmail.com>
Co-authored-by: Marc Barten <mwbarten@hotmail.com>
Co-authored-by: Max Xie <monyxie@gmail.com>
Co-authored-by: MohammedSR Vevo <mohammednajmidin@gmail.com>
Co-authored-by: NTFSynergy <ntfsynergy@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Ray <ray.cfu@protonmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: SEENUVASAN T <seenuthiruvpm@gmail.com>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.com>
Co-authored-by: Terry Louwers <t.louwers@gmail.com>
Co-authored-by: TiA4f8R <avdivers84@gmail.com>
Co-authored-by: Tom Sawyer <weblate@grymkoll.se>
Co-authored-by: Translator <kvb@tuta.io>
Co-authored-by: Vas R <mrkomododragon1234@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: atilluF <atilluf@outlook.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: weughgh <ahmedhuntingpro@proton.me>
Co-authored-by: zmni <zmni@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
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/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/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/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2022-09-25 13:31:17 +02:00
TacoTheDank
fe227d5b94 Utilize useSimpleSummaryProvider attribute 2022-09-23 01:46:34 -04:00
Stypox
489f052ef9 Fix feed menu tooltips (silly copy-paste error) 2022-09-19 11:21:42 +02:00
Stypox
8313f6bb51 Update NewPipeExtractor again 2022-09-19 09:17:25 +02:00
Stypox
bf55ed262f Merge pull request #8972 from Stypox/fix-suggestions-listadapter
Fix various issues in the search suggestions list
2022-09-19 08:54:55 +02:00
Stypox
f26bf33ead Merge pull request #8966 from Stypox/feed-menuitem-tooltip
Show correct tooltips for actions in feed
2022-09-19 08:54:06 +02:00
Stypox
ca29f6cc1f Merge pull request #8899 from Stypox/fix-player-thumbnail-handling
Fix wrong thumbnail in notification on Android 13
2022-09-19 08:49:39 +02:00
Sandelinos
cb80891a5f Add monochrome icon 2022-09-17 17:18:15 +03:00
HybridAU
9db0133a5b Add play queue button to video details fragment
* Add play queue button to video details fragment

* Use existing ic_list icon

* Still open play queue even when queue is empty

* Change app:srcCompat to android:src
2022-09-14 21:00:44 +08:00
Stypox
28b34f3796 Fix scroll issues in suggestion list
Before if the list before updating contained item 'test' at position 0 and after updating that value went to the bottom, the list would incorrectly scroll to the bottom to follow that item. Now the scrolling is done after the list is updated.
2022-09-14 14:39:32 +02:00
Stypox
1f57c87859 Disable suggestion list animations: not meaningful
The animations were just in the way and did not help in choosing items, since the suggestion items keep changing too much.
2022-09-14 14:04:22 +02:00
Stypox
fbf5549182 Fix wrong icons being set on suggestion items
The diff util wrongly considered as equal two items with the same text but with different `fromHistory` value
2022-09-14 14:01:59 +02:00
Stypox
051c572e7f Show correct tooltips for actions in feed 2022-09-13 15:26:04 +02:00
Isira Seneviratne
464a646671 Use LinkifyCompat. 2022-09-06 09:27:50 +05:30
Stypox
ed87465565 Only update notification large icon when it changes 2022-08-28 23:14:02 +02:00
Stypox
f9109ebc81 Use player.getThumbnail() instead of field in VideoPlayerUi 2022-08-28 18:35:21 +02:00
Stypox
4a7af6f9ac Remove thumbnail before sync, if outdated
Also refactor onPlaybackSynchronize and add comments
2022-08-28 18:32:27 +02:00
Stypox
7fbef35daa Unify onThumbnailLoaded calls to ensure UIs always updated 2022-08-28 17:24:51 +02:00
Stypox
e6391a860a Release v0.24.0 (990) 2022-08-27 14:52:49 +02:00
Stypox
ebce4c5b7e Add changelog for v0.24.0 (990) 2022-08-27 14:47:57 +02:00
Hosted Weblate
e7e61a0c4c Merge branch 'origin/dev' into Weblate. 2022-08-27 14:06:04 +02:00
Stypox
131f78c0c2 Merge pull request #8731 from Stypox/player-refactor-wrong-video-size
Fix surface view not resizing video correctly + other player fixes
2022-08-27 12:02:27 +02:00
Hosted Weblate
67b5de38b1 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (70 of 70 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (70 of 70 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (70 of 70 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (69 of 69 strings)

Translated using Weblate (Interlingua)

Currently translated at 35.0% (224 of 640 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 62.3% (43 of 69 strings)

Translated using Weblate (Korean)

Currently translated at 10.1% (7 of 69 strings)

Translated using Weblate (French)

Currently translated at 89.8% (62 of 69 strings)

Translated using Weblate (Hebrew)

Currently translated at 55.0% (38 of 69 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (69 of 69 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (69 of 69 strings)

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

Currently translated at 13.0% (9 of 69 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (640 of 640 strings)

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

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 5.8% (4 of 68 strings)

Translated using Weblate (French)

Currently translated at 89.7% (61 of 68 strings)

Translated using Weblate (French)

Currently translated at 89.7% (61 of 68 strings)

Translated using Weblate (Bengali)

Currently translated at 22.0% (15 of 68 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (French)

Currently translated at 73.5% (50 of 68 strings)

Translated using Weblate (Russian)

Currently translated at 30.8% (21 of 68 strings)

Translated using Weblate (German)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Hungarian)

Currently translated at 7.3% (5 of 68 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Polish)

Currently translated at 58.8% (40 of 68 strings)

Translated using Weblate (Bengali)

Currently translated at 88.7% (568 of 640 strings)

Translated using Weblate (Malayalam)

Currently translated at 90.7% (581 of 640 strings)

Translated using Weblate (Interlingua)

Currently translated at 33.5% (215 of 640 strings)

Translated using Weblate (Croatian)

Currently translated at 98.1% (628 of 640 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Hungarian)

Currently translated at 93.9% (601 of 640 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Somali)

Currently translated at 89.0% (570 of 640 strings)

Translated using Weblate (German)

Currently translated at 66.1% (45 of 68 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Armenian)

Currently translated at 29.2% (187 of 640 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Spanish)

Currently translated at 99.5% (637 of 640 strings)

Translated using Weblate (Urdu)

Currently translated at 67.1% (430 of 640 strings)

Translated using Weblate (Croatian)

Currently translated at 97.5% (624 of 640 strings)

Translated using Weblate (Portuguese)

Currently translated at 61.7% (42 of 68 strings)

Translated using Weblate (Russian)

Currently translated at 22.0% (15 of 68 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (French)

Currently translated at 72.0% (49 of 68 strings)

Translated using Weblate (Italian)

Currently translated at 41.1% (28 of 68 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (68 of 68 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.6% (638 of 640 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Bulgarian)

Currently translated at 72.0% (461 of 640 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 64.6% (414 of 640 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (640 of 640 strings)

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

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (French)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (German)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Slovak)

Currently translated at 8.8% (6 of 68 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (French)

Currently translated at 70.5% (48 of 68 strings)

Translated using Weblate (Filipino)

Currently translated at 36.8% (236 of 640 strings)

Translated using Weblate (Bulgarian)

Currently translated at 70.6% (452 of 640 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Turkish)

Currently translated at 32.3% (22 of 68 strings)

Translated using Weblate (Telugu)

Currently translated at 65.6% (420 of 640 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (640 of 640 strings)

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

Currently translated at 97.1% (622 of 640 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (640 of 640 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (635 of 640 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: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: Davit Mayilyan <davit.mayilyan@protonmail.ch>
Co-authored-by: Edward <edwardchirita@mailbox.org>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Evghenii Botnari <botnarievgheniy@gmail.com>
Co-authored-by: Fjuro <fjuro@seznam.cz>
Co-authored-by: Francisco Ruiz <fjrbas@yahoo.es>
Co-authored-by: Giovanni Donisi <giovannidonisi0701@gmail.com>
Co-authored-by: GnuPGを使うべきだ <dieeeazpnnqbpddh@cock.email>
Co-authored-by: Gontzal Manuel Pujana Onaindia <thadahdenyse@gmail.com>
Co-authored-by: Hin Weisner <translatu.godwit@aleeas.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: JScocktail <yarbutt2005@gmail.com>
Co-authored-by: Jalaluddin <ju81@ymail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Josu <bi000@protonmail.com>
Co-authored-by: Karl Tammik <karltammik@protonmail.com>
Co-authored-by: Laura Vasconcelos Pereira Felippe <lauravpf@gmail.com>
Co-authored-by: Lenn Art <gitlab@wunstblog.de>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Louis V <michumadame1@gmail.com>
Co-authored-by: Marian Hanzel <marulinko@gmail.com>
Co-authored-by: MatthieuPh <matthieu.philippe@protonmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mohammed Anas <triallax@tutanota.com>
Co-authored-by: MΛX <maxkorsov@protonmail.com>
Co-authored-by: Nadir Nour <dudethatwascool2@gmail.com>
Co-authored-by: Napstaguy04 <brokenscreen3@gmail.com>
Co-authored-by: Nizami <nizamismidov4@gmail.com>
Co-authored-by: Oymate <dhruboadittya96@gmail.com>
Co-authored-by: Pieter van der Razemond <pietervanderrazemond@mailbox.org>
Co-authored-by: Ray <ray.cfu@protonmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Ricardo <contatorms7@tutamail.com>
Co-authored-by: S3aBreeze <paperwork@evilcorp.ltd>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: STV <steeven.lombardi@gmail.com>
Co-authored-by: Samar Ali <samarali.dev@gmail.com>
Co-authored-by: Santhosh J <santhoshj296@gmail.com>
Co-authored-by: Software In Interlingua <softinterlingua@gmail.com>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.com>
Co-authored-by: Tadeusz Dudek <t--o_o@outlook.com>
Co-authored-by: ThePlanetaryDroid <gmeyq5y0@duck.com>
Co-authored-by: Translator <kvb@tuta.io>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: Viktor <xasertop@gmail.com>
Co-authored-by: WB <dln0@proton.me>
Co-authored-by: Xəyyam Qocayev <xxmn77@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Zoldtukor <emailekhez@gmail.com>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: i-am-SangWoo-Lee <i.am.sangwoo.lee@gmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: rakijagamer-2003 <rakijaisthebest@abv.bg>
Co-authored-by: remon-drk <omerdoruk005@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: subba raidu <raidu4u@gmail.com>
Co-authored-by: yunna <yunna.in@gmail.com>
Co-authored-by: Симеон Цветков <sicvetkov@uni-sofia.bg>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/az/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bn/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bn_BD/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/
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/ko/
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/ru/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2022-08-27 11:53:53 +02:00
Stypox
f9994abb94 Prevent tapping on thumbnail if video details are not loaded 2022-08-26 18:54:51 +02:00
Stypox
ca0f56eea8 Avoid setting invalid states to bottom sheet callback 2022-08-26 18:54:51 +02:00
Stypox
500acce178 Fix regression in screen rotation animation 2022-08-26 18:54:51 +02:00
Stypox
6805c75c9c Fix surface view not resizing video correctly
Also fix yet another random null pointer exception that could happen when adding the video player view
2022-08-26 18:54:51 +02:00
litetex
75917c7f61 Merge pull request #8678 from Stypox/media-session-ui
Create media session UI and fix player notification
2022-08-25 17:18:36 +02:00
litetex
59d1ded94e Fixed sonar detected problems
+ Automatically fixed code style (imports)
2022-08-25 17:02:53 +02:00
Stypox
973a966011 Review suggestions 2022-08-25 17:02:53 +02:00
Stypox
510efaae97 Keep strong reference to Picasso thumbnail loading target
Before the Target would sometimes be garbage collected before being called with the loaded thumbnail, since Picasso holds weak references to targets
2022-08-25 17:02:52 +02:00
Stypox
11bd2369e5 Merge MediaSessionManager into MediaSessionPlayerUi 2022-08-25 17:02:52 +02:00
Stypox
f80d1dc48d Let exoplayer decide when to update metadata
Though still make sure metadata is updated after the thumbnail is loaded.
This fixes the wrong seekbar properties (duration and current position) being shown in the notification sometimes.
2022-08-25 17:02:52 +02:00
Stypox
8bff445ec3 Remove useless checks before updating metadata
A while ago NewPipe called the metadata update function very often, so checks were needed to ensure not wasting time updating metadata if it were already up to date. Now, instead, the metadata update function is called exactly when needed, i.e. when metadata changes, so such checks are not needed anymore (and were probably also a little resource-heavy).
2022-08-25 17:02:51 +02:00
Stypox
d73ca41cfe Even when thumbnails should not be shown, set it to null in notification
This makes sure the thumbnail is removed from the notification if the user disables thumbnails
2022-08-25 17:02:51 +02:00
Stypox
f3a9b81b67 Fix sometimes seeing outdated thumbnail in notification
Before the thumbnail finishes loading for the new video the player is now playing, the old thumbnail was being used, leading to wrong thumbnails set in the media session and the notification.
2022-08-25 17:02:51 +02:00
Stypox
3cc43e9fb9 Fix thumbnail sometimes not set to media session metadata
The thumbnail was not being updated in the media session metadata after it was loaded, since there was no metadata update in that case, only a notification update.
2022-08-25 17:00:41 +02:00
Stypox
bc33322d4b Remove useless MediaSessionCallback
The player is now passed directly, it made no sense to wrap around it in a callback that was not really a callback but rather, actually, a wrapper.
2022-08-25 17:00:41 +02:00
Stypox
c054ea0737 Create MediaSessionPlayerUi 2022-08-25 17:00:41 +02:00
litetex
ce6f3ca5df Merge pull request #8677 from Stypox/fix-picasso-target
Keep strong references to Picasso notification icon loading targets
2022-08-25 16:55:31 +02:00
Stypox
52dbfdee00 Keep strong references to Picasso notification icon loading targets
Before the Target would sometimes be garbage collected before being called with the loaded channel icon, since Picasso holds weak references to targets. This meant that sometimes a new streams notification  would not be shown, because the lambda that should have shown it had already been garbage collected.
2022-08-25 16:41:51 +02:00
litetex
1e964a36a9 Merge pull request #8754 from mhmdanas/clarify-that-span-shouldnt-be-in-translated-readme
Clarify that span shouldn't be in translated READMEs
2022-08-25 16:27:16 +02:00
litetex
679e81e091 Merge pull request #8755 from mhmdanas/remove-whitespace
Remove extra whitespace from issue and PR templates
2022-08-25 16:26:31 +02:00
litetex
2e3e4f5a84 Merge pull request #8751 from TacoTheDank/bumpGradle
Update Gradle to 7.5.1
2022-08-25 16:25:32 +02:00
Stypox
208dde631f Merge branch 'master' into dev 2022-08-25 10:45:24 +02:00
opusforlife2
a50b9bd6ff Add FAQ entry to the template checklists (#8822)
Co-authored-by: Mohammed Anas <triallax@tutanota.com>
2022-08-21 20:39:57 +03:00
Taco
2089f3e54c Merge pull request #8719 from Isira-Seneviratne/Use_ListAdapter
Use ListAdapter for search predictions.
2022-08-19 15:50:41 -04:00
Isira Seneviratne
5e0788b99c Use ListAdapter in PreferenceSearchAdapter. 2022-08-18 19:52:51 +05:30
Isira Seneviratne
67669c286b Use ListAdapter in SuggestionListAdapter. 2022-08-18 19:52:51 +05:30
Isira Seneviratne
408a71cfdc Calculate search score using streams.
Co-authored-by: Stypox <stypox@pm.me>
2022-08-15 07:26:56 +05:30
Isira Seneviratne
6399e39507 Remove from playlist only upon selecting the option and not afterwards. 2022-08-15 07:26:36 +05:30
Isira Seneviratne
f9443f7421 Refactor removeWatchedStreams() in LocalPlaylistFragment. 2022-08-15 07:26:36 +05:30
Isira Seneviratne
4f6b5b3b89 Use ListAdapter in PeertubeInstanceListFragment. 2022-08-15 07:26:02 +05:30
Stypox
b9b09d325a Merge branch 'master' into dev 2022-08-14 17:23:30 +02:00
Isira Seneviratne
697b8411df Use Okio's ByteString. 2022-08-13 08:27:55 +05:30
Stypox
571bf397c5 Merge pull request #8666 from TacoTheDank/filepicker
Update FilePicker to our custom fork and disable Jetifier
2022-08-12 09:15:34 +02:00
mhmdanas
737a331c85 Remove extra whitespace from issue and PR templates 2022-08-11 19:34:23 +03:00
mhmdanas
2de33d8d07 Clarify that span shouldn't be in translated READMEs 2022-08-11 19:24:14 +03:00
TacoTheDank
7f21f6e80e Update AGP and clojars Maven URL 2022-08-09 19:19:17 -04:00
TacoTheDank
0b11afaf2f Update Gradle to 7.5.1 2022-08-08 19:32:21 -04:00
Isira Seneviratne
e136a6f915 Use range-limiting methods in more places. 2022-08-08 07:10:16 +05:30
Stypox
74921d3afa Merge pull request #8740 from Isira-Seneviratne/Cleanup_methods
Remove some unused methods.
2022-08-06 22:45:40 +02:00
Stypox
edd2b110b0 Merge pull request #8738 from Isira-Seneviratne/Collectors_joining
Use Collectors.joining().
2022-08-06 22:31:31 +02:00
Stypox
80fb21e031 Merge pull request #8728 from Isira-Seneviratne/Comparator_factory
Use Comparator factory methods.
2022-08-06 11:56:44 +02:00
Stypox
ebd06bdd24 Improve comment 2022-08-06 11:56:00 +02:00
Stypox
6f86e21605 Merge pull request #8724 from Isira-Seneviratne/toArray_improvements
Use toArray() with zero-length arrays.
2022-08-06 11:33:05 +02:00
Stypox
816154c7cb Merge pull request #8737 from Isira-Seneviratne/Fix_coerceIn
Replace coerceIn() with MathUtils.clamp().
2022-08-06 11:16:06 +02:00
Stypox
d9230c0103 Merge pull request #8708 from Isira-Seneviratne/Reduce_View.kt_size
Reduce View.kt size.
2022-08-06 10:59:10 +02:00
Isira Seneviratne
5c7dfd1d69 Remove unused method. 2022-08-06 06:54:21 +05:30
Isira Seneviratne
7aacaf8c38 Use Collectors.joining(). 2022-08-06 06:54:21 +05:30
Isira Seneviratne
ee6a279596 Remove unused methods in HistoryRecordManager. 2022-08-06 05:09:54 +05:30
Isira Seneviratne
a9af1dfdd2 Applied code review changes. 2022-08-05 06:54:03 +05:30
Isira Seneviratne
fc46233baf Use toArray() with zero-length arrays. 2022-08-05 06:50:55 +05:30
Isira Seneviratne
2eec2e9128 Replace coerceIn() with MathUtils.clamp(). 2022-08-05 06:19:06 +05:30
Isira Seneviratne
8024b437e9 Add reusable classes extending AnimatorListenerAdapter. 2022-08-05 06:08:13 +05:30
Isira Seneviratne
d1f3f15478 Use Comparator.comparingDouble(). 2022-08-05 05:36:39 +05:30
Isira Seneviratne
059cfcbad2 Use Comparator factory methods in ListHelper. 2022-08-05 05:36:21 +05:30
Stypox
1a8f396e77 Merge pull request #8721 from Isira-Seneviratne/OnClickGesture_interface
Make OnClickGesture an interface.
2022-08-04 16:15:54 +02:00
Stypox
5640365fbd Merge pull request #8682 from Isira-Seneviratne/Refactor_LicenseFragmentHelper
Refactor LicenseFragmentHelper.
2022-08-04 11:18:40 +02:00
Isira Seneviratne
4b7de86a92 Clean up getLicenseStylesheet(). 2022-08-04 11:17:30 +02:00
Stypox
24ec642181 Merge pull request #8669 from Isira-Seneviratne/Remove_setBottomSheetCallback
Remove uses of setBottomSheetCallback().
2022-08-04 11:03:57 +02:00
Stypox
8dce66d76f Improve FeedGroupDialogViewModel factory 2022-08-04 10:49:33 +02:00
Stypox
22d75f3ecb Merge pull request #8716 from TacoTheDank/bumpRoom
Update AndroidX Room to 2.4.3
2022-08-04 10:40:26 +02:00
Stypox
7972678fe6 Merge branch 'dev' into bumpRoom 2022-08-04 10:40:09 +02:00
Stypox
ffc1d9a212 Merge pull request #8656 from Isira-Seneviratne/Use_WindowMetrics
Use WindowMetrics API.
2022-08-04 10:12:32 +02:00
Isira Seneviratne
7f018b90db Merge branch 'dev' into OnClickGesture_interface 2022-08-04 06:10:39 +05:30
Stypox
8a774dc90d Merge pull request #8667 from Isira-Seneviratne/Update_AppCompat
Update AppCompat to 1.4.2.
2022-08-03 22:59:45 +02:00
Stypox
368c6c0ccb Merge pull request #8709 from Isira-Seneviratne/Tags_case_insensitive
Sort tags case-insensitively.
2022-08-03 22:43:50 +02:00
Stypox
5c4874b90f Merge pull request #8701 from Isira-Seneviratne/Use_stackTraceToString
Use Throwable.stackTraceToString().
2022-08-03 21:04:22 +02:00
Stypox
3420faab08 Merge pull request #8661 from Stypox/player-refactor-npe
Fix random NullPointerException when adding video player view
2022-08-02 11:09:50 +02:00
Stypox
a548b34811 Merge pull request #8692 from TacoTheDank/bumpMaterial
Update Google Material to 1.6.1
2022-08-02 11:05:24 +02:00
Stypox
56cbf3736b Merge pull request #8691 from TacoTheDank/bumpFragment
Update AndroidX Fragment to 1.4.1
2022-08-02 10:54:52 +02:00
Stypox
ad30eb809c Merge branch 'dev' into bumpFragment 2022-08-02 10:54:39 +02:00
Stypox
ee368452ae Merge pull request #8687 from TacoTheDank/bumpExoPlayer
Update ExoPlayer to 2.18.1
2022-08-02 10:34:58 +02:00
Isira Seneviratne
a9095ca2ad Make block parameter an extension lambda. 2022-08-01 08:29:59 +05:30
Isira Seneviratne
013522c376 Convert LicenseFragmentHelper methods to top-level declarations. 2022-08-01 08:27:09 +05:30
Isira Seneviratne
947242d913 Update AppCompat to 1.4.2. 2022-08-01 08:26:07 +05:30
Isira Seneviratne
8a896114c1 Apply code review change. 2022-08-01 08:25:24 +05:30
Isira Seneviratne
47f58040d1 Make OnClickGesture an interface. 2022-08-01 06:47:00 +05:30
Stypox
35a118a2a7 Merge pull request #8683 from Isira-Seneviratne/Update_Lifecycle
Update Lifecycle to 2.5.1.
2022-07-31 09:49:36 +02:00
TacoTheDank
582032f372 Update AndroidX Room to 2.4.3 2022-07-31 00:14:23 -04:00
Isira Seneviratne
311d392386 Use Application instead of Context in FeedViewModel. 2022-07-31 08:37:16 +05:30
Stypox
404c13d4c1 Improve FeedViewModel factory 2022-07-31 08:30:17 +05:30
Isira Seneviratne
5c68c8ece8 Update Lifecycle to 2.5.1. 2022-07-31 08:30:17 +05:30
Isira Seneviratne
4d7a6fb6de Use WindowMetrics API in VideoDetailFragment and PopupPlayerUi. 2022-07-30 19:22:39 +05:30
Isira Seneviratne
630558ed4f Use nested functions. 2022-07-30 07:59:36 +05:30
Isira Seneviratne
69942003f7 Sort tags case-insensitively. 2022-07-29 09:21:48 +05:30
Isira Seneviratne
af9c2bd59d Use stackTraceToString(). 2022-07-27 07:54:49 +05:30
Mohammed Anas
81c4b822e0 Add "needs triage" label to issue templates (#8643)
This label would make it easier for issue triagers to know what they
haven't triaged yet.
2022-07-26 23:29:43 +03:00
Isira Seneviratne
81fb44c45c Remove uses of setBottomSheetCallback(). 2022-07-25 18:44:30 +05:30
TacoTheDank
d66997c2ed Update Google Material to 1.6.1 2022-07-24 16:51:26 -04:00
TacoTheDank
d7a654fc27 Update AndroidX Fragment to 1.4.1 2022-07-24 15:35:33 -04:00
TacoTheDank
229422bfa9 Update ExoPlayer to 2.18.1 2022-07-24 14:11:31 -04:00
TacoTheDank
baabba1dea Disable Jetifier 2022-07-24 15:07:31 +02:00
TacoTheDank
8f5d564f84 Migrate NoNonsense-FilePicker to our updated fork 2022-07-24 15:07:31 +02:00
litetex
dcb332e08d Merge pull request #8624 from TacoTheDank/bumpOkhttp
Update OkHttp to 4.10.0
2022-07-24 15:02:08 +02:00
litetex
51e72d1a05 Removed the "(beta)"-tag from services (#8637) 2022-07-24 15:57:23 +03:00
litetex
8f37015dbb Merge pull request #8621 from Stypox/deduplicate-feed
Deduplicate SQL queries to get feed streams
2022-07-24 14:52:35 +02:00
Stypox
74df7fcd66 Merge pull request #8670 from Isira-Seneviratne/Update_FocusAwareCoordinator
Remove deprecated method calls in FocusAwareCoordinator.
2022-07-23 17:12:29 +02:00
Stypox
bfaf074f4e Merge pull request #8663 from Isira-Seneviratne/Remove_unnecessary_methods
Remove unnecessary methods.
2022-07-23 16:31:28 +02:00
Stypox
3281ed2ef1 Merge pull request #8648 from Isira-Seneviratne/Use_IO_extensions
Use IO extensions.
2022-07-22 18:30:51 +02:00
Stypox
b2c2570a85 Merge pull request #8676 from Stypox/fix-channel-placeholders
Fix wrong thumbnail used as placeholder for channel
2022-07-22 18:16:42 +02:00
Stypox
abf185c691 Merge pull request #8679 from Stypox/fix-listhelpertest
Fix ListHelperTest failure caused by immutable list being used
2022-07-22 18:15:39 +02:00
Stypox
f4fe5fcb16 Fix ListHelperTest failure caused by immutable list being used 2022-07-22 16:09:43 +02:00
Stypox
37275e8fe3 Fix wrong thumbnail used as placeholder for channel 2022-07-22 15:13:47 +02:00
Isira Seneviratne
f1dab11f1f Remove deprecated method calls in FocusAwareCoordinator. 2022-07-21 09:01:19 +05:30
Isira Seneviratne
6d1c61407d Remove unnecessary method in ChannelFragment. 2022-07-21 08:02:23 +05:30
Isira Seneviratne
8b400b48f7 Refactor notifying method in PlayQueue. 2022-07-21 08:02:23 +05:30
Isira Seneviratne
b845645b80 Use IO extensions.
Co-authored-by: Stypox <stypox@pm.me>
2022-07-21 05:15:39 +05:30
Stypox
cacce6d2d0 Merge pull request #8651 from Isira-Seneviratne/Use_limiting_methods
Use range-limiting methods.
2022-07-20 15:06:45 +02:00
Stypox
373ee53143 Improve code style 2022-07-20 15:05:25 +02:00
Stypox
344c33d9a1 Merge pull request #8631 from Isira-Seneviratne/Use_collection_factories
Use Java 9 collection factories.
2022-07-20 14:52:18 +02:00
Stypox
c5b970cca3 Improve code style in List.of() 2022-07-20 14:50:23 +02:00
Stypox
15947161e6 Merge pull request #8635 from Isira-Seneviratne/Use_stream_sort
Use stream sorting.
2022-07-20 11:06:56 +02:00
Isira Seneviratne
394eb92e71 Use coerceIn(). 2022-07-20 05:36:01 +05:30
Isira Seneviratne
d62cdc659f Use MathUtils.clamp().
Co-authored-by: Stypox <stypox@pm.me>
2022-07-20 05:36:01 +05:30
Isira Seneviratne
a6cc13845a Use Map.of(). 2022-07-20 04:39:11 +05:30
Isira Seneviratne
55a995c4cd Replace LinkedHashMap with List.of(). 2022-07-20 04:39:11 +05:30
Isira Seneviratne
ca26fcb0eb Use List.of(). 2022-07-20 04:39:11 +05:30
Stypox
4eddd2c3d1 Fix random NullPointerException when adding video player view 2022-07-19 20:01:46 +02:00
Isira Seneviratne
c53143ef4f Use Set.of(). 2022-07-19 08:53:15 +05:30
Isira Seneviratne
e772244440 Update app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java
Co-authored-by: Stypox <stypox@pm.me>
2022-07-19 05:13:38 +05:30
Stypox
ae369ec9ba Merge pull request #8475 from TacoTheDank/bumpMiscLibraries
Update some misc libraries
2022-07-18 23:56:41 +02:00
Stypox
e8669d4ab5 Deduplicate SQL queries to get feed streams 2022-07-18 23:39:57 +02:00
Stypox
cd14096dbe Merge pull request #8633 from Isira-Seneviratne/Use_ViewCompat_setBackgroundTintList
Use ViewCompat.setBackgroundTintList().
2022-07-18 23:31:56 +02:00
Stypox
d9ff114e1a Merge pull request #8601 from litetex/checkstyle-assign-on-same-line
Checkstyle assign on same line
2022-07-18 16:39:02 +02:00
Mohammed Anas
a1c6f0073e Rename "waiting-for-author" label to "waiting for author" (#8642) 2022-07-17 20:37:15 +03:00
Isira Seneviratne
f1de353b74 Use stream sorting. 2022-07-16 08:34:04 +05:30
Isira Seneviratne
5da8d5fc73 Use ViewCompat.setBackgroundTintList(). 2022-07-16 05:49:52 +05:30
litetex
3ba04f179f Fixed conflicts/build 2022-07-15 20:00:08 +02:00
litetex
3890d0abdb Added note that explains that unused code was removed. 2022-07-15 19:55:19 +02:00
litetex
8b209df253 Changed the code accordingly
+ Removed some unused code
2022-07-15 19:55:19 +02:00
litetex
25a43b57b2 Updated checkstyle
So that the assign operators are used on the same branch
2022-07-15 19:54:32 +02:00
litetex
b7a44560f5 Merge pull request #8170 from Stypox/player-refactor
Refactor player and extract UI components
2022-07-15 19:41:23 +02:00
Stypox
0e8cc72b13 Fix random NullPointerException when adding video player view 2022-07-14 22:14:03 +02:00
Stypox
33e20766c9 Merge pull request #8530 from krlvm/improve_placeholder_images
Improve image placeholders
2022-07-14 15:55:36 +02:00
Stypox
9f993e0c49 Make video and playlist placeholder thumbnails 16:9
After making the playlist and video thumbnails' scaleType fitCenter, the 24dp*24dp thumbnails would appear as a square, which would be strange, since the image view is 16:9.
2022-07-14 14:47:54 +02:00
Stypox
6ea85e6380 Rename dummy_* and more to placeholder_* 2022-07-14 14:27:33 +02:00
Stypox
4d58026d06 Improve placeholder thumbnail SVGs and remove theme customization
Theme customization does not seem to work well with Picasso: square/picasso#1275
2022-07-14 14:14:33 +02:00
Stypox
7b9b9218dc Remove bottom-sheet-thumbnail placeholder, clear the image instead 2022-07-14 14:14:33 +02:00
krlvm
dff1adb1ad Fix swapped colors in video and playlist thumbnails 2022-07-14 14:14:32 +02:00
krlvm
35eeccd45a Rename buddy.xml to dummy_person.xml 2022-07-14 14:14:32 +02:00
krlvm
429f2536af Optimize thumbnail placeholder drawables 2022-07-14 14:14:32 +02:00
krlvm
7b41acb781 Use corresponding material icon in user profile thumbnail 2022-07-14 14:14:32 +02:00
krlvm
cc7a8fb1a6 Improve image placeholders
- Show placeholders until the image is loaded because timeout can be very long and missing profile pictures and video thumbnails make app inconvenient to use

- Adapt profile picture and video thumbnail placeholders to light theme

- Replace profile picture and video thumbnail placeholders with vector graphics
2022-07-14 14:14:32 +02:00
TacoTheDank
c1e78cf55b Update OkHttp to 4.x 2022-07-14 03:23:45 -04:00
TacoTheDank
4536e8b55b Update some miscellaneous libraries 2022-07-14 01:48:52 -04:00
Stypox
70e3c9805a Merge pull request #8542 from carmebar/share-playlist
Add 'Share playlist' option to Playlist fragment
2022-07-13 23:45:08 +02:00
Stypox
8187a3bc04 Move PlayerType into its own class and add documentation
Also replace some `isPlayerOpen` with direct `playerType == null` checks.
2022-07-13 23:33:18 +02:00
Stypox
4443c908cb Fix SonarLint java:S5320, restrict broadcasts to app package 2022-07-13 23:33:18 +02:00
Stypox
c03eac1dc9 Some SonarLint refactors 2022-07-13 23:33:18 +02:00
Stypox
61c1da144e Some refactorings after review comments 2022-07-13 23:33:18 +02:00
Stypox
3692858a3d Move popup layout param to PopupPlayerUi 2022-07-13 23:33:18 +02:00
Stypox
9c51fc3ade Move functions to get Android dimen to ThemeHelper 2022-07-13 23:33:18 +02:00
Stypox
1cf746f721 Fix volume gestures not working anymore 2022-07-13 23:33:18 +02:00
Stypox
4979f84e41 Solve some Sonarlint warnings 2022-07-13 23:33:16 +02:00
Stypox
a19073ec01 Restore checkstyle and solve its errors 2022-07-13 23:32:27 +02:00
Stypox
1b39b5376f Add some javadocs; move preparing player uis to PlayerUiList 2022-07-13 23:31:59 +02:00
Stypox
6559416bd8 Improve //region comments in player UIs 2022-07-13 23:30:30 +02:00
Stypox
fa25ecf521 Add comment about broadcast receiver 2022-07-13 23:27:24 +02:00
Stypox
6fb0256997 Remove unused PlayerServiceBinder 2022-07-13 23:27:24 +02:00
Stypox
8c26403e91 Remove unused PlayerState 2022-07-13 23:27:24 +02:00
Stypox
90a89f8ca5 Move player-notification files into their package 2022-07-13 23:27:24 +02:00
Stypox
0bba1d95de Move all notification-related calls to NotificationPlayerUi 2022-07-13 23:27:24 +02:00
Stypox
b3f99645a3 Fix some crashes / issues after player refactor 2022-07-13 23:27:23 +02:00
Stypox
76ced59b62 Refactor player: separate UIs and more 2022-07-13 23:25:26 +02:00
Stypox
bc3731265e Merge pull request #7613 from litetex/increase-minsdk
Bump minSdk to 21 - Android 5 / Lollipop
2022-07-13 19:09:05 +02:00
TacoTheDank
189c92affa More minSdk 21 cleanup 2022-07-13 19:03:47 +02:00
TacoTheDank
4ec9cbe379 Remove AndroidX Webkit 2022-07-13 19:03:47 +02:00
litetex
9648525ac1 Clean up pre-Lollipop theming 2022-07-13 19:03:47 +02:00
litetex
b125780991 Clean up pre-Lollipop compat attributes 2022-07-13 19:03:45 +02:00
litetex
99104fc11d Clean up pre-Lollipop checks 2022-07-13 19:02:24 +02:00
litetex
7cb137ae8d Remove MultiDex 2022-07-13 19:02:24 +02:00
litetex
e55e79bcca Bump minSdk to 21 (Android 5 / Lollipop) 2022-07-13 19:02:23 +02:00
Stypox
0b644fd794 Merge pull request #8569 from mhmdanas/add-workflow-permissions
Use minimum required permissions for GitHub workflows
2022-07-13 18:56:10 +02:00
Stypox
d5599ebfa3 Merge pull request #8573 from Stypox/better-thumbnails
Make thumbnails' `scaleType` `fitCenter`
2022-07-13 18:33:19 +02:00
Taco
f7d8781bac Specify used ExoPlayer libraries (#8469) 2022-07-13 17:57:14 +02:00
Alex
6f7298b9db Crop the notification thumbnail in 1:1 mode instead of stretching it (#8533)
Change square bitmap transformation strategy: change the bitmap transformation strategy when a 1:1 aspect ratio is
enabled to not stretch the bitmap but rather crop it.

On Android 11/12, the way the whole thumbnail was used for the
notification icon was not ideal, however the setting to toggle a 1:1
(as it states in settings) resulted in distortions.

Fix this by simply cropping the bitmap.

Also update the 1:1 mode strings to remove mentions of scaling or
distortions, as those no longer apply.
2022-07-13 17:19:44 +02:00
Carlos Melero
d0b6d95f1b Add Share option to local playlists
A newline-separated text is shared
2022-07-13 14:32:23 +02:00
Robin
93b913e14d Merge pull request #8536 from TacoTheDank/bumpExoPlayer
Update ExoPlayer to 2.18.0
2022-07-13 12:59:12 +02:00
Stypox
b96c8a0c2f Merge pull request #8545 from carmebar/hide-future-videos
Add option to hide future videos in feed
2022-07-13 11:07:38 +02:00
Stypox
a392a06cc0 Fix feed menu items order in category to leave space for search
The search menu item gets added in first place when the feed fragment is added as a tab to the main fragment. So the main fragment's menu items' orderInCategory should start from 2.
2022-07-13 11:04:40 +02:00
Stypox
d9af788514 Merge pull request #8397 from notaLonelyDay/add-download-to-longpress-menu
Add download to longpress menu
2022-07-06 11:47:50 +02:00
nikita.artikhovich
a4724fec4a Add download option to long-press menu 2022-07-06 11:42:57 +02:00
Stypox
0e5580390f Merge pull request #8468 from TacoTheDank/cleanProguard
Clean up proguard file
2022-07-06 11:18:03 +02:00
Stypox
acc34cb618 Merge pull request #8549 from chr56/langcode_zh-rCN
[Localization] Fix lang code for Chinese Simplified (again!)
2022-07-05 23:03:31 +02:00
chr_56
d033a6e40d Fix lang code 2/2: rename b+zh+HANS+CN to zh-rCN 2022-07-05 22:55:29 +02:00
chr_56
4fd8294b09 Fix lang code 1/2: remove localization zh-rCN 2022-07-05 22:55:29 +02:00
Hosted Weblate
8d26d9da46 Translated using Weblate (Greek)
Currently translated at 100.0% (640 of 640 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Andrij Mizyk <andmiz@proton.me>
Co-authored-by: D āvis <dlektauers@gmail.com>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: David Braz <davidbrazps2@gmail.com>
Co-authored-by: Davit Mayilyan <davit.mayilyan@protonmail.ch>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Erik J <ej_rostock@web.de>
Co-authored-by: Giovanni Donisi <giovannidonisi0701@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Julie WF <julie-99@live.no>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Nizami <nizamismidov4@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Ray <ray.cfu@protonmail.com>
Co-authored-by: ReVanced <revanced2022@gmail.com>
Co-authored-by: Retrial <giwrgosmant@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: S3aBreeze <paperwork@evilcorp.ltd>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: SameenAhnaf <sameenahnaf@yahoo.com>
Co-authored-by: Stypox <stypox@pm.me>
Co-authored-by: Translator <kvb@tuta.io>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Yeldar Kudaibergenov <mail@yeldar.org>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: patrik <jpekman@gmail.com>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: riveravaldez <riveravaldezmail@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: zica <9918800@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/az/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bn/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/kk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2022-07-05 22:53:06 +02:00
Stypox
d81607c9d5 Merge branch 'master' into dev 2022-07-05 16:10:27 +02:00
Stypox
81369d7e04 Merge pull request #8531 from litetex/fix-extractor-compile-errors-846
Updated extractor to latest revision and fix compile errors
2022-07-04 23:16:55 +02:00
Stypox
70b20f90cd Align playlist thumbnail to left for more visibility 2022-07-04 22:37:27 +02:00
Stypox
47a2adca96 Make thumbnails' scaleType fitCenter
Otherwise e.g. shorts thumbnails would be cropped too much
2022-07-04 21:43:31 +02:00
mhmdanas
a1f1acfbf9 Use minimum required permissions for GitHub workflows
This reduces the attack surface if the workflows are ever compromised.
2022-07-03 20:38:51 +03:00
TacoTheDank
00b9c082a3 Set setUsePlatformDiagnostics to false 2022-07-02 15:01:39 -04:00
Stypox
45d2492bcb Run CI on all release branches (#8565)
We don't seem to agree on which character to use as a separator between `release` and `X.X.X` in release branch names (e.g. `release-0.23.1` or `release/0.23.0`). All those names start with `release`, though, so let's use that to identify releases.
2022-07-02 00:08:44 +03:00
TacoTheDank
1404581e9b Update ExoPlayer to 2.18.0 2022-06-25 21:14:42 -04:00
opusforlife2
d5985be94a Made some much needed changes to the ReadMe (#8372)
Co-authored-by: Mohammed Anas <triallax@tutanota.com>
Co-authored-by: Poolitzer <github@poolitzer.eu>
2022-06-26 01:13:54 +03:00
Carlos Melero
f22417e7e7 Add option to hide future videos in feed 2022-06-24 18:03:48 +02:00
litetex
ad97b3d995 Use correct `NonNull` 2022-06-22 19:54:46 +02:00
litetex
04e8e03d8f Fix compile errors 2022-06-22 19:47:03 +02:00
litetex
bd19013771 Updated extractor to latest revision 2022-06-22 19:47:02 +02:00
TacoTheDank
24cf19710f Clean up proguard file 2022-06-09 11:34:57 -04:00
579 changed files with 13393 additions and 9938 deletions

View File

@@ -22,6 +22,7 @@ You'll see *exactly* what is sent, be able to add **your comments**, and then se
* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). Log in there with your GitHub account, or register.
* Add the language you want to translate if it is not there already: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki.
* NewPipe uses the [PrettyTime](https://github.com/ocpsoft/prettytime) library to display localized versions of dates and times. It needs to be translated, too. Read [these instructions to add a new language](https://www.ocpsoft.org/prettytime/#section-14) and [this issue](https://github.com/TeamNewPipe/NewPipe/issues/9134) for more info.
## Code contribution

View File

@@ -1,6 +1,6 @@
name: Bug report
description: Create a bug report to help us improve
labels: [bug]
labels: [bug, needs triage]
body:
- type: markdown
attributes:
@@ -18,6 +18,8 @@ body:
required: true
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my problem isn't listed."
required: true
- label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise."
required: true
- label: "This issue contains only one bug."
@@ -40,7 +42,7 @@ body:
label: Steps to reproduce the bug
description: |
What did you do for the bug to show up?
If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug.
placeholder: |
1. Go to '...'
@@ -69,11 +71,11 @@ body:
label: Screenshots/Screen recordings
description: |
A picture or video is worth a thousand words.
If applicable, add screenshots or a screen recording to help explain your problem.
GitHub supports uploading them directly in the text box.
If your file is too big for Github to accept, try to compress it (ZIP-file) or feel free to paste a link to an image/video hoster here instead.
:heavy_exclamation_mark: DON'T POST SCREENSHOTS OF THE ERROR PAGE.
Instead, follow the instructions in the "Logs" section below.

View File

@@ -1,6 +1,6 @@
name: Feature request
description: Suggest an idea for this project
labels: [enhancement]
labels: [feature request, needs triage]
body:
- type: markdown
attributes:
@@ -8,7 +8,6 @@ body:
Thank you for helping to make NewPipe better by suggesting a feature. :hugs:
Your ideas are highly welcome! The app is made for you, the users, after all.
- type: checkboxes
id: checklist
attributes:
@@ -16,6 +15,8 @@ body:
options:
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my problem isn't listed."
required: true
- label: "I'm aware that this is a request for NewPipe itself and that requests for adding a new service need to be made at [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor/issues)."
required: true
- label: "I have taken the time to fill in all the required details. I understand that the feature request will be dismissed otherwise."
@@ -43,7 +44,7 @@ body:
Describe any problem or limitation you come across while using the app which would be solved by this feature.
validations:
required: true
- type: textarea
id: additional-information
attributes:

View File

@@ -1,6 +1,6 @@
name: Question
description: Ask about anything NewPipe-related
labels: [question]
labels: [question, needs triage]
body:
- type: markdown
attributes:
@@ -16,6 +16,8 @@ body:
options:
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my question isn't listed."
required: true
- label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise."
required: true
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
@@ -27,7 +29,7 @@ body:
label: What is/are your question(s)?
validations:
required: true
- type: textarea
id: additional-information
attributes:

View File

@@ -25,7 +25,7 @@
<!-- Delete this if it doesn't apply to your PR. -->
-
#### APK testing
#### APK testing
<!-- Use a new, meaningfully named branch. The name is used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe, e.g. "commentfix", if your PR implements a bugfix for comments. (No names like "patch-0" and "feature-1".) -->
<!-- Remove the following line if you directly link the APK created by the CI pipeline. Directly linking is preferred if you need to let users test.-->
The APK can be found by going to the "Checks" tab below the title. On the left pane, click on "CI", scroll down to "artifacts" and click "app" to download the zip file which contains the debug APK of this PR.

View File

@@ -6,7 +6,7 @@ on:
branches:
- dev
- master
- release/**
- release**
paths-ignore:
- 'README.md'
- 'doc/**'
@@ -31,6 +31,10 @@ on:
jobs:
build-and-test-jvm:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
@@ -64,6 +68,10 @@ jobs:
matrix:
# api-level 19 is min sdk, but throws errors related to desugaring
api-level: [ 21, 29 ]
permissions:
contents: read
steps:
- uses: actions/checkout@v3
@@ -81,7 +89,7 @@ jobs:
# workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
emulator-build: 7425822
script: ./gradlew connectedCheck --stacktrace
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
uses: actions/upload-artifact@v3
if: failure()
@@ -91,6 +99,10 @@ jobs:
sonar:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v3
with:

View File

@@ -6,6 +6,10 @@ on:
issues:
types: [opened, edited]
permissions:
issues: write
pull-requests: write
jobs:
try-minimize:
runs-on: ubuntu-latest

View File

@@ -9,6 +9,10 @@ on:
# Run daily at midnight.
- cron: '0 0 * * *'
permissions:
issues: write
pull-requests: write
jobs:
noResponse:
runs-on: ubuntu-latest
@@ -17,4 +21,4 @@ jobs:
with:
token: ${{ github.token }}
daysUntilClose: 14
responseRequiredLabel: waiting-for-author
responseRequiredLabel: waiting for author

124
README.md
View File

@@ -1,6 +1,6 @@
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
<h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">A libre lightweight streaming frontend for Android.</h4>
<h4 align="center">A libre lightweight streaming front-end for Android.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-en.svg" alt="Get it on F-Droid" height=80/></a></p>
@@ -13,15 +13,15 @@
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#installation-and-updates">Installation and updates</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#supported-services">Supported Services</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#installation-and-updates">Installation and updates</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p>
<p align="center"><a href="https://newpipe.net">Website</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <a href="https://newpipe.net/press/">Press</a></p>
<hr>
*Read this in other languages: [English](README.md), [Español](doc/README.es.md), [हिन्दी](doc/README.hi.md), [한국어](doc/README.ko.md), [Soomaali](doc/README.so.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md).*
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
<b>WARNING: THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
<b>PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
## Screenshots
@@ -38,62 +38,66 @@
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
### Supported Services
NewPipe currently supports these services:
<!-- We link to the service websites separately to avoid people accidentally opening a website they didn't want to. -->
* YouTube ([website](https://www.youtube.com/)) and YouTube Music ([website](https://music.youtube.com/)) ([wiki](https://en.wikipedia.org/wiki/YouTube))
* PeerTube ([website](https://joinpeertube.org/)) and all its instances (open the website to know what that means!) ([wiki](https://en.wikipedia.org/wiki/PeerTube))
* Bandcamp ([website](https://bandcamp.com/)) ([wiki](https://en.wikipedia.org/wiki/Bandcamp))
* SoundCloud ([website](https://soundcloud.com/)) ([wiki](https://en.wikipedia.org/wiki/SoundCloud))
* media.ccc.de ([website](https://media.ccc.de/)) ([wiki](https://en.wikipedia.org/wiki/Chaos_Computer_Club))
As you can see, NewPipe supports multiple video and audio services. Though it started off with YouTube, other people have added more services over the years, making NewPipe more and more versatile!
Partially due to circumstance, and partially due to its popularity, YouTube is the best supported out of these services. If you use or are familiar with any of these other services, please help us improve support for them! We're looking for maintainers for SoundCloud and PeerTube.
If you intend to add a new service, please get in touch with us first! Our [docs](https://teamnewpipe.github.io/documentation/) provide more information on how a new service can be added to the app and to the [NewPipe Extractor](https://github.com/TeamNewPipe/NewPipeExtractor).
## Description
NewPipe does not use any Google framework libraries, nor the YouTube API. Websites are only parsed to fetch required info, so this app can be used on devices without Google services installed. Also, you don't need a YouTube account to use NewPipe, which is copylefted libre software.
NewPipe works by fetching the required data from the official API (e.g. PeerTube) of the service you're using. If the official API is restricted (e.g. YouTube) for our purposes, or is proprietary, the app parses the website or uses an internal API instead. This means that you don't need an account on any service to use NewPipe.
Also, since they are free and open source software, neither the app nor the Extractor use any proprietary libraries or frameworks, such as Google Play Services. This means you can use NewPipe on devices or custom ROMs that do not have Google apps installed.
### Features
* Search videos
* No Login Required
* Display general info about videos
* Watch YouTube videos
* Listen to YouTube videos
* Popup mode (floating player)
* Select streaming player to watch video with
* Download videos
* Download audio only
* Open a video in Kodi
* Show next/related videos
* Search YouTube in a specific language
* Watch/Block age restricted material
* Display general info about channels
* Search channels
* Watch videos from a channel
* Orbot/Tor support (not yet directly)
* 1080p/2K/4K support
* View history
* Subscribe to channels
* Search history
* Search/watch playlists
* Watch as enqueued playlists
* Enqueue videos
* Local playlists
* Subtitles
* Livestream support
* Show comments
* Watch videos at resolutions up to 4K
* Listen to audio in the background, only loading the audio stream to save data
* Popup mode (floating player, aka Picture-in-Picture)
* Watch live streams
* Show/hide subtitles/closed captions
* Search videos and audios (on YouTube, you can specify the content language as well)
* Enqueue videos (and optionally save them as local playlists)
* Show/hide general information about videos (such as description and tags)
* Show/hide next/related videos
* Show/hide comments
* Search videos, audios, channels, playlists and albums
* Browse videos and audios within a channel
* Subscribe to channels (yes, without logging into any account!)
* Get notifications about new videos from channels you're subscribed to
* Create and edit channel groups (for easier browsing and management)
* Browse video feeds generated from your channel groups
* View and search your watch history
* Search and watch playlists (these are remote playlists, which means they're fetched from the service you're browsing)
* Create and edit local playlists (these are created and saved within the app, and have nothing to do with any service)
* Download videos/audios/subtitles (closed captions)
* Open in Kodi
* Watch/Block age-restricted material
### Supported Services
NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/documentation/) provide more info on how a new service can be added to the app and the extractor. Please get in touch with us if you intend to add a new one. Currently supported services are:
* YouTube
* SoundCloud \[beta\]
* media.ccc.de \[beta\]
* PeerTube instances \[beta\]
* Bandcamp \[beta\]
<!-- Hidden span to keep old links compatible. -->
<!-- Hidden span to keep old links compatible. You should remove this span if you're translating the README into another language.-->
<span id="updates"></span>
## Installation and updates
You can install NewPipe using one of the following methods:
1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users.
3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, and then push the update to users.
4. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
5. If you're interested in a specific feature or bugfix provided in a Pull Request in this repo, you can also download its APK from within the PR. Read the PR description for instructions. The great thing about PR-specific APKs is that they're installed side-by-side the official app, so you don't have to worry about losing your data or messing anything up.
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other, but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app.
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other (meaning that if you installed NewPipe using either method 1 or 2, you can also update NewPipe using the other), but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. When using method 5, each APK is signed with a different random key supplied by GitHub Actions, so you cannot even update it. You will have to backup and restore the app data each time you wish to use a new APK.
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest update yet), we recommend following this procedure:
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlists
@@ -101,30 +105,29 @@ In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's
3. Download the APK from the new source and install it
4. Import the data from step 1 via Settings > Content > Import Database
## Contribution
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
The more is done the better it gets!
<b>Note: when you're importing a database into the official app, always make sure that it is the one you exported _from_ the official app. If you import a database exported from an APK other than the official app, it may break things. Such an action is unsupported, and you should only do so when you're absolutely certain you know what you're doing.</b>
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
## Contribution
Whether you have ideas, translations, design changes, code cleaning, or even major code changes, help is always welcome. The app gets better and better with each contribution, no matter how big or small! If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
<a href="https://hosted.weblate.org/engage/newpipe/">
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
</a>
## Donate
If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.net/donate).
If you like NewPipe, you're welcome to send a donation. We prefer Liberapay, as it is both open-source and non-profit. For further info on donating to NewPipe, please visit our [website](https://newpipe.net/donate).
<table>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
</tr>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
@@ -134,14 +137,9 @@ If you like NewPipe we'd be happy about a donation. You can either send bitcoin
## Privacy Policy
The NewPipe project aims to provide a private, anonymous experience for using media web services.
Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/).
The NewPipe project aims to provide a private, anonymous experience for using web-based media services. Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or leave a comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/).
## License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe is Free Software: You can use, study, share, and improve it at
will. Specifically you can redistribute and/or modify it under the terms of the
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as
published by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
NewPipe is Free Software: You can use, study, share, and improve it at will. Specifically you can redistribute and/or modify it under the terms of the [GNU General Public License](https://www.gnu.org/licenses/gpl.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

View File

@@ -8,21 +8,18 @@ plugins {
}
android {
compileSdk 31
buildToolsVersion '31.0.0'
compileSdk 32
namespace 'org.schabi.newpipe'
defaultConfig {
applicationId "org.schabi.newpipe"
resValue "string", "app_name", "NewPipe"
minSdk 19
minSdk 21
targetSdk 29
versionCode 989
versionName "0.23.3"
multiDexEnabled true
versionCode 991
versionName "0.24.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
javaCompileOptions {
annotationProcessorOptions {
@@ -98,14 +95,14 @@ android {
}
ext {
checkstyleVersion = '10.0'
checkstyleVersion = '10.3.1'
androidxLifecycleVersion = '2.3.1'
androidxRoomVersion = '2.4.2'
androidxLifecycleVersion = '2.5.1'
androidxRoomVersion = '2.4.3'
androidxWorkVersion = '2.7.1'
icepickVersion = '3.2.0'
exoPlayerVersion = '2.17.1'
exoPlayerVersion = '2.18.1'
googleAutoServiceVersion = '1.0.1'
groupieVersion = '2.10.1'
markwonVersion = '4.6.2'
@@ -113,7 +110,7 @@ ext {
leakCanaryVersion = '2.5'
stethoVersion = '1.6.0'
mockitoVersion = '4.0.0'
assertJVersion = '3.22.0'
assertJVersion = '3.23.1'
}
configurations {
@@ -182,7 +179,7 @@ sonarqube {
dependencies {
/** Desugaring **/
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
/** NewPipe libraries **/
// You can use a local version by uncommenting a few lines in settings.gradle
@@ -190,27 +187,27 @@ 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:6a858368c86bc9a55abee586eb6c733e86c26b97'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:eb07d70a2ce03bee3cc74fc33b2e4173e1c21436'
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
/** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
ktlint 'com.pinterest:ktlint:0.44.0'
ktlint 'com.pinterest:ktlint:0.45.2'
/** Kotlin **/
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
/** AndroidX **/
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.fragment:fragment-ktx:1.3.6'
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
implementation 'androidx.media:media:1.5.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.media:media:1.6.0'
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
@@ -220,10 +217,9 @@ dependencies {
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.webkit:webkit:1.4.0'
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
implementation 'com.google.android.material:material:1.5.0'
implementation 'com.google.android.material:material:1.6.1'
/** Third-party libraries **/
// Instance state boilerplate elimination
@@ -234,11 +230,16 @@ dependencies {
implementation "org.jsoup:jsoup:1.15.3"
// HTTP client
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
implementation "com.squareup.okhttp3:okhttp:3.12.13"
implementation "com.squareup.okhttp3:okhttp:4.10.0"
// Media player
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:exoplayer-dash:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:exoplayer-database:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:exoplayer-datasource:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:exoplayer-hls:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:exoplayer-ui:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
// Metadata generator for service descriptors
@@ -257,9 +258,6 @@ dependencies {
implementation "io.noties.markwon:core:${markwonVersion}"
implementation "io.noties.markwon:linkify:${markwonVersion}"
// File picker
implementation "com.nononsenseapps:filepicker:4.2.1"
// Crash reporting
implementation "ch.acra:acra-core:5.9.3"
@@ -273,7 +271,7 @@ dependencies {
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
// Date and time formatting
implementation "org.ocpsoft.prettytime:prettytime:5.0.2.Final"
implementation "org.ocpsoft.prettytime:prettytime:5.0.6.Final"
/** Debugging **/
// Memory leak detection

View File

@@ -18,7 +18,6 @@
-dontobfuscate
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
-keep class org.ocpsoft.prettytime.i18n.** { *; }
-keep class org.mozilla.javascript.** { *; }
@@ -26,9 +25,6 @@
-keep class com.google.android.exoplayer2.** { *; }
-dontwarn org.mozilla.javascript.tools.**
-dontwarn android.arch.util.paging.CountedDataSource
-dontwarn android.arch.persistence.room.paging.LimitOffsetDataSource
# Rules for icepick. Copy paste from https://github.com/frankiesardo/icepick
-dontwarn icepick.**
@@ -39,12 +35,11 @@
}
-keepnames class * { @icepick.State *;}
# Rules for OkHttp. Copy paste from https://github.com/square/okhttp
## Rules for OkHttp. Copy paste from https://github.com/square/okhttp
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
# A resource is loaded with a relative path so the package of this class must be preserved.
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
##
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
!static !transient <fields>;

View File

@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.schabi.newpipe">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".DebugApp"

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.schabi.newpipe"
android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" />
@@ -14,6 +13,9 @@
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<application
android:name=".App"
@@ -44,7 +46,7 @@
</receiver>
<service
android:name=".player.MainPlayer"
android:name=".player.PlayerService"
android:exported="false"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
@@ -151,6 +153,7 @@
<data android:pathPrefix="/channel/" />
<data android:pathPrefix="/user/" />
<data android:pathPrefix="/c/" />
<data android:pathPrefix="/@" />
<!-- playlist prefix -->
<data android:pathPrefix="/playlist" />
</intent-filter>

View File

@@ -282,11 +282,9 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
@Nullable
public Parcelable saveState() {
Bundle state = null;
if (mSavedState.size() > 0) {
if (!mSavedState.isEmpty()) {
state = new Bundle();
final Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
mSavedState.toArray(fss);
state.putParcelableArray("states", fss);
state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0]));
}
for (int i = 0; i < mFragments.size(); i++) {
final Fragment f = mFragments.get(i);

View File

@@ -14,7 +14,6 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout;
import org.schabi.newpipe.R;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
// See https://stackoverflow.com/questions/56849221#57997489
@@ -27,7 +26,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
private boolean allowScroll = true;
private final Rect globalRect = new Rect();
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
private final List<Integer> skipInterceptionOfElements = List.of(
R.id.itemsListPanel, R.id.playbackSeekBar,
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
@@ -67,7 +66,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
@NonNull final AppBarLayout child,
@NonNull final MotionEvent ev) {
for (final Integer element : skipInterceptionOfElements) {
for (final int element : skipInterceptionOfElements) {
final View view = child.findViewById(element);
if (view != null) {
final boolean visible = view.getGlobalVisibleRect(globalRect);
@@ -132,8 +131,8 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
try {
final Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
if (headerBehaviorType != null) {
final Field field
= headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
final Field field =
headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
field.setAccessible(true);
return field;
}

View File

@@ -1,5 +1,6 @@
package org.schabi.newpipe;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
@@ -7,7 +8,6 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager;
import com.jakewharton.processphoenix.ProcessPhoenix;
@@ -27,9 +27,8 @@ import org.schabi.newpipe.util.StateSaver;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import io.reactivex.rxjava3.exceptions.CompositeException;
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
@@ -56,7 +55,7 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class App extends MultiDexApplication {
public class App extends Application {
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
private static final String TAG = App.class.toString();
private static App app;
@@ -140,7 +139,7 @@ public class App extends MultiDexApplication {
if (throwable instanceof UndeliverableException) {
// As UndeliverableException is a wrapper,
// get the cause of it to get the "real" exception
actualThrowable = throwable.getCause();
actualThrowable = Objects.requireNonNull(throwable.getCause());
} else {
actualThrowable = throwable;
}
@@ -149,7 +148,7 @@ public class App extends MultiDexApplication {
if (actualThrowable instanceof CompositeException) {
errors = ((CompositeException) actualThrowable).getExceptions();
} else {
errors = Collections.singletonList(actualThrowable);
errors = List.of(actualThrowable);
}
for (final Throwable error : errors) {
@@ -213,41 +212,37 @@ public class App extends MultiDexApplication {
private void initNotificationChannels() {
// Keep the importance below DEFAULT to avoid making noise on every notification update for
// the main and update channels
final List<NotificationChannelCompat> notificationChannelCompats = new ArrayList<>();
notificationChannelCompats.add(new NotificationChannelCompat
.Builder(getString(R.string.notification_channel_id),
final List<NotificationChannelCompat> notificationChannelCompats = List.of(
new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.notification_channel_name))
.setDescription(getString(R.string.notification_channel_description))
.build());
notificationChannelCompats.add(new NotificationChannelCompat
.Builder(getString(R.string.app_update_notification_channel_id),
.setName(getString(R.string.notification_channel_name))
.setDescription(getString(R.string.notification_channel_description))
.build(),
new NotificationChannelCompat
.Builder(getString(R.string.app_update_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.app_update_notification_channel_name))
.setDescription(getString(R.string.app_update_notification_channel_description))
.build());
notificationChannelCompats.add(new NotificationChannelCompat
.Builder(getString(R.string.hash_channel_id),
.setName(getString(R.string.app_update_notification_channel_name))
.setDescription(
getString(R.string.app_update_notification_channel_description))
.build(),
new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
NotificationManagerCompat.IMPORTANCE_HIGH)
.setName(getString(R.string.hash_channel_name))
.setDescription(getString(R.string.hash_channel_description))
.build());
notificationChannelCompats.add(new NotificationChannelCompat
.Builder(getString(R.string.error_report_channel_id),
.setName(getString(R.string.hash_channel_name))
.setDescription(getString(R.string.hash_channel_description))
.build(),
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.error_report_channel_name))
.setDescription(getString(R.string.error_report_channel_description))
.build());
notificationChannelCompats.add(new NotificationChannelCompat
.Builder(getString(R.string.streams_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setName(getString(R.string.streams_notification_channel_name))
.setDescription(getString(R.string.streams_notification_channel_description))
.build());
.setName(getString(R.string.error_report_channel_name))
.setDescription(getString(R.string.error_report_channel_description))
.build(),
new NotificationChannelCompat
.Builder(getString(R.string.streams_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setName(getString(R.string.streams_notification_channel_name))
.setDescription(
getString(R.string.streams_notification_channel_description))
.build()
);
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);

View File

@@ -1,7 +1,6 @@
package org.schabi.newpipe;
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -12,40 +11,27 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.downloader.Request;
import org.schabi.newpipe.extractor.downloader.Response;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.util.CookieUtils;
import org.schabi.newpipe.util.InfoCache;
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import static org.schabi.newpipe.MainActivity.DEBUG;
public final class DownloaderImpl extends Downloader {
public static final String USER_AGENT
= "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
= "youtube_restricted_mode_key";
public static final String USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
"youtube_restricted_mode_key";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
public static final String YOUTUBE_DOMAIN = "youtube.com";
@@ -54,9 +40,6 @@ public final class DownloaderImpl extends Downloader {
private final OkHttpClient client;
private DownloaderImpl(final OkHttpClient.Builder builder) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
enableModernTLS(builder);
}
this.client = builder
.readTimeout(30, TimeUnit.SECONDS)
// .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"),
@@ -81,69 +64,16 @@ public final class DownloaderImpl extends Downloader {
return instance;
}
/**
* Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken
* from the documentation of OkHttpClient.Builder.sslSocketFactory(_,_).
* <p>
* If there is an error, the function will safely fall back to doing nothing
* and printing the error to the console.
* </p>
*
* @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place)
*/
private static void enableModernTLS(final OkHttpClient.Builder builder) {
try {
// get the default TrustManager
final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
final TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
final X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
// insert our own TLSSocketFactory
final SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
builder.sslSocketFactory(sslSocketFactory, trustManager);
// This will try to enable all modern CipherSuites(+2 more)
// that are supported on the device.
// Necessary because some servers (e.g. Framatube.org)
// don't support the old cipher suites.
// https://github.com/square/okhttp/issues/4053#issuecomment-402579554
final List<CipherSuite> cipherSuites =
new ArrayList<>(ConnectionSpec.MODERN_TLS.cipherSuites());
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
final ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
.build();
builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
} catch (final KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
if (DEBUG) {
e.printStackTrace();
}
}
}
public String getCookies(final String url) {
final List<String> resultCookies = new ArrayList<>();
if (url.contains(YOUTUBE_DOMAIN)) {
final String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
if (youtubeCookie != null) {
resultCookies.add(youtubeCookie);
}
}
final String youtubeCookie = url.contains(YOUTUBE_DOMAIN)
? getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY) : null;
// Recaptcha cookie is always added TODO: not sure if this is necessary
final String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY);
if (recaptchaCookie != null) {
resultCookies.add(recaptchaCookie);
}
return CookieUtils.concatCookies(resultCookies);
return Stream.of(youtubeCookie, getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY))
.filter(Objects::nonNull)
.flatMap(cookies -> Arrays.stream(cookies.split("; *")))
.distinct()
.collect(Collectors.joining("; "));
}
public String getCookie(final String key) {
@@ -203,7 +133,7 @@ public final class DownloaderImpl extends Downloader {
RequestBody requestBody = null;
if (dataToSend != null) {
requestBody = RequestBody.create(null, dataToSend);
requestBody = RequestBody.create(dataToSend);
}
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()

View File

@@ -3,7 +3,6 @@ package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import org.schabi.newpipe.util.NavigationHelper;
@@ -44,11 +43,7 @@ public class ExitActivity extends Activity {
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
finishAndRemoveTask();
} else {
finish();
}
finishAndRemoveTask();
NavigationHelper.restartApp(this);
}

View File

@@ -28,7 +28,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -86,7 +85,6 @@ import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
@@ -131,11 +129,6 @@ public class MainActivity extends AppCompatActivity {
+ "savedInstanceState = [" + savedInstanceState + "]");
}
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for media.ccc.de sources
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
TLSSocketFactoryCompat.setAsDefault();
}
ThemeHelper.setDayNightMode(this);
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
@@ -381,8 +374,7 @@ public class MainActivity extends AppCompatActivity {
private void showServices() {
for (final StreamingService s : NewPipe.getServices()) {
final String title = s.getServiceInfo().getName()
+ (ServiceHelper.isBeta(s) ? " (beta)" : "");
final String title = s.getServiceInfo().getName();
final MenuItem menuItem = drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
@@ -390,7 +382,7 @@ public class MainActivity extends AppCompatActivity {
// peertube specifics
if (s.getServiceId() == 3) {
enhancePeertubeMenu(s, menuItem);
enhancePeertubeMenu(menuItem);
}
}
drawerLayoutBinding.navigation.getMenu()
@@ -398,9 +390,9 @@ public class MainActivity extends AppCompatActivity {
.setChecked(true);
}
private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) {
private void enhancePeertubeMenu(final MenuItem menuItem) {
final PeertubeInstance currentInstance = PeertubeHelper.getCurrentInstance();
menuItem.setTitle(currentInstance.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
menuItem.setTitle(currentInstance.getName());
final Spinner spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this))
.getRoot();
final List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
@@ -480,8 +472,8 @@ public class MainActivity extends AppCompatActivity {
ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e);
}
final SharedPreferences sharedPreferences
= PreferenceManager.getDefaultSharedPreferences(this);
final SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(this);
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
if (DEBUG) {
Log.d(TAG, "Theme has changed, recreating activity...");
@@ -653,8 +645,8 @@ public class MainActivity extends AppCompatActivity {
}
super.onCreateOptionsMenu(menu);
final Fragment fragment
= getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
final Fragment fragment =
getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
if (!(fragment instanceof SearchFragment)) {
toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE);
}

View File

@@ -3,7 +3,6 @@ package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
/*
@@ -40,10 +39,6 @@ public class PanicResponderActivity extends Activity {
ExitActivity.exitAndRemoveFromRecentApps(this);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
finishAndRemoveTask();
} else {
finish();
}
finishAndRemoveTask();
}
}

View File

@@ -16,7 +16,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.SparseItemUtil;
import java.util.Collections;
import java.util.List;
public final class QueueItemMenuUtil {
private QueueItemMenuUtil() {
@@ -53,7 +53,7 @@ public final class QueueItemMenuUtil {
case R.id.menu_item_append_playlist:
PlaylistDialog.createCorrespondingDialog(
context,
Collections.singletonList(new StreamEntity(item)),
List.of(new StreamEntity(item)),
dialog -> dialog.show(
fragmentManager,
"QueueItemMenuUtil@append_playlist"

View File

@@ -30,6 +30,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.app.NotificationCompat;
import androidx.core.app.ServiceCompat;
import androidx.core.math.MathUtils;
import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager;
@@ -60,7 +61,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
@@ -81,7 +82,6 @@ import org.schabi.newpipe.views.FocusOverlayView;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import icepick.Icepick;
@@ -452,7 +452,7 @@ public class RouterActivity extends AppCompatActivity {
}
}
selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.size() - 1);
selectedRadioPosition = MathUtils.clamp(selectedRadioPosition, -1, choices.size() - 1);
if (selectedRadioPosition != -1) {
((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true);
}
@@ -630,8 +630,8 @@ public class RouterActivity extends AppCompatActivity {
}
// ...the player is not running or in normal Video-mode/type
final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
return playerType == null || playerType == MainPlayer.PlayerType.VIDEO;
final PlayerType playerType = PlayerHolder.getInstance().getType();
return playerType == null || playerType == PlayerType.MAIN;
}
private void openAddToPlaylistDialog() {
@@ -649,7 +649,7 @@ public class RouterActivity extends AppCompatActivity {
.subscribe(
info -> PlaylistDialog.createCorrespondingDialog(
getThemeWrapperContext(),
Collections.singletonList(new StreamEntity(info)),
List.of(new StreamEntity(info)),
playlistDialog -> {
playlistDialog.setOnDismissListener(dialog -> finish());

View File

@@ -78,6 +78,7 @@ class AboutActivity : AppCompatActivity() {
aboutDonationLink.openLink(R.string.donation_url)
aboutWebsiteLink.openLink(R.string.website_url)
aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
faqLink.openLink(R.string.faq_url)
return root
}
}

View File

@@ -8,7 +8,6 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.schabi.newpipe.R
import org.schabi.newpipe.about.LicenseFragmentHelper.showLicense
import org.schabi.newpipe.databinding.FragmentLicensesBinding
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding

View File

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

View File

@@ -3,7 +3,6 @@ package org.schabi.newpipe.database;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Update;
import java.util.Collection;
@@ -14,13 +13,10 @@ import io.reactivex.rxjava3.core.Flowable;
@Dao
public interface BasicDAO<Entity> {
/* Inserts */
@Insert(onConflict = OnConflictStrategy.ABORT)
@Insert
long insert(Entity entity);
@Insert(onConflict = OnConflictStrategy.ABORT)
List<Long> insertAll(Entity... entities);
@Insert(onConflict = OnConflictStrategy.ABORT)
@Insert
List<Long> insertAll(Collection<Entity> entities);
/* Searches */
@@ -32,9 +28,6 @@ public interface BasicDAO<Entity> {
@Delete
void delete(Entity entity);
@Delete
int delete(Collection<Entity> entities);
int deleteAll();
/* Updates */

View File

@@ -9,6 +9,7 @@ import androidx.room.Update
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamStateEntity
@@ -21,56 +22,16 @@ abstract class FeedDAO {
@Query("DELETE FROM feed")
abstract fun deleteAll(): Int
@Query(
"""
SELECT s.*, sst.progress_time
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getAllStreams(): Maybe<List<StreamWithState>>
@Query(
"""
SELECT s.*, sst.progress_time
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
INNER JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = f.subscription_id
WHERE fgs.group_id = :groupId
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getAllStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
/**
* @param groupId the group id to get feed streams of; use
* [FeedGroupEntity.GROUP_ALL_ID] to not filter by group
* @param includePlayed if false, only return all of the live, never-played or non-finished
* feed streams (see `@see` items); if true no filter is applied
* @param uploadDateBefore get only streams uploaded before this date (useful to filter out
* future streams); use null to not filter by upload date
* @return the feed streams filtered according to the conditions provided in the parameters
* @see StreamStateEntity.isFinished()
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
* @return all of the non-live, never-played and non-finished streams in the feed
* (all of the cited conditions must hold for a stream to be in the returned list)
*/
@Query(
"""
@@ -79,67 +40,47 @@ abstract class FeedDAO {
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
LEFT JOIN feed_group_subscription_join fgs
ON (
:groupId <> ${FeedGroupEntity.GROUP_ALL_ID}
AND fgs.subscription_id = f.subscription_id
)
WHERE (
sh.stream_id IS NULL
OR sst.stream_id IS NULL
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
OR sst.progress_time < s.duration * 1000 * 3 / 4
OR s.stream_type = 'LIVE_STREAM'
OR s.stream_type = 'AUDIO_LIVE_STREAM'
:groupId = ${FeedGroupEntity.GROUP_ALL_ID}
OR fgs.group_id = :groupId
)
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getLiveOrNotPlayedStreams(): Maybe<List<StreamWithState>>
/**
* @see StreamStateEntity.isFinished()
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
* @param groupId the group id to get streams of
* @return all of the non-live, never-played and non-finished streams for the given feed group
* (all of the cited conditions must hold for a stream to be in the returned list)
*/
@Query(
"""
SELECT s.*, sst.progress_time
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
INNER JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = f.subscription_id
WHERE fgs.group_id = :groupId
AND (
sh.stream_id IS NULL
:includePlayed
OR sh.stream_id IS NULL
OR sst.stream_id IS NULL
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
OR sst.progress_time < s.duration * 1000 * 3 / 4
OR s.stream_type = 'LIVE_STREAM'
OR s.stream_type = 'AUDIO_LIVE_STREAM'
)
AND (
:uploadDateBefore IS NULL
OR s.upload_date IS NULL
OR s.upload_date < :uploadDateBefore
)
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
abstract fun getStreams(
groupId: Long,
includePlayed: Boolean,
uploadDateBefore: OffsetDateTime?
): Maybe<List<StreamWithState>>
@Query(
"""

View File

@@ -3,10 +3,10 @@ package org.schabi.newpipe.database.playlist;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import java.util.ArrayList;
import java.util.Collections;
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();
@@ -14,14 +14,9 @@ public interface PlaylistLocalItem extends LocalItem {
static List<PlaylistLocalItem> merge(
final List<PlaylistMetadataEntry> localPlaylists,
final List<PlaylistRemoteEntity> remotePlaylists) {
final List<PlaylistLocalItem> items = new ArrayList<>(
localPlaylists.size() + remotePlaylists.size());
items.addAll(localPlaylists);
items.addAll(remotePlaylists);
Collections.sort(items, Comparator.comparing(PlaylistLocalItem::getOrderingName,
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
return items;
return Stream.concat(localPlaylists.stream(), remotePlaylists.stream())
.sorted(Comparator.comparing(PlaylistLocalItem::getOrderingName,
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)))
.collect(Collectors.toList());
}
}

View File

@@ -1,5 +1,9 @@
package org.schabi.newpipe.download;
import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP;
import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
@@ -82,10 +86,6 @@ import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
import us.shandian.giga.service.MissionState;
import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP;
import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class DownloadDialog extends DialogFragment
implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
private static final String TAG = "DialogFragment";
@@ -145,6 +145,12 @@ public class DownloadDialog extends DialogFragment
// Instance creation
//////////////////////////////////////////////////////////////////////////*/
public DownloadDialog() {
// Just an empty default no-arg ctor to keep Fragment.instantiate() happy
// otherwise InstantiationException will be thrown when fragment is recreated
// TODO: Maybe use a custom FragmentFactory instead?
}
/**
* Create a new download dialog with the video, audio and subtitle streams from the provided
* stream info. Video streams and video-only streams will be put into a single list menu,
@@ -153,7 +159,7 @@ public class DownloadDialog extends DialogFragment
* @param context the context to use just to obtain preferences and strings (will not be stored)
* @param info the info from which to obtain downloadable streams and other info (e.g. title)
*/
public DownloadDialog(final Context context, @NonNull final StreamInfo info) {
public DownloadDialog(@NonNull final Context context, @NonNull final StreamInfo info) {
this.currentInfo = info;
// TODO: Adapt this code when the downloader support other types of stream deliveries
@@ -205,8 +211,8 @@ public class DownloadDialog extends DialogFragment
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
Icepick.restoreInstanceState(this, savedInstanceState);
final SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams
= new SparseArray<>(4);
final SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams =
new SparseArray<>(4);
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
for (int i = 0; i < videoStreams.size(); i++) {

View File

@@ -31,6 +31,7 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.stream.Collectors;
/*
* Created by Christian Schabesberger on 24.10.15.
@@ -65,11 +66,11 @@ public class ErrorActivity extends AppCompatActivity {
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
public static final String ERROR_GITHUB_ISSUE_URL
= "https://github.com/TeamNewPipe/NewPipe/issues";
public static final String ERROR_GITHUB_ISSUE_URL =
"https://github.com/TeamNewPipe/NewPipe/issues";
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER
= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private ErrorInfo errorInfo;
@@ -182,14 +183,9 @@ public class ErrorActivity extends AppCompatActivity {
}
private String formErrorText(final String[] el) {
final StringBuilder text = new StringBuilder();
if (el != null) {
for (final String e : el) {
text.append("-------------------------------------\n").append(e);
}
}
text.append("-------------------------------------");
return text.toString();
final String separator = "-------------------------------------";
return Arrays.stream(el)
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
}
/**

View File

@@ -14,8 +14,6 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.util.ServiceHelper
import java.io.PrintWriter
import java.io.StringWriter
@Parcelize
class ErrorInfo(
@@ -80,19 +78,10 @@ class ErrorInfo(
companion object {
const val SERVICE_NONE = "none"
private fun getStackTrace(throwable: Throwable): String {
StringWriter().use { stringWriter ->
PrintWriter(stringWriter, true).use { printWriter ->
throwable.printStackTrace(printWriter)
return stringWriter.buffer.toString()
}
}
}
fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString())
fun throwableToStringList(throwable: Throwable) = arrayOf(getStackTrace(throwable))
fun throwableListToStringList(throwable: List<Throwable>) =
Array(throwable.size) { i -> getStackTrace(throwable[i]) }
fun throwableListToStringList(throwableList: List<Throwable>) =
throwableList.map { it.stackTraceToString() }.toTypedArray()
private fun getInfoServiceName(info: Info?) =
if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId)

View File

@@ -30,6 +30,7 @@ import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.isInterruptedCaused
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
import java.util.concurrent.TimeUnit
class ErrorPanelHelper(
@@ -52,6 +53,8 @@ class ErrorPanelHelper(
errorPanelRoot.findViewById(R.id.error_action_button)
private val errorRetryButton: Button =
errorPanelRoot.findViewById(R.id.error_retry_button)
private val errorOpenInBrowserButton: Button =
errorPanelRoot.findViewById(R.id.error_open_in_browser)
private var errorDisposable: Disposable? = null
@@ -69,6 +72,7 @@ class ErrorPanelHelper(
errorServiceExplanationTextView.isVisible = false
errorActionButton.isVisible = false
errorRetryButton.isVisible = false
errorOpenInBrowserButton.isVisible = false
}
fun showError(errorInfo: ErrorInfo) {
@@ -99,6 +103,7 @@ class ErrorPanelHelper(
}
errorRetryButton.isVisible = true
showAndSetOpenInBrowserButtonAction(errorInfo)
} else if (errorInfo.throwable is AccountTerminatedException) {
errorTextView.setText(R.string.account_terminated)
@@ -128,6 +133,7 @@ class ErrorPanelHelper(
// show retry button only for content which is not unavailable or unsupported
errorRetryButton.isVisible = true
}
showAndSetOpenInBrowserButtonAction(errorInfo)
}
setRootVisible()
@@ -145,6 +151,15 @@ class ErrorPanelHelper(
errorActionButton.setOnClickListener(listener)
}
fun showAndSetOpenInBrowserButtonAction(
errorInfo: ErrorInfo
) {
errorOpenInBrowserButton.isVisible = true
errorOpenInBrowserButton.setOnClickListener {
ShareUtils.openUrlInBrowser(context, errorInfo.request, true)
}
}
fun showTextError(errorString: String) {
ensureDefaultVisibility()

View File

@@ -114,13 +114,7 @@ class ErrorUtil {
context,
context.getString(R.string.error_report_channel_id)
)
.setSmallIcon(
// the vector drawable icon causes crashes on KitKat devices
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
R.drawable.ic_bug_report
else
android.R.drawable.stat_notify_error
)
.setSmallIcon(R.drawable.ic_bug_report)
.setContentTitle(context.getString(R.string.error_report_notification_title))
.setContentText(context.getString(errorInfo.messageStringId))
.setAutoCancel(true)

View File

@@ -3,14 +3,15 @@ package org.schabi.newpipe.error;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.webkit.CookieManager;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -18,7 +19,6 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NavUtils;
import androidx.preference.PreferenceManager;
import androidx.webkit.WebViewClientCompat;
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
import org.schabi.newpipe.DownloaderImpl;
@@ -86,14 +86,15 @@ public class ReCaptchaActivity extends AppCompatActivity {
webSettings.setJavaScriptEnabled(true);
webSettings.setUserAgentString(DownloaderImpl.USER_AGENT);
recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClientCompat() {
recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
public boolean shouldOverrideUrlLoading(final WebView view,
final WebResourceRequest request) {
if (MainActivity.DEBUG) {
Log.d(TAG, "shouldOverrideUrlLoading: url=" + url);
Log.d(TAG, "shouldOverrideUrlLoading: url=" + request.getUrl().toString());
}
handleCookiesFromUrl(url);
handleCookiesFromUrl(request.getUrl().toString());
return false;
}
@@ -107,12 +108,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
// cleaning cache, history and cookies from webView
recaptchaBinding.reCaptchaWebView.clearCache(true);
recaptchaBinding.reCaptchaWebView.clearHistory();
final CookieManager cookieManager = CookieManager.getInstance();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.removeAllCookies(value -> { });
} else {
cookieManager.removeAllCookie();
}
CookieManager.getInstance().removeAllCookies(null);
recaptchaBinding.reCaptchaWebView.loadUrl(url);
}

View File

@@ -1,5 +1,9 @@
package org.schabi.newpipe.fragments.detail;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -26,17 +30,9 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.external_communication.TextLinkifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import icepick.State;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
public class DescriptionFragment extends BaseFragment {
@State
@@ -185,8 +181,8 @@ public class DescriptionFragment extends BaseFragment {
return;
}
final ItemMetadataBinding itemBinding
= ItemMetadataBinding.inflate(inflater, layout, false);
final ItemMetadataBinding itemBinding =
ItemMetadataBinding.inflate(inflater, layout, false);
itemBinding.metadataTypeView.setText(type);
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
@@ -206,19 +202,16 @@ public class DescriptionFragment extends BaseFragment {
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) {
final ItemMetadataTagsBinding itemBinding
= ItemMetadataTagsBinding.inflate(inflater, layout, false);
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
final List<String> tags = new ArrayList<>(streamInfo.getTags());
Collections.sort(tags);
for (final String tag : tags) {
streamInfo.getTags().stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
itemBinding.metadataTagsChips, false);
chip.setText(tag);
chip.setOnClickListener(this::onTagClick);
chip.setOnLongClickListener(this::onTagLongClick);
itemBinding.metadataTagsChips.addView(chip);
}
});
layout.addView(itemBinding.getRoot());
}

View File

@@ -1,5 +1,16 @@
package org.schabi.newpipe.fragments.detail;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.BroadcastReceiver;
@@ -10,7 +21,6 @@ import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.database.ContentObserver;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
@@ -77,9 +87,9 @@ import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.MainPlayer.PlayerType;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
import org.schabi.newpipe.player.helper.PlayerHelper;
@@ -87,6 +97,8 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.player.ui.MainPlayerUi;
import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
@@ -101,11 +113,11 @@ import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import icepick.State;
@@ -114,17 +126,6 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
public final class VideoDetailFragment
extends BaseStateFragment<StreamInfo>
implements BackPressable,
@@ -179,6 +180,8 @@ public final class VideoDetailFragment
@State
int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
@State
int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
@State
protected boolean autoPlayEnabled = true;
@Nullable
@@ -190,6 +193,7 @@ public final class VideoDetailFragment
private Disposable positionSubscriber = null;
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
private BottomSheetBehavior.BottomSheetCallback bottomSheetCallback;
private BroadcastReceiver broadcastReceiver;
/*//////////////////////////////////////////////////////////////////////////
@@ -202,7 +206,7 @@ public final class VideoDetailFragment
private ContentObserver settingsContentObserver;
@Nullable
private MainPlayer playerService;
private PlayerService playerService;
private Player player;
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
@@ -211,7 +215,7 @@ public final class VideoDetailFragment
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onServiceConnected(final Player connectedPlayer,
final MainPlayer connectedPlayerService,
final PlayerService connectedPlayerService,
final boolean playAfterConnect) {
player = connectedPlayer;
playerService = connectedPlayerService;
@@ -219,6 +223,7 @@ public final class VideoDetailFragment
// It will do nothing if the player is not in fullscreen mode
hideSystemUiIfNeeded();
final Optional<MainPlayerUi> playerUi = player.UIs().get(MainPlayerUi.class);
if (!player.videoPlayerSelected() && !playAfterConnect) {
return;
}
@@ -227,25 +232,23 @@ public final class VideoDetailFragment
// If the video is playing but orientation changed
// let's make the video in fullscreen again
checkLandscape();
} else if (player.isFullscreen() && !player.isVerticalVideo()
} else if (playerUi.map(ui -> ui.isFullscreen() && !ui.isVerticalVideo()).orElse(false)
// Tablet UI has orientation-independent fullscreen
&& !DeviceUtils.isTablet(activity)) {
// Device is in portrait orientation after rotation but UI is in fullscreen.
// Return back to non-fullscreen state
player.toggleFullscreen();
}
if (playerIsNotStopped() && player.videoPlayerSelected()) {
addVideoPlayerView();
playerUi.ifPresent(MainPlayerUi::toggleFullscreen);
}
//noinspection SimplifyOptionalCallChains
if (playAfterConnect
|| (currentInfo != null
&& isAutoplayEnabled()
&& player.getParentActivity() == null)) {
&& !playerUi.isPresent())) {
autoPlayEnabled = true; // forcefully start playing
openVideoPlayerAutoFullscreen();
}
updateOverlayPlayQueueButtonVisibility();
}
@Override
@@ -269,7 +272,7 @@ public final class VideoDetailFragment
public static VideoDetailFragment getInstanceInCollapsedState() {
final VideoDetailFragment instance = new VideoDetailFragment();
instance.bottomSheetState = BottomSheetBehavior.STATE_COLLAPSED;
instance.updateBottomSheetState(BottomSheetBehavior.STATE_COLLAPSED);
return instance;
}
@@ -329,9 +332,14 @@ public final class VideoDetailFragment
@Override
public void onResume() {
super.onResume();
if (DEBUG) {
Log.d(TAG, "onResume() called");
}
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
updateOverlayPlayQueueButtonVisibility();
setupBrightness();
if (tabSettingsChanged) {
@@ -383,7 +391,7 @@ public final class VideoDetailFragment
disposables.clear();
positionSubscriber = null;
currentWorker = null;
bottomSheetBehavior.setBottomSheetCallback(null);
bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback);
if (activity.isFinishing()) {
playQueue = null;
@@ -449,7 +457,7 @@ public final class VideoDetailFragment
disposables.add(
PlaylistDialog.createCorrespondingDialog(
getContext(),
Collections.singletonList(new StreamEntity(currentInfo)),
List.of(new StreamEntity(currentInfo)),
dialog -> dialog.show(getFM(), TAG)
)
);
@@ -500,12 +508,18 @@ public final class VideoDetailFragment
}
break;
case R.id.detail_thumbnail_root_layout:
autoPlayEnabled = true; // forcefully start playing
// FIXME Workaround #7427
if (isPlayerAvailable()) {
player.setRecovery();
// make sure not to open any player if there is nothing currently loaded!
// FIXME removing this `if` causes the player service to start correctly, then stop,
// then restart badly without calling `startForeground()`, causing a crash when
// later closing the detail fragment
if (currentInfo != null) {
autoPlayEnabled = true; // forcefully start playing
// FIXME Workaround #7427
if (isPlayerAvailable()) {
player.setRecovery();
}
openVideoPlayerAutoFullscreen();
}
openVideoPlayerAutoFullscreen();
break;
case R.id.detail_title_root_layout:
toggleTitleAndSecondaryControls();
@@ -515,10 +529,13 @@ public final class VideoDetailFragment
case R.id.overlay_buttons_layout:
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
break;
case R.id.overlay_play_queue_button:
NavigationHelper.openPlayQueue(getContext());
break;
case R.id.overlay_play_pause_button:
if (playerIsNotStopped()) {
player.playPause();
player.hideControls(0, 0);
player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
showSystemUi();
} else {
autoPlayEnabled = true; // forcefully start playing
@@ -583,12 +600,12 @@ public final class VideoDetailFragment
if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) {
binding.detailVideoTitleView.setMaxLines(10);
animateRotation(binding.detailToggleSecondaryControlsView,
Player.DEFAULT_CONTROLS_DURATION, 180);
VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180);
binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE);
} else {
binding.detailVideoTitleView.setMaxLines(1);
animateRotation(binding.detailToggleSecondaryControlsView,
Player.DEFAULT_CONTROLS_DURATION, 0);
VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 0);
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
}
// view pager height has changed, update the tab layout
@@ -673,6 +690,7 @@ public final class VideoDetailFragment
binding.overlayMetadataLayout.setOnClickListener(this);
binding.overlayMetadataLayout.setOnLongClickListener(this);
binding.overlayButtonsLayout.setOnClickListener(this);
binding.overlayPlayQueueButton.setOnClickListener(this);
binding.overlayCloseButton.setOnClickListener(this);
binding.overlayPlayPauseButton.setOnClickListener(this);
@@ -714,7 +732,7 @@ public final class VideoDetailFragment
}
private void initThumbnailViews(@NonNull final StreamInfo info) {
PicassoHelper.loadThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
PicassoHelper.loadDetailsThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailThumbnailImageView, new Callback() {
@Override
public void onSuccess() {
@@ -746,7 +764,9 @@ public final class VideoDetailFragment
@Override
public boolean onKeyDown(final int keyCode) {
return isPlayerAvailable() && player.onKeyDown(keyCode);
return isPlayerAvailable()
&& player.UIs().get(VideoPlayerUi.class)
.map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false);
}
@Override
@@ -756,7 +776,7 @@ public final class VideoDetailFragment
}
// If we are in fullscreen mode just exit from it via first back press
if (isPlayerAvailable() && player.isFullscreen()) {
if (isFullscreen()) {
if (!DeviceUtils.isTablet(activity)) {
player.pause();
}
@@ -1006,8 +1026,7 @@ public final class VideoDetailFragment
getChildFragmentManager().beginTransaction()
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
.commitAllowingStateLoss();
binding.relatedItemsLayout.setVisibility(
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.VISIBLE);
binding.relatedItemsLayout.setVisibility(isFullscreen() ? View.GONE : View.VISIBLE);
}
}
@@ -1047,15 +1066,13 @@ public final class VideoDetailFragment
// call `post()` to be sure `viewPager.getHitRect()`
// is up to date and not being currently recomputed
binding.tabLayout.post(() -> {
if (getContext() != null) {
final var activity = getActivity();
if (activity != null) {
final Rect pagerHitRect = new Rect();
binding.viewPager.getHitRect(pagerHitRect);
final Point displaySize = new Point();
Objects.requireNonNull(ContextCompat.getSystemService(getContext(),
WindowManager.class)).getDefaultDisplay().getSize(displaySize);
final int viewPagerVisibleHeight = displaySize.y - pagerHitRect.top;
final int height = DeviceUtils.getWindowHeight(activity.getWindowManager());
final int viewPagerVisibleHeight = height - pagerHitRect.top;
// see TabLayout.DEFAULT_HEIGHT, which is equal to 48dp
final float tabLayoutHeight = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics());
@@ -1087,8 +1104,12 @@ public final class VideoDetailFragment
private void toggleFullscreenIfInFullscreenMode() {
// If a user watched video inside fullscreen mode and than chose another player
// return to non-fullscreen mode
if (isPlayerAvailable() && player.isFullscreen()) {
player.toggleFullscreen();
if (isPlayerAvailable()) {
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
if (playerUi.isFullscreen()) {
playerUi.toggleFullscreen();
}
});
}
}
@@ -1164,7 +1185,7 @@ public final class VideoDetailFragment
// doesn't tell which state it was settling to, and thus the bottom sheet settles to
// STATE_COLLAPSED. This can be solved by manually setting the state that will be
// restored (i.e. bottomSheetState) to STATE_EXPANDED.
bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
updateBottomSheetState(BottomSheetBehavior.STATE_EXPANDED);
// toggle landscape in order to open directly in fullscreen
onScreenRotationButtonClicked();
}
@@ -1214,16 +1235,10 @@ public final class VideoDetailFragment
}
final PlayQueue queue = setupPlayQueueForIntent(false);
// Video view can have elements visible from popup,
// We hide it here but once it ready the view will be shown in handleIntent()
if (playerService.getView() != null) {
playerService.getView().setVisibility(View.GONE);
}
addVideoPlayerView();
tryAddVideoPlayerView();
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
MainPlayer.class, queue, true, autoPlayEnabled);
PlayerService.class, queue, true, autoPlayEnabled);
ContextCompat.startForegroundService(activity, playerIntent);
}
@@ -1235,8 +1250,8 @@ public final class VideoDetailFragment
* be reused in a few milliseconds and the flickering would be annoying.
*/
private void hideMainPlayerOnLoadingNewStream() {
if (!isPlayerServiceAvailable()
|| playerService.getView() == null
//noinspection SimplifyOptionalCallChains
if (!isPlayerServiceAvailable() || !getRoot().isPresent()
|| !player.videoPlayerSelected()) {
return;
}
@@ -1244,7 +1259,7 @@ public final class VideoDetailFragment
removeVideoPlayerView();
if (isAutoplayEnabled()) {
playerService.stopForImmediateReusing();
playerService.getView().setVisibility(View.GONE);
getRoot().ifPresent(view -> view.setVisibility(View.GONE));
} else {
playerHolder.stopService();
}
@@ -1301,27 +1316,41 @@ public final class VideoDetailFragment
&& PlayerHelper.isAutoplayAllowedByUser(requireContext());
}
private void addVideoPlayerView() {
if (!isPlayerAvailable() || getView() == null) {
return;
private void tryAddVideoPlayerView() {
if (isPlayerAvailable() && getView() != null) {
// Setup the surface view height, so that it fits the video correctly; this is done also
// here, and not only in the Handler, to avoid a choppy fullscreen rotation animation.
setHeightThumbnail();
}
// Check if viewHolder already contains a child
if (player.getRootView().getParent() != binding.playerPlaceholder) {
playerService.removeViewFromParent();
}
setHeightThumbnail();
// do all the null checks in the posted lambda, too, since the player, the binding and the
// view could be set or unset before the lambda gets executed on the next main thread cycle
new Handler(Looper.getMainLooper()).post(() -> {
if (!isPlayerAvailable() || getView() == null) {
return;
}
// Prevent from re-adding a view multiple times
if (player.getRootView().getParent() == null) {
binding.playerPlaceholder.addView(player.getRootView());
}
// setup the surface view height, so that it fits the video correctly
setHeightThumbnail();
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
// sometimes binding would be null here, even though getView() != null above u.u
if (binding != null) {
// prevent from re-adding a view multiple times
playerUi.removeViewFromParent();
binding.playerPlaceholder.addView(playerUi.getBinding().getRoot());
playerUi.setupVideoSurfaceIfNeeded();
}
});
});
}
private void removeVideoPlayerView() {
makeDefaultHeightForVideoPlaceholder();
playerService.removeViewFromParent();
if (player != null) {
player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
}
}
private void makeDefaultHeightForVideoPlaceholder() {
@@ -1362,7 +1391,7 @@ public final class VideoDetailFragment
final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
if (isPlayerAvailable() && player.isFullscreen()) {
if (isFullscreen()) {
final int height = (DeviceUtils.isInMultiWindow(activity)
? requireView()
: activity.getWindow().getDecorView()).getHeight();
@@ -1387,8 +1416,9 @@ public final class VideoDetailFragment
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
if (isPlayerAvailable()) {
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
player.getSurfaceView()
.setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight);
player.UIs().get(VideoPlayerUi.class).ifPresent(ui ->
ui.getBinding().surfaceView.setHeights(newHeight,
ui.isFullscreen() ? newHeight : maxHeight));
}
}
@@ -1517,7 +1547,7 @@ public final class VideoDetailFragment
if (binding.relatedItemsLayout != null) {
if (showRelatedItems) {
binding.relatedItemsLayout.setVisibility(
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.INVISIBLE);
isFullscreen() ? View.GONE : View.INVISIBLE);
} else {
binding.relatedItemsLayout.setVisibility(View.GONE);
}
@@ -1551,7 +1581,8 @@ public final class VideoDetailFragment
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
}
final Drawable buddyDrawable = AppCompatResources.getDrawable(activity, R.drawable.buddy);
final Drawable buddyDrawable =
AppCompatResources.getDrawable(activity, R.drawable.placeholder_person);
binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable);
binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable);
@@ -1778,6 +1809,11 @@ public final class VideoDetailFragment
// Player event listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onViewCreated() {
tryAddVideoPlayerView();
}
@Override
public void onQueueUpdate(final PlayQueue queue) {
playQueue = queue;
@@ -1787,6 +1823,14 @@ public final class VideoDetailFragment
+ title + "], playQueue = [" + playQueue + "]");
}
// Register broadcast receiver to listen to playQueue changes
// and hide the overlayPlayQueueButton when the playQueue is empty / destroyed.
if (playQueue != null && playQueue.getBroadcastReceiver() != null) {
playQueue.getBroadcastReceiver().subscribe(
event -> updateOverlayPlayQueueButtonVisibility()
);
}
// This should be the only place where we push data to stack.
// It will allow to have live instance of PlayQueue with actual information about
// deleted/added items inside Channel/Playlist queue and makes possible to have
@@ -1893,20 +1937,16 @@ public final class VideoDetailFragment
currentInfo.getUploaderName(),
currentInfo.getThumbnailUrl());
}
updateOverlayPlayQueueButtonVisibility();
}
@Override
public void onFullscreenStateChanged(final boolean fullscreen) {
setupBrightness();
//noinspection SimplifyOptionalCallChains
if (!isPlayerAndPlayerServiceAvailable()
|| playerService.getView() == null
|| player.getParentActivity() == null) {
return;
}
final View view = playerService.getView();
final ViewGroup parent = (ViewGroup) view.getParent();
if (parent == null) {
|| !player.UIs().get(MainPlayerUi.class).isPresent()
|| getRoot().map(View::getParent).orElse(null) == null) {
return;
}
@@ -1922,13 +1962,7 @@ public final class VideoDetailFragment
}
scrollToTop();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
addVideoPlayerView();
} else {
// KitKat needs a delay before addVideoPlayerView call or it reports wrong height in
// activity.getWindow().getDecorView().getHeight()
new Handler().post(this::addVideoPlayerView);
}
tryAddVideoPlayerView();
}
@Override
@@ -1940,7 +1974,7 @@ public final class VideoDetailFragment
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
if (DeviceUtils.isTablet(activity)
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
player.toggleFullscreen();
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
return;
}
@@ -1991,10 +2025,8 @@ public final class VideoDetailFragment
}
activity.getWindow().getDecorView().setSystemUiVisibility(0);
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr(
requireContext(), android.R.attr.colorPrimary));
}
activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr(
requireContext(), android.R.attr.colorPrimary));
}
private void hideSystemUi() {
@@ -2025,8 +2057,7 @@ public final class VideoDetailFragment
}
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) {
if (isInMultiWindow || isFullscreen()) {
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
}
@@ -2034,14 +2065,19 @@ public final class VideoDetailFragment
}
// Listener implementation
@Override
public void hideSystemUiIfNeeded() {
if (isPlayerAvailable()
&& player.isFullscreen()
if (isFullscreen()
&& bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
hideSystemUi();
}
}
private boolean isFullscreen() {
return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class)
.map(VideoPlayerUi::isFullscreen).orElse(false);
}
private boolean playerIsNotStopped() {
return isPlayerAvailable() && !player.isStopped();
}
@@ -2064,10 +2100,7 @@ public final class VideoDetailFragment
}
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
if (!isPlayerAvailable()
|| !player.videoPlayerSelected()
|| !player.isFullscreen()
|| bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
if (!isFullscreen() || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
// Apply system brightness when the player is not in fullscreen
restoreDefaultBrightness();
} else {
@@ -2091,7 +2124,7 @@ public final class VideoDetailFragment
setAutoPlay(true);
}
player.checkLandscape();
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape);
// Let's give a user time to look at video information page if video is not playing
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
player.play();
@@ -2170,12 +2203,8 @@ public final class VideoDetailFragment
} else {
final int selectedVideoStreamIndexForExternalPlayers =
ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
final CharSequence[] resolutions =
new CharSequence[videoStreamsForExternalPlayers.size()];
for (int i = 0; i < videoStreamsForExternalPlayers.size(); i++) {
resolutions[i] = videoStreamsForExternalPlayers.get(i).getResolution();
}
final CharSequence[] resolutions = videoStreamsForExternalPlayers.stream()
.map(VideoStream::getResolution).toArray(CharSequence[]::new);
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
null);
@@ -2279,7 +2308,9 @@ public final class VideoDetailFragment
final FrameLayout bottomSheetLayout = activity.findViewById(R.id.fragment_player_holder);
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
bottomSheetBehavior.setState(bottomSheetState);
bottomSheetBehavior.setState(lastStableBottomSheetState);
updateBottomSheetState(lastStableBottomSheetState);
final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height);
if (bottomSheetState != BottomSheetBehavior.STATE_HIDDEN) {
manageSpaceAtTheBottom(false);
@@ -2292,10 +2323,10 @@ public final class VideoDetailFragment
}
}
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
bottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull final View bottomSheet, final int newState) {
bottomSheetState = newState;
updateBottomSheetState(newState);
switch (newState) {
case BottomSheetBehavior.STATE_HIDDEN:
@@ -2318,10 +2349,10 @@ public final class VideoDetailFragment
if (DeviceUtils.isLandscape(requireContext())
&& isPlayerAvailable()
&& player.isPlaying()
&& !player.isFullscreen()
&& !DeviceUtils.isTablet(activity)
&& player.videoPlayerSelected()) {
player.toggleFullscreen();
&& !isFullscreen()
&& !DeviceUtils.isTablet(activity)) {
player.UIs().get(MainPlayerUi.class)
.ifPresent(MainPlayerUi::toggleFullscreen);
}
setOverlayLook(binding.appBarLayout, behavior, 1);
break;
@@ -2334,19 +2365,26 @@ public final class VideoDetailFragment
// Re-enable clicks
setOverlayElementsClickable(true);
if (isPlayerAvailable()) {
player.closeItemsList();
player.UIs().get(MainPlayerUi.class)
.ifPresent(MainPlayerUi::closeItemsList);
}
setOverlayLook(binding.appBarLayout, behavior, 0);
break;
case BottomSheetBehavior.STATE_DRAGGING:
case BottomSheetBehavior.STATE_SETTLING:
if (isPlayerAvailable() && player.isFullscreen()) {
if (isFullscreen()) {
showSystemUi();
}
if (isPlayerAvailable() && player.isControlsVisible()) {
player.hideControls(0, 0);
if (isPlayerAvailable()) {
player.UIs().get(MainPlayerUi.class).ifPresent(ui -> {
if (ui.isControlsVisible()) {
ui.hideControls(0, 0);
}
});
}
break;
case BottomSheetBehavior.STATE_HALF_EXPANDED:
break;
}
}
@@ -2354,7 +2392,9 @@ public final class VideoDetailFragment
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
setOverlayLook(binding.appBarLayout, behavior, slideOffset);
}
});
};
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback);
// User opened a new page and the player will hide itself
activity.getSupportFragmentManager().addOnBackStackChangedListener(() -> {
@@ -2364,13 +2404,25 @@ public final class VideoDetailFragment
});
}
private void updateOverlayPlayQueueButtonVisibility() {
final boolean isPlayQueueEmpty =
player == null // no player => no play queue :)
|| player.getPlayQueue() == null
|| player.getPlayQueue().isEmpty();
if (binding != null) {
// binding is null when rotating the device...
binding.overlayPlayQueueButton.setVisibility(
isPlayQueueEmpty ? View.GONE : View.VISIBLE);
}
}
private void updateOverlayData(@Nullable final String overlayTitle,
@Nullable final String uploader,
@Nullable final String thumbnailUrl) {
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
binding.overlayThumbnail.setImageResource(R.drawable.dummy_thumbnail_dark);
PicassoHelper.loadThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
binding.overlayThumbnail.setImageDrawable(null);
PicassoHelper.loadDetailsThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.overlayThumbnail);
}
@@ -2402,6 +2454,7 @@ public final class VideoDetailFragment
binding.overlayMetadataLayout.setClickable(enable);
binding.overlayMetadataLayout.setLongClickable(enable);
binding.overlayButtonsLayout.setClickable(enable);
binding.overlayPlayQueueButton.setClickable(enable);
binding.overlayPlayPauseButton.setClickable(enable);
binding.overlayCloseButton.setClickable(enable);
}
@@ -2418,4 +2471,21 @@ public final class VideoDetailFragment
boolean isPlayerAndPlayerServiceAvailable() {
return (player != null && playerService != null);
}
public Optional<View> getRoot() {
if (player == null) {
return Optional.empty();
}
return player.UIs().get(VideoPlayerUi.class)
.map(playerUi -> playerUi.getBinding().getRoot());
}
private void updateBottomSheetState(final int newState) {
bottomSheetState = newState;
if (newState != BottomSheetBehavior.STATE_DRAGGING
&& newState != BottomSheetBehavior.STATE_SETTLING) {
lastStableBottomSheetState = newState;
}
}
}

View File

@@ -6,6 +6,7 @@ import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECI
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.ViewGroup;
@@ -28,9 +29,7 @@ import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.List;
import java.util.function.Supplier;
/**
@@ -43,50 +42,34 @@ public final class VideoDetailPlayerCrasher {
// https://stackoverflow.com/a/54744028
private static final String TAG = "VideoDetPlayerCrasher";
private static final Map<String, Supplier<ExoPlaybackException>> AVAILABLE_EXCEPTION_TYPES =
getExceptionTypes();
private static final String DEFAULT_MSG = "Dummy";
private static final List<Pair<String, Supplier<ExoPlaybackException>>>
AVAILABLE_EXCEPTION_TYPES = List.of(
new Pair<>("Source", () -> ExoPlaybackException.createForSource(
new IOException(DEFAULT_MSG),
ERROR_CODE_BEHIND_LIVE_WINDOW
)),
new Pair<>("Renderer", () -> ExoPlaybackException.createForRenderer(
new Exception(DEFAULT_MSG),
"Dummy renderer",
0,
null,
C.FORMAT_HANDLED,
/*isRecoverable=*/false,
ERROR_CODE_DECODING_FAILED
)),
new Pair<>("Unexpected", () -> ExoPlaybackException.createForUnexpected(
new RuntimeException(DEFAULT_MSG),
ERROR_CODE_UNSPECIFIED
)),
new Pair<>("Remote", () -> ExoPlaybackException.createForRemote(DEFAULT_MSG))
);
private VideoDetailPlayerCrasher() {
// No impls
}
private static Map<String, Supplier<ExoPlaybackException>> getExceptionTypes() {
final String defaultMsg = "Dummy";
final Map<String, Supplier<ExoPlaybackException>> exceptionTypes = new LinkedHashMap<>();
exceptionTypes.put(
"Source",
() -> ExoPlaybackException.createForSource(
new IOException(defaultMsg),
ERROR_CODE_BEHIND_LIVE_WINDOW
)
);
exceptionTypes.put(
"Renderer",
() -> ExoPlaybackException.createForRenderer(
new Exception(defaultMsg),
"Dummy renderer",
0,
null,
C.FORMAT_HANDLED,
/*isRecoverable=*/false,
ERROR_CODE_DECODING_FAILED
)
);
exceptionTypes.put(
"Unexpected",
() -> ExoPlaybackException.createForUnexpected(
new RuntimeException(defaultMsg),
ERROR_CODE_UNSPECIFIED
)
);
exceptionTypes.put(
"Remote",
() -> ExoPlaybackException.createForRemote(defaultMsg)
);
return Collections.unmodifiableMap(exceptionTypes);
}
private static Context getThemeWrapperContext(final Context context) {
return new ContextThemeWrapper(
context,
@@ -121,10 +104,9 @@ public final class VideoDetailPlayerCrasher {
.setNegativeButton(R.string.cancel, null)
.create();
for (final Map.Entry<String, Supplier<ExoPlaybackException>> entry
: AVAILABLE_EXCEPTION_TYPES.entrySet()) {
for (final Pair<String, Supplier<ExoPlaybackException>> entry : AVAILABLE_EXCEPTION_TYPES) {
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
radioButton.setText(entry.getKey());
radioButton.setText(entry.first);
radioButton.setChecked(false);
radioButton.setLayoutParams(
new RadioGroup.LayoutParams(
@@ -133,7 +115,7 @@ public final class VideoDetailPlayerCrasher {
)
);
radioButton.setOnClickListener(v -> {
tryCrashPlayerWith(player, entry.getValue().get());
tryCrashPlayerWith(player, entry.second.get());
alertDialog.cancel();
});
binding.list.addView(radioButton);

View File

@@ -5,7 +5,6 @@ import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingSc
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
@@ -23,17 +22,15 @@ import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.SuperScrollLayoutManager;
import java.util.List;
@@ -264,45 +261,28 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
}
});
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final ChannelInfoItem selectedItem) {
try {
onItemSelected(selectedItem);
NavigationHelper.openChannelFragment(getFM(),
selectedItem.getServiceId(),
selectedItem.getUrl(),
selectedItem.getName());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(
BaseListFragment.this, "Opening channel fragment", e);
}
}
});
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final PlaylistInfoItem selectedItem) {
try {
onItemSelected(selectedItem);
NavigationHelper.openPlaylistFragment(getFM(),
selectedItem.getServiceId(),
selectedItem.getUrl(),
selectedItem.getName());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(BaseListFragment.this,
"Opening playlist fragment", e);
}
}
});
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final CommentsInfoItem selectedItem) {
infoListAdapter.setOnChannelSelectedListener(selectedItem -> {
try {
onItemSelected(selectedItem);
NavigationHelper.openChannelFragment(getFM(), selectedItem.getServiceId(),
selectedItem.getUrl(), selectedItem.getName());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
}
});
infoListAdapter.setOnPlaylistSelectedListener(selectedItem -> {
try {
onItemSelected(selectedItem);
NavigationHelper.openPlaylistFragment(getFM(), selectedItem.getServiceId(),
selectedItem.getUrl(), selectedItem.getName());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Opening playlist fragment", e);
}
});
infoListAdapter.setOnCommentsSelectedListener(this::onItemSelected);
// Ensure that there is always a scroll listener (e.g. when rotating the device)
useNormalItemListScrollListener();
}
@@ -496,15 +476,6 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
}
protected boolean isGridLayout() {
final String listMode = PreferenceManager.getDefaultSharedPreferences(activity)
.getString(getString(R.string.list_view_mode_key),
getString(R.string.list_view_mode_value));
if ("auto".equals(listMode)) {
final Configuration configuration = getResources().getConfiguration();
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
} else {
return "grid".equals(listMode);
}
return ThemeHelper.shouldUseGridLayout(activity);
}
}

View File

@@ -43,7 +43,7 @@ import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
import org.schabi.newpipe.player.MainPlayer.PlayerType;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
@@ -578,17 +578,13 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
}
private PlayQueue getPlayQueue() {
return getPlayQueue(0);
}
private PlayQueue getPlayQueue(final int index) {
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
.filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast)
.collect(Collectors.toList());
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
currentInfo.getNextPage(), streamItems, index);
currentInfo.getNextPage(), streamItems, 0);
}
/*//////////////////////////////////////////////////////////////////////////

View File

@@ -43,7 +43,7 @@ import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.player.MainPlayer.PlayerType;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;

View File

@@ -200,7 +200,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
showLocalSuggestions = NewPipeSettings.showLocalSearchSuggestions(activity, prefs);
showRemoteSuggestions = NewPipeSettings.showRemoteSearchSuggestions(activity, prefs);
suggestionListAdapter = new SuggestionListAdapter(activity);
suggestionListAdapter = new SuggestionListAdapter();
historyRecordManager = new HistoryRecordManager(context);
}
@@ -340,6 +340,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
super.initViews(rootView, savedInstanceState);
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
// animations are just strange and useless, since the suggestions keep changing too much
searchBinding.suggestionsList.setItemAnimator(null);
new ItemTouchHelper(new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(@NonNull final RecyclerView recyclerView,
@@ -497,9 +499,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
+ lastSearchedString);
}
searchEditText.setText(searchString);
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
searchEditText.setHintTextColor(searchEditText.getTextColors().withAlpha(128));
}
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
searchToolbarContainer.setTranslationX(100);
@@ -533,7 +532,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
searchBinding.correctSuggestion.setVisibility(View.GONE);
searchEditText.setText("");
suggestionListAdapter.setItems(new ArrayList<>());
suggestionListAdapter.submitList(null);
showKeyboardSearch();
});
@@ -922,7 +921,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
filterItemCheckedId = item.getItemId();
item.setChecked(true);
contentFilter = new String[]{theContentFilter.get(0)};
contentFilter = theContentFilter.toArray(new String[0]);
if (!TextUtils.isEmpty(searchString)) {
search(searchString, contentFilter, sortFilter);
@@ -947,8 +946,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (DEBUG) {
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
}
searchBinding.suggestionsList.smoothScrollToPosition(0);
searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions));
suggestionListAdapter.submitList(suggestions,
() -> searchBinding.suggestionsList.scrollToPosition(0));
if (suggestionsPanelVisible && isErrorPanelVisible()) {
hideLoading();
@@ -983,8 +982,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
isCorrectedSearch = result.isCorrectedSearch();
// List<MetaInfo> cannot be bundled without creating some containers
metaInfo = new MetaInfo[result.getMetaInfo().size()];
metaInfo = result.getMetaInfo().toArray(metaInfo);
metaInfo = result.getMetaInfo().toArray(new MetaInfo[0]);
showMetaInfoInTextView(result.getMetaInfo(), searchBinding.searchMetaInfoTextView,
searchBinding.searchMetaInfoSeparator, disposables);
@@ -1070,14 +1068,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
return 0;
}
final SuggestionItem item = suggestionListAdapter.getItem(position);
final SuggestionItem item = suggestionListAdapter.getCurrentList().get(position);
return item.fromHistory ? makeMovementFlags(0,
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
}
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) {
final int position = viewHolder.getBindingAdapterPosition();
final String query = suggestionListAdapter.getItem(position).query;
final String query = suggestionListAdapter.getCurrentList().get(position).query;
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(

View File

@@ -1,34 +1,22 @@
package org.schabi.newpipe.fragments.list.search;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
import java.util.ArrayList;
import java.util.List;
import org.schabi.newpipe.databinding.ItemSearchSuggestionBinding;
public class SuggestionListAdapter
extends RecyclerView.Adapter<SuggestionListAdapter.SuggestionItemHolder> {
private final ArrayList<SuggestionItem> items = new ArrayList<>();
private final Context context;
extends ListAdapter<SuggestionItem, SuggestionListAdapter.SuggestionItemHolder> {
private OnSuggestionItemSelected listener;
public SuggestionListAdapter(final Context context) {
this.context = context;
}
public void setItems(final List<SuggestionItem> items) {
this.items.clear();
this.items.addAll(items);
notifyDataSetChanged();
public SuggestionListAdapter() {
super(new SuggestionItemCallback());
}
public void setListener(final OnSuggestionItemSelected listener) {
@@ -39,45 +27,32 @@ public class SuggestionListAdapter
@Override
public SuggestionItemHolder onCreateViewHolder(@NonNull final ViewGroup parent,
final int viewType) {
return new SuggestionItemHolder(LayoutInflater.from(context)
.inflate(R.layout.item_search_suggestion, parent, false));
return new SuggestionItemHolder(ItemSearchSuggestionBinding
.inflate(LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
public void onBindViewHolder(final SuggestionItemHolder holder, final int position) {
final SuggestionItem currentItem = getItem(position);
holder.updateFrom(currentItem);
holder.queryView.setOnClickListener(v -> {
holder.itemBinding.suggestionSearch.setOnClickListener(v -> {
if (listener != null) {
listener.onSuggestionItemSelected(currentItem);
}
});
holder.queryView.setOnLongClickListener(v -> {
holder.itemBinding.suggestionSearch.setOnLongClickListener(v -> {
if (listener != null) {
listener.onSuggestionItemLongClick(currentItem);
}
return true;
});
holder.insertView.setOnClickListener(v -> {
holder.itemBinding.suggestionInsert.setOnClickListener(v -> {
if (listener != null) {
listener.onSuggestionItemInserted(currentItem);
}
});
}
SuggestionItem getItem(final int position) {
return items.get(position);
}
@Override
public int getItemCount() {
return items.size();
}
public boolean isEmpty() {
return getItemCount() == 0;
}
public interface OnSuggestionItemSelected {
void onSuggestionItemSelected(SuggestionItem item);
@@ -87,30 +62,32 @@ public class SuggestionListAdapter
}
public static final class SuggestionItemHolder extends RecyclerView.ViewHolder {
private final TextView itemSuggestionQuery;
private final ImageView suggestionIcon;
private final View queryView;
private final View insertView;
private final ItemSearchSuggestionBinding itemBinding;
// Cache some ids, as they can potentially be constantly updated/recycled
private final int historyResId;
private final int searchResId;
private SuggestionItemHolder(final View rootView) {
super(rootView);
suggestionIcon = rootView.findViewById(R.id.item_suggestion_icon);
itemSuggestionQuery = rootView.findViewById(R.id.item_suggestion_query);
queryView = rootView.findViewById(R.id.suggestion_search);
insertView = rootView.findViewById(R.id.suggestion_insert);
historyResId = R.drawable.ic_history;
searchResId = R.drawable.ic_search;
private SuggestionItemHolder(final ItemSearchSuggestionBinding binding) {
super(binding.getRoot());
this.itemBinding = binding;
}
private void updateFrom(final SuggestionItem item) {
suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId);
itemSuggestionQuery.setText(item.query);
itemBinding.itemSuggestionIcon.setImageResource(item.fromHistory ? R.drawable.ic_history
: R.drawable.ic_search);
itemBinding.itemSuggestionQuery.setText(item.query);
}
}
private static class SuggestionItemCallback extends DiffUtil.ItemCallback<SuggestionItem> {
@Override
public boolean areItemsTheSame(@NonNull final SuggestionItem oldItem,
@NonNull final SuggestionItem newItem) {
return oldItem.fromHistory == newItem.fromHistory
&& oldItem.query.equals(newItem.query);
}
@Override
public boolean areContentsTheSame(@NonNull final SuggestionItem oldItem,
@NonNull final SuggestionItem newItem) {
return true; // items' contents never change; the list of items themselves does
}
}
}

View File

@@ -67,8 +67,8 @@ public class InfoItemBuilder {
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager,
final boolean useMiniVariant) {
final InfoItemHolder holder
= holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
final InfoItemHolder holder =
holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
holder.updateFromItem(infoItem, historyRecordManager);
return holder.itemView;
}

View File

@@ -61,5 +61,6 @@ class StreamSegmentAdapter(
interface StreamSegmentListener {
fun onItemClick(item: StreamSegmentItem, seconds: Int)
fun onItemLongClick(item: StreamSegmentItem, seconds: Int)
}
}

View File

@@ -41,6 +41,7 @@ class StreamSegmentItem(
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
Localization.getDurationString(item.startTimeSeconds.toLong())
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewHolder.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
viewHolder.root.isSelected = isSelected
}

View File

@@ -321,6 +321,7 @@ public final class InfoItemDialog {
*/
public Builder addDefaultEndEntries() {
addAllEntries(
StreamDialogDefaultEntry.DOWNLOAD,
StreamDialogDefaultEntry.APPEND_PLAYLIST,
StreamDialogDefaultEntry.SHARE,
StreamDialogDefaultEntry.OPEN_IN_BROWSER

View File

@@ -2,6 +2,7 @@ package org.schabi.newpipe.info_list.dialog;
import static org.schabi.newpipe.util.NavigationHelper.openChannelFragment;
import static org.schabi.newpipe.util.SparseItemUtil.fetchItemInfoIfSparse;
import static org.schabi.newpipe.util.SparseItemUtil.fetchStreamInfoAndSaveToDatabase;
import static org.schabi.newpipe.util.SparseItemUtil.fetchUploaderUrlIfSparse;
import android.net.Uri;
@@ -11,6 +12,7 @@ import androidx.annotation.StringRes;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
@@ -18,7 +20,7 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.Collections;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@@ -87,7 +89,7 @@ public enum StreamDialogDefaultEntry {
APPEND_PLAYLIST(R.string.add_to_playlist, (fragment, item) ->
PlaylistDialog.createCorrespondingDialog(
fragment.getContext(),
Collections.singletonList(new StreamEntity(item)),
List.of(new StreamEntity(item)),
dialog -> dialog.show(
fragment.getParentFragmentManager(),
"StreamDialogEntry@"
@@ -110,6 +112,22 @@ public enum StreamDialogDefaultEntry {
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
item.getThumbnailUrl())),
/**
* Opens a {@link DownloadDialog} after fetching some stream info.
* If the user quits the current fragment, it will not open a DownloadDialog.
*/
DOWNLOAD(R.string.download, (fragment, item) ->
fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(),
item.getUrl(), info -> {
if (fragment.getContext() != null) {
final DownloadDialog downloadDialog =
new DownloadDialog(fragment.requireContext(), info);
downloadDialog.show(fragment.getChildFragmentManager(),
"downloadDialog");
}
})
),
OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) ->
ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())),

View File

@@ -42,7 +42,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemTitleView.setText(item.getName());
itemAdditionalDetailView.setText(getDetailLine(item));
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
PicassoHelper.loadAvatar(item.getThumbnailUrl()).into(itemThumbnailView);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnChannelSelectedListener() != null) {

View File

@@ -12,6 +12,7 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.text.util.LinkifyCompat;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorUtil;
@@ -23,11 +24,11 @@ import org.schabi.newpipe.util.CommentTextOnTouchListener;
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.TimestampExtractor;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.external_communication.TimestampExtractor;
import java.util.regex.Matcher;
import java.util.Objects;
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private static final String TAG = "CommentsMiniIIHolder";
@@ -39,7 +40,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private final int commentVerticalPadding;
private final RelativeLayout itemRoot;
public final ImageView itemThumbnailView;
private final ImageView itemThumbnailView;
private final TextView itemContentView;
private final TextView itemLikesCountView;
private final TextView itemPublishedTime;
@@ -47,27 +48,6 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private String commentText;
private String streamUrl;
private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() {
@Override
public String transformUrl(final Matcher match, final String url) {
try {
final TimestampExtractor.TimestampMatchDTO timestampMatchDTO =
TimestampExtractor.getTimestampFromMatcher(match, commentText);
if (timestampMatchDTO == null) {
return url;
}
return streamUrl + url.replace(
match.group(0),
"#timestamp=" + timestampMatchDTO.seconds());
} catch (final Exception ex) {
Log.e(TAG, "Unable to process url='" + url + "' as timestampLink", ex);
return url;
}
}
};
CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
final ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
@@ -115,7 +95,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
itemContentView.setLines(COMMENT_DEFAULT_LINES);
commentText = item.getCommentText();
itemContentView.setText(commentText);
itemContentView.setText(commentText, TextView.BufferType.SPANNABLE);
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
if (itemContentView.getLineCount() == 0) {
@@ -204,8 +184,9 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
boolean hasEllipsis = false;
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
final int endOfLastLine
= itemContentView.getLayout().getLineEnd(COMMENT_DEFAULT_LINES - 1);
final int endOfLastLine = itemContentView
.getLayout()
.getLineEnd(COMMENT_DEFAULT_LINES - 1);
int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2);
if (end == -1) {
end = Math.max(endOfLastLine - 2, 0);
@@ -242,14 +223,21 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
}
private void linkify() {
Linkify.addLinks(
itemContentView,
Linkify.WEB_URLS);
Linkify.addLinks(
itemContentView,
TimestampExtractor.TIMESTAMPS_PATTERN,
null,
null,
timestampLink);
LinkifyCompat.addLinks(itemContentView, Linkify.WEB_URLS);
LinkifyCompat.addLinks(itemContentView, TimestampExtractor.TIMESTAMPS_PATTERN, null, null,
(match, url) -> {
try {
final var timestampMatch = TimestampExtractor
.getTimestampFromMatcher(match, commentText);
if (timestampMatch == null) {
return url;
}
return streamUrl + url.replace(Objects.requireNonNull(match.group(0)),
"#timestamp=" + timestampMatch.seconds());
} catch (final Exception ex) {
Log.e(TAG, "Unable to process url='" + url + "' as timestampLink", ex);
return url;
}
});
}
}

View File

@@ -14,8 +14,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.views.AnimatedProgressBar;
@@ -111,8 +111,9 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
final HistoryRecordManager historyRecordManager) {
final StreamInfoItem item = (StreamInfoItem) infoItem;
final StreamStateEntity state
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
final StreamStateEntity state = historyRecordManager
.loadStreamState(infoItem)
.blockingGet()[0];
if (state != null && item.getDuration() > 0
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
itemProgressView.setMax((int) item.getDuration());

View File

@@ -21,10 +21,6 @@ import org.schabi.newpipe.MainActivity
private const val TAG = "ViewUtils"
inline var View.backgroundTintListCompat: ColorStateList?
get() = ViewCompat.getBackgroundTintList(this)
set(value) = ViewCompat.setBackgroundTintList(this, value)
/**
* Animate the view.
*
@@ -96,62 +92,43 @@ fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @Colo
if (MainActivity.DEBUG) {
Log.d(
TAG,
"animateBackgroundColor() called with: " +
"view = [" + this + "], duration = [" + duration + "], " +
"colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"
"animateBackgroundColor() called with: view = [$this], duration = [$duration], " +
"colorStart = [$colorStart], colorEnd = [$colorEnd]"
)
}
val empty = arrayOf(IntArray(0))
val viewPropertyAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorStart, colorEnd)
viewPropertyAnimator.interpolator = FastOutSlowInInterpolator()
viewPropertyAnimator.duration = duration
viewPropertyAnimator.addUpdateListener { animation: ValueAnimator ->
backgroundTintListCompat = ColorStateList(empty, intArrayOf(animation.animatedValue as Int))
fun listenerAction(color: Int) {
ViewCompat.setBackgroundTintList(this, ColorStateList.valueOf(color))
}
viewPropertyAnimator.addListener(
onCancel = { backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd)) },
onEnd = { backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd)) }
)
viewPropertyAnimator.addUpdateListener { listenerAction(it.animatedValue as Int) }
viewPropertyAnimator.addListener(onCancel = { listenerAction(colorEnd) }, onEnd = { listenerAction(colorEnd) })
viewPropertyAnimator.start()
}
fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
if (MainActivity.DEBUG) {
Log.d(
TAG,
"animateHeight: duration = [" + duration + "], " +
"from " + height + " to → " + targetHeight + " in: " + this
)
Log.d(TAG, "animateHeight: duration = [$duration], from $height to → $targetHeight in: $this")
}
val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat())
animator.interpolator = FastOutSlowInInterpolator()
animator.duration = duration
animator.addUpdateListener { animation: ValueAnimator ->
val value = animation.animatedValue as Float
layoutParams.height = value.toInt()
fun listenerAction(value: Int) {
layoutParams.height = value
requestLayout()
}
animator.addListener(
onCancel = {
layoutParams.height = targetHeight
requestLayout()
},
onEnd = {
layoutParams.height = targetHeight
requestLayout()
}
)
animator.addUpdateListener { listenerAction((it.animatedValue as Float).toInt()) }
animator.addListener(onCancel = { listenerAction(targetHeight) }, onEnd = { listenerAction(targetHeight) })
animator.start()
return animator
}
fun View.animateRotation(duration: Long, targetRotation: Int) {
if (MainActivity.DEBUG) {
Log.d(
TAG,
"animateRotation: duration = [" + duration + "], " +
"from " + rotation + " to → " + targetRotation + " in: " + this
)
Log.d(TAG, "animateRotation: duration = [$duration], from $rotation to → $targetRotation in: $this")
}
animate().setListener(null).cancel()
animate()
@@ -172,20 +149,13 @@ private fun View.animateAlpha(enterOrExit: Boolean, duration: Long, delay: Long,
if (enterOrExit) {
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(1f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
}).start()
.setListener(ExecOnEndListener(execOnEnd))
.start()
} else {
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(0f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
isGone = true
execOnEnd?.run()
}
}).start()
.setListener(HideAndExecOnEndListener(this, execOnEnd))
.start()
}
}
@@ -197,11 +167,8 @@ private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, dela
.setInterpolator(FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
}).start()
.setListener(ExecOnEndListener(execOnEnd))
.start()
} else {
scaleX = 1f
scaleY = 1f
@@ -209,12 +176,8 @@ private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, dela
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).scaleX(.8f).scaleY(.8f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
isGone = true
execOnEnd?.run()
}
}).start()
.setListener(HideAndExecOnEndListener(this, execOnEnd))
.start()
}
}
@@ -227,11 +190,8 @@ private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long,
.setInterpolator(FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
}).start()
.setListener(ExecOnEndListener(execOnEnd))
.start()
} else {
alpha = 1f
scaleX = 1f
@@ -240,12 +200,8 @@ private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long,
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).scaleX(.95f).scaleY(.95f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
isGone = true
execOnEnd?.run()
}
}).start()
.setListener(HideAndExecOnEndListener(this, execOnEnd))
.start()
}
}
@@ -256,22 +212,15 @@ private fun View.animateSlideAndAlpha(enterOrExit: Boolean, duration: Long, dela
animate()
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
}).start()
.setListener(ExecOnEndListener(execOnEnd))
.start()
} else {
animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).translationY(-height.toFloat())
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
isGone = true
execOnEnd?.run()
}
}).start()
.setListener(HideAndExecOnEndListener(this, execOnEnd))
.start()
}
}
@@ -282,21 +231,14 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long,
animate()
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
}).start()
.setListener(ExecOnEndListener(execOnEnd))
.start()
} else {
animate().setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).translationY(-height / 2.0f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
isGone = true
execOnEnd?.run()
}
}).start()
.setListener(HideAndExecOnEndListener(this, execOnEnd))
.start()
}
}
@@ -318,11 +260,7 @@ fun View.slideUp(
.setStartDelay(delay)
.setDuration(duration)
.setInterpolator(FastOutSlowInInterpolator())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
})
.setListener(ExecOnEndListener(execOnEnd))
.start()
}
@@ -336,6 +274,20 @@ fun View.animateHideRecyclerViewAllowingScrolling() {
animate().alpha(0.0f).setDuration(200).start()
}
private open class ExecOnEndListener(private val execOnEnd: Runnable?) : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
}
private class HideAndExecOnEndListener(private val view: View, execOnEnd: Runnable?) :
ExecOnEndListener(execOnEnd) {
override fun onAnimationEnd(animation: Animator) {
view.isGone = true
super.onAnimationEnd(animation)
}
}
enum class AnimationType {
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
}

View File

@@ -98,7 +98,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
protected void initListeners() {
super.initListeners();
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final LocalItem selectedItem) {
final FragmentManager fragmentManager = getFM();
@@ -256,8 +256,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
}
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
final DialogEditTextBinding dialogBinding
= DialogEditTextBinding.inflate(getLayoutInflater());
final DialogEditTextBinding dialogBinding =
DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
dialogBinding.dialogEditText.setText(selectedItem.name);

View File

@@ -13,12 +13,10 @@ import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.local.LocalItemListAdapter;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.util.OnClickGesture;
import java.util.List;
@@ -63,18 +61,10 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
playlistAdapter = new LocalItemListAdapter(getActivity());
playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
@Override
public void selected(final LocalItem selectedItem) {
if (!(selectedItem instanceof PlaylistMetadataEntry)
|| getStreamEntities() == null) {
return;
}
onPlaylistSelected(
playlistManager,
(PlaylistMetadataEntry) selectedItem,
getStreamEntities()
);
playlistAdapter.setSelectedListener(selectedItem -> {
final List<StreamEntity> entities = getStreamEntities();
if (selectedItem instanceof PlaylistMetadataEntry && entities != null) {
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, entities);
}
});
@@ -138,14 +128,11 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
@NonNull final PlaylistMetadataEntry playlist,
@NonNull final List<StreamEntity> streams) {
if (getStreamEntities() == null) {
return;
}
final Toast successToast = Toast.makeText(getContext(),
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
if (playlist.thumbnailUrl.equals("drawable://" + R.drawable.dummy_thumbnail_playlist)) {
if (playlist.thumbnailUrl
.equals("drawable://" + R.drawable.placeholder_thumbnail_playlist)) {
playlistDisposables.add(manager
.changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl())
.observeOn(AndroidSchedulers.mainThread())

View File

@@ -45,8 +45,8 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
return super.onCreateDialog(savedInstanceState);
}
final DialogEditTextBinding dialogBinding
= DialogEditTextBinding.inflate(getLayoutInflater());
final DialogEditTextBinding dialogBinding =
DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.getRoot().getContext().setTheme(ThemeHelper.getDialogTheme(requireContext()));
dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);

View File

@@ -9,15 +9,20 @@ import android.view.Window;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.util.StateSaver;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
@@ -131,13 +136,13 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
* @param context context used for accessing the database
* @param streamEntities used for crating the dialog
* @param onExec execution that should occur after a dialog got created, e.g. showing it
* @return Disposable
* @return the disposable that was created
*/
public static Disposable createCorrespondingDialog(
final Context context,
final List<StreamEntity> streamEntities,
final Consumer<PlaylistDialog> onExec
) {
final Consumer<PlaylistDialog> onExec) {
return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
.hasPlaylists()
.observeOn(AndroidSchedulers.mainThread())
@@ -147,4 +152,30 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
: PlaylistCreationDialog.newInstance(streamEntities))
);
}
/**
* Creates a {@link PlaylistAppendDialog} when playlists exists,
* otherwise a {@link PlaylistCreationDialog}. If the player's play queue is null or empty, no
* dialog will be created.
*
* @param player the player from which to extract the context and the play queue
* @param fragmentManager the fragment manager to use to show the dialog
* @return the disposable that was created
*/
public static Disposable showForPlayQueue(
final Player player,
@NonNull final FragmentManager fragmentManager) {
final List<StreamEntity> streamEntities = Stream.of(player.getPlayQueue())
.filter(Objects::nonNull)
.flatMap(playQueue -> playQueue.getStreams().stream())
.map(StreamEntity::new)
.collect(Collectors.toList());
if (streamEntities.isEmpty()) {
return Disposable.empty();
}
return PlaylistDialog.createCorrespondingDialog(player.getContext(), streamEntities,
dialog -> dialog.show(fragmentManager, "PlaylistDialog"));
}
}

View File

@@ -41,19 +41,15 @@ class FeedDatabaseManager(context: Context) {
fun database() = database
fun getStreams(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
getPlayedStreams: Boolean = true
groupId: Long,
includePlayedStreams: Boolean,
includeFutureStreams: Boolean
): Maybe<List<StreamWithState>> {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> {
if (getPlayedStreams) feedTable.getAllStreams()
else feedTable.getLiveOrNotPlayedStreams()
}
else -> {
if (getPlayedStreams) feedTable.getAllStreamsForGroup(groupId)
else feedTable.getLiveOrNotPlayedStreamsForGroup(groupId)
}
}
return feedTable.getStreams(
groupId,
includePlayedStreams,
if (includeFutureStreams) null else OffsetDateTime.now()
)
}
fun outdatedSubscriptions(outdatedThreshold: OffsetDateTime) = feedTable.getAllOutdated(outdatedThreshold)

View File

@@ -40,7 +40,9 @@ import androidx.annotation.Nullable
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.edit
import androidx.core.math.MathUtils
import androidx.core.os.bundleOf
import androidx.core.view.MenuItemCompat
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
@@ -98,6 +100,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
private lateinit var groupAdapter: GroupieAdapter
@State @JvmField var showPlayedItems: Boolean = true
@State @JvmField var showFutureItems: Boolean = true
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
private var updateListViewModeOnResume = false
@@ -134,9 +137,10 @@ class FeedFragment : BaseStateFragment<FeedState>() {
_feedBinding = FragmentFeedBinding.bind(rootView)
super.onViewCreated(rootView, savedInstanceState)
val factory = FeedViewModel.Factory(requireContext(), groupId)
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
val factory = FeedViewModel.getFactory(requireContext(), groupId)
viewModel = ViewModelProvider(this, factory)[FeedViewModel::class.java]
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
showFutureItems = viewModel.getShowFutureItemsFromPreferences()
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
groupAdapter = GroupieAdapter().apply {
@@ -212,6 +216,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
inflater.inflate(R.menu.menu_feed_fragment, menu)
updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items))
updateToggleFutureItemsButton(menu.findItem(R.id.menu_item_feed_toggle_future_items))
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -241,6 +246,11 @@ class FeedFragment : BaseStateFragment<FeedState>() {
updateTogglePlayedItemsButton(item)
viewModel.togglePlayedItems(showPlayedItems)
viewModel.saveShowPlayedItemsToPreferences(showPlayedItems)
} else if (item.itemId == R.id.menu_item_feed_toggle_future_items) {
showFutureItems = !item.isChecked
updateToggleFutureItemsButton(item)
viewModel.toggleFutureItems(showFutureItems)
viewModel.saveShowFutureItemsToPreferences(showFutureItems)
}
return super.onOptionsItemSelected(item)
@@ -278,6 +288,32 @@ class FeedFragment : BaseStateFragment<FeedState>() {
requireContext(),
if (showPlayedItems) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off
)
MenuItemCompat.setTooltipText(
menuItem,
getString(
if (showPlayedItems)
R.string.feed_toggle_hide_played_items
else
R.string.feed_toggle_show_played_items
)
)
}
private fun updateToggleFutureItemsButton(menuItem: MenuItem) {
menuItem.isChecked = showFutureItems
menuItem.icon = AppCompatResources.getDrawable(
requireContext(),
if (showFutureItems) R.drawable.ic_history_future else R.drawable.ic_history
)
MenuItemCompat.setTooltipText(
menuItem,
getString(
if (showFutureItems)
R.string.feed_toggle_hide_future_items
else
R.string.feed_toggle_show_future_items
)
)
}
// //////////////////////////////////////////////////////////////////////////
@@ -568,7 +604,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
// state until the user scrolls them out of the visible area which causes a update/bind-call
groupAdapter.notifyItemRangeChanged(
0,
minOf(groupAdapter.itemCount, maxOf(highlightCount, lastNewItemsCount))
MathUtils.clamp(highlightCount, lastNewItemsCount, groupAdapter.itemCount)
)
if (highlightCount > 0) {

View File

@@ -1,17 +1,20 @@
package org.schabi.newpipe.local.feed
import android.app.Application
import android.content.Context
import androidx.core.content.edit
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.preference.PreferenceManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.functions.Function4
import io.reactivex.rxjava3.functions.Function5
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.App
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.stream.StreamWithState
@@ -26,17 +29,23 @@ import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit
class FeedViewModel(
private val applicationContext: Context,
private val application: Application,
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
initialShowPlayedItems: Boolean = true
initialShowPlayedItems: Boolean = true,
initialShowFutureItems: Boolean = true
) : ViewModel() {
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
private val feedDatabaseManager = FeedDatabaseManager(application)
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems
.startWithItem(initialShowPlayedItems)
.distinctUntilChanged()
private val toggleShowFutureItems = BehaviorProcessor.create<Boolean>()
private val toggleShowFutureItemsFlowable = toggleShowFutureItems
.startWithItem(initialShowFutureItems)
.distinctUntilChanged()
private val mutableStateLiveData = MutableLiveData<FeedState>()
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
@@ -44,21 +53,22 @@ class FeedViewModel(
.combineLatest(
FeedEventManager.events(),
toggleShowPlayedItemsFlowable,
toggleShowFutureItemsFlowable,
feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
Function4 { t1: FeedEventManager.Event, t2: Boolean,
t3: Long, t4: List<OffsetDateTime> ->
return@Function4 CombineResultEventHolder(t1, t2, t3, t4.firstOrNull())
Function5 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean,
t4: Long, t5: List<OffsetDateTime> ->
return@Function5 CombineResultEventHolder(t1, t2, t3, t4, t5.firstOrNull())
}
)
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.map { (event, showPlayedItems, notLoadedCount, oldestUpdate) ->
.map { (event, showPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) ->
val streamItems = if (event is SuccessResultEvent || event is IdleEvent)
feedDatabaseManager
.getStreams(groupId, showPlayedItems)
.getStreams(groupId, showPlayedItems, showFutureItems)
.blockingGet(arrayListOf())
else
arrayListOf()
@@ -89,8 +99,9 @@ class FeedViewModel(
private data class CombineResultEventHolder(
val t1: FeedEventManager.Event,
val t2: Boolean,
val t3: Long,
val t4: OffsetDateTime?
val t3: Boolean,
val t4: Long,
val t5: OffsetDateTime?
)
private data class CombineResultDataHolder(
@@ -105,31 +116,42 @@ class FeedViewModel(
}
fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) =
PreferenceManager.getDefaultSharedPreferences(applicationContext).edit {
this.putBoolean(applicationContext.getString(R.string.feed_show_played_items_key), showPlayedItems)
PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_played_items_key), showPlayedItems)
this.apply()
}
fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(applicationContext)
fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(application)
fun toggleFutureItems(showFutureItems: Boolean) {
toggleShowFutureItems.onNext(showFutureItems)
}
fun saveShowFutureItemsToPreferences(showFutureItems: Boolean) =
PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
this.apply()
}
fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(application)
companion object {
private fun getShowPlayedItemsFromPreferences(context: Context) =
PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.feed_show_played_items_key), true)
}
class Factory(
private val context: Context,
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return FeedViewModel(
context.applicationContext,
groupId,
// Read initial value from preferences
getShowPlayedItemsFromPreferences(context.applicationContext)
) as T
private fun getShowFutureItemsFromPreferences(context: Context) =
PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.feed_show_future_items_key), true)
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
initializer {
FeedViewModel(
App.getApp(),
groupId,
// Read initial value from preferences
getShowPlayedItemsFromPreferences(context.applicationContext),
getShowFutureItemsFromPreferences(context.applicationContext)
)
}
}
}
}

View File

@@ -4,6 +4,8 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.provider.Settings
@@ -11,6 +13,8 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
@@ -27,6 +31,8 @@ class NotificationHelper(val context: Context) {
Context.NOTIFICATION_SERVICE
) as NotificationManager
private val iconLoadingTargets = ArrayList<Target>()
/**
* Show a notification about new streams from a single channel.
* Opening the notification will open the corresponding channel page.
@@ -77,10 +83,29 @@ class NotificationHelper(val context: Context) {
)
)
PicassoHelper.loadNotificationIcon(data.avatarUrl) { bitmap ->
bitmap?.let { builder.setLargeIcon(it) } // set only if != null
manager.notify(data.pseudoId, builder.build())
// a Target is like a listener for image loading events
val target = object : Target {
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
builder.setLargeIcon(bitmap) // set only if there is actually one
manager.notify(data.pseudoId, builder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected
}
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
manager.notify(data.pseudoId, builder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected
}
override fun onPrepareLoad(placeHolderDrawable: Drawable) {
// Nothing to do
}
}
// add the target to the list to hold a strong reference and prevent it from being garbage
// collected, since Picasso only holds weak references to targets
iconLoadingTargets.add(target)
PicassoHelper.loadNotificationIcon(data.avatarUrl).into(target)
}
companion object {

View File

@@ -28,7 +28,6 @@ import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.feed.dao.FeedDAO;
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
@@ -51,7 +50,6 @@ import org.schabi.newpipe.util.ExtractorHelper;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import io.reactivex.rxjava3.core.Completable;
@@ -89,7 +87,6 @@ public class HistoryRecordManager {
* Marks a stream item as watched such that it is hidden from the feed if watched videos are
* hidden. Adds a history entry and updates the stream progress to 100%.
*
* @see FeedDAO#getLiveOrNotPlayedStreams
* @see FeedViewModel#togglePlayedItems
* @param info the item to mark as watched
* @return a Maybe containing the ID of the item if successful
@@ -176,10 +173,6 @@ public class HistoryRecordManager {
.subscribeOn(Schedulers.io());
}
public Flowable<List<StreamHistoryEntry>> getStreamHistory() {
return streamHistoryTable.getHistory().subscribeOn(Schedulers.io());
}
public Flowable<List<StreamHistoryEntry>> getStreamHistorySortedById() {
return streamHistoryTable.getHistorySortedById().subscribeOn(Schedulers.io());
}
@@ -188,24 +181,6 @@ public class HistoryRecordManager {
return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io());
}
public Single<List<Long>> insertStreamHistory(final Collection<StreamHistoryEntry> entries) {
final List<StreamHistoryEntity> entities = new ArrayList<>(entries.size());
for (final StreamHistoryEntry entry : entries) {
entities.add(entry.toStreamHistoryEntity());
}
return Single.fromCallable(() -> streamHistoryTable.insertAll(entities))
.subscribeOn(Schedulers.io());
}
public Single<Integer> deleteStreamHistory(final Collection<StreamHistoryEntry> entries) {
final List<StreamHistoryEntity> entities = new ArrayList<>(entries.size());
for (final StreamHistoryEntry entry : entries) {
entities.add(entry.toStreamHistoryEntity());
}
return Single.fromCallable(() -> streamHistoryTable.delete(entities))
.subscribeOn(Schedulers.io());
}
private boolean isStreamHistoryEnabled() {
return sharedPreferences.getBoolean(streamHistoryKey, false);
}
@@ -259,13 +234,6 @@ public class HistoryRecordManager {
// Stream State History
///////////////////////////////////////////////////////
public Maybe<StreamHistoryEntity> getStreamHistory(final StreamInfo info) {
return Maybe.fromCallable(() -> {
final long streamId = streamTable.upsert(new StreamEntity(info));
return streamHistoryTable.getLatestEntry(streamId);
}).subscribeOn(Schedulers.io());
}
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
return queueItem.getStream()
.map(info -> streamTable.upsert(new StreamEntity(info)))
@@ -311,28 +279,6 @@ public class HistoryRecordManager {
}).subscribeOn(Schedulers.io());
}
public Single<List<StreamStateEntity>> loadStreamStateBatch(final List<InfoItem> infos) {
return Single.fromCallable(() -> {
final List<StreamStateEntity> result = new ArrayList<>(infos.size());
for (final InfoItem info : infos) {
final List<StreamEntity> entities = streamTable
.getStream(info.getServiceId(), info.getUrl()).blockingFirst();
if (entities.isEmpty()) {
result.add(null);
continue;
}
final List<StreamStateEntity> states = streamStateTable
.getState(entities.get(0).getUid()).blockingFirst();
if (states.isEmpty()) {
result.add(null);
} else {
result.add(states.get(0));
}
}
return result;
}).subscribeOn(Schedulers.io());
}
public Single<List<StreamStateEntity>> loadLocalStreamStateBatch(
final List<? extends LocalItem> items) {
return Single.fromCallable(() -> {

View File

@@ -135,7 +135,7 @@ public class StatisticsPlaylistFragment
protected void initListeners() {
super.initListeners();
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final LocalItem selectedItem) {
if (selectedItem instanceof StreamStatisticsEntry) {

View File

@@ -1,5 +1,6 @@
package org.schabi.newpipe.local.playlist;
import static org.schabi.newpipe.error.ErrorUtil.showUiErrorSnackbar;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
@@ -10,6 +11,7 @@ import android.os.Parcelable;
import android.text.InputType;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -21,6 +23,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewbinding.ViewBinding;
@@ -33,7 +36,6 @@ import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.databinding.DialogEditTextBinding;
import org.schabi.newpipe.databinding.LocalPlaylistHeaderBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
@@ -41,26 +43,27 @@ import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainPlayer.PlayerType;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
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.Flowable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
@@ -163,7 +166,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(itemsList);
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final LocalItem selectedItem) {
if (selectedItem instanceof PlaylistStreamEntry) {
@@ -305,7 +308,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
///////////////////////////////////////////////////////////////////////////
private Subscriber<List<PlaylistStreamEntry>> getPlaylistObserver() {
return new Subscriber<List<PlaylistStreamEntry>>() {
return new Subscriber<>() {
@Override
public void onSubscribe(final Subscription s) {
showLoading();
@@ -345,7 +348,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
if (item.getItemId() == R.id.menu_item_remove_watched) {
if (item.getItemId() == R.id.menu_item_share_playlist) {
sharePlaylist();
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
createRenameDialog();
} else if (item.getItemId() == R.id.menu_item_remove_watched) {
if (!isRemovingWatched) {
new AlertDialog.Builder(requireContext())
.setMessage(R.string.remove_watched_popup_warning)
@@ -360,14 +367,26 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
.create()
.show();
}
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
createRenameDialog();
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
/**
* Share the playlist as a newline-separated list of stream URLs.
*/
public void sharePlaylist() {
disposables.add(playlistManager.getPlaylistStreams(playlistId)
.flatMapSingle(playlist -> Single.just(playlist.stream()
.map(PlaylistStreamEntry::getStreamEntity)
.map(StreamEntity::getUrl)
.collect(Collectors.joining("\n"))))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(urlsText -> ShareUtils.shareText(requireContext(), name, urlsText),
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)));
}
public void removeWatchedStreams(final boolean removePartiallyWatched) {
if (isRemovingWatched) {
return;
@@ -375,31 +394,21 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
isRemovingWatched = true;
showLoading();
disposables.add(playlistManager.getPlaylistStreams(playlistId)
.subscribeOn(Schedulers.io())
.map((List<PlaylistStreamEntry> playlist) -> {
// Playlist data
final Iterator<PlaylistStreamEntry> playlistIter = playlist.iterator();
// History data
final HistoryRecordManager recordManager
= new HistoryRecordManager(getContext());
final Iterator<StreamHistoryEntry> historyIter = recordManager
.getStreamHistorySortedById().blockingFirst().iterator();
final var recordManager = new HistoryRecordManager(getContext());
final var historyIdsMaybe = recordManager.getStreamHistorySortedById()
.firstElement()
// already sorted by ^ getStreamHistorySortedById(), binary search can be used
.map(historyList -> historyList.stream().map(StreamHistoryEntry::getStreamId)
.collect(Collectors.toList()));
final var streamsMaybe = playlistManager.getPlaylistStreams(playlistId)
.firstElement()
.zipWith(historyIdsMaybe, (playlist, historyStreamIds) -> {
// Remove Watched, Functionality data
final List<PlaylistStreamEntry> notWatchedItems = new ArrayList<>();
boolean thumbnailVideoRemoved = false;
// already sorted by ^ getStreamHistorySortedById(), binary search can be used
final ArrayList<Long> historyStreamIds = new ArrayList<>();
while (historyIter.hasNext()) {
historyStreamIds.add(historyIter.next().getStreamId());
}
if (removePartiallyWatched) {
while (playlistIter.hasNext()) {
final PlaylistStreamEntry playlistItem = playlistIter.next();
for (final var playlistItem : playlist) {
final int indexInHistory = Collections.binarySearch(historyStreamIds,
playlistItem.getStreamId());
@@ -412,14 +421,15 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
}
} else {
final Iterator<StreamStateEntity> streamStatesIter = recordManager
.loadLocalStreamStateBatch(playlist).blockingGet().iterator();
final var streamStates = recordManager
.loadLocalStreamStateBatch(playlist).blockingGet();
for (int i = 0; i < playlist.size(); i++) {
final var playlistItem = playlist.get(i);
final var streamStateEntity = streamStates.get(i);
while (playlistIter.hasNext()) {
final PlaylistStreamEntry playlistItem = playlistIter.next();
final int indexInHistory = Collections.binarySearch(historyStreamIds,
playlistItem.getStreamId());
final StreamStateEntity streamStateEntity = streamStatesIter.next();
final long duration = playlistItem.toStreamInfoItem().getDuration();
if (indexInHistory < 0 || (streamStateEntity != null
@@ -433,19 +443,19 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
}
return Flowable.just(notWatchedItems, thumbnailVideoRemoved);
})
return new Pair<>(notWatchedItems, thumbnailVideoRemoved);
});
disposables.add(streamsMaybe.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(flow -> {
final List<PlaylistStreamEntry> notWatchedItems =
(List<PlaylistStreamEntry>) flow.blockingFirst();
final boolean thumbnailVideoRemoved = (Boolean) flow.blockingLast();
final List<PlaylistStreamEntry> notWatchedItems = flow.first;
final boolean thumbnailVideoRemoved = flow.second;
itemListAdapter.clearStreamItemList();
itemListAdapter.addItems(notWatchedItems);
saveChanges();
if (thumbnailVideoRemoved) {
updateThumbnailUrl();
}
@@ -483,13 +493,18 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
setVideoCount(itemListAdapter.getItemsList().size());
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> {
NavigationHelper.playOnMainPlayer(activity, getPlayQueue());
showHoldToAppendTipIfNeeded();
});
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> {
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false);
showHoldToAppendTipIfNeeded();
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> {
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false);
showHoldToAppendTipIfNeeded();
});
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
return true;
@@ -503,6 +518,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
hideLoading();
}
private void showHoldToAppendTipIfNeeded() {
if (PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
Toast.makeText(activity, R.string.hold_to_append, Toast.LENGTH_SHORT).show();
}
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@@ -524,8 +546,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
return;
}
final DialogEditTextBinding dialogBinding
= DialogEditTextBinding.inflate(getLayoutInflater());
final DialogEditTextBinding dialogBinding =
DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
dialogBinding.dialogEditText.setSelection(dialogBinding.dialogEditText.getText().length());
@@ -593,7 +615,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
newThumbnailUrl = ((PlaylistStreamEntry) itemListAdapter.getItemsList().get(0))
.getStreamEntity().getThumbnailUrl();
} else {
newThumbnailUrl = "drawable://" + R.drawable.dummy_thumbnail_playlist;
newThumbnailUrl = "drawable://" + R.drawable.placeholder_thumbnail_playlist;
}
changeThumbnailUrl(newThumbnailUrl);

View File

@@ -22,13 +22,12 @@ import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import com.xwray.groupie.Group
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Item
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
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
import org.schabi.newpipe.databinding.DialogTitleBinding
import org.schabi.newpipe.databinding.FeedItemCarouselBinding
import org.schabi.newpipe.databinding.FragmentSubscriptionBinding
@@ -43,11 +42,13 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialog
import org.schabi.newpipe.local.subscription.item.ChannelItem
import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem
import org.schabi.newpipe.local.subscription.item.FeedGroupAddItem
import org.schabi.newpipe.local.subscription.item.FeedGroupAddNewGridItem
import org.schabi.newpipe.local.subscription.item.FeedGroupAddNewItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardGridItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem.Companion.PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM
import org.schabi.newpipe.local.subscription.item.GroupsHeader
import org.schabi.newpipe.local.subscription.item.Header
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
@@ -74,9 +75,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private val disposables: CompositeDisposable = CompositeDisposable()
private val groupAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
private val feedGroupsSection = Section()
private var feedGroupsCarousel: FeedGroupCarouselItem? = null
private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem
private lateinit var carouselAdapter: GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>
private lateinit var feedGroupsCarousel: FeedGroupCarouselItem
private lateinit var feedGroupsSortMenuItem: GroupsHeader
private val subscriptionsSection = Section()
private val requestExportLauncher =
@@ -90,7 +91,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
@State
@JvmField
var feedGroupsListState: Parcelable? = null
var feedGroupsCarouselState: Parcelable? = null
init {
setHasOptionsMenu(true)
@@ -100,11 +101,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
// Fragment LifeCycle
// /////////////////////////////////////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupInitialLayout()
}
override fun onAttach(context: Context) {
super.onAttach(context)
subscriptionManager = SubscriptionManager(requireContext())
@@ -117,7 +113,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun onPause() {
super.onPause()
itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState()
feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState()
feedGroupsCarouselState = feedGroupsCarousel.onSaveInstanceState()
}
override fun onDestroy() {
@@ -184,7 +180,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
menuItem: MenuItem,
onClick: Runnable
): MenuItem {
menuItem.setOnMenuItemClickListener { _ ->
menuItem.setOnMenuItemClickListener {
onClick.run()
true
}
@@ -245,51 +241,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
// Fragment Views
// ////////////////////////////////////////////////////////////////////////
private fun setupInitialLayout() {
Section().apply {
val carouselAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
carouselAdapter.add(FeedGroupCardItem(-1, getString(R.string.all), FeedGroupIcon.RSS))
carouselAdapter.add(feedGroupsSection)
carouselAdapter.add(FeedGroupAddItem())
carouselAdapter.setOnItemClickListener { item, _ ->
listenerFeedGroups.selected(item)
}
carouselAdapter.setOnItemLongClickListener { item, _ ->
if (item is FeedGroupCardItem) {
if (item.groupId == FeedGroupEntity.GROUP_ALL_ID) {
return@setOnItemLongClickListener false
}
}
listenerFeedGroups.held(item)
return@setOnItemLongClickListener true
}
feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter)
feedGroupsSortMenuItem = HeaderWithMenuItem(
getString(R.string.feed_groups_header_title),
R.drawable.ic_sort,
menuItemOnClickListener = ::openReorderDialog
)
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
groupAdapter.add(this)
}
subscriptionsSection.setPlaceholder(EmptyPlaceholderItem())
subscriptionsSection.setHideWhenEmpty(true)
groupAdapter.add(
Section(
HeaderWithMenuItem(
getString(R.string.tab_subscriptions)
),
listOf(subscriptionsSection)
)
)
}
override fun initViews(rootView: View, savedInstanceState: Bundle?) {
super.initViews(rootView, savedInstanceState)
_binding = FragmentSubscriptionBinding.bind(rootView)
@@ -299,10 +250,81 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
spanSizeLookup = groupAdapter.spanSizeLookup
}
binding.itemsList.adapter = groupAdapter
binding.itemsList.itemAnimator = null
viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java)
viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java]
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { it?.let(this::handleFeedGroups) }
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) {
it?.let { (groups, listViewMode) ->
handleFeedGroups(groups, listViewMode)
}
}
setupInitialLayout()
}
private fun setupInitialLayout() {
Section().apply {
carouselAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
carouselAdapter.setOnItemClickListener { item, _ ->
when (item) {
is FeedGroupCardItem ->
NavigationHelper.openFeedFragment(fm, item.groupId, item.name)
is FeedGroupCardGridItem ->
NavigationHelper.openFeedFragment(fm, item.groupId, item.name)
is FeedGroupAddNewItem ->
FeedGroupDialog.newInstance().show(fm, null)
is FeedGroupAddNewGridItem ->
FeedGroupDialog.newInstance().show(fm, null)
}
}
carouselAdapter.setOnItemLongClickListener { item, _ ->
if ((item is FeedGroupCardItem && item.groupId == GROUP_ALL_ID) ||
(item is FeedGroupCardGridItem && item.groupId == GROUP_ALL_ID)
) {
return@setOnItemLongClickListener false
}
when (item) {
is FeedGroupCardItem ->
FeedGroupDialog.newInstance(item.groupId).show(fm, null)
is FeedGroupCardGridItem ->
FeedGroupDialog.newInstance(item.groupId).show(fm, null)
}
return@setOnItemLongClickListener true
}
feedGroupsCarousel = FeedGroupCarouselItem(
carouselAdapter = carouselAdapter,
listViewMode = viewModel.getListViewMode()
)
feedGroupsSortMenuItem = GroupsHeader(
title = getString(R.string.feed_groups_header_title),
onSortClicked = ::openReorderDialog,
onToggleListViewModeClicked = ::toggleListViewMode,
listViewMode = viewModel.getListViewMode(),
)
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
groupAdapter.clear()
groupAdapter.add(this)
}
subscriptionsSection.setPlaceholder(EmptyPlaceholderItem())
subscriptionsSection.setHideWhenEmpty(true)
groupAdapter.add(
Section(
Header(getString(R.string.tab_subscriptions)),
listOf(subscriptionsSection)
)
)
}
private fun toggleListViewMode() {
viewModel.setListViewMode(!viewModel.getListViewMode())
}
private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
@@ -346,22 +368,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun doInitialLoadLogic() = Unit
override fun startLoading(forceLoad: Boolean) = Unit
private val listenerFeedGroups = object : OnClickGesture<Item<*>>() {
override fun selected(selectedItem: Item<*>?) {
when (selectedItem) {
is FeedGroupCardItem -> NavigationHelper.openFeedFragment(fm, selectedItem.groupId, selectedItem.name)
is FeedGroupAddItem -> FeedGroupDialog.newInstance().show(fm, null)
}
}
override fun held(selectedItem: Item<*>?) {
when (selectedItem) {
is FeedGroupCardItem -> FeedGroupDialog.newInstance(selectedItem.groupId).show(fm, null)
}
}
}
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem>() {
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem> {
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
fm,
selectedItem.serviceId, selectedItem.url, selectedItem.name
@@ -402,16 +409,38 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
}
private fun handleFeedGroups(groups: List<Group>) {
feedGroupsSection.update(groups)
if (feedGroupsListState != null) {
feedGroupsCarousel?.onRestoreInstanceState(feedGroupsListState)
feedGroupsListState = null
private fun handleFeedGroups(groups: List<Group>, listViewMode: Boolean) {
if (feedGroupsCarouselState != null) {
feedGroupsCarousel.onRestoreInstanceState(feedGroupsCarouselState)
feedGroupsCarouselState = null
}
feedGroupsSortMenuItem.showMenuItem = groups.size > 1
binding.itemsList.post { feedGroupsSortMenuItem.notifyChanged(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM) }
binding.itemsList.post {
if (context == null) {
// since this part was posted to the next UI cycle, the fragment might have been
// removed in the meantime
return@post
}
feedGroupsCarousel.listViewMode = listViewMode
feedGroupsSortMenuItem.showSortButton = groups.size > 1
feedGroupsSortMenuItem.listViewMode = listViewMode
feedGroupsCarousel.notifyChanged(FeedGroupCarouselItem.PAYLOAD_UPDATE_LIST_VIEW_MODE)
feedGroupsSortMenuItem.notifyChanged(GroupsHeader.PAYLOAD_UPDATE_ICONS)
// update items here to prevent flickering
carouselAdapter.apply {
clear()
if (listViewMode) {
add(FeedGroupAddNewItem())
add(FeedGroupCardItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.RSS))
} else {
add(FeedGroupAddNewGridItem())
add(FeedGroupCardGridItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.RSS))
}
addAll(groups)
}
}
}
// /////////////////////////////////////////////////////////////////////////

View File

@@ -5,25 +5,45 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.xwray.groupie.Group
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.subscription.item.ChannelItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardGridItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
import org.schabi.newpipe.util.ThemeHelper
import java.util.concurrent.TimeUnit
class SubscriptionViewModel(application: Application) : AndroidViewModel(application) {
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application)
private var subscriptionManager = SubscriptionManager(application)
private val mutableStateLiveData = MutableLiveData<SubscriptionState>()
private val mutableFeedGroupsLiveData = MutableLiveData<List<Group>>()
val stateLiveData: LiveData<SubscriptionState> = mutableStateLiveData
val feedGroupsLiveData: LiveData<List<Group>> = mutableFeedGroupsLiveData
// true -> list view, false -> grid view
private val listViewMode = BehaviorProcessor.createDefault(
!ThemeHelper.shouldUseGridLayout(application)
)
private val listViewModeFlowable = listViewMode.distinctUntilChanged()
private var feedGroupItemsDisposable = feedDatabaseManager.groups()
private val mutableStateLiveData = MutableLiveData<SubscriptionState>()
private val mutableFeedGroupsLiveData = MutableLiveData<Pair<List<Group>, Boolean>>()
val stateLiveData: LiveData<SubscriptionState> = mutableStateLiveData
val feedGroupsLiveData: LiveData<Pair<List<Group>, Boolean>> = mutableFeedGroupsLiveData
private var feedGroupItemsDisposable = Flowable
.combineLatest(
feedDatabaseManager.groups(),
listViewModeFlowable,
::Pair
)
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
.map { it.map(::FeedGroupCardItem) }
.map { (feedGroups, listViewMode) ->
Pair(
feedGroups.map(if (listViewMode) ::FeedGroupCardItem else ::FeedGroupCardGridItem),
listViewMode
)
}
.subscribeOn(Schedulers.io())
.subscribe(
{ mutableFeedGroupsLiveData.postValue(it) },
@@ -45,6 +65,14 @@ class SubscriptionViewModel(application: Application) : AndroidViewModel(applica
feedGroupItemsDisposable.dispose()
}
fun setListViewMode(newListViewMode: Boolean) {
listViewMode.onNext(newListViewMode)
}
fun getListViewMode(): Boolean {
return listViewMode.value ?: true
}
sealed class SubscriptionState {
data class LoadedState(val subscriptions: List<Group>) : SubscriptionState()
data class ErrorState(val error: Throwable? = null) : SubscriptionState()

View File

@@ -1,35 +0,0 @@
package org.schabi.newpipe.local.subscription.decoration
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.schabi.newpipe.R
class FeedGroupCarouselDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val marginStartEnd: Int
private val marginTopBottom: Int
private val marginBetweenItems: Int
init {
with(context.resources) {
marginStartEnd = getDimensionPixelOffset(R.dimen.feed_group_carousel_start_end_margin)
marginTopBottom = getDimensionPixelOffset(R.dimen.feed_group_carousel_top_bottom_margin)
marginBetweenItems = getDimensionPixelOffset(R.dimen.feed_group_carousel_between_items_margin)
}
}
override fun getItemOffsets(outRect: Rect, child: View, parent: RecyclerView, state: RecyclerView.State) {
val childAdapterPosition = parent.getChildAdapterPosition(child)
val childAdapterCount = parent.adapter?.itemCount ?: 0
outRect.set(marginBetweenItems, marginTopBottom, 0, marginTopBottom)
if (childAdapterPosition == 0) {
outRect.left = marginStartEnd
} else if (childAdapterPosition == childAdapterCount - 1) {
outRect.right = marginStartEnd
}
}
}

View File

@@ -8,12 +8,10 @@ import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
@@ -124,21 +122,15 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
_feedGroupCreateBinding = DialogFeedGroupCreateBinding.bind(view)
_searchLayoutBinding = feedGroupCreateBinding.subscriptionsHeaderSearchContainer
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
// KitKat doesn't apply container's theme to <include> content
val contrastColor = AppCompatResources.getColorStateList(requireContext(), R.color.contrastColor)
searchLayoutBinding.toolbarSearchEditText.setTextColor(contrastColor)
searchLayoutBinding.toolbarSearchEditText.setHintTextColor(contrastColor.withAlpha(128))
ImageViewCompat.setImageTintList(searchLayoutBinding.toolbarSearchClearIcon, contrastColor)
}
viewModel = ViewModelProvider(
this,
FeedGroupDialogViewModel.Factory(
FeedGroupDialogViewModel.getFactory(
requireContext(),
groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped
groupId,
subscriptionsCurrentSearchQuery,
subscriptionsShowOnlyUngrouped
)
).get(FeedGroupDialogViewModel::class.java)
)[FeedGroupDialogViewModel::class.java]
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
viewModel.subscriptionsLiveData.observe(viewLifecycleOwner) {

View File

@@ -4,7 +4,8 @@ import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.Disposable
@@ -115,18 +116,18 @@ class FeedGroupDialogViewModel(
data class Filter(val query: String, val showOnlyUngrouped: Boolean)
class Factory(
private val context: Context,
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
private val initialQuery: String = "",
private val initialShowOnlyUngrouped: Boolean = false
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return FeedGroupDialogViewModel(
context.applicationContext,
groupId, initialQuery, initialShowOnlyUngrouped
) as T
companion object {
fun getFactory(
context: Context,
groupId: Long,
initialQuery: String,
initialShowOnlyUngrouped: Boolean
) = viewModelFactory {
initializer {
FeedGroupDialogViewModel(
context.applicationContext, groupId, initialQuery, initialShowOnlyUngrouped
)
}
}
}
}

View File

@@ -39,7 +39,7 @@ class ChannelItem(
itemChannelDescriptionView.text = infoItem.description
}
PicassoHelper.loadThumbnail(infoItem.thumbnailUrl).into(itemThumbnailView)
PicassoHelper.loadAvatar(infoItem.thumbnailUrl).into(itemThumbnailView)
gesturesListener?.run {
viewHolder.root.setOnClickListener { selected(infoItem) }

View File

@@ -0,0 +1,14 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FeedGroupAddNewGridItemBinding
class FeedGroupAddNewGridItem : BindableItem<FeedGroupAddNewGridItemBinding>() {
override fun getLayout(): Int = R.layout.feed_group_add_new_grid_item
override fun initializeViewBinding(view: View) = FeedGroupAddNewGridItemBinding.bind(view)
override fun bind(viewBinding: FeedGroupAddNewGridItemBinding, position: Int) {
// this is a static item, nothing to do here
}
}

View File

@@ -5,8 +5,10 @@ import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FeedGroupAddNewItemBinding
class FeedGroupAddItem : BindableItem<FeedGroupAddNewItemBinding>() {
class FeedGroupAddNewItem : BindableItem<FeedGroupAddNewItemBinding>() {
override fun getLayout(): Int = R.layout.feed_group_add_new_item
override fun bind(viewBinding: FeedGroupAddNewItemBinding, position: Int) {}
override fun initializeViewBinding(view: View) = FeedGroupAddNewItemBinding.bind(view)
override fun bind(viewBinding: FeedGroupAddNewItemBinding, position: Int) {
// this is a static item, nothing to do here
}
}

View File

@@ -0,0 +1,32 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.FeedGroupCardGridItemBinding
import org.schabi.newpipe.local.subscription.FeedGroupIcon
data class FeedGroupCardGridItem(
val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
val name: String,
val icon: FeedGroupIcon,
) : BindableItem<FeedGroupCardGridItemBinding>() {
constructor (feedGroupEntity: FeedGroupEntity) : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon)
override fun getId(): Long {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> super.getId()
else -> groupId
}
}
override fun getLayout(): Int = R.layout.feed_group_card_grid_item
override fun bind(viewBinding: FeedGroupCardGridItemBinding, position: Int) {
viewBinding.title.text = name
viewBinding.icon.setImageResource(icon.getDrawableRes())
}
override fun initializeViewBinding(view: View) = FeedGroupCardGridItemBinding.bind(view)
}

View File

@@ -1,60 +1,82 @@
package org.schabi.newpipe.local.subscription.item
import android.content.Context
import android.os.Parcelable
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FeedItemCarouselBinding
import org.schabi.newpipe.local.subscription.decoration.FeedGroupCarouselDecoration
import org.schabi.newpipe.util.DeviceUtils
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCount
class FeedGroupCarouselItem(
context: Context,
private val carouselAdapter: GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>
private val carouselAdapter: GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>,
var listViewMode: Boolean
) : BindableItem<FeedItemCarouselBinding>() {
private val feedGroupCarouselDecoration = FeedGroupCarouselDecoration(context)
companion object {
const val PAYLOAD_UPDATE_LIST_VIEW_MODE = 2
}
private var linearLayoutManager: LinearLayoutManager? = null
private var carouselLayoutManager: LinearLayoutManager? = null
private var listState: Parcelable? = null
override fun getLayout() = R.layout.feed_item_carousel
fun onSaveInstanceState(): Parcelable? {
listState = linearLayoutManager?.onSaveInstanceState()
listState = carouselLayoutManager?.onSaveInstanceState()
return listState
}
fun onRestoreInstanceState(state: Parcelable?) {
linearLayoutManager?.onRestoreInstanceState(state)
carouselLayoutManager?.onRestoreInstanceState(state)
listState = state
}
override fun initializeViewBinding(view: View): FeedItemCarouselBinding {
val viewHolder = FeedItemCarouselBinding.bind(view)
val viewBinding = FeedItemCarouselBinding.bind(view)
updateViewMode(viewBinding)
return viewBinding
}
linearLayoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
viewHolder.recyclerView.apply {
layoutManager = linearLayoutManager
adapter = carouselAdapter
addItemDecoration(feedGroupCarouselDecoration)
override fun bind(
viewBinding: FeedItemCarouselBinding,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.contains(PAYLOAD_UPDATE_LIST_VIEW_MODE)) {
updateViewMode(viewBinding)
return
}
return viewHolder
super.bind(viewBinding, position, payloads)
}
override fun bind(viewBinding: FeedItemCarouselBinding, position: Int) {
viewBinding.recyclerView.apply { adapter = carouselAdapter }
linearLayoutManager?.onRestoreInstanceState(listState)
carouselLayoutManager?.onRestoreInstanceState(listState)
}
override fun unbind(viewHolder: GroupieViewHolder<FeedItemCarouselBinding>) {
super.unbind(viewHolder)
listState = carouselLayoutManager?.onSaveInstanceState()
}
listState = linearLayoutManager?.onSaveInstanceState()
private fun updateViewMode(viewBinding: FeedItemCarouselBinding) {
viewBinding.recyclerView.apply { adapter = carouselAdapter }
val context = viewBinding.root.context
carouselLayoutManager = if (listViewMode) {
LinearLayoutManager(context)
} else {
GridLayoutManager(context, getGridSpanCount(context, DeviceUtils.dpToPx(112, context)))
}
viewBinding.recyclerView.apply {
layoutManager = carouselLayoutManager
adapter = carouselAdapter
}
}
}

View File

@@ -0,0 +1,50 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import androidx.core.view.isVisible
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.SubscriptionGroupsHeaderBinding
class GroupsHeader(
private val title: String,
private val onSortClicked: () -> Unit,
private val onToggleListViewModeClicked: () -> Unit,
var showSortButton: Boolean = true,
var listViewMode: Boolean = true
) : BindableItem<SubscriptionGroupsHeaderBinding>() {
companion object {
const val PAYLOAD_UPDATE_ICONS = 1
}
override fun getLayout(): Int = R.layout.subscription_groups_header
override fun bind(
viewBinding: SubscriptionGroupsHeaderBinding,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.contains(PAYLOAD_UPDATE_ICONS)) {
updateIcons(viewBinding)
return
}
super.bind(viewBinding, position, payloads)
}
override fun bind(viewBinding: SubscriptionGroupsHeaderBinding, position: Int) {
viewBinding.headerTitle.text = title
viewBinding.headerSort.setOnClickListener { onSortClicked() }
viewBinding.headerToggleViewMode.setOnClickListener { onToggleListViewModeClicked() }
updateIcons(viewBinding)
}
override fun initializeViewBinding(view: View) = SubscriptionGroupsHeaderBinding.bind(view)
private fun updateIcons(viewBinding: SubscriptionGroupsHeaderBinding) {
viewBinding.headerToggleViewMode.setImageResource(
if (listViewMode) R.drawable.ic_apps else R.drawable.ic_list
)
viewBinding.headerSort.isVisible = showSortButton
}
}

View File

@@ -0,0 +1,17 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.SubscriptionHeaderBinding
class Header(private val title: String) : BindableItem<SubscriptionHeaderBinding>() {
override fun getLayout(): Int = R.layout.subscription_header
override fun bind(viewBinding: SubscriptionHeaderBinding, position: Int) {
viewBinding.root.text = title
}
override fun initializeViewBinding(view: View) = SubscriptionHeaderBinding.bind(view)
}

View File

@@ -1,50 +0,0 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import android.view.View.OnClickListener
import androidx.annotation.DrawableRes
import androidx.core.view.isVisible
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.HeaderWithMenuItemBinding
class HeaderWithMenuItem(
val title: String,
@DrawableRes val itemIcon: Int = 0,
var showMenuItem: Boolean = true,
private val onClickListener: (() -> Unit)? = null,
private val menuItemOnClickListener: (() -> Unit)? = null
) : BindableItem<HeaderWithMenuItemBinding>() {
companion object {
const val PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM = 1
}
override fun getLayout(): Int = R.layout.header_with_menu_item
override fun bind(viewBinding: HeaderWithMenuItemBinding, position: Int, payloads: MutableList<Any>) {
if (payloads.contains(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM)) {
updateMenuItemVisibility(viewBinding)
return
}
super.bind(viewBinding, position, payloads)
}
override fun bind(viewBinding: HeaderWithMenuItemBinding, position: Int) {
viewBinding.headerTitle.text = title
viewBinding.headerMenuItem.setImageResource(itemIcon)
val listener = onClickListener?.let { OnClickListener { onClickListener.invoke() } }
viewBinding.root.setOnClickListener(listener)
val menuItemListener = menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } }
viewBinding.headerMenuItem.setOnClickListener(menuItemListener)
updateMenuItemVisibility(viewBinding)
}
override fun initializeViewBinding(view: View) = HeaderWithMenuItemBinding.bind(view)
private fun updateMenuItemVisibility(viewBinding: HeaderWithMenuItemBinding) {
viewBinding.headerMenuItem.isVisible = showMenuItem
}
}

View File

@@ -19,6 +19,8 @@
package org.schabi.newpipe.local.subscription.services;
import static org.schabi.newpipe.MainActivity.DEBUG;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
@@ -43,8 +45,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.MainActivity.DEBUG;
public class SubscriptionsExportService extends BaseImportExportService {
public static final String KEY_FILE_PATH = "key_file_path";
@@ -109,8 +109,8 @@ public class SubscriptionsExportService extends BaseImportExportService {
subscriptionManager.subscriptionTable().getAll().take(1)
.map(subscriptionEntities -> {
final List<SubscriptionItem> result
= new ArrayList<>(subscriptionEntities.size());
final List<SubscriptionItem> result =
new ArrayList<>(subscriptionEntities.size());
for (final SubscriptionEntity entity : subscriptionEntities) {
result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(),
entity.getName()));

View File

@@ -1,259 +0,0 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* Part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.player;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import org.schabi.newpipe.App;
import org.schabi.newpipe.databinding.PlayerBinding;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
/**
* One service for all players.
*
* @author mauriciocolli
*/
public final class MainPlayer extends Service {
private static final String TAG = "MainPlayer";
private static final boolean DEBUG = Player.DEBUG;
private Player player;
private WindowManager windowManager;
private final IBinder mBinder = new MainPlayer.LocalBinder();
public enum PlayerType {
VIDEO,
AUDIO,
POPUP
}
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
static final String ACTION_CLOSE
= App.PACKAGE_NAME + ".player.MainPlayer.CLOSE";
static final String ACTION_PLAY_PAUSE
= App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE";
static final String ACTION_REPEAT
= App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
static final String ACTION_PLAY_NEXT
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_NEXT";
static final String ACTION_PLAY_PREVIOUS
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
static final String ACTION_FAST_REWIND
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_REWIND";
static final String ACTION_FAST_FORWARD
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_FORWARD";
static final String ACTION_SHUFFLE
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_SHUFFLE";
public static final String ACTION_RECREATE_NOTIFICATION
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate() {
if (DEBUG) {
Log.d(TAG, "onCreate() called");
}
assureCorrectAppLanguage(this);
windowManager = ContextCompat.getSystemService(this, WindowManager.class);
ThemeHelper.setTheme(this);
createView();
}
private void createView() {
final PlayerBinding binding = PlayerBinding.inflate(LayoutInflater.from(this));
player = new Player(this);
player.setupFromView(binding);
NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (DEBUG) {
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
+ "], flags = [" + flags + "], startId = [" + startId + "]");
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
&& player.getPlayQueue() == null) {
// Player is not working, no need to process media button's action
return START_NOT_STICKY;
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|| intent.getStringExtra(Player.PLAY_QUEUE_KEY) != null) {
NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
}
player.handleIntent(intent);
if (player.getMediaSessionManager() != null) {
player.getMediaSessionManager().handleMediaButtonIntent(intent);
}
return START_NOT_STICKY;
}
public void stopForImmediateReusing() {
if (DEBUG) {
Log.d(TAG, "stopForImmediateReusing() called");
}
if (!player.exoPlayerIsNull()) {
player.saveWasPlaying();
// Releases wifi & cpu, disables keepScreenOn, etc.
// We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth
player.smoothStopPlayer();
player.setRecovery();
// Android TV will handle back button in case controls will be visible
// (one more additional unneeded click while the player is hidden)
player.hideControls(0, 0);
player.closeItemsList();
// Notification shows information about old stream but if a user selects
// a stream from backStack it's not actual anymore
// So we should hide the notification at all.
// When autoplay enabled such notification flashing is annoying so skip this case
}
}
@Override
public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent);
if (!player.videoPlayerSelected()) {
return;
}
onDestroy();
// Unload from memory completely
Runtime.getRuntime().halt(0);
}
@Override
public void onDestroy() {
if (DEBUG) {
Log.d(TAG, "destroy() called");
}
cleanup();
}
private void cleanup() {
if (player != null) {
// Exit from fullscreen when user closes the player via notification
if (player.isFullscreen()) {
player.toggleFullscreen();
}
removeViewFromParent();
player.saveStreamProgressState();
player.setRecovery();
player.stopActivityBinding();
player.removePopupFromView();
player.destroy();
player = null;
}
}
public void stopService() {
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
cleanup();
stopSelf();
}
@Override
protected void attachBaseContext(final Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
}
@Override
public IBinder onBind(final Intent intent) {
return mBinder;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
boolean isLandscape() {
// DisplayMetrics from activity context knows about MultiWindow feature
// while DisplayMetrics from app context doesn't
return DeviceUtils.isLandscape(player != null && player.getParentActivity() != null
? player.getParentActivity() : this);
}
@Nullable
public View getView() {
if (player == null) {
return null;
}
return player.getRootView();
}
public void removeViewFromParent() {
if (getView() != null && getView().getParent() != null) {
if (player.getParentActivity() != null) {
// This means view was added to fragment
final ViewGroup parent = (ViewGroup) getView().getParent();
parent.removeView(getView());
} else {
// This means view was added by windowManager for popup player
windowManager.removeViewImmediate(getView());
}
}
}
public class LocalBinder extends Binder {
public MainPlayer getService() {
return MainPlayer.this;
}
public Player getPlayer() {
return MainPlayer.this.player;
}
}
}

View File

@@ -29,6 +29,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.playqueue.PlayQueue;
@@ -51,7 +52,7 @@ public final class PlayQueueActivity extends AppCompatActivity
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
protected Player player;
private Player player;
private boolean serviceBound;
private ServiceConnection serviceConnection;
@@ -126,13 +127,13 @@ public final class PlayQueueActivity extends AppCompatActivity
NavigationHelper.openSettings(this);
return true;
case R.id.action_append_playlist:
player.onAddToPlaylistClicked(getSupportFragmentManager());
PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
return true;
case R.id.action_playback_speed:
openPlaybackParameterDialog();
return true;
case R.id.action_mute:
player.onMuteUnmuteButtonClicked();
player.toggleMute();
return true;
case R.id.action_system_audio:
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
@@ -168,7 +169,7 @@ public final class PlayQueueActivity extends AppCompatActivity
////////////////////////////////////////////////////////////////////////////
private void bind() {
final Intent bindIntent = new Intent(this, MainPlayer.class);
final Intent bindIntent = new Intent(this, PlayerService.class);
final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
if (!success) {
unbindService(serviceConnection);
@@ -184,10 +185,7 @@ public final class PlayQueueActivity extends AppCompatActivity
player.removeActivityListener(this);
}
if (player != null && player.getPlayQueueAdapter() != null) {
player.getPlayQueueAdapter().unsetSelectedListener();
}
queueControlBinding.playQueue.setAdapter(null);
onQueueUpdate(null);
if (itemTouchHelper != null) {
itemTouchHelper.attachToRecyclerView(null);
}
@@ -208,17 +206,14 @@ public final class PlayQueueActivity extends AppCompatActivity
public void onServiceConnected(final ComponentName name, final IBinder service) {
Log.d(TAG, "Player service is connected");
if (service instanceof PlayerServiceBinder) {
player = ((PlayerServiceBinder) service).getPlayerInstance();
} else if (service instanceof MainPlayer.LocalBinder) {
player = ((MainPlayer.LocalBinder) service).getPlayer();
if (service instanceof PlayerService.LocalBinder) {
player = ((PlayerService.LocalBinder) service).getPlayer();
}
if (player == null || player.getPlayQueue() == null
|| player.getPlayQueueAdapter() == null || player.exoPlayerIsNull()) {
if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
unbind();
finish();
} else {
onQueueUpdate(player.getPlayQueue());
buildComponents();
if (player != null) {
player.setActivityListener(PlayQueueActivity.this);
@@ -241,7 +236,6 @@ public final class PlayQueueActivity extends AppCompatActivity
private void buildQueue() {
queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this));
queueControlBinding.playQueue.setAdapter(player.getPlayQueueAdapter());
queueControlBinding.playQueue.setClickable(true);
queueControlBinding.playQueue.setLongClickable(true);
queueControlBinding.playQueue.clearOnScrollListeners();
@@ -249,8 +243,6 @@ public final class PlayQueueActivity extends AppCompatActivity
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue);
player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener());
}
private void buildMetadata() {
@@ -370,7 +362,7 @@ public final class PlayQueueActivity extends AppCompatActivity
}
if (view.getId() == queueControlBinding.controlRepeat.getId()) {
player.onRepeatClicked();
player.cycleNextRepeatMode();
} else if (view.getId() == queueControlBinding.controlBackward.getId()) {
player.playPrevious();
} else if (view.getId() == queueControlBinding.controlFastRewind.getId()) {
@@ -382,7 +374,7 @@ public final class PlayQueueActivity extends AppCompatActivity
} else if (view.getId() == queueControlBinding.controlForward.getId()) {
player.playNext();
} else if (view.getId() == queueControlBinding.controlShuffle.getId()) {
player.onShuffleClicked();
player.toggleShuffleModeEnabled();
} else if (view.getId() == queueControlBinding.metadata.getId()) {
scrollToSelected();
} else if (view.getId() == queueControlBinding.liveSync.getId()) {
@@ -445,7 +437,14 @@ public final class PlayQueueActivity extends AppCompatActivity
////////////////////////////////////////////////////////////////////////////
@Override
public void onQueueUpdate(final PlayQueue queue) {
public void onQueueUpdate(@Nullable final PlayQueue queue) {
if (queue == null) {
queueControlBinding.playQueue.setAdapter(null);
} else {
final PlayQueueAdapter adapter = new PlayQueueAdapter(this, queue);
adapter.setSelectedListener(getOnSelectedListener());
queueControlBinding.playQueue.setAdapter(adapter);
}
}
@Override
@@ -454,7 +453,6 @@ public final class PlayQueueActivity extends AppCompatActivity
onStateChanged(state);
onPlayModeChanged(repeatMode, shuffled);
onPlaybackParameterChanged(parameters);
onMaybePlaybackAdapterChanged();
onMaybeMuteChanged();
}
@@ -582,17 +580,6 @@ public final class PlayQueueActivity extends AppCompatActivity
}
}
private void onMaybePlaybackAdapterChanged() {
if (player == null) {
return;
}
final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter();
if (maybeNewAdapter != null
&& queueControlBinding.playQueue.getAdapter() != maybeNewAdapter) {
queueControlBinding.playQueue.setAdapter(maybeNewAdapter);
}
}
private void onMaybeMuteChanged() {
if (menu != null && player != null) {
final MenuItem item = menu.findItem(R.id.action_mute);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* Part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.player;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.util.ThemeHelper;
/**
* One service for all players.
*/
public final class PlayerService extends Service {
private static final String TAG = PlayerService.class.getSimpleName();
private static final boolean DEBUG = Player.DEBUG;
private Player player;
private final IBinder mBinder = new PlayerService.LocalBinder();
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate() {
if (DEBUG) {
Log.d(TAG, "onCreate() called");
}
assureCorrectAppLanguage(this);
ThemeHelper.setTheme(this);
player = new Player(this);
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (DEBUG) {
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
+ "], flags = [" + flags + "], startId = [" + startId + "]");
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
&& player.getPlayQueue() == null) {
// No need to process media button's actions if the player is not working, otherwise the
// player service would strangely start with nothing to play
return START_NOT_STICKY;
}
player.handleIntent(intent);
player.UIs().get(MediaSessionPlayerUi.class)
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
return START_NOT_STICKY;
}
public void stopForImmediateReusing() {
if (DEBUG) {
Log.d(TAG, "stopForImmediateReusing() called");
}
if (!player.exoPlayerIsNull()) {
player.saveWasPlaying();
// Releases wifi & cpu, disables keepScreenOn, etc.
// We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth
player.smoothStopForImmediateReusing();
}
}
@Override
public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent);
if (!player.videoPlayerSelected()) {
return;
}
onDestroy();
// Unload from memory completely
Runtime.getRuntime().halt(0);
}
@Override
public void onDestroy() {
if (DEBUG) {
Log.d(TAG, "destroy() called");
}
cleanup();
}
private void cleanup() {
if (player != null) {
player.destroy();
player = null;
}
}
public void stopService() {
cleanup();
stopSelf();
}
@Override
protected void attachBaseContext(final Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
}
@Override
public IBinder onBind(final Intent intent) {
return mBinder;
}
public class LocalBinder extends Binder {
public PlayerService getService() {
return PlayerService.this;
}
public Player getPlayer() {
return PlayerService.this.player;
}
}
}

View File

@@ -1,17 +0,0 @@
package org.schabi.newpipe.player;
import android.os.Binder;
import androidx.annotation.NonNull;
class PlayerServiceBinder extends Binder {
private final Player player;
PlayerServiceBinder(@NonNull final Player player) {
this.player = player;
}
Player getPlayerInstance() {
return player;
}
}

View File

@@ -1,79 +0,0 @@
package org.schabi.newpipe.player;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import java.io.Serializable;
public class PlayerState implements Serializable {
@NonNull
private final PlayQueue playQueue;
private final int repeatMode;
private final float playbackSpeed;
private final float playbackPitch;
@Nullable
private final String playbackQuality;
private final boolean playbackSkipSilence;
private final boolean wasPlaying;
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
final float playbackSpeed, final float playbackPitch,
final boolean playbackSkipSilence, final boolean wasPlaying) {
this(playQueue, repeatMode, playbackSpeed, playbackPitch, null,
playbackSkipSilence, wasPlaying);
}
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
final float playbackSpeed, final float playbackPitch,
@Nullable final String playbackQuality, final boolean playbackSkipSilence,
final boolean wasPlaying) {
this.playQueue = playQueue;
this.repeatMode = repeatMode;
this.playbackSpeed = playbackSpeed;
this.playbackPitch = playbackPitch;
this.playbackQuality = playbackQuality;
this.playbackSkipSilence = playbackSkipSilence;
this.wasPlaying = wasPlaying;
}
/*//////////////////////////////////////////////////////////////////////////
// Serdes
//////////////////////////////////////////////////////////////////////////*/
/*//////////////////////////////////////////////////////////////////////////
// Getters
//////////////////////////////////////////////////////////////////////////*/
@NonNull
public PlayQueue getPlayQueue() {
return playQueue;
}
public int getRepeatMode() {
return repeatMode;
}
public float getPlaybackSpeed() {
return playbackSpeed;
}
public float getPlaybackPitch() {
return playbackPitch;
}
@Nullable
public String getPlaybackQuality() {
return playbackQuality;
}
public boolean isPlaybackSkipSilence() {
return playbackSkipSilence;
}
public boolean wasPlaying() {
return wasPlaying;
}
}

View File

@@ -0,0 +1,32 @@
package org.schabi.newpipe.player;
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
import android.content.Intent;
public enum PlayerType {
MAIN,
AUDIO,
POPUP;
/**
* @return an integer representing this {@link PlayerType}, to be used to save it in intents
* @see #retrieveFromIntent(Intent) Use retrieveFromIntent() to retrieve and convert player type
* integers from an intent
*/
public int valueForIntent() {
return ordinal();
}
/**
* @param intent the intent to retrieve a player type from
* @return the player type integer retrieved from the intent, converted back into a {@link
* PlayerType}, or {@link PlayerType#MAIN} if there is no player type extra in the
* intent
* @throws ArrayIndexOutOfBoundsException if the intent contains an invalid player type integer
* @see #valueForIntent() Use valueForIntent() to obtain valid player type integers
*/
public static PlayerType retrieveFromIntent(final Intent intent) {
return values()[intent.getIntExtra(PLAYER_TYPE, MAIN.valueForIntent())];
}
}

View File

@@ -1,5 +1,5 @@
/*
* Based on ExoPlayer's DefaultHttpDataSource, version 2.17.1.
* Based on ExoPlayer's DefaultHttpDataSource, version 2.18.1.
*
* Original source code copyright (C) 2016 The Android Open Source Project, licensed under the
* Apache License, Version 2.0.

View File

@@ -1,520 +0,0 @@
package org.schabi.newpipe.player.event
import android.content.Context
import android.os.Handler
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.player.MainPlayer
import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.player.helper.PlayerHelper.savePopupPositionAndSizeToPrefs
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.max
import kotlin.math.min
/**
* Base gesture handling for [Player]
*
* This class contains the logic for the player gestures like View preparations
* and provides some abstract methods to make it easier separating the logic from the UI.
*/
abstract class BasePlayerGestureListener(
@JvmField
protected val player: Player,
@JvmField
protected val service: MainPlayer
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
// ///////////////////////////////////////////////////////////////////
// Abstract methods for VIDEO and POPUP
// ///////////////////////////////////////////////////////////////////
abstract fun onDoubleTap(event: MotionEvent, portion: DisplayPortion)
abstract fun onSingleTap(playerType: MainPlayer.PlayerType)
abstract fun onScroll(
playerType: MainPlayer.PlayerType,
portion: DisplayPortion,
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
)
abstract fun onScrollEnd(playerType: MainPlayer.PlayerType, event: MotionEvent)
// ///////////////////////////////////////////////////////////////////
// Abstract methods for POPUP (exclusive)
// ///////////////////////////////////////////////////////////////////
abstract fun onPopupResizingStart()
abstract fun onPopupResizingEnd()
private var initialPopupX: Int = -1
private var initialPopupY: Int = -1
private var isMovingInMain = false
private var isMovingInPopup = false
private var isResizing = false
private val tossFlingVelocity = PlayerHelper.getTossFlingVelocity()
// [popup] initial coordinates and distance between fingers
private var initPointerDistance = -1.0
private var initFirstPointerX = -1f
private var initFirstPointerY = -1f
private var initSecPointerX = -1f
private var initSecPointerY = -1f
// ///////////////////////////////////////////////////////////////////
// onTouch implementation
// ///////////////////////////////////////////////////////////////////
override fun onTouch(v: View, event: MotionEvent): Boolean {
return if (player.popupPlayerSelected()) {
onTouchInPopup(v, event)
} else {
onTouchInMain(v, event)
}
}
private fun onTouchInMain(v: View, event: MotionEvent): Boolean {
player.gestureDetector.onTouchEvent(event)
if (event.action == MotionEvent.ACTION_UP && isMovingInMain) {
isMovingInMain = false
onScrollEnd(MainPlayer.PlayerType.VIDEO, event)
}
return when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
v.parent.requestDisallowInterceptTouchEvent(player.isFullscreen)
true
}
MotionEvent.ACTION_UP -> {
v.parent.requestDisallowInterceptTouchEvent(false)
false
}
else -> true
}
}
private fun onTouchInPopup(v: View, event: MotionEvent): Boolean {
player.gestureDetector.onTouchEvent(event)
if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.")
}
onPopupResizingStart()
// record coordinates of fingers
initFirstPointerX = event.getX(0)
initFirstPointerY = event.getY(0)
initSecPointerX = event.getX(1)
initSecPointerY = event.getY(1)
// record distance between fingers
initPointerDistance = hypot(
initFirstPointerX - initSecPointerX.toDouble(),
initFirstPointerY - initSecPointerY.toDouble()
)
isResizing = true
}
if (event.action == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) {
if (DEBUG) {
Log.d(
TAG,
"onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" +
"[${event.rawX}, ${event.rawY}]"
)
}
return handleMultiDrag(event)
}
if (event.action == MotionEvent.ACTION_UP) {
if (DEBUG) {
Log.d(
TAG,
"onTouch() ACTION_UP > v = [$v], e1.getRaw =" +
" [${event.rawX}, ${event.rawY}]"
)
}
if (isMovingInPopup) {
isMovingInPopup = false
onScrollEnd(MainPlayer.PlayerType.POPUP, event)
}
if (isResizing) {
isResizing = false
initPointerDistance = (-1).toDouble()
initFirstPointerX = (-1).toFloat()
initFirstPointerY = (-1).toFloat()
initSecPointerX = (-1).toFloat()
initSecPointerY = (-1).toFloat()
onPopupResizingEnd()
player.changeState(player.currentState)
}
if (!player.isPopupClosing) {
savePopupPositionAndSizeToPrefs(player)
}
}
v.performClick()
return true
}
private fun handleMultiDrag(event: MotionEvent): Boolean {
if (initPointerDistance != -1.0 && event.pointerCount == 2) {
// get the movements of the fingers
val firstPointerMove = hypot(
event.getX(0) - initFirstPointerX.toDouble(),
event.getY(0) - initFirstPointerY.toDouble()
)
val secPointerMove = hypot(
event.getX(1) - initSecPointerX.toDouble(),
event.getY(1) - initSecPointerY.toDouble()
)
// minimum threshold beyond which pinch gesture will work
val minimumMove = ViewConfiguration.get(service).scaledTouchSlop
if (max(firstPointerMove, secPointerMove) > minimumMove) {
// calculate current distance between the pointers
val currentPointerDistance = hypot(
event.getX(0) - event.getX(1).toDouble(),
event.getY(0) - event.getY(1).toDouble()
)
val popupWidth = player.popupLayoutParams!!.width.toDouble()
// change co-ordinates of popup so the center stays at the same position
val newWidth = popupWidth * currentPointerDistance / initPointerDistance
initPointerDistance = currentPointerDistance
player.popupLayoutParams!!.x += ((popupWidth - newWidth) / 2.0).toInt()
player.checkPopupPositionBounds()
player.updateScreenSize()
player.changePopupSize(min(player.screenWidth.toDouble(), newWidth).toInt())
return true
}
}
return false
}
// ///////////////////////////////////////////////////////////////////
// Simple gestures
// ///////////////////////////////////////////////////////////////////
override fun onDown(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDown called with e = [$e]")
if (isDoubleTapping && isDoubleTapEnabled) {
doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e))
return true
}
return if (player.popupPlayerSelected())
onDownInPopup(e)
else
true
}
private fun onDownInPopup(e: MotionEvent): Boolean {
// Fix popup position when the user touch it, it may have the wrong one
// because the soft input is visible (the draggable area is currently resized).
player.updateScreenSize()
player.checkPopupPositionBounds()
player.popupLayoutParams?.let {
initialPopupX = it.x
initialPopupY = it.y
}
return super.onDown(e)
}
override fun onDoubleTap(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDoubleTap called with e = [$e]")
onDoubleTap(e, getDisplayPortion(e))
return true
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
if (isDoubleTapping)
return true
if (player.popupPlayerSelected()) {
if (player.exoPlayerIsNull())
return false
onSingleTap(MainPlayer.PlayerType.POPUP)
return true
} else {
super.onSingleTapConfirmed(e)
if (player.currentState == Player.STATE_BLOCKED)
return true
onSingleTap(MainPlayer.PlayerType.VIDEO)
}
return true
}
override fun onLongPress(e: MotionEvent?) {
if (player.popupPlayerSelected()) {
player.updateScreenSize()
player.checkPopupPositionBounds()
player.changePopupSize(player.screenWidth.toInt())
}
}
override fun onScroll(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
return if (player.popupPlayerSelected()) {
onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY)
} else {
onScrollInMain(initialEvent, movingEvent, distanceX, distanceY)
}
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
return if (player.popupPlayerSelected()) {
val absVelocityX = abs(velocityX)
val absVelocityY = abs(velocityY)
if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) {
if (absVelocityX > tossFlingVelocity) {
player.popupLayoutParams!!.x = velocityX.toInt()
}
if (absVelocityY > tossFlingVelocity) {
player.popupLayoutParams!!.y = velocityY.toInt()
}
player.checkPopupPositionBounds()
player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
return true
}
return false
} else {
true
}
}
private fun onScrollInMain(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (!player.isFullscreen) {
return false
}
val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service)
val isTouchingNavigationBar: Boolean =
initialEvent.y > (player.rootView.height - getNavigationBarHeight(service))
if (isTouchingStatusBar || isTouchingNavigationBar) {
return false
}
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
if (
!isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
player.currentState == Player.STATE_COMPLETED
) {
return false
}
isMovingInMain = true
onScroll(
MainPlayer.PlayerType.VIDEO,
getDisplayHalfPortion(initialEvent),
initialEvent,
movingEvent,
distanceX,
distanceY
)
return true
}
private fun onScrollInPopup(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (isResizing) {
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
}
if (!isMovingInPopup) {
player.closeOverlayButton.animate(true, 200)
}
isMovingInPopup = true
val diffX: Float = (movingEvent.rawX - initialEvent.rawX)
var posX: Float = (initialPopupX + diffX)
val diffY: Float = (movingEvent.rawY - initialEvent.rawY)
var posY: Float = (initialPopupY + diffY)
if (posX > player.screenWidth - player.popupLayoutParams!!.width) {
posX = (player.screenWidth - player.popupLayoutParams!!.width)
} else if (posX < 0) {
posX = 0f
}
if (posY > player.screenHeight - player.popupLayoutParams!!.height) {
posY = (player.screenHeight - player.popupLayoutParams!!.height)
} else if (posY < 0) {
posY = 0f
}
player.popupLayoutParams!!.x = posX.toInt()
player.popupLayoutParams!!.y = posY.toInt()
onScroll(
MainPlayer.PlayerType.POPUP,
getDisplayHalfPortion(initialEvent),
initialEvent,
movingEvent,
distanceX,
distanceY
)
player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
return true
}
// ///////////////////////////////////////////////////////////////////
// Multi double tapping
// ///////////////////////////////////////////////////////////////////
var doubleTapControls: DoubleTapListener? = null
private set
private val isDoubleTapEnabled: Boolean
get() = doubleTapDelay > 0
var isDoubleTapping = false
private set
fun doubleTapControls(listener: DoubleTapListener) = apply {
doubleTapControls = listener
}
private var doubleTapDelay = DOUBLE_TAP_DELAY
private val doubleTapHandler: Handler = Handler()
private val doubleTapRunnable = Runnable {
if (DEBUG)
Log.d(TAG, "doubleTapRunnable called")
isDoubleTapping = false
doubleTapControls?.onDoubleTapFinished()
}
fun startMultiDoubleTap(e: MotionEvent) {
if (!isDoubleTapping) {
if (DEBUG)
Log.d(TAG, "startMultiDoubleTap called with e = [$e]")
keepInDoubleTapMode()
doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e))
}
}
fun keepInDoubleTapMode() {
if (DEBUG)
Log.d(TAG, "keepInDoubleTapMode called")
isDoubleTapping = true
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay)
}
fun endMultiDoubleTap() {
if (DEBUG)
Log.d(TAG, "endMultiDoubleTap called")
isDoubleTapping = false
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapControls?.onDoubleTapFinished()
}
// ///////////////////////////////////////////////////////////////////
// Utils
// ///////////////////////////////////////////////////////////////////
private fun getDisplayPortion(e: MotionEvent): DisplayPortion {
return if (player.playerType == MainPlayer.PlayerType.POPUP && player.popupLayoutParams != null) {
when {
e.x < player.popupLayoutParams!!.width / 3.0 -> DisplayPortion.LEFT
e.x > player.popupLayoutParams!!.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
} else /* MainPlayer.PlayerType.VIDEO */ {
when {
e.x < player.rootView.width / 3.0 -> DisplayPortion.LEFT
e.x > player.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
}
}
// Currently needed for scrolling since there is no action more the middle portion
private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
return if (player.playerType == MainPlayer.PlayerType.POPUP) {
when {
e.x < player.popupLayoutParams!!.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
} else /* MainPlayer.PlayerType.VIDEO */ {
when {
e.x < player.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
}
}
private fun getNavigationBarHeight(context: Context): Int {
val resId = context.resources
.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resId > 0) {
context.resources.getDimensionPixelSize(resId)
} else 0
}
private fun getStatusBarHeight(context: Context): Int {
val resId = context.resources
.getIdentifier("status_bar_height", "dimen", "android")
return if (resId > 0) {
context.resources.getDimensionPixelSize(resId)
} else 0
}
companion object {
private const val TAG = "BasePlayerGestListener"
private val DEBUG = Player.DEBUG
private const val DOUBLE_TAP_DELAY = 550L
private const val MOVEMENT_THRESHOLD = 40
}
}

View File

@@ -1,7 +0,0 @@
package org.schabi.newpipe.player.event
interface DoubleTapListener {
fun onDoubleTapStarted(portion: DisplayPortion) {}
fun onDoubleTapProgressDown(portion: DisplayPortion) {}
fun onDoubleTapFinished() {}
}

View File

@@ -1,6 +1,5 @@
package org.schabi.newpipe.player.event;
import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.extractor.stream.StreamInfo;

View File

@@ -1,256 +0,0 @@
package org.schabi.newpipe.player.event;
import static org.schabi.newpipe.ktx.AnimationType.ALPHA;
import static org.schabi.newpipe.ktx.AnimationType.SCALE_AND_ALPHA;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_DURATION;
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_HIDE_TIME;
import static org.schabi.newpipe.player.Player.STATE_PLAYING;
import android.app.Activity;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.helper.PlayerHelper;
/**
* GestureListener for the player
*
* While {@link BasePlayerGestureListener} contains the logic behind the single gestures
* this class focuses on the visual aspect like hiding and showing the controls or changing
* volume/brightness during scrolling for specific events.
*/
public class PlayerGestureListener
extends BasePlayerGestureListener
implements View.OnTouchListener {
private static final String TAG = PlayerGestureListener.class.getSimpleName();
private static final boolean DEBUG = MainActivity.DEBUG;
private final int maxVolume;
public PlayerGestureListener(final Player player, final MainPlayer service) {
super(player, service);
maxVolume = player.getAudioReactor().getMaxVolume();
}
@Override
public void onDoubleTap(@NonNull final MotionEvent event,
@NonNull final DisplayPortion portion) {
if (DEBUG) {
Log.d(TAG, "onDoubleTap called with playerType = ["
+ player.getPlayerType() + "], portion = [" + portion + "]");
}
if (player.isSomePopupMenuVisible()) {
player.hideControls(0, 0);
}
if (portion == DisplayPortion.LEFT || portion == DisplayPortion.RIGHT) {
startMultiDoubleTap(event);
} else if (portion == DisplayPortion.MIDDLE) {
player.playPause();
}
}
@Override
public void onSingleTap(@NonNull final MainPlayer.PlayerType playerType) {
if (DEBUG) {
Log.d(TAG, "onSingleTap called with playerType = [" + player.getPlayerType() + "]");
}
if (player.isControlsVisible()) {
player.hideControls(150, 0);
return;
}
// -- Controls are not visible --
// When player is completed show controls and don't hide them later
if (player.getCurrentState() == Player.STATE_COMPLETED) {
player.showControls(0);
} else {
player.showControlsThenHide();
}
}
@Override
public void onScroll(@NonNull final MainPlayer.PlayerType playerType,
@NonNull final DisplayPortion portion,
@NonNull final MotionEvent initialEvent,
@NonNull final MotionEvent movingEvent,
final float distanceX, final float distanceY) {
if (DEBUG) {
Log.d(TAG, "onScroll called with playerType = ["
+ player.getPlayerType() + "], portion = [" + portion + "]");
}
if (playerType == MainPlayer.PlayerType.VIDEO) {
// -- Brightness and Volume control --
final boolean isBrightnessGestureEnabled =
PlayerHelper.isBrightnessGestureEnabled(service);
final boolean isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service);
if (isBrightnessGestureEnabled && isVolumeGestureEnabled) {
if (portion == DisplayPortion.LEFT_HALF) {
onScrollMainBrightness(distanceX, distanceY);
} else /* DisplayPortion.RIGHT_HALF */ {
onScrollMainVolume(distanceX, distanceY);
}
} else if (isBrightnessGestureEnabled) {
onScrollMainBrightness(distanceX, distanceY);
} else if (isVolumeGestureEnabled) {
onScrollMainVolume(distanceX, distanceY);
}
} else /* MainPlayer.PlayerType.POPUP */ {
// -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
final View closingOverlayView = player.getClosingOverlayView();
final boolean showClosingOverlayView = player.isInsideClosingRadius(movingEvent);
// Check if an view is in expected state and if not animate it into the correct state
final int expectedVisibility = showClosingOverlayView ? View.VISIBLE : View.GONE;
if (closingOverlayView.getVisibility() != expectedVisibility) {
animate(closingOverlayView, showClosingOverlayView, 200);
}
}
}
private void onScrollMainVolume(final float distanceX, final float distanceY) {
// If we just started sliding, change the progress bar to match the system volume
if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
final float volumePercent = player
.getAudioReactor().getVolume() / (float) maxVolume;
player.getVolumeProgressBar().setProgress(
(int) (volumePercent * player.getMaxGestureLength()));
}
player.getVolumeProgressBar().incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) player
.getVolumeProgressBar().getProgress() / player.getMaxGestureLength();
final int currentVolume = (int) (maxVolume * currentProgressPercent);
player.getAudioReactor().setVolume(currentVolume);
if (DEBUG) {
Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
}
player.getVolumeImageView().setImageDrawable(
AppCompatResources.getDrawable(service, currentProgressPercent <= 0
? R.drawable.ic_volume_off
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down
: R.drawable.ic_volume_up)
);
if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
animate(player.getVolumeRelativeLayout(), true, 200, SCALE_AND_ALPHA);
}
if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
player.getBrightnessRelativeLayout().setVisibility(View.GONE);
}
}
private void onScrollMainBrightness(final float distanceX, final float distanceY) {
final Activity parent = player.getParentActivity();
if (parent == null) {
return;
}
final Window window = parent.getWindow();
final WindowManager.LayoutParams layoutParams = window.getAttributes();
final ProgressBar bar = player.getBrightnessProgressBar();
final float oldBrightness = layoutParams.screenBrightness;
bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness))));
bar.incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) bar.getProgress() / bar.getMax();
layoutParams.screenBrightness = currentProgressPercent;
window.setAttributes(layoutParams);
// Save current brightness level
PlayerHelper.setScreenBrightness(parent, currentProgressPercent);
if (DEBUG) {
Log.d(TAG, "onScroll().brightnessControl, "
+ "currentBrightness = " + currentProgressPercent);
}
player.getBrightnessImageView().setImageDrawable(
AppCompatResources.getDrawable(service,
currentProgressPercent < 0.25
? R.drawable.ic_brightness_low
: currentProgressPercent < 0.75
? R.drawable.ic_brightness_medium
: R.drawable.ic_brightness_high)
);
if (player.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
animate(player.getBrightnessRelativeLayout(), true, 200, SCALE_AND_ALPHA);
}
if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
player.getVolumeRelativeLayout().setVisibility(View.GONE);
}
}
@Override
public void onScrollEnd(@NonNull final MainPlayer.PlayerType playerType,
@NonNull final MotionEvent event) {
if (DEBUG) {
Log.d(TAG, "onScrollEnd called with playerType = ["
+ player.getPlayerType() + "]");
}
if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
if (playerType == MainPlayer.PlayerType.VIDEO) {
if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
animate(player.getVolumeRelativeLayout(), false, 200, SCALE_AND_ALPHA,
200);
}
if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
animate(player.getBrightnessRelativeLayout(), false, 200, SCALE_AND_ALPHA,
200);
}
} else /* Popup-Player */ {
if (player.isInsideClosingRadius(event)) {
player.closePopup();
} else if (!player.isPopupClosing()) {
animate(player.getCloseOverlayButton(), false, 200);
animate(player.getClosingOverlayView(), false, 200);
}
}
}
@Override
public void onPopupResizingStart() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingStart called");
}
player.getLoadingPanel().setVisibility(View.GONE);
player.hideControls(0, 0);
animate(player.getFastSeekOverlay(), false, 0);
animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0);
}
@Override
public void onPopupResizingEnd() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingEnd called");
}
}
}

View File

@@ -3,6 +3,8 @@ package org.schabi.newpipe.player.event;
import com.google.android.exoplayer2.PlaybackException;
public interface PlayerServiceEventListener extends PlayerEventListener {
void onViewCreated();
void onFullscreenStateChanged(boolean fullscreen);
void onScreenRotationButtonClicked();

View File

@@ -1,11 +1,11 @@
package org.schabi.newpipe.player.event;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player;
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
void onServiceConnected(Player player,
MainPlayer playerService,
PlayerService playerService,
boolean playAfterConnect);
void onServiceDisconnected();
}

View File

@@ -0,0 +1,186 @@
package org.schabi.newpipe.player.gesture
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import org.schabi.newpipe.databinding.PlayerBinding
import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.ui.VideoPlayerUi
/**
* Base gesture handling for [Player]
*
* This class contains the logic for the player gestures like View preparations
* and provides some abstract methods to make it easier separating the logic from the UI.
*/
abstract class BasePlayerGestureListener(
private val playerUi: VideoPlayerUi,
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
protected val player: Player = playerUi.player
protected val binding: PlayerBinding = playerUi.binding
override fun onTouch(v: View, event: MotionEvent): Boolean {
playerUi.gestureDetector.onTouchEvent(event)
return false
}
private fun onDoubleTap(
event: MotionEvent,
portion: DisplayPortion
) {
if (DEBUG) {
Log.d(
TAG,
"onDoubleTap called with playerType = [" +
player.playerType + "], portion = [" + portion + "]"
)
}
if (playerUi.isSomePopupMenuVisible) {
playerUi.hideControls(0, 0)
}
if (portion === DisplayPortion.LEFT || portion === DisplayPortion.RIGHT) {
startMultiDoubleTap(event)
} else if (portion === DisplayPortion.MIDDLE) {
player.playPause()
}
}
protected fun onSingleTap() {
if (playerUi.isControlsVisible) {
playerUi.hideControls(150, 0)
return
}
// -- Controls are not visible --
// When player is completed show controls and don't hide them later
if (player.currentState == Player.STATE_COMPLETED) {
playerUi.showControls(0)
} else {
playerUi.showControlsThenHide()
}
}
open fun onScrollEnd(event: MotionEvent) {
if (DEBUG) {
Log.d(
TAG,
"onScrollEnd called with playerType = [" +
player.playerType + "]"
)
}
if (playerUi.isControlsVisible && player.currentState == Player.STATE_PLAYING) {
playerUi.hideControls(
VideoPlayerUi.DEFAULT_CONTROLS_DURATION,
VideoPlayerUi.DEFAULT_CONTROLS_HIDE_TIME
)
}
}
// ///////////////////////////////////////////////////////////////////
// Simple gestures
// ///////////////////////////////////////////////////////////////////
override fun onDown(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDown called with e = [$e]")
if (isDoubleTapping && isDoubleTapEnabled) {
doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e))
return true
}
if (onDownNotDoubleTapping(e)) {
return super.onDown(e)
}
return true
}
/**
* @return true if `super.onDown(e)` should be called, false otherwise
*/
open fun onDownNotDoubleTapping(e: MotionEvent): Boolean {
return false // do not call super.onDown(e) by default, overridden for popup player
}
override fun onDoubleTap(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDoubleTap called with e = [$e]")
onDoubleTap(e, getDisplayPortion(e))
return true
}
// ///////////////////////////////////////////////////////////////////
// Multi double tapping
// ///////////////////////////////////////////////////////////////////
private var doubleTapControls: DoubleTapListener? = null
private val isDoubleTapEnabled: Boolean
get() = doubleTapDelay > 0
var isDoubleTapping = false
private set
fun doubleTapControls(listener: DoubleTapListener) = apply {
doubleTapControls = listener
}
private var doubleTapDelay = DOUBLE_TAP_DELAY
private val doubleTapHandler: Handler = Handler(Looper.getMainLooper())
private val doubleTapRunnable = Runnable {
if (DEBUG)
Log.d(TAG, "doubleTapRunnable called")
isDoubleTapping = false
doubleTapControls?.onDoubleTapFinished()
}
private fun startMultiDoubleTap(e: MotionEvent) {
if (!isDoubleTapping) {
if (DEBUG)
Log.d(TAG, "startMultiDoubleTap called with e = [$e]")
keepInDoubleTapMode()
doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e))
}
}
fun keepInDoubleTapMode() {
if (DEBUG)
Log.d(TAG, "keepInDoubleTapMode called")
isDoubleTapping = true
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay)
}
fun endMultiDoubleTap() {
if (DEBUG)
Log.d(TAG, "endMultiDoubleTap called")
isDoubleTapping = false
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapControls?.onDoubleTapFinished()
}
// ///////////////////////////////////////////////////////////////////
// Utils
// ///////////////////////////////////////////////////////////////////
abstract fun getDisplayPortion(e: MotionEvent): DisplayPortion
// Currently needed for scrolling since there is no action more the middle portion
abstract fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion
companion object {
private const val TAG = "BasePlayerGestListener"
private val DEBUG = Player.DEBUG
private const val DOUBLE_TAP_DELAY = 550L
}
}

View File

@@ -1,4 +1,4 @@
package org.schabi.newpipe.player.event;
package org.schabi.newpipe.player.gesture;
import android.content.Context;
import android.graphics.Rect;
@@ -8,24 +8,25 @@ import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import org.schabi.newpipe.R;
import java.util.Arrays;
import java.util.List;
public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout> {
public CustomBottomSheetBehavior(final Context context, final AttributeSet attrs) {
public CustomBottomSheetBehavior(@NonNull final Context context,
@Nullable final AttributeSet attrs) {
super(context, attrs);
}
Rect globalRect = new Rect();
private boolean skippingInterception = false;
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
private final List<Integer> skipInterceptionOfElements = List.of(
R.id.detail_content_root_layout, R.id.relatedItemsLayout,
R.id.itemsListPanel, R.id.view_pager, R.id.tab_layout, R.id.bottomControls,
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
@@ -33,7 +34,7 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout>
@Override
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
@NonNull final FrameLayout child,
final MotionEvent event) {
@NonNull final MotionEvent event) {
// Drop following when action ends
if (event.getAction() == MotionEvent.ACTION_CANCEL
|| event.getAction() == MotionEvent.ACTION_UP) {
@@ -57,7 +58,7 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout>
if (getState() == BottomSheetBehavior.STATE_EXPANDED
&& event.getAction() == MotionEvent.ACTION_DOWN) {
// Without overriding scrolling will not work when user touches these elements
for (final Integer element : skipInterceptionOfElements) {
for (final int element : skipInterceptionOfElements) {
final View view = child.findViewById(element);
if (view != null) {
final boolean visible = view.getGlobalVisibleRect(globalRect);

View File

@@ -1,4 +1,4 @@
package org.schabi.newpipe.player.event
package org.schabi.newpipe.player.gesture
enum class DisplayPortion {
LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF

View File

@@ -0,0 +1,7 @@
package org.schabi.newpipe.player.gesture
interface DoubleTapListener {
fun onDoubleTapStarted(portion: DisplayPortion)
fun onDoubleTapProgressDown(portion: DisplayPortion)
fun onDoubleTapFinished()
}

View File

@@ -0,0 +1,233 @@
package org.schabi.newpipe.player.gesture
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.math.MathUtils
import androidx.core.view.isVisible
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.helper.AudioReactor
import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.player.ui.MainPlayerUi
import org.schabi.newpipe.util.ThemeHelper.getAndroidDimenPx
import kotlin.math.abs
/**
* GestureListener for the player
*
* While [BasePlayerGestureListener] contains the logic behind the single gestures
* this class focuses on the visual aspect like hiding and showing the controls or changing
* volume/brightness during scrolling for specific events.
*/
class MainPlayerGestureListener(
private val playerUi: MainPlayerUi
) : BasePlayerGestureListener(playerUi), OnTouchListener {
private var isMoving = false
override fun onTouch(v: View, event: MotionEvent): Boolean {
super.onTouch(v, event)
if (event.action == MotionEvent.ACTION_UP && isMoving) {
isMoving = false
onScrollEnd(event)
}
return when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
v.parent?.requestDisallowInterceptTouchEvent(playerUi.isFullscreen)
true
}
MotionEvent.ACTION_UP -> {
v.parent?.requestDisallowInterceptTouchEvent(false)
false
}
else -> true
}
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
if (isDoubleTapping)
return true
super.onSingleTapConfirmed(e)
if (player.currentState != Player.STATE_BLOCKED)
onSingleTap()
return true
}
private fun onScrollVolume(distanceY: Float) {
val bar: ProgressBar = binding.volumeProgressBar
val audioReactor: AudioReactor = player.audioReactor
// If we just started sliding, change the progress bar to match the system volume
if (!binding.volumeRelativeLayout.isVisible) {
val volumePercent: Float = audioReactor.volume / audioReactor.maxVolume.toFloat()
bar.progress = (volumePercent * bar.max).toInt()
}
// Update progress bar
binding.volumeProgressBar.incrementProgressBy(distanceY.toInt())
// Update volume
val currentProgressPercent: Float = bar.progress / bar.max.toFloat()
val currentVolume = (audioReactor.maxVolume * currentProgressPercent).toInt()
audioReactor.volume = currentVolume
if (DEBUG) {
Log.d(TAG, "onScroll().volumeControl, currentVolume = $currentVolume")
}
// Update player center image
binding.volumeImageView.setImageDrawable(
AppCompatResources.getDrawable(
player.context,
when {
currentProgressPercent <= 0 -> R.drawable.ic_volume_off
currentProgressPercent < 0.25 -> R.drawable.ic_volume_mute
currentProgressPercent < 0.75 -> R.drawable.ic_volume_down
else -> R.drawable.ic_volume_up
}
)
)
// Make sure the correct layout is visible
if (!binding.volumeRelativeLayout.isVisible) {
binding.volumeRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
}
binding.brightnessRelativeLayout.isVisible = false
}
private fun onScrollBrightness(distanceY: Float) {
val parent: AppCompatActivity = playerUi.parentActivity.orElse(null) ?: return
val window = parent.window
val layoutParams = window.attributes
val bar: ProgressBar = binding.brightnessProgressBar
// Update progress bar
val oldBrightness = layoutParams.screenBrightness
bar.progress = (bar.max * MathUtils.clamp(oldBrightness, 0f, 1f)).toInt()
bar.incrementProgressBy(distanceY.toInt())
// Update brightness
val currentProgressPercent = bar.progress.toFloat() / bar.max
layoutParams.screenBrightness = currentProgressPercent
window.attributes = layoutParams
// Save current brightness level
PlayerHelper.setScreenBrightness(parent, currentProgressPercent)
if (DEBUG) {
Log.d(
TAG,
"onScroll().brightnessControl, " +
"currentBrightness = " + currentProgressPercent
)
}
// Update player center image
binding.brightnessImageView.setImageDrawable(
AppCompatResources.getDrawable(
player.context,
when {
currentProgressPercent < 0.25 -> R.drawable.ic_brightness_low
currentProgressPercent < 0.75 -> R.drawable.ic_brightness_medium
else -> R.drawable.ic_brightness_high
}
)
)
// Make sure the correct layout is visible
if (!binding.brightnessRelativeLayout.isVisible) {
binding.brightnessRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
}
binding.volumeRelativeLayout.isVisible = false
}
override fun onScrollEnd(event: MotionEvent) {
super.onScrollEnd(event)
if (binding.volumeRelativeLayout.isVisible) {
binding.volumeRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
}
if (binding.brightnessRelativeLayout.isVisible) {
binding.brightnessRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
}
}
override fun onScroll(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (!playerUi.isFullscreen) {
return false
}
// Calculate heights of status and navigation bars
val statusBarHeight = getAndroidDimenPx(player.context, "status_bar_height")
val navigationBarHeight = getAndroidDimenPx(player.context, "navigation_bar_height")
// Do not handle this event if initially it started from status or navigation bars
val isTouchingStatusBar = initialEvent.y < statusBarHeight
val isTouchingNavigationBar = initialEvent.y > (binding.root.height - navigationBarHeight)
if (isTouchingStatusBar || isTouchingNavigationBar) {
return false
}
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
if (
!isMoving && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
player.currentState == Player.STATE_COMPLETED
) {
return false
}
isMoving = true
// -- Brightness and Volume control --
val isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(player.context)
val isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(player.context)
if (isBrightnessGestureEnabled && isVolumeGestureEnabled) {
if (getDisplayHalfPortion(initialEvent) === DisplayPortion.LEFT_HALF) {
onScrollBrightness(distanceY)
} else /* DisplayPortion.RIGHT_HALF */ {
onScrollVolume(distanceY)
}
} else if (isBrightnessGestureEnabled) {
onScrollBrightness(distanceY)
} else if (isVolumeGestureEnabled) {
onScrollVolume(distanceY)
}
return true
}
override fun getDisplayPortion(e: MotionEvent): DisplayPortion {
return when {
e.x < binding.root.width / 3.0 -> DisplayPortion.LEFT
e.x > binding.root.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
}
override fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
return when {
e.x < binding.root.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
}
companion object {
private val TAG = MainPlayerGestureListener::class.java.simpleName
private val DEBUG = MainActivity.DEBUG
private const val MOVEMENT_THRESHOLD = 40
}
}

View File

@@ -0,0 +1,283 @@
package org.schabi.newpipe.player.gesture
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.core.math.MathUtils
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.player.ui.PopupPlayerUi
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.max
import kotlin.math.min
class PopupPlayerGestureListener(
private val playerUi: PopupPlayerUi,
) : BasePlayerGestureListener(playerUi) {
private var isMoving = false
private var initialPopupX: Int = -1
private var initialPopupY: Int = -1
private var isResizing = false
// initial coordinates and distance between fingers
private var initPointerDistance = -1.0
private var initFirstPointerX = -1f
private var initFirstPointerY = -1f
private var initSecPointerX = -1f
private var initSecPointerY = -1f
override fun onTouch(v: View, event: MotionEvent): Boolean {
super.onTouch(v, event)
if (event.pointerCount == 2 && !isMoving && !isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.")
}
onPopupResizingStart()
// record coordinates of fingers
initFirstPointerX = event.getX(0)
initFirstPointerY = event.getY(0)
initSecPointerX = event.getX(1)
initSecPointerY = event.getY(1)
// record distance between fingers
initPointerDistance = hypot(
initFirstPointerX - initSecPointerX.toDouble(),
initFirstPointerY - initSecPointerY.toDouble()
)
isResizing = true
}
if (event.action == MotionEvent.ACTION_MOVE && !isMoving && isResizing) {
if (DEBUG) {
Log.d(
TAG,
"onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" +
"[${event.rawX}, ${event.rawY}]"
)
}
return handleMultiDrag(event)
}
if (event.action == MotionEvent.ACTION_UP) {
if (DEBUG) {
Log.d(
TAG,
"onTouch() ACTION_UP > v = [$v], e1.getRaw =" +
" [${event.rawX}, ${event.rawY}]"
)
}
if (isMoving) {
isMoving = false
onScrollEnd(event)
}
if (isResizing) {
isResizing = false
initPointerDistance = (-1).toDouble()
initFirstPointerX = (-1).toFloat()
initFirstPointerY = (-1).toFloat()
initSecPointerX = (-1).toFloat()
initSecPointerY = (-1).toFloat()
onPopupResizingEnd()
player.changeState(player.currentState)
}
if (!playerUi.isPopupClosing) {
playerUi.savePopupPositionAndSizeToPrefs()
}
}
v.performClick()
return true
}
override fun onScrollEnd(event: MotionEvent) {
super.onScrollEnd(event)
if (playerUi.isInsideClosingRadius(event)) {
playerUi.closePopup()
} else if (!playerUi.isPopupClosing) {
playerUi.closeOverlayBinding.closeButton.animate(false, 200)
binding.closingOverlay.animate(false, 200)
}
}
private fun handleMultiDrag(event: MotionEvent): Boolean {
if (initPointerDistance == -1.0 || event.pointerCount != 2) {
return false
}
// get the movements of the fingers
val firstPointerMove = hypot(
event.getX(0) - initFirstPointerX.toDouble(),
event.getY(0) - initFirstPointerY.toDouble()
)
val secPointerMove = hypot(
event.getX(1) - initSecPointerX.toDouble(),
event.getY(1) - initSecPointerY.toDouble()
)
// minimum threshold beyond which pinch gesture will work
val minimumMove = ViewConfiguration.get(player.context).scaledTouchSlop
if (max(firstPointerMove, secPointerMove) <= minimumMove) {
return false
}
// calculate current distance between the pointers
val currentPointerDistance = hypot(
event.getX(0) - event.getX(1).toDouble(),
event.getY(0) - event.getY(1).toDouble()
)
val popupWidth = playerUi.popupLayoutParams.width.toDouble()
// change co-ordinates of popup so the center stays at the same position
val newWidth = popupWidth * currentPointerDistance / initPointerDistance
initPointerDistance = currentPointerDistance
playerUi.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt()
playerUi.checkPopupPositionBounds()
playerUi.updateScreenSize()
playerUi.changePopupSize(min(playerUi.screenWidth.toDouble(), newWidth).toInt())
return true
}
private fun onPopupResizingStart() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingStart called")
}
binding.loadingPanel.visibility = View.GONE
playerUi.hideControls(0, 0)
binding.fastSeekOverlay.animate(false, 0)
binding.currentDisplaySeek.animate(false, 0, AnimationType.ALPHA, 0)
}
private fun onPopupResizingEnd() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingEnd called")
}
}
override fun onLongPress(e: MotionEvent?) {
playerUi.updateScreenSize()
playerUi.checkPopupPositionBounds()
playerUi.changePopupSize(playerUi.screenWidth)
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
return if (player.popupPlayerSelected()) {
val absVelocityX = abs(velocityX)
val absVelocityY = abs(velocityY)
if (absVelocityX.coerceAtLeast(absVelocityY) > TOSS_FLING_VELOCITY) {
if (absVelocityX > TOSS_FLING_VELOCITY) {
playerUi.popupLayoutParams.x = velocityX.toInt()
}
if (absVelocityY > TOSS_FLING_VELOCITY) {
playerUi.popupLayoutParams.y = velocityY.toInt()
}
playerUi.checkPopupPositionBounds()
playerUi.windowManager.updateViewLayout(binding.root, playerUi.popupLayoutParams)
return true
}
return false
} else {
true
}
}
override fun onDownNotDoubleTapping(e: MotionEvent): Boolean {
// Fix popup position when the user touch it, it may have the wrong one
// because the soft input is visible (the draggable area is currently resized).
playerUi.updateScreenSize()
playerUi.checkPopupPositionBounds()
playerUi.popupLayoutParams.let {
initialPopupX = it.x
initialPopupY = it.y
}
return true // we want `super.onDown(e)` to be called
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
if (isDoubleTapping)
return true
if (player.exoPlayerIsNull())
return false
onSingleTap()
return true
}
override fun onScroll(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (isResizing) {
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
}
if (!isMoving) {
playerUi.closeOverlayBinding.closeButton.animate(true, 200)
}
isMoving = true
val diffX = (movingEvent.rawX - initialEvent.rawX)
val posX = MathUtils.clamp(
initialPopupX + diffX,
0f, (playerUi.screenWidth - playerUi.popupLayoutParams.width).toFloat()
)
val diffY = (movingEvent.rawY - initialEvent.rawY)
val posY = MathUtils.clamp(
initialPopupY + diffY,
0f, (playerUi.screenHeight - playerUi.popupLayoutParams.height).toFloat()
)
playerUi.popupLayoutParams.x = posX.toInt()
playerUi.popupLayoutParams.y = posY.toInt()
// -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
val showClosingOverlayView: Boolean = playerUi.isInsideClosingRadius(movingEvent)
// Check if an view is in expected state and if not animate it into the correct state
val expectedVisibility = if (showClosingOverlayView) View.VISIBLE else View.GONE
if (binding.closingOverlay.visibility != expectedVisibility) {
binding.closingOverlay.animate(showClosingOverlayView, 200)
}
playerUi.windowManager.updateViewLayout(binding.root, playerUi.popupLayoutParams)
return true
}
override fun getDisplayPortion(e: MotionEvent): DisplayPortion {
return when {
e.x < playerUi.popupLayoutParams.width / 3.0 -> DisplayPortion.LEFT
e.x > playerUi.popupLayoutParams.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
}
override fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
return when {
e.x < playerUi.popupLayoutParams.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
}
companion object {
private val TAG = PopupPlayerGestureListener::class.java.simpleName
private val DEBUG = MainActivity.DEBUG
private const val TOSS_FLING_VELOCITY = 2500
}
}

View File

@@ -39,8 +39,8 @@ final class CacheFactory implements DataSource.Factory {
.createDataSource();
final FileDataSource fileSource = new FileDataSource();
final CacheDataSink dataSink
= new CacheDataSink(cache, PlayerHelper.getPreferredFileSize());
final CacheDataSink dataSink =
new CacheDataSink(cache, PlayerHelper.getPreferredFileSize());
return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
}
}

View File

@@ -1,226 +0,0 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media.session.MediaButtonReceiver;
import com.google.android.exoplayer2.ForwardingPlayer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
import java.util.Optional;
public class MediaSessionManager {
private static final String TAG = MediaSessionManager.class.getSimpleName();
public static final boolean DEBUG = MainActivity.DEBUG;
@NonNull
private final MediaSessionCompat mediaSession;
@NonNull
private final MediaSessionConnector sessionConnector;
private int lastTitleHashCode;
private int lastArtistHashCode;
private long lastDuration;
private int lastAlbumArtHashCode;
public MediaSessionManager(@NonNull final Context context,
@NonNull final Player player,
@NonNull final MediaSessionCallback callback) {
mediaSession = new MediaSessionCompat(context, TAG);
mediaSession.setActive(true);
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_NONE, -1, 1)
.setActions(PlaybackStateCompat.ACTION_SEEK_TO
| PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_PAUSE // was play and pause now play/pause
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE
| PlaybackStateCompat.ACTION_STOP)
.build());
sessionConnector = new MediaSessionConnector(mediaSession);
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
sessionConnector.setPlayer(new ForwardingPlayer(player) {
@Override
public void play() {
callback.play();
}
@Override
public void pause() {
callback.pause();
}
});
}
@Nullable
@SuppressWarnings("UnusedReturnValue")
public KeyEvent handleMediaButtonIntent(final Intent intent) {
return MediaButtonReceiver.handleIntent(mediaSession, intent);
}
public MediaSessionCompat.Token getSessionToken() {
return mediaSession.getSessionToken();
}
/**
* sets the Metadata - if required.
*
* @param title {@link MediaMetadataCompat#METADATA_KEY_TITLE}
* @param artist {@link MediaMetadataCompat#METADATA_KEY_ARTIST}
* @param optAlbumArt {@link MediaMetadataCompat#METADATA_KEY_ALBUM_ART}
* @param duration {@link MediaMetadataCompat#METADATA_KEY_DURATION}
* - should be a negative value for unknown durations, e.g. for livestreams
*/
public void setMetadata(@NonNull final String title,
@NonNull final String artist,
@NonNull final Optional<Bitmap> optAlbumArt,
final long duration
) {
if (DEBUG) {
Log.d(TAG, "setMetadata called:"
+ " t: " + title
+ " a: " + artist
+ " thumb: " + (
optAlbumArt.isPresent()
? optAlbumArt.get().hashCode()
: "<none>")
+ " d: " + duration);
}
if (!mediaSession.isActive()) {
if (DEBUG) {
Log.d(TAG, "setMetadata: mediaSession not active - exiting");
}
return;
}
if (!checkIfMetadataShouldBeSet(title, artist, optAlbumArt, duration)) {
if (DEBUG) {
Log.d(TAG, "setMetadata: No update required - exiting");
}
return;
}
if (DEBUG) {
Log.d(TAG, "setMetadata: N_Metadata update:"
+ " t: " + title
+ " a: " + artist
+ " thumb: " + (
optAlbumArt.isPresent()
? optAlbumArt.get().hashCode()
: "<none>")
+ " d: " + duration);
}
final MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration);
if (optAlbumArt.isPresent()) {
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, optAlbumArt.get());
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, optAlbumArt.get());
}
mediaSession.setMetadata(builder.build());
lastTitleHashCode = title.hashCode();
lastArtistHashCode = artist.hashCode();
lastDuration = duration;
optAlbumArt.ifPresent(bitmap -> lastAlbumArtHashCode = bitmap.hashCode());
}
private boolean checkIfMetadataShouldBeSet(
@NonNull final String title,
@NonNull final String artist,
@NonNull final Optional<Bitmap> optAlbumArt,
final long duration
) {
// Check if the values have changed since the last time
if (title.hashCode() != lastTitleHashCode
|| artist.hashCode() != lastArtistHashCode
|| duration != lastDuration
|| (optAlbumArt.isPresent() && optAlbumArt.get().hashCode() != lastAlbumArtHashCode)
) {
if (DEBUG) {
Log.d(TAG,
"checkIfMetadataShouldBeSet: true - reason: changed values since last");
}
return true;
}
// Check if the currently set metadata is valid
if (getMetadataTitle() == null
|| getMetadataArtist() == null
// Note that the duration can be <= 0 for live streams
) {
if (DEBUG) {
if (getMetadataTitle() == null) {
Log.d(TAG,
"N_getMetadataTitle: title == null");
} else if (getMetadataArtist() == null) {
Log.d(TAG,
"N_getMetadataArtist: artist == null");
}
}
return true;
}
// If we got an album art check if the current set AlbumArt is null
if (optAlbumArt.isPresent() && getMetadataAlbumArt() == null) {
if (DEBUG) {
Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
}
return true;
}
// Default - no update required
return false;
}
@Nullable
private Bitmap getMetadataAlbumArt() {
return mediaSession.getController().getMetadata()
.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
}
@Nullable
private String getMetadataTitle() {
return mediaSession.getController().getMetadata()
.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
}
@Nullable
private String getMetadataArtist() {
return mediaSession.getController().getMetadata()
.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
}
/**
* Should be called on player destruction to prevent leakage.
*/
public void dispose() {
sessionConnector.setPlayer(null);
sessionConnector.setQueueNavigator(null);
mediaSession.setActive(false);
mediaSession.release();
}
}

View File

@@ -11,7 +11,6 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.CheckBox;
import android.widget.SeekBar;
@@ -21,16 +20,16 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.math.MathUtils;
import androidx.fragment.app.DialogFragment;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
import org.schabi.newpipe.util.SliderStrategy;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
@@ -149,7 +148,7 @@ public class PlaybackParameterDialog extends DialogFragment {
assureCorrectAppLanguage(getContext());
Icepick.restoreInstanceState(this, savedInstanceState);
binding = DialogPlaybackParameterBinding.inflate(LayoutInflater.from(getContext()));
binding = DialogPlaybackParameterBinding.inflate(getLayoutInflater());
initUI();
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
@@ -207,7 +206,7 @@ public class PlaybackParameterDialog extends DialogFragment {
? View.VISIBLE
: View.GONE);
animateRotation(binding.pitchToogleControlModes,
Player.DEFAULT_CONTROLS_DURATION,
VideoPlayerUi.DEFAULT_CONTROLS_DURATION,
isCurrentlyVisible ? 180 : 0);
});
@@ -334,10 +333,8 @@ public class PlaybackParameterDialog extends DialogFragment {
}
private Map<Boolean, TextView> getPitchControlModeComponentMappings() {
final Map<Boolean, TextView> mappings = new HashMap<>();
mappings.put(PITCH_CTRL_MODE_PERCENT, binding.pitchControlModePercent);
mappings.put(PITCH_CTRL_MODE_SEMITONE, binding.pitchControlModeSemitone);
return mappings;
return Map.of(PITCH_CTRL_MODE_PERCENT, binding.pitchControlModePercent,
PITCH_CTRL_MODE_SEMITONE, binding.pitchControlModeSemitone);
}
private void changePitchControlMode(final boolean semitones) {
@@ -407,13 +404,11 @@ public class PlaybackParameterDialog extends DialogFragment {
}
private Map<Double, TextView> getStepSizeComponentMappings() {
final Map<Double, TextView> mappings = new HashMap<>();
mappings.put(STEP_1_PERCENT_VALUE, binding.stepSizeOnePercent);
mappings.put(STEP_5_PERCENT_VALUE, binding.stepSizeFivePercent);
mappings.put(STEP_10_PERCENT_VALUE, binding.stepSizeTenPercent);
mappings.put(STEP_25_PERCENT_VALUE, binding.stepSizeTwentyFivePercent);
mappings.put(STEP_100_PERCENT_VALUE, binding.stepSizeOneHundredPercent);
return mappings;
return Map.of(STEP_1_PERCENT_VALUE, binding.stepSizeOnePercent,
STEP_5_PERCENT_VALUE, binding.stepSizeFivePercent,
STEP_10_PERCENT_VALUE, binding.stepSizeTenPercent,
STEP_25_PERCENT_VALUE, binding.stepSizeTwentyFivePercent,
STEP_100_PERCENT_VALUE, binding.stepSizeOneHundredPercent);
}
private void setStepSizeToUI(final double newStepSize) {
@@ -532,7 +527,7 @@ public class PlaybackParameterDialog extends DialogFragment {
}
private void setAndUpdateTempo(final double newTempo) {
this.tempo = calcValidTempo(newTempo);
this.tempo = MathUtils.clamp(newTempo, MIN_PITCH_OR_SPEED, MAX_PITCH_OR_SPEED);
binding.tempoSeekbar.setProgress(QUADRATIC_STRATEGY.progressOf(tempo));
setText(binding.tempoCurrentText, PlayerHelper::formatSpeed, tempo);
@@ -551,13 +546,8 @@ public class PlaybackParameterDialog extends DialogFragment {
pitchPercent);
}
private double calcValidTempo(final double newTempo) {
return Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newTempo));
}
private double calcValidPitch(final double newPitch) {
final double calcPitch =
Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newPitch));
final double calcPitch = MathUtils.clamp(newPitch, MIN_PITCH_OR_SPEED, MAX_PITCH_OR_SPEED);
if (!isCurrentPitchControlModeSemitone()) {
return calcPitch;

View File

@@ -208,8 +208,8 @@ public class PlayerDataSource {
Log.w(TAG, "instantiateCacheIfNeeded: could not create cache dir");
}
final LeastRecentlyUsedCacheEvictor evictor
= new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
final LeastRecentlyUsedCacheEvictor evictor =
new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
}
}

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