1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-11-17 15:24:55 +00:00

Compare commits

..

757 Commits
v0.25.0 ... dev

Author SHA1 Message Date
Stypox
6fe417abc6
Merge pull request #11024 from AbdeltwabMF/fix/rtl_lang_adjustment_bookmark
Adjust the playlist bookmark item layout for RTL languages
2024-11-14 16:26:25 +01:00
Stypox
a229ab68d5
Merge pull request #11696 from codyit/history-remove-dialog-override
Remove history dialog override so clicking "Start playing in the background" would only enqueue the current item instead of the full history which is usually massive
2024-11-12 10:43:01 +01:00
Stypox
544b30290d
Merge pull request #11694 from VishramKidPG123/fix-typo-in-readme
Fix a typo in README
2024-11-12 10:32:01 +01:00
Cody T.-H. Chiu
cb300724da Remove history dialog override so clicking "Start playing in the background" would only enqueue the current item instead of the full history which is usually massive 2024-11-12 18:24:23 +13:00
VishramKidPG123
0ac5a269ff
Update README.md 2024-11-11 22:40:29 -05:00
Tobi
0009613608
Merge pull request #11140 from shrimprugbysnowowl/dev
Adding Hash of Signing Key to README
2024-11-11 07:38:13 +01:00
Tobi
7c18d4dd01
Update README.md 2024-11-11 07:35:37 +01:00
Tobi
fe1c538f9c
Update README.md 2024-11-11 07:34:45 +01:00
Stypox
f08e07873a
Merge pull request #11566 from nicholasala/fix/#10993-strange-playlist-order
Fixed playlist order
2024-11-10 15:45:33 +01:00
Tobi
c0b36b86b9
Merge pull request #11614 from rmtilde/fix-related-items-enque-popup-crash
Fix related items list enqueue popup crash
2024-11-03 10:13:45 +01:00
rmtilde
66ec596f67
Update app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.java
Co-authored-by: Tobi <TobiGr@users.noreply.github.com>
2024-11-03 18:26:38 +11:00
Tobi
90404a23ce
Merge pull request #11621 from u7656655/fixing-ui-crash-11468
Fix UI crash when user navigates away before the download dialog appears
2024-11-02 23:30:35 +01:00
Tobi
64ad05d813
Merge pull request #11629 from Two-Ai/kotlin-getStringSafe
Add null-safe SharedPreferences.getStringSafe
2024-10-27 20:58:25 +01:00
TwoAi
734b6e2b67 Add null-safe SharedPreferences.getStringSafe
Null-safe alternative to SharedPreferences.getString that guarantees the return value is non-null when defValue is non-null.
2024-10-27 20:38:28 +01:00
Tobi
94f992a2e2
Merge pull request #11656 from litetex/better-control-over-version
[Build] Make it possible control the version code and name
2024-10-27 20:05:53 +01:00
litetex
c8550695aa
Make it possible control the version code and name 2024-10-27 17:51:22 +01:00
Tobi
cdac50bab3
Merge pull request #11596 from Thompson3142/fix_scrubbing_seekbar_preview_crash
Fix seekbar crashing on drag with faulty frameset
2024-10-27 16:19:44 +01:00
Thompson3142
23961548c0 Formatting changes (back to original) 2024-10-27 14:38:25 +01:00
Thompson3142
ba1e9c8e1b Update comment
Co-authored-by: Tobi <TobiGr@users.noreply.github.com>
2024-10-27 14:17:32 +01:00
Tobi
f4baf4628e
Update app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java 2024-10-27 09:41:45 +01:00
Tobi
05a87da827
Merge pull request #11651 from u7656655/fix-addtoplaylist-crash
Fix crash after adding item to a playlist caused by null thumbnail URL
2024-10-27 09:15:49 +01:00
Jacob Hawkins
fef40014a0 Added not null check for thumbnail URL before performing comparison 2024-10-27 17:38:57 +11:00
rmtilde
1996c1176c
Merge branch 'TeamNewPipe:dev' into fix-related-items-enque-popup-crash 2024-10-26 20:33:17 +11:00
Elva Kang
0190bcee25 Fix line length violation 2024-10-24 16:04:53 +11:00
Elva Kang
1ed4928f40 Add comment for fragment lifecycle checks before showing DownloadDialog 2024-10-24 11:47:23 +11:00
Elva Kang
63bc982cb2
Merge branch 'TeamNewPipe:dev' into fixing-ui-crash-11468 2024-10-24 11:11:37 +11:00
Stypox
3a286515f2
Merge pull request #11636 from litetex/fix-build-2024-10
Fix compilation
2024-10-23 22:18:48 +02:00
litetex
2e96b65fda
Replaced `Icepick with Bridge and Android-State`
* IcePick fails on Java 21 (default in Android Studio 2024.2)
* Bridge is the most modern alternative that is currently available. It is backed by ``Android-State`` and can be configured with various frameworks
* In the long term this should be replaced with something better
2024-10-23 21:28:07 +02:00
litetex
2482615460
Fix Android Gradle plugin warning 2024-10-22 21:40:16 +02:00
litetex
9384365061
Update Gradle to latest version 2024-10-22 21:39:44 +02:00
litetex
b1d4b66aa6
Replace symlink with original
Co-Authored-By: Thompson3142 <115718208+thompson3142@users.noreply.github.com>
2024-10-22 21:24:10 +02:00
litetex
ea0da5fdbd
Delete symlink 2024-10-22 21:24:09 +02:00
litetex
d80b6a759c
Use working Extractor version
The tag can't be resolved by Jitpack so use the commit-hash instead
2024-10-22 21:23:34 +02:00
litetex
8106ba68b5
CI: Use Java 21 2024-10-22 21:23:26 +02:00
litetex
ee15a72e4f
Fix build failing locally due to outdated kotlin version 2024-10-22 21:03:08 +02:00
Elva Kang
2eb256799d Revert "Project now runs"
This reverts commit 53edd054aa.
2024-10-20 10:29:48 +11:00
Elva Kang
0cf4732d8a Fix UI crash when user navigates away before the download dialog appears 2024-10-19 19:43:34 +11:00
Jacob Hawkins
53edd054aa Project now runs 2024-10-17 15:14:15 +11:00
rmtilde
678f0a786a
Merge pull request #1 from rmtilde/fix-related-items-enqueue-on-video-change
Fix Crash on Related Items Modal
2024-10-17 13:37:19 +11:00
rmtilde
b14f65804d Added comments to explain changes 2024-10-16 23:58:32 +11:00
u7310752
781a69d60d Chanegd related videos enqueue modal to attach to parent fragment instead 2024-10-16 20:52:43 +11:00
Thompson3142
eb9f300e60
Fix seekbar preview crashes (#11584)
Fixed crashes from recycled bitmaps by creating real copies of bitmaps if necessary + some minor refactoring
2024-10-10 10:32:06 +02:00
Nicholas Sala
063568b620 Fixed playlist order between "Bookmarked Playlists" list and "add to playlist" dialog list. Now both lists are sorted using case insensitive order if the user has not yet adjusted manually the order. 2024-09-26 13:24:26 +00:00
Mihael_River
035c394cf6
Fixing the 404 page not found, when clicking on "contribution notes" in multiple README.md's translated into different languages (#11487)
Link to contribution notes wasn't working

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

---------

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

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

* Update Matrix room link in CONTRIBUTING.md

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

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

Translated using Weblate (Croatian)

Currently translated at 99.5% (735 of 738 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (738 of 738 strings)

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

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (French (Louisiana))

Currently translated at 0.2% (2 of 738 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (738 of 738 strings)

Added translation using Weblate (French (Louisiana))

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (French)

Currently translated at 99.7% (736 of 738 strings)

Added translation using Weblate (Arabic (Tunisian))

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

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

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Kannada)

Currently translated at 5.8% (43 of 738 strings)

Translated using Weblate (Kannada)

Currently translated at 5.1% (4 of 78 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (737 of 738 strings)

Translated using Weblate (German)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (German)

Currently translated at 100.0% (738 of 738 strings)

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

Currently translated at 99.0% (731 of 738 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (736 of 738 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (736 of 738 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 50.0% (39 of 78 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Japanese)

Currently translated at 99.4% (734 of 738 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Slovak)

Currently translated at 21.7% (17 of 78 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 62.8% (49 of 78 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (78 of 78 strings)

Translated using Weblate (Polish)

Currently translated at 61.5% (48 of 78 strings)

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

Currently translated at 23.0% (18 of 78 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Vietnamese)

Currently translated at 46.7% (36 of 77 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (736 of 738 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (German)

Currently translated at 99.7% (736 of 738 strings)

Translated using Weblate (Korean)

Currently translated at 31.1% (24 of 77 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Korean)

Currently translated at 98.9% (726 of 734 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (734 of 734 strings)

Translated using Weblate (German)

Currently translated at 99.7% (732 of 734 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (German)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Hungarian)

Currently translated at 16.8% (13 of 77 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (730 of 730 strings)

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

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (730 of 730 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (730 of 730 strings)

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

show all videos OverallDuration in local Playlist too

refactor to make implementation in LocalPlaylistFragment and PlaylistFragment more obviously similar

unfortunately could not refactor upto BaseLocalListFragment

revert the changes for online Playlists

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

next try to improve user-experience with online Playlist

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

even more improve user-experience for online Playlist

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

make showing of playlists duration configurable, disabled by default

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

no idea why I handled it as int earlier

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

This reverts commit bc1ba17a20d3dd1763210f81d7ca67c5f1734a3d.

Fix rebase

Apply review

Rename video -> stream

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

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

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

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Danish)

Currently translated at 96.4% (703 of 729 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Danish)

Currently translated at 88.3% (644 of 729 strings)

Translated using Weblate (Danish)

Currently translated at 88.3% (644 of 729 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Georgian)

Currently translated at 92.2% (71 of 77 strings)

Translated using Weblate (Uzbek (latin))

Currently translated at 62.6% (457 of 729 strings)

Translated using Weblate (Santali)

Currently translated at 12.6% (92 of 729 strings)

Translated using Weblate (French)

Currently translated at 89.6% (69 of 77 strings)

Translated using Weblate (Japanese)

Currently translated at 11.6% (9 of 77 strings)

Translated using Weblate (Bulgarian)

Currently translated at 5.1% (4 of 77 strings)

Translated using Weblate (Bengali)

Currently translated at 20.7% (16 of 77 strings)

Translated using Weblate (German)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Bengali (India))

Currently translated at 40.7% (297 of 729 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 85.5% (624 of 729 strings)

Translated using Weblate (Tamil)

Currently translated at 46.6% (340 of 729 strings)

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 55.1% (402 of 729 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (German)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 98.7% (76 of 77 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.4% (725 of 729 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Croatian)

Currently translated at 99.4% (725 of 729 strings)

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

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Croatian)

Currently translated at 2.5% (2 of 77 strings)

Translated using Weblate (Malay)

Currently translated at 48.6% (355 of 729 strings)

Translated using Weblate (Croatian)

Currently translated at 99.3% (724 of 729 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Finnish)

Currently translated at 98.3% (717 of 729 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (729 of 729 strings)

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

Currently translated at 22.0% (17 of 77 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 95.4% (696 of 729 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Vietnamese)

Currently translated at 42.8% (33 of 77 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Esperanto)

Currently translated at 70.7% (516 of 729 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 94.6% (690 of 729 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (French)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Santali)

Currently translated at 10.0% (73 of 729 strings)

Translated using Weblate (Turkish)

Currently translated at 42.8% (33 of 77 strings)

Translated using Weblate (German)

Currently translated at 81.8% (63 of 77 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (729 of 729 strings)

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

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Kannada)

Currently translated at 5.4% (40 of 729 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (729 of 729 strings)

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

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Croatian)

Currently translated at 88.7% (647 of 729 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Slovak)

Currently translated at 20.7% (16 of 77 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (French)

Currently translated at 99.8% (728 of 729 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Catalan)

Currently translated at 86.5% (631 of 729 strings)

Translated using Weblate (Telugu)

Currently translated at 58.9% (430 of 729 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Tigrinya)

Currently translated at 8.9% (65 of 729 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (76 of 77 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Slovak)

Currently translated at 99.7% (727 of 729 strings)

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

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 85.5% (624 of 729 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (728 of 729 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (German)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.8% (728 of 729 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Swedish)

Currently translated at 99.8% (728 of 729 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (728 of 729 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (German)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (German)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (729 of 729 strings)

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

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Serbian)

Currently translated at 18.1% (14 of 77 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (77 of 77 strings)

Translated using Weblate (Greek)

Currently translated at 24.6% (19 of 77 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (German)

Currently translated at 100.0% (728 of 728 strings)

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

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Kannada)

Currently translated at 5.5% (40 of 726 strings)

Translated using Weblate (Sinhala)

Currently translated at 2.6% (2 of 76 strings)

Translated using Weblate (Sinhala)

Currently translated at 4.1% (30 of 726 strings)

Translated using Weblate (Bulgarian)

Currently translated at 5.2% (4 of 76 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Turkish)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Slovak)

Currently translated at 98.4% (715 of 726 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (726 of 726 strings)

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

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Italian)

Currently translated at 99.5% (723 of 726 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (German)

Currently translated at 100.0% (726 of 726 strings)

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

Currently translated at 21.0% (16 of 76 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Romanian)

Currently translated at 99.8% (724 of 725 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (725 of 725 strings)

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

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

So let’s drop the setting write and just rely on the default values.
2024-01-06 17:24:53 +01:00
Roshan Jossy
66e8e2a696
use GitHub markdown to emphasise warning in Readme 2024-01-01 15:07:37 +01:00
Stypox
55373c95d9
Update NewPipeExtractor to include MediaCCC channel fix 2023-12-30 23:49:09 +01:00
Stypox
04bdc1cc0b
Base cache key on info type instead of item type
It didn't really made sense to consider two cache keys as equal based on the type of items contained within that list.
2023-12-30 23:46:16 +01:00
Stypox
1d8850d1b2
Merge pull request #10712 from Stypox/notification-actions-api-33-2
[Android 13+] Restore support of custom notification actions
2023-12-30 21:55:44 +01:00
Stypox
f98548698a
Android 33 -> Android 13
Co-authored-by: Tobi <TobiGr@users.noreply.github.com>
2023-12-30 21:55:32 +01:00
Stypox
4b1824e8c1
Allow play/pausing from notification when buffering
This change is in line with a recent change in how the play/pause button behaves in the player ui: if the buffering indicator is shown, it's still possible to toggle play/pause, to allow e.g. pausing videos before they even start.
This change was needed because on Android 13+ notification actions can't be null, and thus the buffering hourglass action wasn't shown.
2023-12-29 16:18:26 +01:00
Stypox
17e88f1749
Do not update notification actions if nothing changed
This should avoid costly updates of the media session.
2023-12-29 16:16:45 +01:00
Stypox
5edafca05a
Implement notification actions via MediaSessionConnector on Android 13+ 2023-12-29 15:54:15 +01:00
Stypox
2c4c283099
Extract NotificationActionData from NotificationUtil 2023-12-29 15:54:15 +01:00
Stypox
9fb8125655
Allow each notification slot to contain any possible action 2023-12-29 15:54:15 +01:00
Stypox
aab6580195
Extract NotificationSlot from NotificationActionsPreference 2023-12-29 12:31:59 +01:00
Stypox
30f0db1d28
Customize only 2 notification actions on Android 13+ 2023-12-29 12:13:08 +01:00
Stypox
5a4dae2070
Fix settings_notification.xml indentation 2023-12-29 11:37:17 +01:00
Stypox
8345f348f6
Merge pull request #10091 from TeamNewPipe/feat/playlist_description
Add playlist description to playlist fragment
2023-12-29 10:58:13 +01:00
Stypox
9220e32463
Fix FeedDAOTest 2023-12-29 10:54:31 +01:00
Stypox
845e72bf4a
Merge branch 'master' into dev 2023-12-29 10:48:37 +01:00
Tobi
49429ff40a
Merge pull request #10700 from TeamNewPipe/newpipe_0.26.1
Newpipe 0.26.1
2023-12-26 18:26:48 +01:00
TobiGr
3df21ad25e Bump version to 0.26.1 (996) 2023-12-26 16:59:02 +01:00
TobiGr
d0f4600be4 Add changelog for NewPipe 0.26.1 2023-12-26 16:58:49 +01:00
TobiGr
0fa2e76c3e Fix NPE when ChannelTabLHFactory not implemented for a service
Fixes #10698
2023-12-26 16:55:52 +01:00
Stypox
9ff1b5230f
Improve TextEllipsizer class 2023-12-23 18:04:05 +01:00
TobiGr
65eb631711
Ellipsize playlist description if it is longer than 5 lines
The description can be expanded / collapsed via a "show more" / "show less" button.
2023-12-23 12:33:52 +01:00
TobiGr
6c99557553
Add playlist description to PlaylistFragment 2023-12-23 12:13:34 +01:00
Stypox
2b4357fa87
Merge pull request #10530 from TacoTheDank/bumpMiscLibraries
Update miscellaneous libraries
2023-12-23 12:06:42 +01:00
Stypox
cda4b3faaa
Update AGP and Gradle 2023-12-23 12:01:50 +01:00
Stypox
5d09a88335
Update more libraries 2023-12-23 11:58:58 +01:00
TacoTheDank
edd4f6b9f3
Update Studio and desugaring versions 2023-12-23 11:47:57 +01:00
TacoTheDank
1e7e2109d2
Exclude RxJava file from META-INF 2023-12-23 11:47:57 +01:00
TacoTheDank
b31d3831e6
Change Converters to class to fix build 2023-12-23 11:47:57 +01:00
TacoTheDank
0f81a0504c
Use 'tasks.register' for Gradle tasks 2023-12-23 11:47:57 +01:00
TacoTheDank
4a7fda95ae
Update miscellaneous libraries 2023-12-23 11:47:57 +01:00
Stypox
ee3455e1e5
Merge pull request #10086 from TacoTheDank/bumpAndroidX
Update some AndroidX libraries and compileSdk to 34
2023-12-23 11:46:28 +01:00
Isira Seneviratne
f9fc1cd817
Store/retrieve parcelable arrays as lists instead. 2023-12-23 11:38:40 +01:00
TacoTheDank
76f1e588f7
Utilize BundleCompat and IntentCompat methods 2023-12-23 11:38:40 +01:00
Isira Seneviratne
f3b458c803
Bump compileSdk to 34 2023-12-23 11:38:32 +01:00
TacoTheDank
00566ed4d4
Fix AndroidX Work deprecation 2023-12-23 11:36:33 +01:00
TacoTheDank
e4a07411b8
Update some AndroidX libraries 2023-12-23 11:36:32 +01:00
Stypox
2c1bb2706f
Merge pull request #10018 from Stypox/comment-replies
Add support for comment replies
2023-12-23 11:01:07 +01:00
Stypox
aa84d6fc8f
Add @NonNull annotations 2023-12-22 18:52:42 +01:00
Stypox
d76e9b0bd8
Fix scrolling to correct comment after closing replies 2023-12-22 18:52:42 +01:00
TobiGr
b4016c91c1
scroll last comment into view 2023-12-22 11:57:55 +01:00
TobiGr
5f32d001cc
Expand DetailFragment again when exiting the CommentRepliesFragment 2023-12-22 11:57:55 +01:00
Stypox
8c9287d0c8
Revert relying on source ListInfo, use commentsInfoItem.getUrl() instead
This reverts commit bb01da3691ff1d5c3dccd41b7ca1a5deb1b5676f. This commit was not needed
2023-12-22 11:57:55 +01:00
Stypox
3f37e27852
Add some documentation and javadocs
Also further simplify CommentRepliesInfo and RelatedItemsInfo
2023-12-22 11:57:55 +01:00
Stypox
f41ab8b086
Add comment replies fragment header 2023-12-22 11:57:55 +01:00
Stypox
ad68f784ae
Extract some utility methods from CommentInfoItemHolder 2023-12-22 11:48:07 +01:00
Stypox
4b6392df54
Set comment replies fragment title 2023-12-22 11:48:07 +01:00
Stypox
94ea329b50
Always show comment replies in list mode 2023-12-22 11:48:07 +01:00
Stypox
591ed2e01f
Fix some code smells 2023-12-22 11:48:07 +01:00
Stypox
78cf9aaa7d
Save and restore state in CommentRepliesFragment 2023-12-22 11:48:07 +01:00
Stypox
f9494a294f
Implement CommentRepliesFragment 2023-12-22 11:48:07 +01:00
Stypox
0dd4553700
Load more items even if initial related items are empty 2023-12-22 11:48:07 +01:00
Stypox
4f7b36cd70
Keep source list info in InfoItemBuilder
Also remove some unused code
2023-12-22 11:48:07 +01:00
Stypox
5d350aec87
Move RelatedItemInfo next to fragment using it 2023-12-22 11:48:07 +01:00
Stypox
059db6fb31
Add replies button to comments 2023-12-22 11:48:05 +01:00
Stypox
4c709b2c4d
Use start/end instead of left/right in comment layout 2023-12-22 11:46:03 +01:00
Stypox
8f4cd032b7
Remove mini variant and move upload date to top in comments 2023-12-22 11:46:03 +01:00
Stypox
67629938d6
Merge pull request #10470 from TeamNewPipe/release-0.26.0
Release 0.26.0
2023-12-21 22:40:05 +01:00
Hosted Weblate
9aff49bd88
Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Danish)

Currently translated at 88.2% (640 of 725 strings)

Translated using Weblate (Serbian)

Currently translated at 17.1% (13 of 76 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Filipino)

Currently translated at 32.1% (233 of 725 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Romanian)

Currently translated at 99.4% (721 of 725 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Indonesian)

Currently translated at 97.3% (74 of 76 strings)

Translated using Weblate (Arabic (Libya))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (French)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Polish)

Currently translated at 61.8% (47 of 76 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 28.9% (22 of 76 strings)

Translated using Weblate (German)

Currently translated at 81.5% (62 of 76 strings)

Merge branch 'origin/dev' into Weblate.

Translated using Weblate (Bengali)

Currently translated at 78.7% (571 of 725 strings)

Translated using Weblate (Basque)

Currently translated at 95.0% (689 of 725 strings)

Translated using Weblate (Turkish)

Currently translated at 43.4% (33 of 76 strings)

Translated using Weblate (Lithuanian)

Currently translated at 92.9% (674 of 725 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 30.2% (23 of 76 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (724 of 725 strings)

Translated using Weblate (Interlingua)

Currently translated at 32.0% (232 of 725 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 94.6% (686 of 725 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 94.6% (686 of 725 strings)

Translated using Weblate (Romanian)

Currently translated at 89.3% (648 of 725 strings)

Translated using Weblate (German)

Currently translated at 81.5% (62 of 76 strings)

Translated using Weblate (German)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Filipino)

Currently translated at 32.1% (233 of 725 strings)

Translated using Weblate (Filipino)

Currently translated at 32.1% (233 of 725 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Swedish)

Currently translated at 72.3% (55 of 76 strings)

Translated using Weblate (Esperanto)

Currently translated at 3.9% (3 of 76 strings)

Translated using Weblate (Swedish)

Currently translated at 99.7% (723 of 725 strings)

Translated using Weblate (Esperanto)

Currently translated at 71.0% (515 of 725 strings)

Translated using Weblate (Esperanto)

Currently translated at 2.6% (2 of 76 strings)

Translated using Weblate (Tigrinya)

Currently translated at 8.4% (61 of 725 strings)

Translated using Weblate (Vietnamese)

Currently translated at 43.4% (33 of 76 strings)

Translated using Weblate (German)

Currently translated at 81.5% (62 of 76 strings)

Translated using Weblate (Finnish)

Currently translated at 98.3% (713 of 725 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Esperanto)

Currently translated at 70.0% (508 of 725 strings)

Added translation using Weblate (English (Old))

Added translation using Weblate (Aymara)

Added translation using Weblate (English (Middle))

Added translation using Weblate (Arabic (Najdi))

Added translation using Weblate (German (Low))

Added translation using Weblate (Sicilian)

Added translation using Weblate (Kashmiri)

Added translation using Weblate (Burmese)

Translated using Weblate (Tigrinya)

Currently translated at 3.5% (26 of 725 strings)

Translated using Weblate (Georgian)

Currently translated at 91.1% (661 of 725 strings)

Translated using Weblate (French)

Currently translated at 98.8% (717 of 725 strings)

Translated using Weblate (Vietnamese)

Currently translated at 42.1% (32 of 76 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Interlingua)

Currently translated at 31.3% (227 of 725 strings)

Translated using Weblate (Vietnamese)

Currently translated at 35.5% (27 of 76 strings)

Translated using Weblate (Esperanto)

Currently translated at 69.2% (502 of 725 strings)

Translated using Weblate (Esperanto)

Currently translated at 69.2% (502 of 725 strings)

Translated using Weblate (German)

Currently translated at 81.5% (62 of 76 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Bulgarian)

Currently translated at 63.8% (463 of 725 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Esperanto)

Currently translated at 66.3% (481 of 725 strings)

Translated using Weblate (French)

Currently translated at 98.6% (715 of 725 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Turkish)

Currently translated at 31.5% (24 of 76 strings)

Translated using Weblate (Vietnamese)

Currently translated at 97.3% (706 of 725 strings)

Translated using Weblate (Swedish)

Currently translated at 98.4% (714 of 725 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Vietnamese)

Currently translated at 94.6% (686 of 725 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (76 of 76 strings)

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

Currently translated at 21.0% (16 of 76 strings)

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

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (French)

Currently translated at 98.2% (712 of 725 strings)

Deleted translation using Weblate (English (Middle))

Co-authored-by: /dev/urandom <dev.urandom@posteo.org>
Co-authored-by: A <ogloppi@mailbox.org>
Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Alexthegib <traducoes@skiff.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Ans Virlis <tddakk@yahoo.com>
Co-authored-by: AudricV <AudricV@users.noreply.hosted.weblate.org>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Cem TÜRKER <cemburbut@gmail.com>
Co-authored-by: Danr <mdp43140@gmail.com>
Co-authored-by: David Svane <davidcygnus@users.noreply.hosted.weblate.org>
Co-authored-by: Edward <edwardchirita@mailbox.org>
Co-authored-by: Erik Matson <erik@nextleveltranslation.com>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Giorgi Taba K'obakhidze <t@gtk.ge>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihfandi <ihfandicahyo@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jener Gomes <jenerg1@gmail.com>
Co-authored-by: Kristoffer Grundström <swedishsailfishosuser@tutanota.com>
Co-authored-by: LiftedStarfish <liftedstarfish@protonmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Napstaguy04 <brokenscreen3@gmail.com>
Co-authored-by: Nista <42772160+Nista11@users.noreply.github.com>
Co-authored-by: P.O <rasmusson.mikael@protonmail.com>
Co-authored-by: PiryusQ <piryusq@gmail.com>
Co-authored-by: Ray <ray@users.noreply.hosted.weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: ShareASmile <aapshergill@gmail.com>
Co-authored-by: Software In Interlingua <softinterlingua@gmail.com>
Co-authored-by: Stypox <stypox@pm.me>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.com>
Co-authored-by: Tmpod <tom@tmpod.dev>
Co-authored-by: TobiGr <TobiGr@users.noreply.github.com>
Co-authored-by: Translator <kvb@tuta.io>
Co-authored-by: VfBFan <VfBFan@users.noreply.hosted.weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: ferarilalon <ferarilalongpt@gmail.com>
Co-authored-by: fsbat0 <fsbat@duck.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: notlin4 <iamnotlin4@gmail.com>
Co-authored-by: searinminecraft <114207889+searinminecraft@users.noreply.github.com>
Co-authored-by: sum1tookshoe <gamingwithshoe@gmail.com>
Co-authored-by: trunars <trunars@gmail.com>
Co-authored-by: İbrahim Dinç <woltytherespectful@gmail.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/eo/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/vi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2023-12-21 22:37:00 +01:00
Stypox
5b999a88f8
Merge pull request #10673 from Stypox/transaction-too-large
Fix transaction too large in channel tab fragments
2023-12-21 22:30:22 +01:00
Stypox
482531836f
Merge pull request #10670 from Stypox/feed-oom
Fix OutOfMemory when fetching feed
2023-12-21 22:29:46 +01:00
Stypox
b3c82f54df
Merge pull request #10671 from Stypox/channel-main-tab-lag
Fix application lagging with many main page tabs
2023-12-21 22:28:59 +01:00
Stypox
77fa4bbe2f
Update NewPipeExtractor to v0.23.1 2023-12-21 22:28:09 +01:00
Stypox
495c9850b4
Fix transaction too large for channel tab fragments 2023-12-20 23:57:43 +01:00
Stypox
c0f8d145f8
Fix lag with many channels on main page
Disable loading all tabs at once, since there can be many of them, and use default strategy of only keeping in memory the two tabs adjacent to the current tab.
2023-12-20 22:47:57 +01:00
Stypox
80f33daeeb
Fix OutOfMemory when fetching feed
Reduced memory footprint of FeedUpdateInfo objects. Those objects might stay around for a while and accumulate (up to BUFFER_COUNT_BEFORE_INSERT = 20 at the moment), so in order not to fill up the memory it's better to keep as little data as possible.
Previously ChannelInfo data was stored, causing ReadyChannelTabLinkHandler objects to be also stored uselessly (and those channel tabs contain prefetched JSON data which used ~700KB of memory).
2023-12-20 20:22:45 +01:00
Stypox
a16dcb63b5
Merge pull request #10645 from Stypox/fix-fragment-manager
Fix crashes due to wrong root fragment manager
2023-12-20 12:03:24 +01:00
Stypox
b871b5d2dd
Fix crashes due to wrong root fragment manager 2023-12-10 16:06:07 +01:00
Stypox
e876647af5
Update NewPipeExtractor to v0.23.0 2023-12-10 15:58:45 +01:00
Stypox
8d59812827
Fix channel avatar not loading correctly sometimes
The fix just involves removing some really outdated code (6 years ago) added in 33e29be7db (diff-38bd2cf1b92659b499c08e1cf6ac9ef384c7e13381b906f2f98c57cbb758756dR778) (blame: 9318bb5306/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailActivity.java (L778)).
What that code did was setting the 'buddy' image to the uploader avatar as a placeholder, and then setting the actual image if it existed and after it had loaded.
That code remained there up until now, but now it doesn't make sense anymore, since Picasso already takes care of setting placeholders.
The problem is, starting from #10066 the actual uploader image is set before (not after) those lines of code, making them do the wrong thing, i.e. always overwrite the currently set image.
But then why did the channel avatar image work normally sometimes?
My guess is that since Picasso loads images in the background, when opening a video from scratch setting the placeholder still happened before Picasso finished loading the image.
However when the image is already cached it's loaded much faster and therefore setting the placeholder happens after, effectively hiding the loaded image.
2023-12-10 15:12:38 +01:00
TobiGr
e39ac885de
Update new version check to match new API structure
See TeamNewPipe/web-api#17
2023-12-10 15:12:38 +01:00
TobiGr
e6965622bd
Fix crash with disabled thumbnails when trying to play a stream 2023-12-10 15:12:38 +01:00
TobiGr
0d8d3479e1
NewPipe 0.26.0 (995) 2023-12-10 15:12:38 +01:00
Stypox
35c1dfd145
Update changelog for NewPipe 0.26.0 (995) 2023-12-10 15:05:29 +01:00
Hosted Weblate
096115def7
Translated using Weblate (Bengali)
Currently translated at 78.7% (571 of 725 strings)

Translated using Weblate (Basque)

Currently translated at 95.0% (689 of 725 strings)

Translated using Weblate (Turkish)

Currently translated at 43.4% (33 of 76 strings)

Translated using Weblate (Lithuanian)

Currently translated at 92.9% (674 of 725 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 30.2% (23 of 76 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (724 of 725 strings)

Translated using Weblate (Interlingua)

Currently translated at 32.0% (232 of 725 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 94.6% (686 of 725 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 94.6% (686 of 725 strings)

Translated using Weblate (Romanian)

Currently translated at 89.3% (648 of 725 strings)

Translated using Weblate (German)

Currently translated at 81.5% (62 of 76 strings)

Translated using Weblate (German)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Filipino)

Currently translated at 32.1% (233 of 725 strings)

Translated using Weblate (Filipino)

Currently translated at 32.1% (233 of 725 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Swedish)

Currently translated at 72.3% (55 of 76 strings)

Translated using Weblate (Esperanto)

Currently translated at 3.9% (3 of 76 strings)

Translated using Weblate (Swedish)

Currently translated at 99.7% (723 of 725 strings)

Translated using Weblate (Esperanto)

Currently translated at 71.0% (515 of 725 strings)

Translated using Weblate (Esperanto)

Currently translated at 2.6% (2 of 76 strings)

Translated using Weblate (Tigrinya)

Currently translated at 8.4% (61 of 725 strings)

Translated using Weblate (Vietnamese)

Currently translated at 43.4% (33 of 76 strings)

Translated using Weblate (German)

Currently translated at 81.5% (62 of 76 strings)

Translated using Weblate (Finnish)

Currently translated at 98.3% (713 of 725 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Esperanto)

Currently translated at 70.0% (508 of 725 strings)

Added translation using Weblate (English (Old))

Added translation using Weblate (Aymara)

Added translation using Weblate (English (Middle))

Added translation using Weblate (Arabic (Najdi))

Added translation using Weblate (German (Low))

Added translation using Weblate (Sicilian)

Added translation using Weblate (Kashmiri)

Added translation using Weblate (Burmese)

Translated using Weblate (Tigrinya)

Currently translated at 3.5% (26 of 725 strings)

Translated using Weblate (Georgian)

Currently translated at 91.1% (661 of 725 strings)

Translated using Weblate (French)

Currently translated at 98.8% (717 of 725 strings)

Translated using Weblate (Vietnamese)

Currently translated at 42.1% (32 of 76 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Interlingua)

Currently translated at 31.3% (227 of 725 strings)

Translated using Weblate (Vietnamese)

Currently translated at 35.5% (27 of 76 strings)

Translated using Weblate (Esperanto)

Currently translated at 69.2% (502 of 725 strings)

Translated using Weblate (Esperanto)

Currently translated at 69.2% (502 of 725 strings)

Translated using Weblate (German)

Currently translated at 81.5% (62 of 76 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Bulgarian)

Currently translated at 63.8% (463 of 725 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Esperanto)

Currently translated at 66.3% (481 of 725 strings)

Translated using Weblate (French)

Currently translated at 98.6% (715 of 725 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Turkish)

Currently translated at 31.5% (24 of 76 strings)

Translated using Weblate (Vietnamese)

Currently translated at 97.3% (706 of 725 strings)

Translated using Weblate (Swedish)

Currently translated at 98.4% (714 of 725 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Vietnamese)

Currently translated at 94.6% (686 of 725 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (76 of 76 strings)

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

Currently translated at 21.0% (16 of 76 strings)

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

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (French)

Currently translated at 98.2% (712 of 725 strings)

Deleted translation using Weblate (English (Middle))

Co-authored-by: /dev/urandom <dev.urandom@posteo.org>
Co-authored-by: A <ogloppi@mailbox.org>
Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Alexthegib <traducoes@skiff.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Ans Virlis <tddakk@yahoo.com>
Co-authored-by: AudricV <AudricV@users.noreply.hosted.weblate.org>
Co-authored-by: Cem TÜRKER <cemburbut@gmail.com>
Co-authored-by: Edward <edwardchirita@mailbox.org>
Co-authored-by: Erik Matson <erik@nextleveltranslation.com>
Co-authored-by: Giorgi Taba K'obakhidze <t@gtk.ge>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jener Gomes <jenerg1@gmail.com>
Co-authored-by: Kristoffer Grundström <swedishsailfishosuser@tutanota.com>
Co-authored-by: LiftedStarfish <liftedstarfish@protonmail.com>
Co-authored-by: Napstaguy04 <brokenscreen3@gmail.com>
Co-authored-by: P.O <rasmusson.mikael@protonmail.com>
Co-authored-by: PiryusQ <piryusq@gmail.com>
Co-authored-by: Ray <ray@users.noreply.hosted.weblate.org>
Co-authored-by: ShareASmile <aapshergill@gmail.com>
Co-authored-by: Software In Interlingua <softinterlingua@gmail.com>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.com>
Co-authored-by: TobiGr <TobiGr@users.noreply.github.com>
Co-authored-by: Translator <kvb@tuta.io>
Co-authored-by: VfBFan <VfBFan@users.noreply.hosted.weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: ferarilalon <ferarilalongpt@gmail.com>
Co-authored-by: fsbat0 <fsbat@duck.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: notlin4 <iamnotlin4@gmail.com>
Co-authored-by: sum1tookshoe <gamingwithshoe@gmail.com>
Co-authored-by: trunars <trunars@gmail.com>
Co-authored-by: İbrahim Dinç <woltytherespectful@gmail.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/eo/
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/ru/
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/vi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2023-12-10 14:50:31 +01:00
Stypox
e784af3e2d
Merge pull request #10446 from AudricV/dl_improve_video_audio_stream_selection
Improve audio stream selection for video-only streams in the downloader
2023-12-07 16:48:57 +01:00
Stypox
ce30108efc
Improve javadoc for getAudioStreamFor 2023-12-07 16:40:32 +01:00
Stypox
edbd623e21
Fix Matrix channel link
#newpipe:matrix.org is unofficial, #newpipe:libera.chat is the official one
2023-12-07 16:11:59 +01:00
Stypox
7cfd537755
Merge pull request #10494 from TobiGr/fix-new-streams
Fix notifying about old "new" streams
2023-12-07 14:12:54 +01:00
opusforlife2
ddd6d03e0b
Add Matrix room link to ReadMe (#10632) 2023-12-06 18:34:43 +00:00
Tobi
b4a0e08d9d
Update app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt
Co-authored-by: Stypox <stypox@pm.me>
2023-11-23 17:12:16 +01:00
Stypox
545f9ae5f3
Merge pull request #10489 from sqproman/missing_quotation_replace_char_crash
Quote filename replacement characters to fix crashes when downloading streams with special characters
2023-11-16 20:19:01 +01:00
Stypox
be4a5a5f3e
Merge pull request #10576 from AudricV/fix-npe-feed-new-items-button
Fix crash when setting the masking of the new feed items button if the context is null
2023-11-16 09:02:24 +01:00
Stypox
3dc593fe88
Merge pull request #10577 from AudricV/fix-npe-play-queue-audio-track-menu
Fix crash when building the play queue audio track menu if the player is null
2023-11-16 09:01:11 +01:00
Stypox
e8ed18f1cf
Merge pull request #10578 from AudricV/try-fix-player-service-foreground-start
Restore player service start handling before player UI separation and fix some issues in this service
2023-11-16 08:59:56 +01:00
Stypox
bf8890b0df
Merge pull request #10579 from AudricV/exclude-hls-opus-streams-for-playback
Remove OPUS HLS streams from playable streams
2023-11-16 08:30:49 +01:00
AudricV
e5fda35c51
Remove OPUS HLS streams from playable streams
This format is not supported by ExoPlayer when returned as HLS streams, so we
can't play streams using this format and this delivery method.

Also improve the Javadoc of ListHelper.getPlayableStreams.
2023-11-15 23:37:22 +01:00
AudricV
84d50da009
Restore player service start handling before player UI separation
This behavior was present before 0.24.0 and the player UI separation and
avoided crashes for which their exception contained
"Context.startForegroundService() did not then call Service.startForeground()".

Some player nullability checks have been also added, and the player service is
now stopped when it has been started from a media button and there is nothing
to play.
2023-11-15 23:21:20 +01:00
AudricV
2cf7764714
Fix crash when building the play queue audio track menu if the player is null
As the player can be null in some cases, we have to make sure that the player
is not null, by using Optionals on the player itself instead of its methods
returning Optionals.

If the player is null, the play queue audio track menu will now be hidden.
2023-11-15 21:45:56 +01:00
AudricV
9fab0ec94f
Fix crash when setting the masking of the new feed items button if the context is null
As the fragment context can be null in some cases, we have to make sure that
the context is not null before calling
DeviceUtils.hasAnimationsAnimatorDurationEnabled.

If the context is null, the button will now not be hidden automatically.
2023-11-15 19:04:45 +01:00
Stypox
6d694518fe
Merge pull request #10491 from TeamNewPipe/readme
[README] Remove Bitcoin and Bountysource donation options
2023-10-27 05:41:59 +02:00
TobiGr
5265b767cb Fix notifiying about old "new" streams
Add tests for `FeedDAO.unlinkStreamsOlderThan(:offsetDateTime) `
Closes #10237
2023-10-14 18:33:21 +02:00
TobiGr
d10a93fe4f [README] Remove Bountysource badge 2023-10-13 17:30:13 +02:00
TobiGr
995986ecc7 [README] Remove Bitcoin and Bountysource donation options 2023-10-13 17:30:13 +02:00
akko
6d0bb02544 adds quotation to create filename util replacement char 2023-10-13 11:28:09 +07:00
TobiGr
6f51c47dc9
Deleted translation using Weblate (Sicilian) 2023-10-07 17:39:49 +02:00
TobiGr
626daf89c1
Deleted translation using Weblate (Kashmiri) 2023-10-07 17:39:44 +02:00
Hosted Weblate
b18ccffeb4
Added translation using Weblate (English (Middle))
Deleted translation using Weblate (German (Low))

Deleted translation using Weblate (English (Old))

Deleted translation using Weblate (English (Middle))

Deleted translation using Weblate (Burmese)

Deleted translation using Weblate (Aymara)

Deleted translation using Weblate (Arabic (Najdi))

Co-authored-by: TobiGr <TobiGr@users.noreply.github.com>
Co-authored-by: Weblate <noreply@weblate.org>
2023-10-07 15:39:43 +00:00
Hosted Weblate
2ab2185e0a
Translated using Weblate (Odia)
Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Serbian)

Currently translated at 17.1% (13 of 76 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Arabic (Libya))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Finnish)

Currently translated at 98.0% (711 of 725 strings)

Translated using Weblate (French)

Currently translated at 98.2% (712 of 725 strings)

Translated using Weblate (Indonesian)

Currently translated at 94.7% (72 of 76 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Hebrew)

Currently translated at 51.3% (39 of 76 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (76 of 76 strings)

Translated using Weblate (Polish)

Currently translated at 61.8% (47 of 76 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (76 of 76 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: GET100PERCENT <eraofphysics@yahoo.com>
Co-authored-by: Hoseok Seo <ddinghoya@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: ShareASmile <aapshergill@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Åzze <laitinen.jere222@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translation: NewPipe/Metadata
2023-10-07 17:21:24 +02:00
TobiGr
be47609405 Add changelog for NewPipe 0.26.0 (995) 2023-10-05 14:49:35 +02:00
Hosted Weblate
5dee7a5262
Translated using Weblate (Portuguese)
Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Thai)

Currently translated at 29.3% (213 of 725 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 95.0% (689 of 725 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (725 of 725 strings)

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

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (723 of 725 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (German)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Portuguese)

Currently translated at 95.2% (685 of 719 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Arabic (Libya))

Currently translated at 99.1% (713 of 719 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (719 of 719 strings)

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

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (German)

Currently translated at 81.3% (61 of 75 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (French)

Currently translated at 98.4% (708 of 719 strings)

Translated using Weblate (German)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Arabic (Libya))

Currently translated at 99.3% (714 of 719 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.8% (718 of 719 strings)

Translated using Weblate (French)

Currently translated at 97.7% (703 of 719 strings)

Translated using Weblate (German)

Currently translated at 99.4% (715 of 719 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Czech)

Currently translated at 95.5% (687 of 719 strings)

Translated using Weblate (Greek)

Currently translated at 99.8% (718 of 719 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (German)

Currently translated at 99.3% (714 of 719 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alexthegib <traducoes@skiff.com>
Co-authored-by: AudricV <AudricV@users.noreply.hosted.weblate.org>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Eric <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: GET100PERCENT <eraofphysics@yahoo.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: Linerly <linerly@protonmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Nidi <nizamismidov4@gmail.com>
Co-authored-by: Peter Dave Hello <hsu@peterdavehello.org>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Retrial <giwrgosmant@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sergio Marques <so.boston.android@gmail.com>
Co-authored-by: ShareASmile <aapshergill@gmail.com>
Co-authored-by: TobiGr <tobigr@mail.de>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: VfBFan <VfBFan@users.noreply.hosted.weblate.org>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: bowornsin <bowornsin@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kuragehime <kuragehime641@gmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translation: NewPipe/Metadata
2023-10-05 03:09:31 +02:00
Stypox
bff7ada2d1
Merge pull request #8248 from dtcxzyw/fix-readd-to-playlist
Fix inconsistency between user interaction and database commit order when re-adding videos to the playlist
2023-10-03 17:45:44 +02:00
Tobi
ed33d1d4f7
Fix images in README.sr.md 2023-10-02 16:21:27 +02:00
Tobi
64e64f72f7
Update README.asm.md 2023-10-02 16:18:54 +02:00
TobiGr
d3c783832a Fix screenshot URLs in multiple READMEs 2023-10-02 16:16:53 +02:00
TobiGr
d963b69d5c Fix links to README.sr.md 2023-10-02 16:06:33 +02:00
Tobi
49ce9ba387
Fix links to other READMEs in README.sr.md 2023-10-02 16:00:25 +02:00
NEXI
d63a6d3f75
Create Serbian README (#10465) 2023-10-02 15:58:26 +02:00
TobiGr
3d5a8af52b Fix inconsistency when LocalPlaylist is used as MainFragment tab 2023-10-02 02:56:30 +02:00
Yingwei Zheng
1cf670dad9 Fix inconsistency between user interaction and database commit order when re-adding videos to the playlist 2023-10-02 02:56:30 +02:00
Tobi
b50e3c07d2 Use PR labeler fork
This updates some libs
2023-10-02 02:15:25 +02:00
Tobi
fe7d1692c3 Fix PR labeler permissions
Although the permission to modify PRs is granted to the entire workflow, the job still reports that it does not the permission to do so:
GITHUB_TOKEN Permissions
  Contents: read
  Metadata: read
  PullRequests: read
This adds the permission to the job directly
2023-09-27 10:06:34 +02:00
TobiGr
0758cd6980 Fix wrongly formatted string ressources
There were multiple substitutions specified in non-positional format in the ressources video_details_list_item and share_playlist_content_details
2023-09-26 11:22:22 +02:00
Siddhesh Naik
e80b6b3057
Add playlist name and video name to playlist sharing content (#10427)
- Currently, only a list of videos separated by newline are added in
  the share content.
- This makes it difficult to identify a specific video in a list of
  Urls.
- Used string resources for the sharing content formats.
- Added a confirmation dialog for users to choose between sharing
  playlist formats.
- Added Playlist name as the header and corresponding video name for
  each video url in following format.

Playlist
- Music1: https://media-url1
- Music2: https://media-url2
- Music3: https://media-url3


Co-authored-by: TobiGr <tobigr@users.noreply.github.com>
2023-09-26 11:11:33 +02:00
Tobi
9c86afe40d
Merge pull request #10453 from TeamNewPipe/pr-labeler
Add content: read permission to PR size labeler workflow
2023-09-26 10:42:01 +02:00
Tobi
db4619f5a4
Add content: read permission to PR size labeler workflow 2023-09-26 10:40:17 +02:00
Tobi
f90d74ca31
Merge pull request #10447 from TeamNewPipe/pr-labeler
Add write permission to PR labeler workflow
2023-09-24 20:27:58 +02:00
Tobi
609f0a2eee
Add write permission to PR labeler workflow 2023-09-24 20:24:57 +02:00
AudricV
77bbbc88f8
Use ListHelper to get secondary audio streams for video-only streams
Instead of searching for the first audio stream matching a compatible media
format, this change makes SecondaryStreamHelper.getAudioStreamFor use methods
isLimitingDataUsage, getAudioFormatComparator and getAudioIndexByHighestRank of
ListHelper to get an audio stream which can be muxed into a video-only stream,
if available.

This allows users to download videos with the highest audio quality available
if no resolution limit on mobile data usage has been set.

The order of formats used to search a compatible audio stream has been kept.
2023-09-24 18:23:45 +02:00
AudricV
cdb79ef78a
Make isLimitingDataUsage method of ListHelper package-private and fix some typos in the class 2023-09-24 18:23:44 +02:00
Tobi
1630e309fb
Merge pull request #9987 from Edwardsoen/add_high_resolution_to_default_option
Include a high-resolution option in the default resolution settings.
2023-09-24 17:54:18 +02:00
Stypox
2d4f56f57c
Merge pull request #10170 from TeamNewPipe/actions/pr-size-labeler
Add workflow "PR size labeler" to label PRs based on the number of changed lines
2023-09-24 09:34:33 +02:00
TobiGr
d622993483
Add workflow "PR size labeler" to label PRs based on the number of changed lines
This should help reviewers to determine which PRs to review.
2023-09-24 09:33:44 +02:00
Tobi
c68a6ee0ed
Merge pull request #10436 from TeamNewPipe/fix/license-restore
Fix restoring software license dialog
2023-09-23 14:11:09 +02:00
TobiGr
94c1438913 Use "done" button to close license dialogs.
Rename string res "recaptcha_done_button" to "done".
2023-09-23 13:56:49 +02:00
TobiGr
e206a26a85 Restore license dialog buttons to open the SoftwareComponent's website
Do not keep the active License but the active SoftwareComponent.
2023-09-23 13:49:09 +02:00
TobiGr
242e20316b [AboutFragment / LicenseFragment] Fix license restore after rotation
Do not restore last opened license after a rotation change when the license was closed earlier.

This commit adds onCancelListener and onDismissListener to the AlertDialogs which are used to display the licenses.
2023-09-23 13:49:09 +02:00
TobiGr
279fd2399d Remove translations without default value 2023-09-22 21:44:58 +02:00
kuragehime
a5fcb41ab0 Translated using Weblate (ryu (generated) (ryu))
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
Marian Hanzel
cb4f656673 Translated using Weblate (Slovak)
Currently translated at 18.6% (14 of 75 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
2023-09-22 21:38:28 +02:00
ShareASmile
b9e5ee6759 Translated using Weblate (Punjabi)
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
ShareASmile
1084b7c3ad Translated using Weblate (Hindi)
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
Yaron Shahrabani
39c06c5461 Translated using Weblate (Hebrew)
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
Jeff Huang
b9c7f8769b Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
Eric
dc45adf7f2 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
Agnieszka C
a69af42f7f Translated using Weblate (Polish)
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
Ihor Hordiichuk
1a5dfae7a0 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
Linerly
d41b5d80ad Translated using Weblate (Indonesian)
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
Rex_sa
f0bcb3ba28 Translated using Weblate (Arabic)
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
Marian Hanzel
7da35bf71d Translated using Weblate (Slovak)
Currently translated at 99.7% (711 of 713 strings)
2023-09-22 21:38:28 +02:00
Vasilis K
03c339dd4b Translated using Weblate (Greek)
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
kuragehime
11c74bd26b Translated using Weblate (Japanese)
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
gallegonovato
0a292cf893 Translated using Weblate (Spanish)
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
TobiGr
ac6811867f Translated using Weblate (German)
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
VfBFan
0c9df501e8 Translated using Weblate (German)
Currently translated at 100.0% (713 of 713 strings)
2023-09-22 21:38:28 +02:00
Hosted Weblate
4c4f9b45d9 Translated using Weblate (Kazakh)
Currently translated at 0.5% (4 of 702 strings)

Translated using Weblate (Kazakh)

Currently translated at 6.6% (5 of 75 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (702 of 702 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (702 of 702 strings)

Merge branch 'origin/dev' into Weblate.

Translated using Weblate (Spanish)

Currently translated at 100.0% (702 of 702 strings)

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

Currently translated at 100.0% (702 of 702 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (702 of 702 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (702 of 702 strings)

Translated using Weblate (Greek)

Currently translated at 99.8% (701 of 702 strings)

Translated using Weblate (Japanese)

Currently translated at 98.2% (690 of 702 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (702 of 702 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (702 of 702 strings)

Translated using Weblate (German)

Currently translated at 80.0% (60 of 75 strings)

Translated using Weblate (German)

Currently translated at 100.0% (702 of 702 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (702 of 702 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (702 of 702 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (702 of 702 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (702 of 702 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (702 of 702 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (702 of 702 strings)

Translated using Weblate (Greek)

Currently translated at 97.8% (687 of 702 strings)

Translated using Weblate (Serbian)

Currently translated at 98.5% (692 of 702 strings)

Translated using Weblate (German)

Currently translated at 99.5% (699 of 702 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.8% (685 of 686 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.7% (684 of 686 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Serbian)

Currently translated at 16.0% (12 of 75 strings)

Translated using Weblate (Galician)

Currently translated at 99.5% (683 of 686 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Serbian)

Currently translated at 16.0% (12 of 75 strings)

Translated using Weblate (Bengali)

Currently translated at 83.8% (575 of 686 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (686 of 686 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Aidos <goldenbit.kz@yandex.kz>
Co-authored-by: Daniel Rozario <rozario@tuta.io>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Eric <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: GET100PERCENT <eraofphysics@yahoo.com>
Co-authored-by: GnuPGを使うべきだ <dieeeazpnnqbpddh@cock.email>
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: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Nidi <nizamismidov4@gmail.com>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: ShareASmile <aapshergill@gmail.com>
Co-authored-by: Subham Jena <subhamjena8465@gmail.com>
Co-authored-by: Tibor Botfai (gidano) <gidano@gmail.com>
Co-authored-by: TobiGr <tobigr@mail.de>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: VfBFan <VfBFan@users.noreply.hosted.weblate.org>
Co-authored-by: WB <dln0@proton.me>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kuragehime <kuragehime641@gmail.com>
Co-authored-by: nexi <nexiphotographer@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: zmni <zmni@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/kk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sr/
Translation: NewPipe/Metadata
2023-09-22 21:38:28 +02:00
Audric V
5a921c9f10
Merge pull request #10440 from Stypox/remove-deobfuscation-exception
Update extractor and remove DeobfuscateException
2023-09-22 16:29:13 +02:00
Stypox
bdc2aa2b39
Update extractor and remove DeobfuscateException
In ErrorInfo it was treated separately from other ParsingExceptions, including by showing a customized message, which has now been removed.
2023-09-22 10:43:37 +02:00
Stypox
b508dd69be
Merge pull request #10062 from Stypox/multiple-images
Allow selecting image quality among multiple images
2023-09-22 10:20:19 +02:00
Stypox
f8b756c8bc
Make question mark localizable 2023-09-22 10:14:45 +02:00
Stypox
027b829c38
Use @DrawableRes in PicassoHelper 2023-09-22 10:14:45 +02:00
Stypox
0a2d6d1d62
Add test for ImageStrategy 2023-09-22 10:14:45 +02:00
Stypox
1b485ddb5a
Allow using CHECKSTYLE:OFF comments 2023-09-22 10:14:45 +02:00
Stypox
0085ca6416
Fix loading images from db even if disabled 2023-09-22 10:14:44 +02:00
Stypox
87dca0f7ec
Separate imageListToDbUrl from choosePreferredImage
imageListToDbUrl should be used if the URL is going to be saved to the database, to avoid saving nothing in case at the moment of saving the user preference is to not show images.
2023-09-22 10:14:44 +02:00
Stypox
37af2c87e8
Fix possible NPE in PlayQueueNavigator 2023-09-22 10:14:44 +02:00
Stypox
bf908f0b7d
Add documentation and fix SonarCloud issue 2023-09-22 10:14:44 +02:00
Stypox
8d463b9577
Further improve image resolution strategy
Now using multiple comparison steps instead of magic values
2023-09-22 10:14:44 +02:00
Stypox
4f7d206736
Display all thumbnails in description tab 2023-09-22 10:14:44 +02:00
Stypox
35073c780d
Implement better image selection strategy 2023-09-22 10:14:44 +02:00
Stypox
0a8f28b1c6
Add image quality preference 2023-09-22 10:14:43 +02:00
Stypox
af2375948d
Support obtaining multiple images from the extractor 2023-09-22 09:57:33 +02:00
TobiGr
df2e0be08d Add summary to reset preference 2023-09-21 16:01:07 +02:00
TobiGr
ff1aca272e Remove strange change 2023-09-21 16:01:07 +02:00
TobiGr
f2e352832a Create new settings category: Backup and restore
Following settings have been move to the new category:
- import database (from ContenttSettings)
- export database (from ContenttSettings)
- reset settings (from DebugSettings)
2023-09-21 16:01:07 +02:00
vincetzr
ad0855ac83 Update app/src/main/res/xml/debug_settings.xml
Ensuring title to be fully displayed on small devices.

Co-authored-by: Tobi <TobiGr@users.noreply.github.com>
2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
d7ef9b1f0c Added strings to strings.xml to allow translation. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
40a3e1b18a Revert committed file change of settings key. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
25a73090f5 Revert changes made to dev. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
a239a26b17 Revert changes made to dev. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
06d256294f Revert changes made to dev. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
81ad50e82a Added delete xml method inside the yes dialogue. 2023-09-21 16:01:07 +02:00
Zhidong Piao
23de9bf93e clear shared preference xmls 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
5c46412faa Changed alert dialogue. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
076e9eee01 Added alert dialogue and restarts the app when resetting settings. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
2103a04092 Cleaned up xml files. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
58517d1d27 Added reset button but working as intended for theme. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
aa1847189b Added reset button but slightly working as intended. 2023-09-21 16:01:07 +02:00
Vincent Tanumihardja
5d101e7b88 Added reset button but not working as intended. 2023-09-21 16:01:07 +02:00
Tobi
e2de83188a
Merge pull request #10199 from TeamNewPipe/feat/player/accessibility
Improved accessibility of player interfaces
2023-09-21 15:57:38 +02:00
TobiGr
2a1b506d98 Improved accessibility of player interfaces 2023-09-21 12:20:00 +02:00
Stypox
b798ff5c92
Merge pull request #10435 from TeamNewPipe/imp/codequality
Improve codequality
2023-09-20 21:24:44 +02:00
Tobi
673aa0a87b
Merge pull request #10428 from Isira-Seneviratne/AGP_8.1
Bump AGP to 8.1.1
2023-09-20 20:20:36 +02:00
TobiGr
779ea19222 Fix doc formatting 2023-09-20 19:44:23 +02:00
Isira Seneviratne
a1f2b7f8e8 Switch to Files.createDirectories() 2023-09-20 19:44:23 +02:00
Isira Seneviratne
fcb855cea9 Bump AGP to 8.1.1 2023-09-20 19:44:23 +02:00
Tobi
50fb48f66d
Merge pull request #10244 from TacoTheDank/bumpMaterial
Update Google Material library
2023-09-20 18:19:53 +02:00
TobiGr
0acc3532c9 Remove useless override 2023-09-20 15:42:09 +02:00
TobiGr
8bf2d996ea Reorder the modifiers to comply with the Java Language Specification. 2023-09-20 15:41:57 +02:00
TobiGr
748c2babe9 Add comments and annotations 2023-09-20 15:41:21 +02:00
Tobi
6859f73c54
Merge pull request #10224 from TacoTheDank/moreKotlinMath
Replace MathUtils.clamp with Kotlin coerceIn
2023-09-20 11:04:54 +02:00
TacoTheDank
b1faed586d Replace MathUtils.clamp with Kotlin coerceIn 2023-09-19 16:32:37 -04:00
TacoTheDank
6c848b4766 Update Google Material library 2023-09-19 16:30:09 -04:00
Stypox
725c18eada
Merge pull request #10165 from TeamNewPipe/fix/media-format
Fix downloads of streams with missing MediaFormat
2023-09-19 15:54:12 +02:00
Stypox
992bb5d7be
Simplify retrieveMediaFormatFromContentTypeHeader
Also check for nullity
2023-09-19 15:33:49 +02:00
Stypox
9e353f1cdc
Merge pull request #10394 from TeamNewPipe/fix/memory-leaks
Fix memory leaks and add documentation
2023-09-19 14:17:23 +02:00
TobiGr
8f83e39970 Fix three memory leaks
Add documentation to BaseFragment.initViews(View, Bundle) and BaseFragment.initListeners()
2023-09-19 00:13:16 +02:00
Stypox
0eae9e7cdc
Merge pull request #9182 from Theta-Dev/channel-tabs
Add support for channel tabs
2023-09-18 23:46:13 +02:00
TobiGr
031b893196
Remove unused content not supported TextView 2023-09-18 23:22:32 +02:00
TobiGr
64da7a06c0
Fix previous ActionBar title visible for a few miliseconds when opening ChannelFragment 2023-09-18 23:22:32 +02:00
TobiGr
57eaa1bbe1
Apply review
Co-Authored-By:  Audric V <74829229+AudricV@users.noreply.github.com>
2023-09-18 23:22:32 +02:00
TobiGr
109d06b4bb
Deduplicate code to initialize ClickListeners on playlist controls
Add the separate utility class PlayButtonHelper to handle the initialization of the listeners.
The ClickListeners on playlist controls had different behaviours. This commit fixes that.

The commit also refactors the way how the app determines whether it is started for the first time. The previous version was not clean and recent in this PR caused it to fail.
2023-09-18 23:22:32 +02:00
AudricV
0d9910cbbe
Fix SubscriptionManagerTest tests
The breakage of these tests is related to the channel tabs changes.

The testRememberRecentStreams test method has been removed, as it doesn't seem
to be relevant anymore to managing subscriptions.
2023-09-18 23:22:32 +02:00
AudricV
8fbc8ffc7c
Remove unneeded German translation 2023-09-18 23:22:32 +02:00
AudricV
f2ee3859ab
Hide the upload date element on the About tab
This empty element should be always hidden for this tab, as there is no upload
date available for channels.
2023-09-18 23:22:32 +02:00
AudricV
89dc44be61
Always show the About tab and support having no description 2023-09-18 23:22:32 +02:00
TobiGr
6ab8716e69
Extract actual feed loading code into separate method
Increase readability
2023-09-18 23:22:32 +02:00
TobiGr
5c7c382323
Add missing @Override annotations to setupMetadata() implementations 2023-09-18 23:22:32 +02:00
Stypox
78b4b9441e
Update NewPipeExtractor and adapt imports 2023-09-18 23:22:32 +02:00
Stypox
9e55014a13
Fix wrongly themed channel header
Since it is embedded in the app bar and has red as background color, it should be themed in the same way as the toolbar.
2023-09-18 23:22:32 +02:00
Stypox
6f23b56b06
Use consistent name for livestreams tab in settings keys 2023-09-18 23:22:32 +02:00
Stypox
1519527356
Fix loading feed when a channel tab is empty 2023-09-18 23:22:32 +02:00
Stypox
6b3a178f2a
Show snackbar with feed loading errors 2023-09-18 23:22:32 +02:00
Stypox
604419dd1f
Make channel banner placeholder match YouTube's size
YouTube's "Desktop Max" thumbnails are 2560x423, while our previous placeholder banner was 2550x427. The extractor actually returns a lower resolution "Desktop Max" banner at 1060x175, but the ratio wrt 2560x423 is off by ~0.1%

The PNG was optimized with OptiPNG
2023-09-18 23:22:32 +02:00
Stypox
c48e702a50
Improve placeholder channel banner handling
Now the placeholder gets hidden if there is no banner url or the user disabled images, to save space
2023-09-18 23:22:32 +02:00
Stypox
1061bce4f3
Add avatar and bannner URLs to channel About tab 2023-09-18 23:22:32 +02:00
Stypox
013d513450
Add space above channel description (About tab) 2023-09-18 23:22:32 +02:00
ThetaDev
dca32efadf
add channel banner placeholder 2023-09-18 23:22:32 +02:00
ThetaDev
28d952a643
feat: filter fetched channel tabs 2023-09-18 23:22:32 +02:00
ThetaDev
a2a717bd49
update NPE 2023-09-18 23:22:32 +02:00
ThetaDev
753a92055c
feat: add playlist controls to channel tab 2023-09-18 23:22:32 +02:00
Stypox
371f986773
Fix some code smells 2023-09-18 23:22:32 +02:00
Stypox
a1e8b9be4e
Fix channel tabs in main page setting title themselves 2023-09-18 23:22:32 +02:00
Stypox
c076a0f771
Channels are now an Info
The previous "main" tab is now just a normal tab returned in getTabs().
Various part of the code that used to handle channels as ListInfo now either take the first (playable, i.e. with streams) tab (e.g. the ChannelTabPlayQueue), or take all of them combined (e.g. the feed).
2023-09-18 23:22:32 +02:00
ThetaDev
dfbd39e898
fix: limit channel header height 2023-09-18 23:22:32 +02:00
ThetaDev
b5893f3fa3
fix: notification menu option disappears when switching tabs 2023-09-18 23:22:32 +02:00
Stypox
e3614cb932
Move channel header to collapsible app bar 2023-09-18 23:22:32 +02:00
ThetaDev
193c3e5b3d
fix: NPE in ChannelFragment::onSaveInstanceState 2023-09-18 23:22:32 +02:00
ThetaDev
c03c344f49
refactor: rename ChannelInfo to ChannelAbout
fix: localize about tab name
2023-09-18 23:22:32 +02:00
ThetaDev
25e3031830
cleanup: remove empty constructor from ChannelFragment 2023-09-18 23:22:31 +02:00
ThetaDev
b7911a8fd8
remove fragment_channel_info 2023-09-18 23:22:31 +02:00
ThetaDev
88384dc35e
update extractor 2023-09-18 23:22:31 +02:00
ThetaDev
39b4ed082c
refactor: common code from ChannelInfo/Description -> BaseInfoFragment 2023-09-18 23:22:31 +02:00
ThetaDev
d87aa23ae0
update NewPipeExtractor 2023-09-18 23:22:31 +02:00
ThetaDev
be548dcb52
fix: channel tab title not being set 2023-09-18 23:22:31 +02:00
ThetaDev
4357a34339
fix: ChannelFragment: save last tab 2023-09-18 23:22:31 +02:00
ThetaDev
2c03ba204e
refactor: adjustments to updated tab extractor API 2023-09-18 23:22:31 +02:00
ThetaDev
2c98d079de
fix: cache channel data 2023-09-18 23:22:31 +02:00
ThetaDev
16cd47fa2e
fix: missing album tab key 2023-09-18 23:22:31 +02:00
ThetaDev
74a8bfba93
feat: add album tab 2023-09-18 23:22:31 +02:00
ThetaDev
c929f00456
fix: remember selected channel tab on screen rotation 2023-09-18 23:22:31 +02:00
ThetaDev
bb062f07f9
feat: add option to hide channel tabs 2023-09-18 23:22:31 +02:00
ThetaDev
c3d1e75a8f
fix: scrollable channel description 2023-09-18 23:22:31 +02:00
ThetaDev
506e3724a6
fix: add progress spinners 2023-09-18 23:22:31 +02:00
ThetaDev
4859ab67d4
feat: prettier channel info page 2023-09-18 23:22:31 +02:00
ThetaDev
6d84d19520
fix: handle unsupported content 2023-09-18 23:22:31 +02:00
ThetaDev
8627efd0a1
fix: get notified menu option on all tabs 2023-09-18 23:22:31 +02:00
ThetaDev
6d13cf5e71
feat: add channel tabs 2023-09-18 23:22:31 +02:00
Rishab Aggarwal
7e2ab0d384
Improved downloading experience (#10407)
* added LoadingDialog for improving download experience

* [LoadingDialog] Apply some review comments and make title customizable.

* removed permission handling from loading Dialog

* fix checks

* remove <p> Tag from first sentence

---------

Co-authored-by: rishabaggarwal <Rishabaggarwal@sharechat.com>
Co-authored-by: TobiGr <tobigr@users.noreply.github.com>
2023-09-18 05:24:03 +05:30
TobiGr
19640d5e7c Add documentation to increase maintainablilty
Rename a variable
2023-09-18 01:45:53 +02:00
Edward
d1a82a85cd Include a high-resolution option in the default resolution settings. 2023-09-18 01:15:22 +02:00
Isira Seneviratne
b1ab261890
Merge pull request #10248 from Isira-Seneviratne/NIO_downloads
Improve the download helpers using the Java 7 NIO API.
2023-09-17 21:09:02 +05:30
Tobi
038278283a
Merge pull request #10234 from TacoTheDank/bumpMisc
Update miscellaneous libraries
2023-09-17 16:35:40 +02:00
TacoTheDank
c74bd11a6f Update miscellaneous libraries 2023-09-17 15:43:43 +02:00
TobiGr
f2c2f1735e Replace RuntimeException with IOException
The RuntimeException was not explicitly declared and thus not caught at every call of this constructor. This change ensures that this possible exception is handled by the dedicated error handlers.
2023-09-17 15:31:19 +02:00
TobiGr
4e41e12bd2 Small code and doc improvements
Remove unnecessary else-branch and use Collections.isEmpty().
Add doc comment for splitFilename()
2023-09-17 15:31:19 +02:00
Isira Seneviratne
6df808f266 Use Path in the download helper classes. 2023-09-17 14:50:26 +02:00
Isira Seneviratne
2cb973f150 Use desugar_jdk_libs_nio. 2023-09-17 14:50:26 +02:00
Tobi
b5463cf5e1
Merge pull request #10406 from ShareASmile/fix-language-selector
Fix language Picker Selecting Wrong Languages
2023-09-09 00:52:07 +02:00
K Gill
862546205a
fix wrongly placed uchinaguchi in language selector settings caus..
..ing language picker to select wrong languages
2023-09-08 16:33:17 +05:30
Audric V
7c1790bbfd
Merge pull request #10396 from AudricV/one-stream-main-player-queue-button
Show play queue button in main player when there is one stream
2023-08-31 15:06:06 +02:00
AudricV
2d16a06bc4
Show play queue button in main player when there is one stream 2023-08-30 19:45:53 +02:00
Tobi
25cf917969
Merge pull request #10377 from TeamNewPipe/image-minimizer
Image minizier: replace Number.toFixed(0) with Math.floor()
2023-08-25 00:54:55 +02:00
Tobi
d09c650afd
Merge pull request #10376 from TeamNewPipe/fix-string-formats
Fix string formats
2023-08-24 21:14:03 +02:00
AudricV
2b833c5250 Fix audio_track_name string formats 2023-08-24 20:41:11 +02:00
TobiGr
510db568eb Image minizier: replace Number.toFixed(0) with Math.floor()
Number.toFixed returns a string, Math.floor a number
2023-08-24 11:53:35 +02:00
Hosted Weblate
e4003c842b
Translated using Weblate (Arabic)
Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Malayalam)

Currently translated at 6.6% (5 of 75 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (German)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (German)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Swedish)

Currently translated at 99.7% (684 of 686 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Albanian)

Currently translated at 82.7% (568 of 686 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (French)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (686 of 686 strings)

Added translation using Weblate (Burmese)

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

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Slovak)

Currently translated at 98.9% (679 of 686 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (686 of 686 strings)

Translated using Weblate (German)

Currently translated at 100.0% (686 of 686 strings)

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

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (75 of 75 strings)

Translated using Weblate (Croatian)

Currently translated at 91.8% (628 of 684 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (French)

Currently translated at 100.0% (684 of 684 strings)

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

Currently translated at 98.6% (675 of 684 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Urdu)

Currently translated at 74.2% (508 of 684 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.2% (679 of 684 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (75 of 75 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (75 of 75 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (75 of 75 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Galician)

Currently translated at 99.5% (681 of 684 strings)

Translated using Weblate (Finnish)

Currently translated at 89.9% (615 of 684 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (684 of 684 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Alexthegib <jcwkgxc@nightorb.com>
Co-authored-by: AudricV <AudricV@users.noreply.hosted.weblate.org>
Co-authored-by: C. Rüdinger <Mail-an-CR@web.de>
Co-authored-by: Eric <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: GET100PERCENT <eraofphysics@yahoo.com>
Co-authored-by: Hoseok Seo <ddinghoya@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: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jani Kinnunen <janikinnunen340@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Joel A <joeax910@student.liu.se>
Co-authored-by: Jorge Pelaez <jorpelae@yahoo.co.jp>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sergio Marques <so.boston.android@gmail.com>
Co-authored-by: ShareASmile <aapshergill@gmail.com>
Co-authored-by: Shifa Graphics <shifagraphix@gmail.com>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.com>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: W L <wl@mailhole.de>
Co-authored-by: WB <dln0@proton.me>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kuragehime <kuragehime641@gmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: tellmeY18 <vysakh_b190622ec@nitc.ac.in>
Co-authored-by: thami simo <simo.azad@gmail.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ml/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_PT/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translation: NewPipe/Metadata
2023-08-23 21:29:25 +02:00
TobiGr
68957d3880 Fix grammar in JDoc 2023-08-22 16:23:22 +02:00
Tobi
e6747066ae
Merge pull request #10360 from TeamNewPipe/improvement/tabSelected
Simplify `MainActivity.tabSelected(MenuItem)`
2023-08-22 10:57:14 +02:00
TobiGr
62f0abee47 Simplify MainActivity.tabSelected(MenuItem)
Rename variables and skip iterations if kiosk was found.
2023-08-19 21:58:44 +02:00
TobiGr
9118ecd68f Remove unnecessary debug warning and use JDoc instead 2023-08-17 16:51:31 +02:00
TobiGr
15fd47c7f2 Apply review 2023-08-16 22:18:53 +02:00
Yingwei Zheng
ef40ac7bb3 Fix a typo 2023-08-16 22:02:12 +02:00
Yingwei Zheng
881d04ba1e Refactor database migration test and string trimming 2023-08-16 22:02:12 +02:00
TobiGr
4af5b5f6f2 Fix database migration and string trimming
Co-authored-by:  Yingwei Zheng <dtcxzyw@qq.com>
2023-08-16 22:02:12 +02:00
TobiGr
90f0809029 Trim search string and remove duplicate records from the database
Co-authored-by:  Yingwei Zheng <dtcxzyw@qq.com>
2023-08-16 21:26:35 +02:00
TobiGr
db5ed48dbb Fix some sonar warnings and make some smaller improvements 2023-08-14 23:09:50 +02:00
TobiGr
ba84e7eead Display "Unknown quality" if quality is unknown and not MediaFormat name 2023-08-14 23:06:32 +02:00
TobiGr
e51067177e Add tests for new methods retrieving MediaFormats
Fix failing tests
2023-08-14 23:06:32 +02:00
TobiGr
f3859ed710 Retrieve MediaFormat for streams that could not be extracted by the extractor 2023-08-14 23:06:32 +02:00
TobiGr
0db12e5561 Rename StreamSizeWrapper to StreamInfoWrapper 2023-08-14 22:48:39 +02:00
Stypox
ac5f991c0c
Merge pull request #10331 from TeamNewPipe/improve_bug_template
Make "latest release" link more obvious to bug reporters
2023-08-12 19:46:15 +02:00
opusforlife2
4a0ff3f7ef
Make latest release link more obvious to bug reporters 2023-08-12 15:08:03 +00:00
Stypox
601b1ef742
Merge pull request #10313 from mhmdanas/fix-capitalization-of-night-theme
Make capitalization of "Night theme" setting consistent with others
2023-08-07 23:07:48 +02:00
triallax
d957725805 Make capitalization of "Night theme" setting consistent with others
Been a pet peeve of mine for some time.
2023-08-07 20:40:33 +01:00
Stypox
4201723d10
Merge pull request #10304 from TeamNewPipe/fix/media.ccc.de
Adjust empty state message for ListInfoFragments depending on Info stream type
2023-08-06 10:07:33 +02:00
Stypox
bef79e77aa
Update app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java 2023-08-06 10:07:13 +02:00
TobiGr
32f74273f0 Adjust empty state message for ListInfoFragments depending on Info stream type
Show "no streams" for SoundClound.
Show "no live streams" for MeidaCCCLiveStreamKiosk.
Otherwise show "no videos"
2023-08-05 03:50:28 +02:00
TobiGr
c69bcaafbb Fixed some Sonar warnings 2023-08-03 12:02:08 +02:00
Tobi
50d7d1b7b3
Merge pull request #10275 from J-Stutzmann/fix/audio-focus
Fix player audio focus not respecting mute
2023-08-03 11:55:40 +02:00
J-Stutzmann
c06d61a83c Made audio-focus calls respect mute state. 2023-08-02 23:44:23 -04:00
TobiGr
bc4f0c699f Ignore false positive inspection 2023-08-02 20:44:30 +02:00
Stypox
1e8efa7165
Merge pull request #10283 from TeamNewPipe/release/0.25.2
Release v0.25.2 (994)
2023-08-02 20:14:14 +02:00
Stypox
d4019f4b54
Update NewPipeExtractor to v0.22.7 2023-08-02 20:12:55 +02:00
TobiGr
3f0f66f106 Bump version to 0.25.2 (994) 2023-08-02 20:02:22 +02:00
Hosted Weblate
8f644e8aaf
Translated using Weblate (Urdu)
Currently translated at 5.3% (4 of 75 strings)

Translated using Weblate (Urdu)

Currently translated at 73.2% (501 of 684 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (75 of 75 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (75 of 75 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (75 of 75 strings)

Translated using Weblate (Polish)

Currently translated at 61.3% (46 of 75 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (75 of 75 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (75 of 75 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (684 of 684 strings)

Translated using Weblate (German)

Currently translated at 100.0% (684 of 684 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Eric <hamburger2048@users.noreply.hosted.weblate.org>
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: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: ShareASmile <aapshergill@gmail.com>
Co-authored-by: Shifa Graphics <shifagraphix@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: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: rickeesingh <rickeesingh231@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ur/
Translation: NewPipe/Metadata
2023-08-02 19:57:41 +02:00
TobiGr
27f77518fe Update NewPipe Extractor to 39a911db9f 2023-07-31 23:59:28 +02:00
Hosted Weblate
b56f3b3324 Translated using Weblate (Punjabi)
Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (French)

Currently translated at 93.2% (69 of 74 strings)

Translated using Weblate (Toki Pona)

Currently translated at 6.5% (45 of 683 strings)

Translated using Weblate (Toki Pona)

Currently translated at 2.7% (2 of 74 strings)

Translated using Weblate (French)

Currently translated at 100.0% (683 of 683 strings)

Added translation using Weblate (Toki Pona)

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

Currently translated at 96.3% (658 of 683 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 8.3% (57 of 683 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (French)

Currently translated at 100.0% (683 of 683 strings)

Co-authored-by: Eric <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: John Donne <akheron@zaclys.net>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: ShareASmile <aapshergill@gmail.com>
Co-authored-by: Translator <kvb@tuta.io>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: William <Electroboss@users.noreply.hosted.weblate.org>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: kuragehime <kuragehime641@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tok/
Translation: NewPipe/Metadata
2023-07-31 23:46:11 +02:00
TobiGr
0195655479 Add changelog for NewPipe 0.25.2 (994) 2023-07-31 23:43:41 +02:00
Tobi
3c91ec33ae
Merge pull request #10122 from TeamNewPipe/fix/media-tunneling
Disable media tunneling by default on known unsupported devices
2023-07-31 23:30:24 +02:00
Tobi
6b3f51e5ea
Merge pull request #10281 from TeamNewPipe/okio
Update com.squareup.okio:okio to 3.4.0
2023-07-31 23:29:39 +02:00
TobiGr
d6a1170ddb Replace settings migration with automatic check for device blacklist version 2023-07-31 23:00:54 +02:00
TobiGr
428a7d418b Update com.squareup.okio:okio to 3.4.0
Use okio 3.4.0 explicity to fix vulnerability introduced through okhttp3 (3.3.0).
See https://www.cve.org/CVERecord?id=CVE-2023-3635 for more details on the vulnerability.
2023-07-31 21:53:49 +02:00
TobiGr
40d102fcb5 Disable media tunneling by default on new devices
Sony BRAVIA_VH1, BRAVIA_VH2, BRAVIA_ATV2, BRAVIA_ATV3_4K
Phillips 4K (O)LED TV (PH7M_EU_5596)
Panasonic 4KTV-JUP (TX_50JXW834)
Bouygtel4K (HMB9213NW)
2023-07-29 22:08:51 +02:00
TobiGr
1db73370a7 Ensure that imports handle disabling media tunneling correctly
Store in preferences whether media tunneling was disabled automatically.
Show info in ExoPlayer settings if media tunneling was disabled autmatically.
2023-07-29 22:08:51 +02:00
TobiGr
8b63b437d8 Disable media tunneling if the device is known for not supporting it
Revert removing the Utils related to media tunneling.
2023-07-29 14:13:03 +02:00
TobiGr
78e577d260 Make some constants private and annotate params 2023-07-29 14:13:03 +02:00
Tobi
96a7cc2971
Merge pull request #10250 from kuragehimekurara1/dev
Added Uchinaguchi translation and README
2023-07-24 19:36:32 +02:00
TobiGr
9eedbae879 Fix ryu translation syntax 2023-07-24 19:33:27 +02:00
kuragehime
38d3b3c7ef Added Uchinaguchi (ryu) to language selector 2023-07-24 19:31:31 +02:00
kuragehime
e4d3b74f1b Added Uchinaguchi translation 2023-07-24 19:31:31 +02:00
TobiGr
54f3003a6f Added Uchinaguchi README
Co-authored-by:  kuragehime <kuragehime641@gmail.com>
2023-07-24 19:31:31 +02:00
TobiGr
cbc7b8ce18 Fix wrongly formatted string resource audio_track_name
Closes #10243
2023-07-24 19:14:00 +02:00
Hosted Weblate
ec7d01b794
Translated using Weblate (Swedish)
Currently translated at 72.9% (54 of 74 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (German)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Dutch)

Currently translated at 64.8% (48 of 74 strings)

Translated using Weblate (German)

Currently translated at 81.0% (60 of 74 strings)

Translated using Weblate (Albanian)

Currently translated at 81.0% (552 of 681 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (681 of 681 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (681 of 681 strings)

Translated using Weblate (French)

Currently translated at 93.2% (69 of 74 strings)

Translated using Weblate (Vietnamese)

Currently translated at 33.7% (25 of 74 strings)

Translated using Weblate (Bulgarian)

Currently translated at 66.5% (453 of 681 strings)

Translated using Weblate (Finnish)

Currently translated at 87.3% (595 of 681 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (681 of 681 strings)

Translated using Weblate (German)

Currently translated at 81.0% (60 of 74 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (681 of 681 strings)

Translated using Weblate (Japanese)

Currently translated at 12.1% (9 of 74 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (681 of 681 strings)

Translated using Weblate (Vietnamese)

Currently translated at 32.4% (24 of 74 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (681 of 681 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (681 of 681 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (681 of 681 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (681 of 681 strings)

Translated using Weblate (Romanian)

Currently translated at 96.0% (654 of 681 strings)

Translated using Weblate (Japanese)

Currently translated at 99.7% (679 of 681 strings)

Translated using Weblate (Korean)

Currently translated at 12.1% (9 of 74 strings)

Translated using Weblate (Korean)

Currently translated at 12.1% (9 of 74 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (681 of 681 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (681 of 681 strings)

Translated using Weblate (German)

Currently translated at 79.7% (59 of 74 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (681 of 681 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: AhHyeon An <toto1444@gmail.com>
Co-authored-by: AioiLight <info@aioilight.space>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Anonymous <deni76@tutanota.com>
Co-authored-by: Anxhelo Lushka <anxhelo1995@gmail.com>
Co-authored-by: Arsi Kiikka <arsikiikka20@gmail.com>
Co-authored-by: David Braz <davidbrazps2@gmail.com>
Co-authored-by: Edward <edwardchirita@mailbox.org>
Co-authored-by: Eric <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Etienne Barrier <etienne.barrier@gmail.com>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Rückert <igorruckert@yahoo.com.br>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: MJ Kim <faith@users.noreply.hosted.weblate.org>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: ShareASmile <aapshergill@gmail.com>
Co-authored-by: TXRdev Archive <lckphanaf9999@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: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kuragehime <kuragehime641@gmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: zmni <zmni@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
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/ja/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ko/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/vi/
Translation: NewPipe/Metadata
2023-07-24 19:04:23 +02:00
Tobi
3edd4c012d
Merge pull request #10195 from AudricV/player_refactor-renderers-activation-or-deactivation
Refactor Player.useVideoSource logic and improve its comments
2023-07-22 14:12:35 +02:00
Tobi
3243f97ff2
Merge pull request #10233 from TeamNewPipe/actions/mimmizer-pr
Minimize images in PR descriptions
2023-07-20 23:18:03 +02:00
Isira Seneviratne
c658f28b02
Merge pull request #10078 from Isira-Seneviratne/Improve_feed_notifications
Improve new stream notifications
2023-07-20 06:39:19 +05:30
AudricV
5ab3a4a9e0
Refactor Player.useVideoSource logic and improve its comments
- don't check for isAudioOnly == !videoEnabled, as this prevents enabling again
video and text tracks renderers in some cases;
- when reloading play queue manager if that's needed, disable or enable video
and text tracks renderers, as they may need to be enabled again in some cases
like starting a video in main player, opening play queue, switching to
background player on it and switching back to main player;
- disable or enable video renderers also for streams with AUDIO_STREAM
StreamType, as doing so doesn't raise any issue and simplifies code;
- reword and move some comments to make them easier to understand.
2023-07-19 22:52:18 +02:00
Isira Seneviratne
cb00c57009 Set channel icon for stream notifications 2023-07-19 05:52:59 +05:30
Tobi
cd2884d412
Merge pull request #10235 from TacoTheDank/bumpRoom
Update AndroidX Room library
2023-07-18 22:35:30 +02:00
Tobi
471137093a
Merge pull request #9719 from Marius1501/tabs_on_bottom
Added bottom main-tabs feature
2023-07-18 22:03:47 +02:00
Tobi
57064479c8
Merge pull request #8456 from SydneyDrone/database_tests
Add database test for SubscriptionManager
2023-07-18 08:55:35 +02:00
Stypox
528bd502b4 Improve SubscriptionManager tests
- fix checkstyle errors
- tests do not run in order, so each one has to do its own assertions separately from what others did
- the uid of an entity in the database needn't be the same of the one created in-memory, since the uid gets assigned upon inserting in the database
- some database functions return a `Completable` that doesn't do anything until it is subscribed to or awaited, so I added `.awaitBlocking()` where needed
- the data of an entity in-memory does not get updated automatically when the corresponding entity in the database is changed, so some tests have been removed
- `manager.insertSubscription` only inserts recent streams, so they need to have a date set on them (I also made related items hardcoded and not dependent on what the channel is currently doing)
2023-07-18 08:36:29 +02:00
SydneyDrone
90bc1905f5 Create SubscriptionManagerTest.java 2023-07-18 08:36:29 +02:00
TacoTheDank
a01e59e9db Update AndroidX Room library 2023-07-17 21:09:44 -04:00
TacoTheDank
3f944c1bb2 Fix MigrationTestHelper deprecation 2023-07-17 21:09:21 -04:00
Tobi
43ef852117
Merge pull request #10230 from TeamNewPipe/fix/offline-stream-chooser
[Download] Fix audio stream selection
2023-07-17 23:47:21 +02:00
TobiGr
bf22515bcd Fix README 2023-07-17 22:18:08 +02:00
Tobi
4c17c7b45b
Merge pull request #10240 from TeamNewPipe/readmes
Add translated READMEs
2023-07-17 22:08:31 +02:00
TobiGr
ec21200787 Add links to all READMEs 2023-07-17 21:38:26 +02:00
TobiGr
25fea73704 Add Russian README
Taken from #8711 with updated screenshots

Co-authored-by:  Vik <63919734+ViktorOn@users.noreply.github.com>
2023-07-17 21:38:26 +02:00
TobiGr
2377d85efb Add German README
Taken from #8712 with updated screenshots and applied reviews.

Co-authored-by: Nico Haas <nico.haas@aisec.fraunhofer.de>
Co-authored-by: poolitzer <github@poolitzer.eu>
Co-authered-by: TobiGr <tobigr@users.noreply.github.com>
2023-07-17 21:38:26 +02:00
TobiGr
ddef550637 Add French README
Taken from #9296 with updated screenshots

Co-authored-by: eze-kiel <hugoblanc@fastmail.com>
2023-07-17 21:38:26 +02:00
Tobi
2f0ed7f3b7
Merge pull request #10232 from Stypox/leakcanary-fix
Fix LeakCanary startup in debug builds and fix a memory leak
2023-07-17 19:42:24 +02:00
TobiGr
b6bdd359d6 Add Assamese README
Taken from #9618 with updated screenshots

Co-authored-by:  Abhilash <121420261+WirelessAlien@users.noreply.github.com>
2023-07-17 18:54:38 +02:00
TobiGr
a4453bc699 Add Punjabi README
Taken from #9621 with updated screenshots.

Co-authored-by:  K Gill <60492161+ShareASmile@users.noreply.github.com>
2023-07-17 18:54:38 +02:00
TobiGr
3e87c40856 Add Italian README
Taken from #10157

Co-authored-by:  Mirko Di <84203046+mirk0dex@users.noreply.github.com>
2023-07-17 18:54:38 +02:00
Tobi
d80e531a2e
Merge pull request #9421 from seojun0924/patch-2
Update README.ko.md
2023-07-17 16:28:48 +02:00
Tobi
d25e84a461
Merge pull request #9897 from rogerjs93/patch-1
Update README.es.md
2023-07-17 01:40:17 +02:00
TobiGr
05cc520665 Fix pure logo
The previous version was not properly vertically aligned. The default alignement from the logo is used now.
2023-07-17 01:28:55 +02:00
TobiGr
eeec6fd002 Replace null check with use of NotificationManagerCompat.from 2023-07-17 01:28:55 +02:00
Isira Seneviratne
795bc82c7f Show number of new streams in the collapsed summary notification. 2023-07-17 01:28:55 +02:00
Isira Seneviratne
7742c40ac0 Create individual stream notifications for convenience on Android 7.0 and later. 2023-07-17 01:28:55 +02:00
TobiGr
d9e2ada369 Minimize images in PR descriptions 2023-07-15 02:33:53 +02:00
Stypox
5d6158ea76
No need to manually mark fragment as destroyed for LeakCanary
It already does so automatically.
2023-07-14 20:48:05 +02:00
Stypox
00257e969e
Fix PlayerService leakead by Binder instance
Also see https://stackoverflow.com/q/63787707
2023-07-14 18:34:20 +02:00
Stypox
135f0f7249
Make all leak canary libs debugImplementation-only 2023-07-14 18:32:30 +02:00
Stypox
fdd8b76add
Fix DebugApp doing unneeded AppWatcher.manualInstall 2023-07-14 18:32:04 +02:00
TobiGr
6b7ffbba4c [Download] Fix audio stream selection
Closes #10180
2023-07-14 17:06:12 +02:00
Tobi
8cfba4003d
Merge pull request #10229 from Koitharu/bugfix/feed_crash
Fix crash after feed update
2023-07-14 15:31:12 +02:00
Koitharu
01b46edf1a
Fix crash after feed update 2023-07-14 11:41:52 +03:00
Stypox
f8599d17c2
Merge pull request #10085 from TacoTheDank/bumpLeakCanary
Update LeakCanary library
2023-07-12 19:05:38 +02:00
Stypox
5c7a9a52f5
Merge pull request #10223 from TacoTheDank/cleanAlertDialogs
Clean up AlertDialogs
2023-07-12 19:02:57 +02:00
TacoTheDank
c1f0a945c0 Clean up AlertDialogs 2023-07-11 21:54:10 -04:00
TacoTheDank
db7de05f2b Update LeakCanary library 2023-07-11 20:32:29 -04:00
Stypox
e33bb676f9
Merge pull request #10219 from TeamNewPipe/PR-template-wiki-link
Add link to wiki page for APK download
2023-07-08 23:01:48 +02:00
Tobi
30724dbc50
Add link to wiki page for APK download 2023-07-08 22:49:10 +02:00
Stypox
e765343162
Merge pull request #10166 from TeamNewPipe/fix/image-workflow
Add support for new GitHub assets URLs in image minimizer workflow
2023-07-08 22:18:16 +02:00
Tobi
62ce0b0408
Merge pull request #10213 from Stypox/update-screenshots
Update screenshots
2023-07-08 12:33:59 +02:00
Stypox
3bbc606694
Update screenshots in translated READMEs 2023-07-07 20:57:42 +02:00
Stypox
56eec9fed1
Add separation between tablet and phone images 2023-07-07 20:57:42 +02:00
Stypox
ea0d798ea0
Update README screenshots 2023-07-07 20:57:41 +02:00
Stypox
5716d51112
Update screenshots 2023-07-07 20:57:41 +02:00
Tobi
d845a158f0
Merge pull request #10200 from TeamNewPipe/fix/acra
Update ACRA and Checkstyle to fix a dependency vulnerability
2023-07-02 00:16:54 +02:00
TobiGr
1a2fbd8122 Update acra and checkstyle fixing vulnerability in dependency com.google.guava
See https://app.snyk.io/org/thescrabi/project/27dc214e-7f4f-47bb-a77c-443201491254
2023-07-01 13:30:37 +02:00
TobiGr
8bdeed8f28 Add support for new GitHub assetes URLs in image minimizer workflow 2023-06-15 16:03:30 +02:00
Tobi
3c87462203
Merge pull request #10164 from TeamNewPipe/weblate
Update weblate & fix conflicts
2023-06-14 12:55:16 +02:00
TobiGr
3622438a9d Translated using Weblate (Arabic)
Currently translated at 55.4% (41 of 74 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Malayalam)

Currently translated at 83.7% (572 of 683 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (French)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Tigrinya)

Currently translated at 2.1% (15 of 683 strings)

Translated using Weblate (Tigrinya)

Currently translated at 4.0% (3 of 74 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 22.2% (152 of 683 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (675 of 683 strings)

Added translation using Weblate (Tigrinya)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (683 of 683 strings)

Added translation using Weblate (Kannada)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 13.4% (92 of 683 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Korean)

Currently translated at 12.1% (9 of 74 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (683 of 683 strings)

Added translation using Weblate (English (Middle))

Added translation using Weblate (English (Old))

Added translation using Weblate (Sicilian)

Added translation using Weblate (Aymara)

Added translation using Weblate (Arabic (Najdi))

Added translation using Weblate (Kashmiri)

Added translation using Weblate (German (Low))

Translated using Weblate (Korean)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Latvian)

Currently translated at 93.7% (640 of 683 strings)

Translated using Weblate (Dutch)

Currently translated at 64.8% (48 of 74 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 95.9% (71 of 74 strings)

Translated using Weblate (German)

Currently translated at 74.3% (55 of 74 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (683 of 683 strings)

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

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (683 of 683 strings)

Deleted translation using Weblate (Kashmiri)

Deleted translation using Weblate (Arabic (Najdi))

Deleted translation using Weblate (Aymara)

Deleted translation using Weblate (Sicilian)

Deleted translation using Weblate (English (Old))

Deleted translation using Weblate (English (Middle))

Deleted translation using Weblate (German (Low))

Translated using Weblate (Belarusian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Slovak)

Currently translated at 17.5% (13 of 74 strings)

Translated using Weblate (French)

Currently translated at 90.5% (67 of 74 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.2% (678 of 683 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Italian)

Currently translated at 97.2% (664 of 683 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (French)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (German)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (German)

Currently translated at 100.0% (683 of 683 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: AhHyeon An <toto1444@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Alexthegib <jcwkgxc@nightorb.com>
Co-authored-by: Arnis Jaundzeikars <sangsatori@theradiant.space>
Co-authored-by: AudricV <AudricV@users.noreply.hosted.weblate.org>
Co-authored-by: Balázs Meskó <meskobalazs@mailbox.org>
Co-authored-by: Domokun <domokun@asdasd.nl>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Eric <hamburger2048@users.noreply.hosted.weblate.org>
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: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Marian Hanzel <marulinko@gmail.com>
Co-authored-by: Nidi <nizamismidov4@gmail.com>
Co-authored-by: Olivia Ng <uloo592@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Surfoo <surfooo@gmail.com>
Co-authored-by: TobiGr <tobigr@mail.de>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: ai <woldu@duck.com>
Co-authored-by: atilluF <110931720+atilluF@users.noreply.github.com>
Co-authored-by: ctntt <pavlov_mainstreamed@slmail.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gbpu <gui.beppu@gmail.com>
Co-authored-by: gymka <gymka@archlinux.lt>
Co-authored-by: jeffin-v <jeffin@posteo.net>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: thami simo <simo.azad@gmail.com>
Co-authored-by: tryvseu <tryvseu@tuta.io>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: 이정희 <daemul72@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/az/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ko/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ti/
Translation: NewPipe/Metadata

Translated using Weblate (Hungarian)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (683 of 683 strings)

Translated using Weblate (Galician)

Currently translated at 99.7% (681 of 683 strings)

Translated using Weblate (Hungarian)

Currently translated at 17.5% (13 of 74 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (74 of 74 strings)

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

Translated using Weblate (N’Ko)

Currently translated at 98.5% (673 of 683 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (683 of 683 strings)
2023-06-14 12:01:36 +02:00
Isira Seneviratne
1848892ff8
Merge pull request #10076 from Isira-Seneviratne/Remove_unused_resources
Remove unused resources
2023-06-09 12:49:45 +05:30
Tobi
72c6ed2804
Merge pull request #10066 from Stypox/swap-subchannel-avatar
Fix uploader and subchannel avatars being swapped and disable loading thumbnail message failure on content details page
2023-06-05 23:24:12 +02:00
Tobi
42de2c7033
Merge pull request #10141 from quarthex/add-peertube.stream
handle links to the PeerTube instance “peertube.stream”
2023-06-05 23:20:38 +02:00
Romain
6bcc8691fa
handle links to the PeerTube instance “peertube.stream”
also sort the lines, because it will quickly become a mess otherwise
2023-06-05 20:34:55 +02:00
Isira Seneviratne
6cf13ed8fb
Merge pull request #10087 from TacoTheDank/bumpLibraries
Update some libraries
2023-06-02 08:49:12 +05:30
Isira Seneviratne
ad75db40df
Merge pull request #10088 from TacoTheDank/organizeProguard
Organize the proguard file
2023-06-02 06:00:22 +05:30
TacoTheDank
4e3bf3c2f9 Update some libraries 2023-05-30 12:22:05 -04:00
Stypox
1925687f18
Merge pull request #10089 from TacoTheDank/fixUnresolvedExtractor
Fix unresolved extractor
2023-05-26 11:55:41 +02:00
Stypox
577301c4eb
Proper filename for questions discussion template 2023-05-26 11:52:26 +02:00
Tobi
c87b42de1c
Merge pull request #10120 from Stypox/move-questions
Move questions to Discussions
2023-05-26 11:15:01 +02:00
Stypox
c8e8915c2e
Move questions to Discussions 2023-05-26 11:05:32 +02:00
Stypox
17cdedfa85
Merge pull request #10119 from Stypox/rewrite-announcement
Add rewrite announcement to readme
2023-05-26 10:35:37 +02:00
Stypox
677bb4070f
Add rewrite announcement to readme 2023-05-26 10:34:33 +02:00
TacoTheDank
fe82029dc7 Fix unresolved extractor 2023-05-12 01:12:12 -04:00
TacoTheDank
0ab9961908 Organize the proguard file 2023-05-12 01:09:08 -04:00
Isira Seneviratne
ecbf5d5ead Remove unused resources. 2023-05-08 06:27:41 +05:30
Tobi
df430badbc
Merge pull request #10042 from MBKaba/patch-1
add language ߒߞߏ (nqo)
2023-05-06 23:28:55 +02:00
Tobi
8639972a54
Merge pull request #10074 from TeamNewPipe/weblate
Update translations
2023-05-06 23:03:54 +02:00
TobiGr
41038f452d Remove translations of previously deleted string brightness_gesture_control_summary 2023-05-06 02:01:54 +02:00
TobiGr
2f31ea8864 Remove translations of previously deleted string brightness_gesture_control_title 2023-05-06 01:58:33 +02:00
TobiGr
e831059162 Remove translations of previously deleted string volume_gesture_control_summary 2023-05-06 01:55:36 +02:00
TobiGr
e109e8cf1c Remove translations of previously deleted string volume_gesture_control_title 2023-05-06 01:52:31 +02:00
TobiGr
f1524b6aba Remove translations of previously deleted string feed_toggle_show_played_items 2023-05-06 01:48:16 +02:00
TobiGr
51ee6f87e0 Remove translations of previously deleted string feed_toggle_hide_played_items 2023-05-06 01:48:08 +02:00
TobiGr
0bb3e7cb86 Translated using Weblate (Belarusian)
Currently translated at 6.7% (5 of 74 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (N’Ko)

Currently translated at 12.9% (86 of 664 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 99.3% (660 of 664 strings)

Translated using Weblate (Estonian)

Currently translated at 99.6% (662 of 664 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (664 of 664 strings)

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

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (German)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (N’Ko)

Currently translated at 0.0% (0 of 74 strings)

Translated using Weblate (N’Ko)

Currently translated at 8.9% (59 of 661 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (661 of 661 strings)

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

Currently translated at 100.0% (661 of 661 strings)

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

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.6% (659 of 661 strings)

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

Currently translated at 100.0% (661 of 661 strings)

Added translation using Weblate (N’Ko)

Translated using Weblate (Bambara)

Currently translated at 0.1% (1 of 661 strings)

Translated using Weblate (Bambara)

Currently translated at 1.3% (1 of 74 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (74 of 74 strings)

Added translation using Weblate (Bambara)

Merge branch 'origin/dev' into Weblate.

Translated using Weblate (Estonian)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (661 of 661 strings)

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

Currently translated at 100.0% (661 of 661 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Alexthegib <jcwkgxc@nightorb.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Bakary Kaba <mbkaba@live.fr>
Co-authored-by: Bdd55oo <giggzuv9z.eofjx@aleeas.com>
Co-authored-by: Edward <edwardchirita@mailbox.org>
Co-authored-by: Eric <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hoàng Lâm Lê <work.lehoanglam@gmail.com>
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: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Nidi <nizamismidov4@gmail.com>
Co-authored-by: Olivia Ng <uloo592@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Retrial <giwrgosmant@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: 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: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: mono <monodevx@gmail.com>
Co-authored-by: random r <epsilin@yopmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/be/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bm/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nqo/
Translation: NewPipe/Metadata

Translated using Weblate (German)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (French)

Currently translated at 99.3% (660 of 664 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Catalan)

Currently translated at 93.2% (619 of 664 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (N’Ko)

Currently translated at 41.4% (275 of 664 strings)

Translated using Weblate (N’Ko)

Currently translated at 4.0% (3 of 74 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Turkish)

Currently translated at 99.3% (660 of 664 strings)

Translated using Weblate (Georgian)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (N’Ko)

Currently translated at 67.9% (451 of 664 strings)

Translated using Weblate (French)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (French)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Odia)

Currently translated at 98.9% (657 of 664 strings)

Translated using Weblate (N’Ko)

Currently translated at 84.6% (562 of 664 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Turkish)

Currently translated at 99.5% (661 of 664 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (N’Ko)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (N’Ko)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (French)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (N’Ko)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (German)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (N’Ko)

Currently translated at 100.0% (664 of 664 strings)

Translated using Weblate (N’Ko)

Currently translated at 5.4% (4 of 74 strings)

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

Translated using Weblate (N’Ko)

Currently translated at 9.4% (7 of 74 strings)

Translation: NewPipe/Metadata
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nqo/
2023-05-06 01:47:58 +02:00
Tobi
4bf063645a
Merge pull request #10067 from Stypox/update-gradle-again
Update Android Gradle Plugin to 8.0.1
2023-05-04 10:33:57 +00:00
Stypox
9866eab60f
Update Android Gradle Plugin to 8.0.1 2023-05-03 11:18:12 +02:00
Stypox
10c42de2f1
Fix uploader and subchannel avatars swapped 2023-05-03 10:35:28 +02:00
Stypox
e1fd25fb71
Merge pull request #10046 from Stypox/ktlint-java17
Fix ktlint formatter after upgrade to Java 17
2023-05-02 13:43:44 +02:00
Stypox
2315b082ff
Merge pull request #9937 from Theta-Dev/alang-selector
Add support for multiple audio tracks
2023-05-02 10:07:21 +02:00
AudricV
023f6166ab
Add Open in browser button to audio external players dialog
This change makes the dialog consistent with the video one.
2023-05-02 00:18:46 +02:00
AudricV
d89a3c6c4d
Remove "default" from audio track already present message
We don't know if, on muxed video streams we get for all services which support
multiple audio languages, that the audio language returned is the original one
or not, even if it should be the case.

In order to avoid saying potential false information, this word has been
removed from the string resource (ID and value) and the corresponding layout ID
in the download dialog.
2023-05-02 00:11:09 +02:00
ThetaDev
fb00ee8cf9
[YouTube] Improve download speed (#9948) 2023-05-01 19:26:42 +02:00
ThetaDev
22671ca16c fix: audio stream cache key, code fmt 2023-05-01 00:04:04 +02:00
ThetaDev
4e837e838d
fix docs in app/src/main/java/org/schabi/newpipe/util/Localization.java
Co-authored-by: Audric V. <74829229+AudricV@users.noreply.github.com>
2023-05-01 00:02:37 +02:00
Tobi
ed1781133c
Merge pull request from GHSA-r3gv-6fw7-hc52
Fix CI command injection vulnerability
2023-04-27 22:51:54 +02:00
MBKaba
60fc662a26
Update settings_keys.xml
Add ߒߞߏ (nqo) language code to <string-array name="app_language_code">

Add ߒߞߏ (nqo) language name to <string-array name="app_language_name">
2023-04-27 12:21:59 +00:00
Stypox
43b0167a3a
Fix CI command injection vulnerability
See https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions
2023-04-26 16:01:20 +02:00
Stypox
8519897089
Fix ktlint formatter after upgrade to Java 17
See https://github.com/pinterest/ktlint/issues/1195 and https://github.com/ScoopInstaller/Extras/issues/10313. Note that although this should have been fixed in the latest version of ktlint (we are using an old one), the fix doesn't seems to have worked for me.
2023-04-25 18:37:04 +02:00
MBKaba
60a5d02018
add language ߒߞߏ (nqo)
The translation is already done:
https://hosted.weblate.org/projects/newpipe/strings/nqo/
2023-04-24 10:44:26 +00:00
ThetaDev
c377ffbce8 Merge branch 'dev' of github.com:TeamNewPipe/NewPipe into alang-selector 2023-04-21 23:32:33 +02:00
ThetaDev
b567d428ad fix: small codestyle fixes 2023-04-21 23:15:37 +02:00
Stypox
da30e539df
Merge pull request #9748 from TeamNewPipe/feat/av1-tags
Add whitelist to only retrieve supported YouTube itags/streams
2023-04-20 16:42:31 +02:00
Isira Seneviratne
f74d794b2a
Merge pull request #10035 from Stypox/update-gradle
Upgrade Gradle and AGP from 7.x to 8.x and use Java 17
2023-04-20 19:55:31 +05:30
Stypox
69ef4a987e
Update CI Java version 2023-04-20 15:42:33 +02:00
Stypox
78e1e0508e
Fix gradle build for Java 17 and 19 2023-04-20 11:19:21 +02:00
Stypox
6d98ad7abc
Further upgrade gradle to 8.1 2023-04-20 10:50:48 +02:00
Stypox
70b3ba310a
Upgrade to Gradle 8.0 2023-04-20 10:36:23 +02:00
ThetaDev
2edc223e77 Merge branch 'dev' into alang-selector 2023-04-17 23:01:07 +02:00
TobiGr
e18a6b09f8 Apply new itag filter only to YouTube streams 2023-04-17 13:10:29 +02:00
TobiGr
f8c3ec4be7 Use a whitelist to filter all streams retrieved by the extractor.
NewPipe Extractor now extracts all YouTube Itags and therefore only those which can be handled by the player need to be retrieved from the list of all available streams.
2023-04-17 13:00:11 +02:00
Stypox
ba3afd1e35
Merge pull request #10021 from Isira-Seneviratne/PendingIntentCompat
Switch to AndroidX's PendingIntentCompat.
2023-04-14 14:20:47 +02:00
Isira Seneviratne
20f0011921 Fix Sonar failure. 2023-04-13 10:53:46 +05:30
Isira Seneviratne
acebabd028 Use AndroidX's PendingIntentCompat class. 2023-04-13 10:53:46 +05:30
Stypox
6243f34946
Merge pull request #8875 from AudricV/exoplayer-settings
Add an ExoPlayer settings page
2023-04-10 17:40:33 +02:00
AudricV
787758a436
[Android 6+] Add ability to always use ExoPlayer's MediaVideoCodecRenderer setOutputSurface workaround
As some devices not present in ExoPlayer's list may not implement
MediaCodec.setOutputSurface(Surface) properly, this workaround could be useful
on these devices.

It forces ExoPlayer to fall back on releasing and re-instantiating video codec
instances, which is always used on Android 5 and lower due to addition of this
method in Android 6.

To do so, a CustomMediaCodecVideoRenderer, based on ExoPlayer's
MediaVideoCodecRenderer which always return true for the
codecNeedsSetOutputSurfaceWorkaround method has been added, which is used in
CustomRenderersFactory, a class based on DefaultRenderersFactory which always
returns our CustomMediaCodecVideoRenderer as the video renderers.

CustomRenderersFactory replaces DefaultRenderersFactory in the player, in the
case this setting is enabled.
2023-04-10 17:39:27 +02:00
AudricV
a02b92fd59
Update playback load interval size setting description
- Remove redundant player restart requirement note, as it is written on the
ExoPlayer settings description page;
- Add precision about the setting effect/limitation, as it only applies on
progressive contents/media sources and not on every content/media source;
- Remove translations of this description, to ensure that they will be updated
by translators.
2023-04-10 17:39:25 +02:00
AudricV
a6ff85a208
Move media tunneling setting to ExoPlayer settings and make this setting available on release builds
Media tunneling may be not supported by more devices than the ones we
whitelisted before.

As a matter of fact, the list of devices on which media tunneling is disabled
could be not maintainable in the future, especially if the list of devices
grows more and more.

A preferable solution is to allow users to configure this setting themselves,
allowing them to not wait for their device(s) to be whitelisted in a future
NewPipe update.

This solution has been applied in this commit and works on every build type.

The corresponding preference in the debug settings has been of course removed
and the code used to prevent media tunneling activation on specific devices has
been removed.
2023-04-10 17:37:30 +02:00
AudricV
41da8fc05f
Add ability to use ExoPlayer's decoder fallback option
This option could help to avoid decoder initialization issues, which falls back
to lower-priority decoders if decoder initialization fails. This may result in
poor playback performance than when using primary decoders.

It is disabled by default, but can be enabled in ExoPlayer settings.
2023-04-10 17:37:30 +02:00
AudricV
a4a9957a15
Add ExoPlayerSettingsFragment and move playback load interval size setting into it
This fragment has been added into SettingsResourceRegistry, to allow searches
in its options.

It has been placed at the place of the previous playback load interval size
setting (so in Video and Audio settings).
2023-04-10 17:37:30 +02:00
Stypox
29318c64ed
Merge pull request #10004 from TeamNewPipe/fix/apk-jsoup
Remove jsoup files from APK
2023-04-10 17:22:20 +02:00
Isira Seneviratne
74bd28cbd9 Update AndroidX Core to 1.10.0. 2023-04-09 18:45:57 +05:30
ThetaDev
365bb2d0e4 Merge branch 'dev' of github.com:TeamNewPipe/NewPipe into alang-selector 2023-04-05 14:06:14 +02:00
TobiGr
c08538d25d Remove jsoup files from APK
Two jsoup files slipped into the META-INF dir of the APK for some reason. README.md and CHANGES are removed automatically now.
2023-04-04 17:42:34 +02:00
Stypox
140ea8642c
Merge pull request #10002 from Stypox/readme-notice
Add notice to README to not open feature PRs
2023-04-04 14:58:27 +02:00
Stypox
445d364193
Merge pull request #9708 from Marius1501/switch_setting_brightness_volume
Created a setting to switch the sides of volume and brightness
2023-04-04 14:19:08 +02:00
Stypox
4bb45c001d Fix settings migration 2023-04-04 11:06:20 +02:00
Stypox
7350b1f32e Add notice to README to not open feature PRs 2023-04-04 10:54:00 +02:00
Stypox
4a33ee6045
Merge pull request #10001 from Stypox/remove-exoplayer-from-changelogs
Remove "ExoPlayer settings" from 0.25.1 changelogs
2023-04-04 10:09:45 +02:00
Stypox
704e9bd7b6 Fix checkstyle 2023-04-04 10:02:01 +02:00
ge78fug
d2735607b8 Changed the default of the switches 2023-04-04 09:57:06 +02:00
Marius Wagner
3c72992c39 Update app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
Co-authored-by: Stypox <stypox@pm.me>
2023-04-04 09:57:06 +02:00
ge78fug
7689d1d15c Added the migration 2023-04-04 09:57:06 +02:00
ge78fug
65d8589e7a Changed the naming 2023-04-04 09:57:06 +02:00
ge78fug
32cec6c9a7 Changed the naming 2023-04-04 09:57:06 +02:00
Marius Wagner
72ca52a29b Made the requested changes 2023-04-04 09:57:06 +02:00
ge78fug
2ded8c7cc1 Made two list options 2023-04-04 09:57:06 +02:00
ge78fug
759a9080a8 Fixed a bug 2023-04-04 09:57:06 +02:00
ge78fug
2ba649949f Updated the gesture-switch-toggle 2023-04-04 09:57:06 +02:00
ge78fug
c8d54ec6c7 Changed to val 2023-04-04 09:57:06 +02:00
Marius Wagner
96e9242431 Update app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt
Co-authored-by: Stypox <stypox@pm.me>
2023-04-04 09:57:06 +02:00
ge78fug
3c74cb3439 Created a setting to switch the sides of volume and brightness 2023-04-04 09:57:06 +02:00
Stypox
7a8116b2cf Remove "ExoPlayer settings" from 0.25.1 changelogs
Also remove "hi" changelog completely, as it was not really translated
2023-04-04 09:47:51 +02:00
ThetaDev
d010384c88 Merge branch 'dev' of github.com:TeamNewPipe/NewPipe into alang-selector 2023-04-03 22:13:16 +02:00
Stypox
07111d86d4
Merge pull request #9869 from TeamNewPipe/release-0.25.1
Release v0.25.1 (993)
2023-04-03 14:50:23 +02:00
Hosted Weblate
ec974a2b3d Translated using Weblate (Estonian)
Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (661 of 661 strings)

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

Currently translated at 100.0% (661 of 661 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nidi <nizamismidov4@gmail.com>
Co-authored-by: Olivia Ng <uloo592@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
2023-04-03 14:49:41 +02:00
Stypox
02906e8132
Merge pull request #9812 from TeamNewPipe/revert-8894-WindowCompat
Revert "Use WindowCompat."
2023-04-03 14:23:17 +02:00
Stypox
6f428d0c6b
Merge pull request #9890 from Redirion/bumpexo184
Bump ExoPlayer to 2.18.5
2023-04-03 14:11:15 +02:00
TobiGr
41da2bfb00 Bump NewPipe Extractor to 0.22.6 2023-04-02 23:02:19 +02:00
TobiGr
746b1f7eb2 Merge branch 'dev' into release-0.25.1 2023-04-02 22:54:00 +02:00
Hosted Weblate
03fd286956
Translated using Weblate (Odia)
Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Swedish)

Currently translated at 59.4% (44 of 74 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (74 of 74 strings)

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

Currently translated at 18.9% (14 of 74 strings)

Translated using Weblate (Danish)

Currently translated at 98.3% (650 of 661 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (661 of 661 strings)

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

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Korean)

Currently translated at 99.8% (660 of 661 strings)

Translated using Weblate (French)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (German)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Indonesian)

Currently translated at 74.3% (55 of 74 strings)

Translated using Weblate (Czech)

Currently translated at 98.6% (73 of 74 strings)

Translated using Weblate (Ukrainian)

Currently translated at 98.6% (73 of 74 strings)

Translated using Weblate (Basque)

Currently translated at 43.2% (32 of 74 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 85.6% (566 of 661 strings)

Translated using Weblate (Belarusian)

Currently translated at 92.7% (613 of 661 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Latvian)

Currently translated at 89.7% (593 of 661 strings)

Translated using Weblate (Malay)

Currently translated at 54.3% (359 of 661 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 98.3% (650 of 661 strings)

Translated using Weblate (Vietnamese)

Currently translated at 97.7% (646 of 661 strings)

Translated using Weblate (Lithuanian)

Currently translated at 97.4% (644 of 661 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (661 of 661 strings)

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

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Korean)

Currently translated at 99.8% (660 of 661 strings)

Translated using Weblate (Hungarian)

Currently translated at 97.4% (644 of 661 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (English)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Assamese)

Currently translated at 14.8% (98 of 661 strings)

Translated using Weblate (Georgian)

Currently translated at 96.6% (639 of 661 strings)

Translated using Weblate (Bosnian)

Currently translated at 17.8% (118 of 661 strings)

Translated using Weblate (Gujarati)

Currently translated at 9.9% (66 of 661 strings)

Translated using Weblate (Marathi)

Currently translated at 8.4% (56 of 661 strings)

Translated using Weblate (Odia)

Currently translated at 99.8% (660 of 661 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Bengali)

Currently translated at 87.7% (580 of 661 strings)

Translated using Weblate (Bengali (India))

Currently translated at 45.9% (304 of 661 strings)

Translated using Weblate (Filipino)

Currently translated at 34.9% (231 of 661 strings)

Translated using Weblate (Danish)

Currently translated at 97.5% (645 of 661 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Belarusian)

Currently translated at 91.2% (603 of 661 strings)

Translated using Weblate (Belarusian)

Currently translated at 91.2% (603 of 661 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Albanian)

Currently translated at 83.9% (555 of 661 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 85.3% (564 of 661 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Catalan)

Currently translated at 93.3% (617 of 661 strings)

Translated using Weblate (Bulgarian)

Currently translated at 68.9% (456 of 661 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Finnish)

Currently translated at 90.0% (595 of 661 strings)

Translated using Weblate (Croatian)

Currently translated at 96.2% (636 of 661 strings)

Translated using Weblate (Vietnamese)

Currently translated at 97.7% (646 of 661 strings)

Translated using Weblate (Hebrew)

Currently translated at 98.7% (653 of 661 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Asturian)

Currently translated at 71.5% (473 of 661 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (661 of 661 strings)

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

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Dutch)

Currently translated at 98.3% (650 of 661 strings)

Translated using Weblate (French)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (German)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (English)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Korean)

Currently translated at 10.8% (8 of 74 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Belarusian)

Currently translated at 84.2% (557 of 661 strings)

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

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 95.9% (71 of 74 strings)

Translated using Weblate (Belarusian)

Currently translated at 78.9% (522 of 661 strings)

Translated using Weblate (Persian)

Currently translated at 99.3% (657 of 661 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Filipino)

Currently translated at 34.9% (231 of 661 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (French)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Russian)

Currently translated at 89.1% (66 of 74 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Sinhala)

Currently translated at 3.4% (23 of 661 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 64.8% (48 of 74 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Polish)

Currently translated at 60.8% (45 of 74 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Russian)

Currently translated at 78.3% (58 of 74 strings)

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

Currently translated at 18.9% (14 of 74 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (661 of 661 strings)

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

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (661 of 661 strings)

Translated using Weblate (German)

Currently translated at 100.0% (661 of 661 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: Alexthegib <jcwkgxc@nightorb.com>
Co-authored-by: Alfred Makne Poulsen <alfred@omj.dk>
Co-authored-by: Cyndaquissshhh <iversonbriones123@gmail.com>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: Eric <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: GET100PERCENT <eraofphysics@yahoo.com>
Co-authored-by: Gontzal Manuel Pujana Onaindia <thadahdenyse@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Rückert <igorruckert@yahoo.com.br>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Maday <royalcoolness7898@gmail.com>
Co-authored-by: Nidi <nizamismidov4@gmail.com>
Co-authored-by: Olivia Ng <uloo592@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: Rui Martins <martins.ro@gmail.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Sean Minnaert <sean.minnaert@gmail.com>
Co-authored-by: ShareASmile <aapshergill@gmail.com>
Co-authored-by: Simon Nilsson <Observeramera@pm.me>
Co-authored-by: Subham Jena <subhamjena8465@gmail.com>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.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: 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: fincent <fincentpm@protonmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: jc <jcwkgxc@nightorb.com>
Co-authored-by: komiratsu192 <502badgateway@duck.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: tndsG <tharushtnds@gmail.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: 모르것다 <jjs4809@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/az/
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/eu/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ko/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_PT/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ru/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2023-04-02 22:48:15 +02:00
ThetaDev
39a5c8bdfb fix: reset video stream sizes on audio track selection 2023-03-29 13:39:29 +02:00
Audric V
fb1b1c5be1
Merge pull request #9968 from AudricV/fix-open-in-browser-without-browser
Use a system chooser when opening links in browser in the case there is no browser available
2023-03-26 12:41:42 +02:00
AudricV
1a8aa8b17e
Use a system chooser when opening links in browser in the case there is no browser available
This change makes the app using the behavior when there is no default browser
on Android 11 and lower, by opening a system chooser when there is no browser
available (on all Android versions).

Also catch any exception when the system chooser cannot be opened and show the
"No app on your device can open this" toast in this case, as an
`ActivityNotFoundException` could be thrown if no app is available to open a
given web link.
2023-03-26 00:04:38 +01:00
Robin
2317864422 Bump ExoPlayer to 2.18.5 2023-03-23 15:03:07 +01:00
ThetaDev
694418d30d fix: update stream sizes when audio track changed 2023-03-21 16:58:36 +01:00
ThetaDev
ed06f559ae feat: add track selection to downloader 2023-03-20 21:41:28 +01:00
ThetaDev
fdd3b03fe5 fix: audio stream format selection 2023-03-19 23:47:33 +01:00
ThetaDev
dbd6e4d11f fix: sonarcloud lint 2023-03-19 22:55:37 +01:00
ThetaDev
61a14765f3 fix: ListHelper tests 2023-03-19 22:31:31 +01:00
ThetaDev
9b8ffdd2aa fix: improve track name localization 2023-03-19 21:20:21 +01:00
ThetaDev
ef0a4cf8b2 feat: add external audio playback language selector 2023-03-19 21:05:48 +01:00
ThetaDev
7aed2eed8a feat: add prefer original option, improve audio stream ordering 2023-03-19 20:40:27 +01:00
ThetaDev
87a88e4df7 feat: localized audio track names 2023-03-19 15:45:52 +01:00
ThetaDev
366c39d4c6 feat: add language selector to audio player 2023-03-19 01:15:36 +01:00
ThetaDev
77649d388c fix: reduce complexity 2023-03-18 16:29:22 +01:00
ThetaDev
dba53d23aa fix: remove todo 2023-03-18 16:14:07 +01:00
ThetaDev
208887d538 feat: improve audio track sorting, add prefer_descriptive_audio option 2023-03-18 14:50:19 +01:00
Isira Seneviratne
0cd1a86aa5
Merge pull request #9872 from TeamNewPipe/fix-lint
Remove wrong annotation
2023-03-18 15:00:34 +05:30
ThetaDev
de7872d8f2 feat: add audio language selector 2023-03-17 21:51:40 +01:00
Roger Salas
bb1f5d8f38
Update README.es.md 2023-03-08 19:27:13 +02:00
Robin
7c39421297 bump ExoPlayer to 2.18.4 2023-03-06 16:49:43 +01:00
TobiGr
d06cc862c8 Remove wrong annotation 2023-03-03 11:58:44 +01:00
Stypox
c5cf2f4514
Release v0.25.1 (993) 2023-03-01 10:52:05 +01:00
Stypox
3f8e44dc66
Update NewPipeExtractor 2023-03-01 10:51:17 +01:00
Stypox
d33229a3b8
Add changelog for v0.25.1 (993) 2023-03-01 10:45:25 +01:00
Hosted Weblate
bb57f9cc9d
Merge branch 'origin/dev' into Weblate.
Translated using Weblate (Hindi)

Currently translated at 100.0% (73 of 73 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.8% (654 of 655 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (654 of 655 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Russian)

Currently translated at 75.3% (55 of 73 strings)

Translated using Weblate (Belarusian)

Currently translated at 74.4% (487 of 654 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 99.8% (653 of 654 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.2% (649 of 654 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Slovenian)

Currently translated at 64.2% (420 of 654 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (German)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (English)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Belarusian)

Currently translated at 6.8% (5 of 73 strings)

Translated using Weblate (Belarusian)

Currently translated at 74.3% (486 of 654 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (German)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Vietnamese)

Currently translated at 98.4% (644 of 654 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (German)

Currently translated at 100.0% (654 of 654 strings)

Merge branch 'origin/dev' into Weblate.

Translated using Weblate (Bengali)

Currently translated at 21.9% (16 of 73 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 69.8% (51 of 73 strings)

Translated using Weblate (Portuguese)

Currently translated at 69.8% (51 of 73 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Odia)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Dutch)

Currently translated at 65.7% (48 of 73 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (73 of 73 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 28.7% (21 of 73 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (654 of 654 strings)

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

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (653 of 654 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (73 of 73 strings)

Translated using Weblate (Basque)

Currently translated at 45.2% (33 of 73 strings)

Translated using Weblate (German)

Currently translated at 73.9% (54 of 73 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (653 of 654 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (French)

Currently translated at 99.6% (652 of 654 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (German)

Currently translated at 100.0% (654 of 654 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 15.0% (11 of 73 strings)

Translated using Weblate (German)

Currently translated at 73.9% (54 of 73 strings)

Translated using Weblate (Thai)

Currently translated at 32.0% (209 of 652 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (652 of 652 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (652 of 652 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (652 of 652 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (652 of 652 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 15.0% (11 of 73 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (73 of 73 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (652 of 652 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (652 of 652 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (652 of 652 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (652 of 652 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (652 of 652 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (652 of 652 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 64.3% (47 of 73 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (73 of 73 strings)

Translated using Weblate (Indonesian)

Currently translated at 76.7% (56 of 73 strings)

Translated using Weblate (Polish)

Currently translated at 60.2% (44 of 73 strings)

Translated using Weblate (Hindi)

Currently translated at 21.9% (16 of 73 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (73 of 73 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (73 of 73 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (73 of 73 strings)

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

Currently translated at 17.8% (13 of 73 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (652 of 652 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (652 of 652 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Eric <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Florian <flo.site@zaclys.net>
Co-authored-by: GET100PERCENT <eraofphysics@yahoo.com>
Co-authored-by: GnuPGを使うべきだ <dieeeazpnnqbpddh@cock.email>
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: Igor Rückert <igorruckert@yahoo.com.br>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Issa1553 <fairfull.playing@gmail.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: Jonatan Nyberg <jonatan@autistici.org>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Marian Hanzel <marulinko@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Mateus <mateusbernardo@protonmail.com>
Co-authored-by: Nidi <nizamismidov4@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Phahim Hasan <phahimhasanrakib@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Ray <ray.cfu@protonmail.com>
Co-authored-by: Retrial <giwrgosmant@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: S3aBreeze <S3aBreeze@users.noreply.hosted.weblate.org>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: ShareASmile <aapshergill@gmail.com>
Co-authored-by: Sierzh <my-email@tut.by>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.com>
Co-authored-by: ThePsychoBuck <Thepsychobuck@protonmail.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: bowornsin <bowornsin@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: komiratsu19273240ad76c354986 <2011945@naver.com>
Co-authored-by: petlyh <88139840+petlyh@users.noreply.github.com>
Co-authored-by: phneutral26 <github@phileric.anonaddy.com>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Štefan Baebler <stefan.baebler@gmail.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/be/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bn/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/eu/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/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/ru/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant_HK/
Translation: NewPipe/Metadata
2023-03-01 10:17:40 +01:00
Stypox
23a20712da
Merge pull request #9707 from Jared234/1473_remove_duplicates_from_playlist
Remove duplicates from playlist feature
2023-02-28 22:14:01 +01:00
Stypox
43f46e29ad
Update app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java 2023-02-28 21:40:11 +01:00
Stypox
7617f8cdc7
Update app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java 2023-02-28 21:35:57 +01:00
Stypox
2e3490bce2
Merge pull request #9747 from Jared234/9126_remove_partially_watched_from_feed
Added option to remove partially watched videos from the 'Whats new' feed
2023-02-28 19:10:11 +01:00
Jared Fantaye
1dd0930b83 Fixed some small issues 2023-02-28 17:30:17 +01:00
Jared Fantaye
265de55a07 Merge remote-tracking branch 'origin/1473_remove_duplicates_from_playlist' into 1473_remove_duplicates_from_playlist 2023-02-28 16:44:13 +01:00
Jared Fantaye
d8ed2c8503 Refactoring removeDuplicates function and preventing concurrent calls. 2023-02-28 16:43:58 +01:00
Stypox
73aebc1110
Merge pull request #9847 from Redirion/exo183
Update to ExoPlayer 2.18.3
2023-02-26 19:16:25 +01:00
Stypox
3cb76e4c34
Merge pull request #9746 from NyanCatTW1/issue9745
Add an option to Ignore hardware media button events
2023-02-26 16:35:12 +01:00
Stypox
65680b2ccf
Only update main tabs position when it changes 2023-02-26 15:58:09 +01:00
Stypox
c8ffe65acf
Simplify code to set tab layout position 2023-02-26 15:42:49 +01:00
Stypox
a4767fc48a
Listen to ignore hardware buttons pref changes 2023-02-26 14:28:57 +01:00
Nyan Cat
42d861688e
Implement Ignore hardware media button events option 2023-02-26 14:02:50 +01:00
Stypox
2ee4c6e289
Merge pull request #9728 from mahendranv/channel_card
Larger channel cards in search results
2023-02-26 13:43:09 +01:00
Stypox
097c2368f4
Merge pull request #8180 from Trust04zh/fix-4053-8176
Make UI behavior for playback information display more consistent
2023-02-26 13:22:13 +01:00
Stypox
80e0c6ab0e
Merge pull request #9755 from Jared234/9458_faulty_playlist_thumbnail_update
Fixed a bug that caused erroneous updates of the playlist thumbnails
2023-02-26 13:13:12 +01:00
Jared Fantaye
9067c770a7 Made some small code improvements 2023-02-25 22:14:49 +01:00
Stypox
f1a071b668
Merge pull request #9858 from Stypox/fit-more-grid-columns
Reduce the size of thumbnails on big screens to fit more grid columns
2023-02-25 20:42:36 +01:00
Stypox
8e888ebdf7
Reduce the size of thumbnails on big screens to fit more grid columns
Reverts part of #9310, which introduced bigger grid thumbnail sizes on big screens, because some users reported not being happy about having too few grid columns. See https://github.com/TeamNewPipe/NewPipe/pull/9310#discussion_r1070670806 .
2023-02-25 15:03:58 +01:00
Stypox
612122997b
Merge pull request #9769 from pratyaksh1610/branch-9765
Fix progress bar scaling on thumbnail in playlists card view
2023-02-25 14:50:07 +01:00
Stypox
4b050c0dd8
Merge pull request #9850 from Stypox/fix-api33-links-again3
[Android 11+] Correctly open URLs in browser and fix opening downloads and external players
2023-02-25 14:33:41 +01:00
Stypox
be4f3d9d62
Improve javadocs in ShareUtils 2023-02-25 13:14:31 +01:00
Stypox
24ff6a4313
Rename videoURL to streamUrl 2023-02-25 13:14:31 +01:00
Stypox
c2968a3ff2
Use non-deprecated resolveActivity method on API 33+
But such method is not available before API 33
2023-02-25 13:14:31 +01:00
Stypox
671dd4afd3
Merge pull request #9777 from pratyaksh1610/branch-9774
[Bug] Crash fix when click on empty comment
2023-02-25 09:44:31 +01:00
Stypox
600ebdae18
Correctly open urls in browser on Android 11+
- Fix misconfiguration in manifest ('http|https|market' is not valid)
- Split ShareUtils functions taking a boolean parameter into pairs of functions with better names and less runtime checks
- Move all Kore-related functions to KoreUtils
- Remove the toast_no_player string
2023-02-25 09:13:59 +01:00
Robin
5560cea470 Update to ExoPlayer 2.18.3 2023-02-23 12:46:05 +01:00
Stypox
39c500f33c
Revert "Use WindowCompat." 2023-02-14 08:27:04 +01:00
pratyaksh1610
624ad6a47c
Prevent NPEs when comment text is null 2023-02-14 08:18:13 +01:00
Jared Fantaye
68ea99d6e6 Made some small code improvements 2023-02-09 23:17:36 +01:00
Jared Fantaye
bc29f40d69 Implemented the suggested changes 2023-02-09 21:18:21 +01:00
Jared234
42fb13f17a
Merge branch 'dev' into 1473_remove_duplicates_from_playlist 2023-02-09 20:47:10 +01:00
Jared Fantaye
d5b54c85ed Made some small adjustments to the database query 2023-02-09 20:41:22 +01:00
pratyaksh1610
f0307b1b48 fix progress bar scaling in card view 2023-02-09 21:38:02 +05:30
Mahendran
75292e099c Larger channel cards in search results
- Thumbnail larger (100dp) than the usual (92dp) throughout the app
- Description lint count is 8 (normally 3)
2023-02-09 06:15:22 +05:30
Stypox
e0cb2892b8
Merge branch 'master' into dev 2023-02-08 22:48:14 +01:00
Stypox
4c5c2a3d79
Merge pull request #9693 from Redirion/accelerometerfix
Orientation is locked if there is no sensor for it
2023-02-07 20:07:45 +01:00
ge78fug
26b29ca78d Made the requested color changes 2023-02-07 18:46:40 +01:00
Trust_04zh
e947e86eae
Make positions in list depend on watch history, remove confusing animations
The following is the list of all commits squashed together:

Regain function for option `Positions in lists`

use option `Resume playback` to control display of progress info in VideoDetailFragment, remove this (extra) function from option `Positions in lists`.
remove extra check for live streams, live streams updates just as non-live streams.

fix #8176 by eliminating exit delay

Regain function for option `Positions in lists`

update code with developer's comments

 apply static import to methods in util class DependentPreferenceHelper

Regain function for option `Positions in lists`

use option `Resume playback` to control display of progress info in VideoDetailFragment, remove this (extra) function from option `Positions in lists`.
remove extra check for live streams, live streams updates just as non-live streams.

fix behavior for displaying progress bar when autoplay off but video resume on

not to retrieve unnecessary states when position in lists disabled

fix mistake in code

simplify conditional logic

update doc comment and remove unused method

Fix not showing duration if position indicators disabled

Positions in lists only depends on watch history
2023-02-07 09:48:18 +01:00
Jared Fantaye
5d3955854e Fixed the merge conflict 2023-02-05 21:21:02 +01:00
Jared234
3ff4b713e8
Merge branch 'dev' into 9458_faulty_playlist_thumbnail_update 2023-02-05 20:45:44 +01:00
Jared Fantaye
68097568d5 Fixed the bug by replacing the thumbnail_url with the thumbnail_stream_id 2023-02-05 20:32:34 +01:00
Jared Fantaye
cd8d57040c Implemented the feature using multiple checkboxes 2023-02-04 18:48:27 +01:00
Jared Fantaye
9c82441c19 Implemented the feature and fixed some small issues 2023-02-01 23:10:31 +01:00
ge78fug
38db0cc713 Changed the color 2023-01-31 16:07:57 +01:00
Marius Wagner
ee217eb9b7
Merge branch 'TeamNewPipe:dev' into tabs_on_bottom 2023-01-31 13:37:47 +01:00
Jared Fantaye
3d36eb5baf Fixed a small commit mistake 2023-01-30 22:39:16 +01:00
Jared Fantaye
d2d324f2dd First draft of the new feature 2023-01-30 22:37:24 +01:00
Marius Wagner
2b37721a6e
Update app/src/main/res/values/strings.xml
Co-authored-by: Stypox <stypox@pm.me>
2023-01-29 19:37:46 +01:00
Stypox
ca421c28a1
Merge pull request #9538 from Jared234/4186_warning_duplicates_in_playlist
Handle duplicate streams in the "Add to playlist" dialog
2023-01-29 10:36:31 +01:00
Stypox
711345eff7
Improve playlist duplicate indicator layout 2023-01-29 10:32:44 +01:00
Stypox
102975aeb3
Improve handling playlist duplicate indicator 2023-01-29 10:32:32 +01:00
Jared Fantaye
c70ce791db Added the duplicate indicator explanation & removed some unnecessary functions 2023-01-27 15:37:33 +01:00
ge78fug
0821f6463a Added bottom main-tabs feature 2023-01-25 19:25:57 +01:00
Tobi
444ac5fe95
Merge pull request #9709 from Stypox/reproducible-build
Fix reproducible builds
2023-01-24 22:05:22 +01:00
Stypox
a69f74f51b
Add snippet to ensure baseline.profm file is sorted
Thanks to obfusk, see https://issuetracker.google.com/issues/231837768 and #6486
2023-01-20 18:39:16 +01:00
Jared Fantaye
e26c038565 Made some small adjustments 2023-01-20 11:55:50 +01:00
Robin
9ecd5dff09 Orientation is locked if there is no sensor for it 2023-01-16 13:56:45 +01:00
Stypox
ef4a6238c8
See if playlists already contain a stream from db 2023-01-14 18:01:48 +01:00
Jared Fantaye
b3554a6a49
Added the number of duplicates to the toast text. 2023-01-14 18:01:48 +01:00
Jared Fantaye
5fb7b3266b
Removed the duplicate dialog and added another toast option 2023-01-14 18:01:48 +01:00
Jared Fantaye
8b6e110635
Fixed the functionality, improved performance & general code cleanup 2023-01-14 18:01:47 +01:00
Jared Fantaye
f5a1f915be
Continued working on a way to show that items are already in a playlist 2023-01-14 18:01:47 +01:00
Jared Fantaye
ac15339911
Started working on a way to show that items are already in a playlist 2023-01-14 18:01:47 +01:00
Jared Fantaye
fdfeac081a
Implemented a warning before adding duplicate to playlist. 2023-01-14 18:01:46 +01:00
Jared Fantaye
135fc08212 Implemented the "remove duplicates" feature. 2023-01-13 21:35:22 +01:00
Jared Fantaye
eb3363d4dd Created the first draft. 2023-01-10 20:55:18 +01:00
seojun0924
16b0df69b1
Update README.ko.md
Updated Korean README with latest version of README
2022-11-19 01:03:18 +09:00
GGAutomaton
8ad7bf60d7 Delete saveImmediate warnings & add comments 2022-06-23 23:31:56 +08:00
GGAutomaton
898a936064 Update index modification logic & redo sorting in the merge algorithm 2022-06-23 23:19:59 +08:00
GGAutomaton
4e401bc059 Update playlists in parallel 2022-06-23 20:36:21 +08:00
GGAutomaton
9ecef6f011 Add abstract methods in PlaylistLocalItem & rename setIsModified 2022-06-23 19:20:16 +08:00
GGAutomaton
ba394a7ab4 Update test and Javadoc 2022-05-11 18:08:14 +08:00
GGAutomaton
d32490a4be Create sub-package and default interval for DebounceSaver & sort playlists in db 2022-05-11 16:47:34 +08:00
GGAutomaton
6526ff1612 Add tests 2022-04-17 20:20:20 +08:00
GGAutomaton
bb5390d63a Reuse DebounceSaver 2022-04-17 14:53:02 +08:00
GGAutomaton
bd1aae8d66 Fix sonar warning 2022-04-16 12:44:24 +08:00
GGAutomaton
c24aed054f Fix sonar warning and typo 2022-04-16 12:00:02 +08:00
GGAutomaton
0aa08a5e40 Use new item holder 2022-04-15 23:19:24 +08:00
GGAutomaton
3c48825699 Debounced saver & bugfix & clean code 2022-04-15 20:44:54 +08:00
GGAutomaton
bfb56b4144 UI design and behavior 2022-04-14 16:59:52 +08:00
GGAutomaton
ba8370bcfd Save changes to the database and bugfix 2022-04-14 12:13:42 +08:00
GGAutomaton
813f55152a
Merge branch 'TeamNewPipe:dev' into feature-7870 2022-04-13 22:48:26 +08:00
GGAutomaton
270a541a7c Implement algorithm to merge playlists 2022-04-13 22:46:24 +08:00
GGAutomaton
c34549a47d Update database migrations and getter/setter 2022-04-13 21:35:38 +08:00
GGAutomaton
96d6b309ec Migrate database 2022-04-13 19:41:07 +08:00
993 changed files with 32375 additions and 9443 deletions

View File

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

View File

@ -1,11 +1,8 @@
name: Question
description: Ask about anything NewPipe-related
labels: [question, needs triage]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thanks for taking the time to fill out this issue! :hugs: Thanks for taking the time to fill out this form! :hugs:
Note that you can also ask questions on our [IRC channel](https://web.libera.chat/#newpipe). Note that you can also ask questions on our [IRC channel](https://web.libera.chat/#newpipe).
@ -14,7 +11,7 @@ body:
attributes: attributes:
label: "Checklist" label: "Checklist"
options: 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." - label: "I made sure that there are *no existing issues or discussions* - [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 required: true
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my question isn't listed." - label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my question isn't listed."
required: true required: true

View File

@ -14,7 +14,7 @@ body:
attributes: attributes:
label: "Checklist" label: "Checklist"
options: options:
- label: "I am able to reproduce the bug with the [latest version](https://github.com/TeamNewPipe/NewPipe/releases/latest)." - label: "I am able to reproduce the bug with the latest version given here: [CLICK THIS LINK](https://github.com/TeamNewPipe/NewPipe/releases/latest)."
required: true 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." - 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 required: true

View File

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

View File

@ -28,7 +28,7 @@
#### 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".) --> <!-- 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.--> <!-- 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. 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. You can find more info and a video demonstration [on this wiki page](https://github.com/TeamNewPipe/NewPipe/wiki/Download-APK-for-PR).
#### Due diligence #### Due diligence
- [ ] I read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md). - [ ] I read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md).

17
.github/changed-lines-count-labeler.yml vendored Normal file
View File

@ -0,0 +1,17 @@
# Add 'size/small' label to any changes with less than 50 lines
size/small:
max: 49
# Add 'size/medium' label to any changes between 50 and 249 lines
size/medium:
min: 50
max: 249
# Add 'size/large' label to any changes between 250 and 749 lines
size/large:
min: 250
max: 749
# Add 'size/giant' label to any changes for more than 749 lines
size/giant:
min: 750

View File

@ -6,6 +6,7 @@ on:
branches: branches:
- dev - dev
- master - master
- refactor
- release** - release**
paths-ignore: paths-ignore:
- 'README.md' - 'README.md'
@ -36,18 +37,20 @@ jobs:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v1 - uses: gradle/wrapper-validation-action@v2
- name: create and checkout branch - name: create and checkout branch
# push events already checked out the branch # push events already checked out the branch
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
run: git checkout -B ${{ github.head_ref }} env:
BRANCH: ${{ github.head_ref }}
run: git checkout -B "$BRANCH"
- name: set up JDK 11 - name: set up JDK
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
java-version: 11 java-version: 21
distribution: "temurin" distribution: "temurin"
cache: 'gradle' cache: 'gradle'
@ -55,30 +58,40 @@ jobs:
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: app name: app
path: app/build/outputs/apk/debug/*.apk path: app/build/outputs/apk/debug/*.apk
test-android: test-android:
# macos has hardware acceleration. See android-emulator-runner action runs-on: ubuntu-latest
runs-on: macos-latest
timeout-minutes: 20 timeout-minutes: 20
strategy: strategy:
matrix: matrix:
# api-level 19 is min sdk, but throws errors related to desugaring include:
api-level: [ 21, 29 ] - api-level: 21
target: default
arch: x86
- api-level: 33
target: google_apis # emulator API 33 only exists with Google APIs
arch: x86_64
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: set up JDK 11 - name: Enable KVM
uses: actions/setup-java@v3 run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: set up JDK
uses: actions/setup-java@v4
with: with:
java-version: 11 java-version: 21
distribution: "temurin" distribution: "temurin"
cache: 'gradle' cache: 'gradle'
@ -86,12 +99,12 @@ jobs:
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2
with: with:
api-level: ${{ matrix.api-level }} api-level: ${{ matrix.api-level }}
# workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160 target: ${{ matrix.target }}
emulator-build: 7425822 arch: ${{ matrix.arch }}
script: ./gradlew connectedCheck --stacktrace script: ./gradlew connectedCheck --stacktrace
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553 - name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: android-test-report-api${{ matrix.api-level }} name: android-test-report-api${{ matrix.api-level }}
@ -104,19 +117,19 @@ jobs:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up JDK 11 - name: Set up JDK
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
java-version: 11 # Sonar requires JDK 11 java-version: 21
distribution: "temurin" distribution: "temurin"
cache: 'gradle' cache: 'gradle'
- name: Cache SonarCloud packages - name: Cache SonarCloud packages
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.sonar/cache path: ~/.sonar/cache
key: ${{ runner.os }}-sonar key: ${{ runner.os }}-sonar

View File

@ -17,6 +17,8 @@ module.exports = async ({github, context}) => {
initialBody = context.payload.comment.body; initialBody = context.payload.comment.body;
} else if (context.eventName == 'issues') { } else if (context.eventName == 'issues') {
initialBody = context.payload.issue.body; initialBody = context.payload.issue.body;
} else if (context.eventName == 'pull_request') {
initialBody = context.payload.pull_request.body;
} else { } else {
console.log('Aborting: No body found'); console.log('Aborting: No body found');
return; return;
@ -30,10 +32,12 @@ module.exports = async ({github, context}) => {
} }
// Regex for finding images (simple variant) ![ALT_TEXT](https://*.githubusercontent.com/<number>/<variousHexStringsAnd->.<fileExtension>) // Regex for finding images (simple variant) ![ALT_TEXT](https://*.githubusercontent.com/<number>/<variousHexStringsAnd->.<fileExtension>)
const REGEX_IMAGE_LOOKUP = /\!\[(.*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm; const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[(.*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
const REGEX_ASSETS_IMAGE_LOCKUP = /\!\[(.*)\]\((https:\/\/github\.com\/[-\w\d]+\/[-\w\d]+\/assets\/\d+\/[\-0-9a-f]{32,512})\)/gm;
// Check if we found something // Check if we found something
let foundSimpleImages = REGEX_IMAGE_LOOKUP.test(initialBody); let foundSimpleImages = REGEX_USER_CONTENT_IMAGE_LOOKUP.test(initialBody)
|| REGEX_ASSETS_IMAGE_LOCKUP.test(initialBody);
if (!foundSimpleImages) { if (!foundSimpleImages) {
console.log('Found no simple images to process'); console.log('Found no simple images to process');
return; return;
@ -47,53 +51,8 @@ module.exports = async ({github, context}) => {
var wasMatchModified = false; var wasMatchModified = false;
// Try to find and replace the images with minimized ones // Try to find and replace the images with minimized ones
let newBody = await replaceAsync(initialBody, REGEX_IMAGE_LOOKUP, async (match, g1, g2) => { let newBody = await replaceAsync(initialBody, REGEX_USER_CONTENT_IMAGE_LOOKUP, minimizeAsync);
console.log(`Found match '${match}'`); newBody = await replaceAsync(newBody, REGEX_ASSETS_IMAGE_LOCKUP, minimizeAsync);
if (g1.endsWith(IGNORE_ALT_NAME_END)) {
console.log(`Ignoring match '${match}': IGNORE_ALT_NAME_END`);
return match;
}
let probeAspectRatio = 0;
let shouldModify = false;
try {
console.log(`Probing ${g2}`);
let probeResult = await probe(g2);
if (probeResult == null) {
throw 'No probeResult';
}
if (probeResult.hUnits != 'px') {
throw `Unexpected probeResult.hUnits (expected px but got ${probeResult.hUnits})`;
}
if (probeResult.height <= 0) {
throw `Unexpected probeResult.height (height is invalid: ${probeResult.height})`;
}
if (probeResult.wUnits != 'px') {
throw `Unexpected probeResult.wUnits (expected px but got ${probeResult.wUnits})`;
}
if (probeResult.width <= 0) {
throw `Unexpected probeResult.width (width is invalid: ${probeResult.width})`;
}
console.log(`Probing resulted in ${probeResult.width}x${probeResult.height}px`);
probeAspectRatio = probeResult.width / probeResult.height;
shouldModify = probeResult.height > IMG_MAX_HEIGHT_PX && probeAspectRatio < MIN_ASPECT_RATIO;
} catch(e) {
console.log('Probing failed:', e);
// Immediately abort
return match;
}
if (shouldModify) {
wasMatchModified = true;
console.log(`Modifying match '${match}'`);
return `<img alt="${g1}" src="${g2}" width=${Math.min(600, (IMG_MAX_HEIGHT_PX * probeAspectRatio).toFixed(0))} />`;
}
console.log(`Match '${match}' is ok/will not be modified`);
return match;
});
if (!wasMatchModified) { if (!wasMatchModified) {
console.log('Nothing was modified. Skipping update'); console.log('Nothing was modified. Skipping update');
@ -117,9 +76,17 @@ module.exports = async ({github, context}) => {
repo: context.repo.repo, repo: context.repo.repo,
body: newBody body: newBody
}); });
} else if (context.eventName == 'pull_request') {
console.log('Updating pull request', context.payload.pull_request.number);
await github.rest.pulls.update({
pull_number: context.payload.pull_request.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: newBody
});
} }
// Asnyc replace function from https://stackoverflow.com/a/48032528 // Async replace function from https://stackoverflow.com/a/48032528
async function replaceAsync(str, regex, asyncFn) { async function replaceAsync(str, regex, asyncFn) {
const promises = []; const promises = [];
str.replace(regex, (match, ...args) => { str.replace(regex, (match, ...args) => {
@ -129,4 +96,52 @@ module.exports = async ({github, context}) => {
const data = await Promise.all(promises); const data = await Promise.all(promises);
return str.replace(regex, () => data.shift()); return str.replace(regex, () => data.shift());
} }
async function minimizeAsync(match, g1, g2) {
console.log(`Found match '${match}'`);
if (g1.endsWith(IGNORE_ALT_NAME_END)) {
console.log(`Ignoring match '${match}': IGNORE_ALT_NAME_END`);
return match;
}
let probeAspectRatio = 0;
let shouldModify = false;
try {
console.log(`Probing ${g2}`);
let probeResult = await probe(g2);
if (probeResult == null) {
throw 'No probeResult';
}
if (probeResult.hUnits != 'px') {
throw `Unexpected probeResult.hUnits (expected px but got ${probeResult.hUnits})`;
}
if (probeResult.height <= 0) {
throw `Unexpected probeResult.height (height is invalid: ${probeResult.height})`;
}
if (probeResult.wUnits != 'px') {
throw `Unexpected probeResult.wUnits (expected px but got ${probeResult.wUnits})`;
}
if (probeResult.width <= 0) {
throw `Unexpected probeResult.width (width is invalid: ${probeResult.width})`;
}
console.log(`Probing resulted in ${probeResult.width}x${probeResult.height}px`);
probeAspectRatio = probeResult.width / probeResult.height;
shouldModify = probeResult.height > IMG_MAX_HEIGHT_PX && probeAspectRatio < MIN_ASPECT_RATIO;
} catch(e) {
console.log('Probing failed:', e);
// Immediately abort
return match;
}
if (shouldModify) {
wasMatchModified = true;
console.log(`Modifying match '${match}'`);
return `<img alt="${g1}" src="${g2}" width=${Math.min(600, Math.floor(IMG_MAX_HEIGHT_PX * probeAspectRatio))} />`;
}
console.log(`Match '${match}' is ok/will not be modified`);
return match;
}
} }

View File

@ -5,6 +5,8 @@ on:
types: [created, edited] types: [created, edited]
issues: issues:
types: [opened, edited] types: [opened, edited]
pull_request:
types: [opened, edited]
permissions: permissions:
issues: write issues: write
@ -15,9 +17,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
node-version: 16 node-version: 16
@ -25,7 +27,7 @@ jobs:
run: npm i probe-image-size@7.2.3 --ignore-scripts run: npm i probe-image-size@7.2.3 --ignore-scripts
- name: Minimize simple images - name: Minimize simple images
uses: actions/github-script@v6 uses: actions/github-script@v7
timeout-minutes: 3 timeout-minutes: 3
with: with:
script: | script: |

18
.github/workflows/pr-labeler.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: "PR size labeler"
on: [pull_request_target]
permissions:
contents: read
pull-requests: write
jobs:
changed-lines-count-labeler:
runs-on: ubuntu-latest
name: Automatically labelling pull requests based on the changed lines count
permissions:
pull-requests: write
steps:
- name: Set a label
uses: TeamNewPipe/changed-lines-count-labeler@main
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/changed-lines-count-labeler.yml

21
.idea/icon.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 850 B

View File

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

View File

@ -8,11 +8,11 @@ plugins {
id "kotlin-kapt" id "kotlin-kapt"
id "kotlin-parcelize" id "kotlin-parcelize"
id "checkstyle" id "checkstyle"
id "org.sonarqube" version "3.5.0.2730" id "org.sonarqube" version "4.0.0.2929"
} }
android { android {
compileSdk 33 compileSdk 34
namespace 'org.schabi.newpipe' namespace 'org.schabi.newpipe'
defaultConfig { defaultConfig {
@ -20,8 +20,15 @@ android {
resValue "string", "app_name", "NewPipe" resValue "string", "app_name", "NewPipe"
minSdk 21 minSdk 21
targetSdk 33 targetSdk 33
versionCode 992 if (System.properties.containsKey('versionCodeOverride')) {
versionName "0.25.0" versionCode System.getProperty('versionCodeOverride') as Integer
} else {
versionCode 999
}
versionName "0.27.2"
if (System.properties.containsKey('versionNameSuffix')) {
versionNameSuffix System.getProperty('versionNameSuffix')
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -50,9 +57,6 @@ android {
} }
} }
// Keep the release build type at the end of the list to override 'archivesBaseName' of
// debug build. This seems to be a Gradle bug, therefore
// TODO: update Gradle version
release { release {
if (System.properties.containsKey('packageSuffix')) { if (System.properties.containsKey('packageSuffix')) {
applicationIdSuffix System.getProperty('packageSuffix') applicationIdSuffix System.getProperty('packageSuffix')
@ -80,13 +84,13 @@ android {
// Flag to enable support for the new language APIs // Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_17
encoding 'utf-8' encoding 'utf-8'
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_11 jvmTarget = JavaVersion.VERSION_17
} }
sourceSets { sourceSets {
@ -95,26 +99,35 @@ android {
buildFeatures { buildFeatures {
viewBinding true viewBinding true
buildConfig true
}
packagingOptions {
resources {
// remove two files which belong to jsoup
// no idea how they ended up in the META-INF dir...
excludes += ['META-INF/README.md', 'META-INF/CHANGES',
// 'COPYRIGHT' belongs to RxJava...
'META-INF/COPYRIGHT']
}
} }
} }
ext { ext {
checkstyleVersion = '10.3.1' checkstyleVersion = '10.12.1'
androidxLifecycleVersion = '2.5.1' androidxLifecycleVersion = '2.6.2'
androidxRoomVersion = '2.4.3' androidxRoomVersion = '2.6.1'
androidxWorkVersion = '2.7.1' androidxWorkVersion = '2.8.1'
icepickVersion = '3.2.0' stateSaverVersion = '1.4.1'
exoPlayerVersion = '2.18.1' exoPlayerVersion = '2.18.7'
googleAutoServiceVersion = '1.0.1' googleAutoServiceVersion = '1.1.1'
groupieVersion = '2.10.1' groupieVersion = '2.10.1'
markwonVersion = '4.6.2' markwonVersion = '4.6.2'
leakCanaryVersion = '2.9.1' leakCanaryVersion = '2.12'
stethoVersion = '1.6.0' stethoVersion = '1.6.0'
mockitoVersion = '4.0.0'
assertJVersion = '3.23.1'
} }
configurations { configurations {
@ -129,7 +142,7 @@ checkstyle {
toolVersion = checkstyleVersion toolVersion = checkstyleVersion
} }
task runCheckstyle(type: Checkstyle) { tasks.register('runCheckstyle', Checkstyle) {
source 'src' source 'src'
include '**/*.java' include '**/*.java'
exclude '**/gen/**' exclude '**/gen/**'
@ -150,20 +163,22 @@ task runCheckstyle(type: Checkstyle) {
def outputDir = "${project.buildDir}/reports/ktlint/" def outputDir = "${project.buildDir}/reports/ktlint/"
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt") def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
task runKtlint(type: JavaExec) { tasks.register('runKtlint', JavaExec) {
inputs.files(inputFiles) inputs.files(inputFiles)
outputs.dir(outputDir) outputs.dir(outputDir)
getMainClass().set("com.pinterest.ktlint.Main") getMainClass().set("com.pinterest.ktlint.Main")
classpath = configurations.ktlint classpath = configurations.ktlint
args "src/**/*.kt" args "src/**/*.kt"
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
} }
task formatKtlint(type: JavaExec) { tasks.register('formatKtlint', JavaExec) {
inputs.files(inputFiles) inputs.files(inputFiles)
outputs.dir(outputDir) outputs.dir(outputDir)
getMainClass().set("com.pinterest.ktlint.Main") getMainClass().set("com.pinterest.ktlint.Main")
classpath = configurations.ktlint classpath = configurations.ktlint
args "-F", "src/**/*.kt" args "-F", "src/**/*.kt"
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
} }
afterEvaluate { afterEvaluate {
@ -183,7 +198,7 @@ sonar {
dependencies { dependencies {
/** Desugaring **/ /** Desugaring **/
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
/** NewPipe libraries **/ /** NewPipe libraries **/
// You can use a local version by uncommenting a few lines in settings.gradle // You can use a local version by uncommenting a few lines in settings.gradle
@ -191,7 +206,8 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test // name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/ // This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:7e793c11aec46358ccbfd8bcfcf521105f4f093a' // WORKAROUND: v0.24.2 can't be resolved by jitpack -> use git commit hash instead
implementation 'com.github.TeamNewPipe:NewPipeExtractor:176da72cb4c3ec4679211339b0e59f6b01bf2f52'
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
/** Checkstyle **/ /** Checkstyle **/
@ -199,42 +215,43 @@ dependencies {
ktlint 'com.pinterest:ktlint:0.45.2' ktlint 'com.pinterest:ktlint:0.45.2'
/** Kotlin **/ /** Kotlin **/
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}" implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
/** AndroidX **/ /** AndroidX **/
implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.fragment:fragment-ktx:1.4.1' implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
implementation 'androidx.media:media:1.6.0' implementation 'androidx.media:media:1.7.0'
implementation 'androidx.preference:preference:1.2.0' implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation "androidx.room:room-runtime:${androidxRoomVersion}" implementation "androidx.room:room-runtime:${androidxRoomVersion}"
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}" implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
kapt "androidx.room:room-compiler:${androidxRoomVersion}" kapt "androidx.room:room-compiler:${androidxRoomVersion}"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
// Newer version specified to prevent accessibility regressions with RecyclerView, see: // Newer version specified to prevent accessibility regressions with RecyclerView, see:
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01 // https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}" implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}" implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
implementation 'com.google.android.material:material:1.6.1' implementation 'com.google.android.material:material:1.11.0'
/** Third-party libraries **/ /** Third-party libraries **/
// Instance state boilerplate elimination // Instance state boilerplate elimination
implementation "frankiesardo:icepick:${icepickVersion}" implementation 'com.github.livefront:bridge:v2.0.2'
kapt "frankiesardo:icepick-processor:${icepickVersion}" implementation "com.evernote:android-state:$stateSaverVersion"
kapt "com.evernote:android-state-processor:$stateSaverVersion"
// HTML parser // HTML parser
implementation "org.jsoup:jsoup:1.15.3" implementation "org.jsoup:jsoup:1.17.2"
// HTTP client // HTTP client
implementation "com.squareup.okhttp3:okhttp:4.10.0" implementation "com.squareup.okhttp3:okhttp:4.12.0"
// Media player // Media player
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}" implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
@ -263,38 +280,37 @@ dependencies {
implementation "io.noties.markwon:linkify:${markwonVersion}" implementation "io.noties.markwon:linkify:${markwonVersion}"
// Crash reporting // Crash reporting
implementation "ch.acra:acra-core:5.9.7" implementation "ch.acra:acra-core:5.11.3"
// Properly restarting // Properly restarting
implementation 'com.jakewharton:process-phoenix:2.1.2' implementation 'com.jakewharton:process-phoenix:2.1.2'
// Reactive extensions for Java VM // Reactive extensions for Java VM
implementation "io.reactivex.rxjava3:rxjava:3.1.5" implementation "io.reactivex.rxjava3:rxjava:3.1.8"
implementation "io.reactivex.rxjava3:rxandroid:3.0.2" implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
// RxJava binding APIs for Android UI widgets // RxJava binding APIs for Android UI widgets
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0" implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
// Date and time formatting // Date and time formatting
implementation "org.ocpsoft.prettytime:prettytime:5.0.6.Final" implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final"
/** Debugging **/ /** Debugging **/
// Memory leak detection // Memory leak detection
implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}" debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
implementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}" debugImplementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}" debugImplementation "com.squareup.leakcanary:leakcanary-android-core:${leakCanaryVersion}"
// Debug bridge for Android // Debug bridge for Android
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}" debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}" debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
/** Testing **/ /** Testing **/
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation "org.mockito:mockito-core:${mockitoVersion}" testImplementation 'org.mockito:mockito-core:5.6.0'
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
androidTestImplementation "androidx.test.ext:junit:1.1.3" androidTestImplementation "androidx.test.ext:junit:1.1.5"
androidTestImplementation "androidx.test:runner:1.4.0" androidTestImplementation "androidx.test:runner:1.5.2"
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}" androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
androidTestImplementation "org.assertj:assertj-core:${assertJVersion}" androidTestImplementation "org.assertj:assertj-core:3.24.2"
} }
static String getGitWorkingBranch() { static String getGitWorkingBranch() {
@ -313,6 +329,7 @@ static String getGitWorkingBranch() {
} }
} }
// fix reproducible builds
project.afterEvaluate { project.afterEvaluate {
tasks.compileReleaseArtProfile.doLast { tasks.compileReleaseArtProfile.doLast {
outputs.files.each { file -> outputs.files.each { file ->

View File

@ -1,45 +1,23 @@
# Add project specific ProGuard rules here. # https://developer.android.com/build/shrink-code
# By default, the flags in this file are appended to flags specified
# in /home/the-scrabi/bin/Android/Sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
## Helps debug release versions
-dontobfuscate -dontobfuscate
## Rules for NewPipeExtractor
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; } -keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
-keep class org.mozilla.javascript.** { *; } -keep class org.mozilla.javascript.** { *; }
-keep class org.mozilla.classfile.ClassFileWriter -keep class org.mozilla.classfile.ClassFileWriter
-keep class com.google.android.exoplayer2.** { *; } -dontwarn org.mozilla.javascript.JavaToJSONConverters
-dontwarn org.mozilla.javascript.tools.** -dontwarn org.mozilla.javascript.tools.**
# Rules for icepick. Copy paste from https://github.com/frankiesardo/icepick ## Rules for ExoPlayer
-dontwarn icepick.** -keep class com.google.android.exoplayer2.** { *; }
-keep class icepick.** { *; }
-keep class **$$Icepick { *; }
-keepclasseswithmembernames class * {
@icepick.* <fields>;
}
-keepnames class * { @icepick.State *;}
## Rules for OkHttp. Copy paste from https://github.com/square/okhttp ## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp
-dontwarn okhttp3.** -dontwarn okhttp3.**
-dontwarn okio.** -dontwarn okio.**
##
## See https://github.com/TeamNewPipe/NewPipe/pull/1441
-keepclassmembers class * implements java.io.Serializable { -keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID; static final long serialVersionUID;
!static !transient <fields>; !static !transient <fields>;
@ -47,5 +25,5 @@
private void readObject(java.io.ObjectInputStream); private void readObject(java.io.ObjectInputStream);
} }
# for some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml) ## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
-keep class org.schabi.newpipe.settings.notifications.** { *; } -keep class org.schabi.newpipe.settings.notifications.** { *; }

View File

@ -0,0 +1,19 @@
{
"data": [
{
"name": "BBC",
"additional": "12K subscribers•233 videos",
"description": "The BBC is the worlds leading public service broadcaster. Were impartial and independent, and every day we create distinctive, world-class programmes and content which inform, educate and entertain millions of people in the UK and around the world. SUBSCRIBE to our YouTube channel to get the best of BBC entertainment and comedy programmes, stories from science and nature documentaries, and much more! https://bit.ly/2IXqEIn Get ALL your fresh TV, and sofa-hugging box sets on iPlayer https://bbc.in/2J18jYJ"
},
{
"name": "Linus Tech Tips",
"additional": "1M subscribers•233 videos",
"description": "Looking for a Tech YouTuber?\n\nLinus Tech Tips is a passionate team of \"professionally curious\" experts in consumer technology and video production which aims to inform and educate people of all ages through our entertaining videos. We create product reviews, step-by-step computer build guides, and a variety of other tech-focused content.\n\nSchedule:\nNew videos every Saturday to Thursday @ 10:00am Pacific\nLive WAN Show podcasts every Friday @ ~5:00pm Pacific"
},
{
"name": "Marques Brownlee",
"additional": "13 subscribers•12K videos",
"description": "MKBHD: Quality Tech Videos | YouTuber | Geek | Consumer Electronics | Tech Head | Internet Personality!\n\nbusiness@MKBHD.com\n\nNYC"
}
]
}

View File

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

View File

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

View File

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

View File

@ -4,15 +4,18 @@ import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import androidx.room.Room import androidx.room.Room
import androidx.room.testing.MigrationTestHelper import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@ -21,20 +24,23 @@ class DatabaseMigrationTest {
private const val DEFAULT_SERVICE_ID = 0 private const val DEFAULT_SERVICE_ID = 0
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4" private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
private const val DEFAULT_TITLE = "Test Title" private const val DEFAULT_TITLE = "Test Title"
private const val DEFAULT_NAME = "Test Name"
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
private const val DEFAULT_DURATION = 480L private const val DEFAULT_DURATION = 480L
private const val DEFAULT_UPLOADER_NAME = "Uploader Test" private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg" private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
private const val DEFAULT_SECOND_SERVICE_ID = 0 private const val DEFAULT_SECOND_SERVICE_ID = 1
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc" private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
private const val DEFAULT_THIRD_SERVICE_ID = 2
private const val DEFAULT_THIRD_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
} }
@get:Rule @get:Rule
val testHelper = MigrationTestHelper( val testHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(), InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName, AppDatabase::class.java
FrameworkSQLiteOpenHelperFactory()
) )
@Test @Test
@ -101,6 +107,27 @@ class DatabaseMigrationTest {
Migrations.MIGRATION_5_6 Migrations.MIGRATION_5_6
) )
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_7,
true,
Migrations.MIGRATION_6_7
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_8,
true,
Migrations.MIGRATION_7_8
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
)
val migratedDatabaseV3 = getMigratedDatabase() val migratedDatabaseV3 = getMigratedDatabase()
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst() val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
@ -135,6 +162,157 @@ class DatabaseMigrationTest {
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation) assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
} }
@Test
fun migrateDatabaseFrom7to8() {
val databaseInV7 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_7)
val defaultSearch1 = " abc "
val defaultSearch2 = " abc"
val serviceId = DEFAULT_SERVICE_ID // YouTube
// Use id different to YouTube because two searches with the same query
// but different service are considered not equal.
val otherServiceId = ServiceList.SoundCloud.serviceId
databaseInV7.run {
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", serviceId)
put("search", defaultSearch1)
}
)
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", serviceId)
put("search", defaultSearch2)
}
)
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", otherServiceId)
put("search", defaultSearch1)
}
)
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", otherServiceId)
put("search", defaultSearch2)
}
)
close()
}
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_8,
true, Migrations.MIGRATION_7_8
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_9,
true, Migrations.MIGRATION_8_9
)
val migratedDatabaseV8 = getMigratedDatabase()
val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
assertEquals(2, listFromDB.size)
assertEquals("abc", listFromDB[0].search)
assertEquals("abc", listFromDB[1].search)
assertNotEquals(listFromDB[0].serviceId, listFromDB[1].serviceId)
}
@Test
fun migrateDatabaseFrom8to9() {
val databaseInV8 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_8)
val localUid1: Long
val localUid2: Long
val remoteUid1: Long
val remoteUid2: Long
databaseInV8.run {
localUid1 = insert(
"playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("name", DEFAULT_NAME + "1")
put("is_thumbnail_permanent", false)
put("thumbnail_stream_id", -1)
}
)
localUid2 = insert(
"playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("name", DEFAULT_NAME + "2")
put("is_thumbnail_permanent", false)
put("thumbnail_stream_id", -1)
}
)
delete(
"playlists", "uid = ?",
Array(1) { localUid1 }
)
remoteUid1 = insert(
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SERVICE_ID)
put("url", DEFAULT_URL)
}
)
remoteUid2 = insert(
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SECOND_SERVICE_ID)
put("url", DEFAULT_SECOND_URL)
}
)
delete(
"remote_playlists", "uid = ?",
Array(1) { remoteUid2 }
)
close()
}
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
)
val migratedDatabaseV9 = getMigratedDatabase()
var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
assertEquals(1, localListFromDB.size)
assertEquals(localUid2, localListFromDB[0].uid)
assertEquals(-1, localListFromDB[0].displayIndex)
assertEquals(1, remoteListFromDB.size)
assertEquals(remoteUid1, remoteListFromDB[0].uid)
assertEquals(-1, remoteListFromDB[0].displayIndex)
val localUid3 = migratedDatabaseV9.playlistDAO().insert(
PlaylistEntity(DEFAULT_NAME + "3", false, -1, -1)
)
val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert(
PlaylistRemoteEntity(
DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL,
DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10
)
)
localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
assertEquals(2, localListFromDB.size)
assertEquals(localUid3, localListFromDB[1].uid)
assertEquals(-1, localListFromDB[1].displayIndex)
assertEquals(2, remoteListFromDB.size)
assertEquals(remoteUid3, remoteListFromDB[1].uid)
assertEquals(-1, remoteListFromDB[1].displayIndex)
}
private fun getMigratedDatabase(): AppDatabase { private fun getMigratedDatabase(): AppDatabase {
val database: AppDatabase = Room.databaseBuilder( val database: AppDatabase = Room.databaseBuilder(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext(),

View File

@ -0,0 +1,130 @@
package org.schabi.newpipe.database
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import io.reactivex.rxjava3.core.Single
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import org.schabi.newpipe.database.feed.dao.FeedDAO
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.dao.StreamDAO
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.subscription.SubscriptionDAO
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.stream.StreamType
import java.io.IOException
import java.time.OffsetDateTime
import kotlin.streams.toList
class FeedDAOTest {
private lateinit var db: AppDatabase
private lateinit var feedDAO: FeedDAO
private lateinit var streamDAO: StreamDAO
private lateinit var subscriptionDAO: SubscriptionDAO
private val serviceId = ServiceList.YouTube.serviceId
private val stream1 = StreamEntity(1, serviceId, "https://youtube.com/watch?v=1", "stream 1", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-01", OffsetDateTime.parse("2023-01-01T00:00:00Z"))
private val stream2 = StreamEntity(2, serviceId, "https://youtube.com/watch?v=2", "stream 2", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-02", OffsetDateTime.parse("2023-01-02T00:00:00Z"))
private val stream3 = StreamEntity(3, serviceId, "https://youtube.com/watch?v=3", "stream 3", StreamType.LIVE_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-03", OffsetDateTime.parse("2023-01-03T00:00:00Z"))
private val stream4 = StreamEntity(4, serviceId, "https://youtube.com/watch?v=4", "stream 4", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
private val stream5 = StreamEntity(5, serviceId, "https://youtube.com/watch?v=5", "stream 5", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-20", OffsetDateTime.parse("2023-08-20T00:00:00Z"))
private val stream6 = StreamEntity(6, serviceId, "https://youtube.com/watch?v=6", "stream 6", StreamType.VIDEO_STREAM, 1000, "channel-3", "https://youtube.com/channel/3", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-09-01", OffsetDateTime.parse("2023-09-01T00:00:00Z"))
private val stream7 = StreamEntity(7, serviceId, "https://youtube.com/watch?v=7", "stream 7", StreamType.VIDEO_STREAM, 1000, "channel-4", "https://youtube.com/channel/4", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
private val allStreams = listOf(
stream1, stream2, stream3, stream4, stream5, stream6, stream7
)
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context, AppDatabase::class.java
).build()
feedDAO = db.feedDAO()
streamDAO = db.streamDAO()
subscriptionDAO = db.subscriptionDAO()
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
@Test
fun testUnlinkStreamsOlderThan_KeepOne() {
setupUnlinkDelete("2023-08-15T00:00:00Z")
val streams = feedDAO.getStreams(
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
)
.blockingGet()
val allowedStreams = listOf(stream3, stream5, stream6, stream7)
assertEqual(streams, allowedStreams)
}
@Test
fun testUnlinkStreamsOlderThan_KeepMultiple() {
setupUnlinkDelete("2023-08-01T00:00:00Z")
val streams = feedDAO.getStreams(
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
)
.blockingGet()
val allowedStreams = listOf(stream3, stream4, stream5, stream6, stream7)
assertEqual(streams, allowedStreams)
}
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
assertNotNull(streams)
assertEquals(
allowedStreams,
streams!!
.map { it.stream }
.sortedBy { it.uid }
.toList()
)
}
private fun setupUnlinkDelete(time: String) {
clearAndFillTables()
Single.fromCallable {
feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time))
}.blockingSubscribe()
Single.fromCallable {
streamDAO.deleteOrphans()
}.blockingSubscribe()
}
private fun clearAndFillTables() {
db.clearAllTables()
streamDAO.insertAll(allStreams)
subscriptionDAO.insertAll(
listOf(
SubscriptionEntity.from(ChannelInfo(serviceId, "1", "https://youtube.com/channel/1", "https://youtube.com/channel/1", "channel-1")),
SubscriptionEntity.from(ChannelInfo(serviceId, "2", "https://youtube.com/channel/2", "https://youtube.com/channel/2", "channel-2")),
SubscriptionEntity.from(ChannelInfo(serviceId, "3", "https://youtube.com/channel/3", "https://youtube.com/channel/3", "channel-3")),
SubscriptionEntity.from(ChannelInfo(serviceId, "4", "https://youtube.com/channel/4", "https://youtube.com/channel/4", "channel-4")),
)
)
feedDAO.insertAll(
listOf(
FeedEntity(1, 1),
FeedEntity(2, 1),
FeedEntity(3, 1),
FeedEntity(4, 2),
FeedEntity(5, 2),
FeedEntity(6, 3),
FeedEntity(7, 4),
)
)
}
}

View File

@ -0,0 +1,82 @@
package org.schabi.newpipe.local.subscription;
import static org.junit.Assert.assertEquals;
import androidx.test.core.app.ApplicationProvider;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.testUtil.TestDatabase;
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule;
import java.io.IOException;
import java.util.List;
public class SubscriptionManagerTest {
private AppDatabase database;
private SubscriptionManager manager;
@Rule
public TrampolineSchedulerRule trampolineScheduler = new TrampolineSchedulerRule();
private SubscriptionEntity getAssertOneSubscriptionEntity() {
final List<SubscriptionEntity> entities = manager
.getSubscriptions(FeedGroupEntity.GROUP_ALL_ID, "", false)
.blockingFirst();
assertEquals(1, entities.size());
return entities.get(0);
}
@Before
public void setup() {
database = TestDatabase.Companion.createReplacingNewPipeDatabase();
manager = new SubscriptionManager(ApplicationProvider.getApplicationContext());
}
@After
public void cleanUp() {
database.close();
}
@Test
public void testInsert() throws ExtractionException, IOException {
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown");
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
manager.insertSubscription(subscription);
final SubscriptionEntity readSubscription = getAssertOneSubscriptionEntity();
// the uid has changed, since the uid is chosen upon inserting, but the rest should match
assertEquals(subscription.getServiceId(), readSubscription.getServiceId());
assertEquals(subscription.getUrl(), readSubscription.getUrl());
assertEquals(subscription.getName(), readSubscription.getName());
assertEquals(subscription.getAvatarUrl(), readSubscription.getAvatarUrl());
assertEquals(subscription.getSubscriberCount(), readSubscription.getSubscriberCount());
assertEquals(subscription.getDescription(), readSubscription.getDescription());
}
@Test
public void testUpdateNotificationMode() throws ExtractionException, IOException {
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/veritasium");
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
subscription.setNotificationMode(0);
manager.insertSubscription(subscription);
manager.updateNotificationMode(subscription.getServiceId(), subscription.getUrl(), 1)
.blockingAwait();
final SubscriptionEntity anotherSubscription = getAssertOneSubscriptionEntity();
assertEquals(0, subscription.getNotificationMode());
assertEquals(subscription.getUrl(), anotherSubscription.getUrl());
assertEquals(1, anotherSubscription.getNotificationMode());
}
}

View File

@ -12,15 +12,21 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import androidx.test.internal.runner.junit4.statement.UiThreadStatement import androidx.test.internal.runner.junit4.statement.UiThreadStatement
import org.junit.Assert import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.MediaFormat import org.schabi.newpipe.extractor.MediaFormat
import org.schabi.newpipe.extractor.downloader.Response
import org.schabi.newpipe.extractor.stream.AudioStream import org.schabi.newpipe.extractor.stream.AudioStream
import org.schabi.newpipe.extractor.stream.Stream import org.schabi.newpipe.extractor.stream.Stream
import org.schabi.newpipe.extractor.stream.SubtitlesStream import org.schabi.newpipe.extractor.stream.SubtitlesStream
import org.schabi.newpipe.extractor.stream.VideoStream import org.schabi.newpipe.extractor.stream.VideoStream
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper
@MediumTest @MediumTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@ -84,7 +90,7 @@ class StreamItemAdapterTest {
@Test @Test
fun subtitleStreams_noIcon() { fun subtitleStreams_noIcon() {
val adapter = StreamItemAdapter<SubtitlesStream, Stream>( val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
StreamItemAdapter.StreamSizeWrapper( StreamItemAdapter.StreamInfoWrapper(
(0 until 5).map { (0 until 5).map {
SubtitlesStream.Builder() SubtitlesStream.Builder()
.setContent("https://example.com", true) .setContent("https://example.com", true)
@ -105,7 +111,7 @@ class StreamItemAdapterTest {
@Test @Test
fun audioStreams_noIcon() { fun audioStreams_noIcon() {
val adapter = StreamItemAdapter<AudioStream, Stream>( val adapter = StreamItemAdapter<AudioStream, Stream>(
StreamItemAdapter.StreamSizeWrapper( StreamItemAdapter.StreamInfoWrapper(
(0 until 5).map { (0 until 5).map {
AudioStream.Builder() AudioStream.Builder()
.setId(Stream.ID_UNKNOWN) .setId(Stream.ID_UNKNOWN)
@ -123,12 +129,109 @@ class StreamItemAdapterTest {
} }
} }
@Test
fun retrieveMediaFormatFromFileTypeHeaders() {
val streams = getIncompleteAudioStreams(5)
val wrapper = StreamInfoWrapper(streams, context)
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
StreamInfoWrapper.retrieveMediaFormatFromFileTypeHeaders(stream, wrapper, response)
}
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
helper.assertInvalidResponse(getResponse(mapOf(Pair("file-type", "mp0"))), 1)
helper.assertValidResponse(getResponse(mapOf(Pair("x-amz-meta-file-type", "aiff"))), 2, MediaFormat.AIFF)
helper.assertValidResponse(getResponse(mapOf(Pair("file-type", "mp3"))), 3, MediaFormat.MP3)
}
@Test
fun retrieveMediaFormatFromContentDispositionHeader() {
val streams = getIncompleteAudioStreams(11)
val wrapper = StreamInfoWrapper(streams, context)
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
StreamInfoWrapper.retrieveMediaFormatFromContentDispositionHeader(stream, wrapper, response)
}
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))), 1
)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))), 2
)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; filename=\"data.csv\""))), 3
)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"fieldName\"; filename*=\"filename.jpg\""))), 4
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.ogg\""))),
5, MediaFormat.OGG
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "some-form-data; filename=\"audio.flac\""))),
6, MediaFormat.FLAC
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.aiff\"; filename=\"audio.aiff\""))),
7, MediaFormat.AIFF
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"alien?\"; filename*=UTF-8''%CE%B1%CE%BB%CE%B9%CF%B5%CE%BD.m4a"))),
8, MediaFormat.M4A
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=UTF-8''alien.opus"))),
9, MediaFormat.OPUS
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=\"UTF-8''alien.opus\""))),
10, MediaFormat.OPUS
)
}
@Test
fun retrieveMediaFormatFromContentTypeHeader() {
val streams = getIncompleteAudioStreams(12)
val wrapper = StreamInfoWrapper(streams, context)
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
StreamInfoWrapper.retrieveMediaFormatFromContentTypeHeader(stream, wrapper, response)
}
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "984501"))), 0)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/xyz"))), 1)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 2)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 3)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/mpeg"))), 4)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/aif"))), 5)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "whatever"))), 6)
helper.assertInvalidResponse(getResponse(mapOf()), 7)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 8, MediaFormat.FLAC
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 9, MediaFormat.WAV
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 10, MediaFormat.OPUS
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 11, MediaFormat.AIFF
)
}
/** /**
* @return a list of video streams, in which their video only property mirrors the provided * @return a list of video streams, in which their video only property mirrors the provided
* [videoOnly] vararg. * [videoOnly] vararg.
*/ */
private fun getVideoStreams(vararg videoOnly: Boolean) = private fun getVideoStreams(vararg videoOnly: Boolean) =
StreamItemAdapter.StreamSizeWrapper( StreamItemAdapter.StreamInfoWrapper(
videoOnly.map { videoOnly.map {
VideoStream.Builder() VideoStream.Builder()
.setId(Stream.ID_UNKNOWN) .setId(Stream.ID_UNKNOWN)
@ -161,6 +264,19 @@ class StreamItemAdapterTest {
} }
) )
private fun getIncompleteAudioStreams(size: Int): List<AudioStream> {
val list = ArrayList<AudioStream>(size)
for (i in 1..size) {
list.add(
AudioStream.Builder()
.setId(Stream.ID_UNKNOWN)
.setContent("https://example.com/$i", true)
.build()
)
}
return list
}
/** /**
* Checks whether the item at [position] in the [spinner] has the correct icon visibility when * Checks whether the item at [position] in the [spinner] has the correct icon visibility when
* it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list). * it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list).
@ -196,11 +312,56 @@ class StreamItemAdapterTest {
streams.forEachIndexed { index, stream -> streams.forEachIndexed { index, stream ->
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let { val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
SecondaryStreamHelper( SecondaryStreamHelper(
StreamItemAdapter.StreamSizeWrapper(streams, context), StreamItemAdapter.StreamInfoWrapper(streams, context),
it it
) )
} }
put(index, secondaryStreamHelper) put(index, secondaryStreamHelper)
} }
} }
private fun getResponse(headers: Map<String, String>): Response {
val listHeaders = HashMap<String, List<String>>()
headers.forEach { entry ->
listHeaders[entry.key] = listOf(entry.value)
}
return Response(200, null, listHeaders, "", "")
}
/**
* Helper class for assertion related to extractions of [MediaFormat]s.
*/
class AssertionHelper<T : Stream>(
private val streams: List<T>,
private val wrapper: StreamInfoWrapper<T>,
private val retrieveMediaFormat: (stream: T, response: Response) -> Boolean
) {
/**
* Assert that an invalid response does not result in wrongly extracted [MediaFormat].
*/
fun assertInvalidResponse(
response: Response,
index: Int
) {
assertFalse(
"invalid header returns valid value", retrieveMediaFormat(streams[index], response)
)
assertNull("Media format extracted although stated otherwise", wrapper.getFormat(index))
}
/**
* Assert that a valid response results in correctly extracted and handled [MediaFormat].
*/
fun assertValidResponse(
response: Response,
index: Int,
format: MediaFormat
) {
assertTrue(
"header was not recognized", retrieveMediaFormat(streams[index], response)
)
assertEquals("Wrong media format extracted", format, wrapper.getFormat(index))
}
}
} }

View File

@ -3,7 +3,6 @@ package org.schabi.newpipe
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.facebook.stetho.Stetho import com.facebook.stetho.Stetho
import com.facebook.stetho.okhttp3.StethoInterceptor import com.facebook.stetho.okhttp3.StethoInterceptor
import leakcanary.AppWatcher
import leakcanary.LeakCanary import leakcanary.LeakCanary
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.schabi.newpipe.extractor.downloader.Downloader import org.schabi.newpipe.extractor.downloader.Downloader
@ -13,8 +12,6 @@ class DebugApp : App() {
super.onCreate() super.onCreate()
initStetho() initStetho()
// Give each object 10 seconds to be GC'ed, before LeakCanary gets nosy on it
AppWatcher.config = AppWatcher.config.copy(watchDurationMillis = 10000)
LeakCanary.config = LeakCanary.config.copy( LeakCanary.config = LeakCanary.config.copy(
dumpHeap = PreferenceManager dumpHeap = PreferenceManager
.getDefaultSharedPreferences(this).getBoolean( .getDefaultSharedPreferences(this).getBoolean(

View File

@ -15,7 +15,7 @@
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<data android:scheme="http|https|market" /> <data android:scheme="http" />
</intent> </intent>
</queries> </queries>
@ -357,15 +357,17 @@
<data android:host="eduvid.org" /> <data android:host="eduvid.org" />
<data android:host="framatube.org" /> <data android:host="framatube.org" />
<data android:host="media.assassinate-you.net" /> <data android:host="media.assassinate-you.net" />
<data android:host="media.fsfe.org" />
<data android:host="peertube.co.uk" /> <data android:host="peertube.co.uk" />
<data android:host="peertube.cpy.re" /> <data android:host="peertube.cpy.re" />
<data android:host="peertube.mastodon.host" />
<data android:host="peertube.fr" /> <data android:host="peertube.fr" />
<data android:host="tilvids.com" /> <data android:host="peertube.mastodon.host" />
<data android:host="video.ploud.fr" /> <data android:host="peertube.stream" />
<data android:host="video.lqdn.fr" />
<data android:host="skeptikon.fr" /> <data android:host="skeptikon.fr" />
<data android:host="media.fsfe.org" /> <data android:host="tilvids.com" />
<data android:host="video.lqdn.fr" />
<data android:host="video.ploud.fr" />
<data android:host="subscribeto.me" />
<data android:pathPrefix="/videos/" /> <!-- it contains playlists --> <data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
<data android:pathPrefix="/w/" /> <!-- short video URLs --> <data android:pathPrefix="/w/" /> <!-- short video URLs -->

View File

@ -25,6 +25,7 @@ import android.view.ViewGroup;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.os.BundleCompat;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.PagerAdapter;
@ -284,7 +285,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
Bundle state = null; Bundle state = null;
if (!mSavedState.isEmpty()) { if (!mSavedState.isEmpty()) {
state = new Bundle(); state = new Bundle();
state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0])); state.putParcelableArrayList("states", mSavedState);
} }
for (int i = 0; i < mFragments.size(); i++) { for (int i = 0; i < mFragments.size(); i++) {
final Fragment f = mFragments.get(i); final Fragment f = mFragments.get(i);
@ -311,13 +312,12 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
if (state != null) { if (state != null) {
final Bundle bundle = (Bundle) state; final Bundle bundle = (Bundle) state;
bundle.setClassLoader(loader); bundle.setClassLoader(loader);
final Parcelable[] fss = bundle.getParcelableArray("states"); final var states = BundleCompat.getParcelableArrayList(bundle, "states",
Fragment.SavedState.class);
mSavedState.clear(); mSavedState.clear();
mFragments.clear(); mFragments.clear();
if (fss != null) { if (states != null) {
for (final Parcelable parcelable : fss) { mSavedState.addAll(states);
mSavedState.add((Fragment.SavedState) parcelable);
}
} }
final Iterable<String> keys = bundle.keySet(); final Iterable<String> keys = bundle.keySet();
for (final String key : keys) { for (final String key : keys) {

View File

@ -19,10 +19,13 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.BridgeStateSaverInitializer;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
@ -58,6 +61,8 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
public class App extends Application { public class App extends Application {
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID; public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
private static final String TAG = App.class.toString(); private static final String TAG = App.class.toString();
private boolean isFirstRun = false;
private static App app; private static App app;
@NonNull @NonNull
@ -83,7 +88,13 @@ public class App extends Application {
return; return;
} }
// Initialize settings first because others inits can use its values // check if the last used preference version is set
// to determine whether this is the first app run
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this)
.getInt(getString(R.string.last_used_preferences_version), -1);
isFirstRun = lastUsedPrefVersion == -1;
// Initialize settings first because other initializations can use its values
NewPipeSettings.initSettings(this); NewPipeSettings.initSettings(this);
NewPipe.init(getDownloader(), NewPipe.init(getDownloader(),
@ -91,6 +102,7 @@ public class App extends Application {
Localization.getPreferredContentCountry(this)); Localization.getPreferredContentCountry(this));
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext())); Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
BridgeStateSaverInitializer.init(this);
StateSaver.init(this); StateSaver.init(this);
initNotificationChannels(); initNotificationChannels();
@ -99,8 +111,9 @@ public class App extends Application {
// Initialize image loader // Initialize image loader
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
PicassoHelper.init(this); PicassoHelper.init(this);
PicassoHelper.setShouldLoadImages( ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
prefs.getBoolean(getString(R.string.download_thumbnail_key), true)); prefs.getString(getString(R.string.image_quality_key),
getString(R.string.image_quality_default))));
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false)); && prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
@ -252,4 +265,7 @@ public class App extends Application {
return false; return false;
} }
public boolean isFirstRun() {
return isFirstRun;
}
} }

View File

@ -10,9 +10,9 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import icepick.Icepick; import com.evernote.android.state.State;
import icepick.State; import com.livefront.bridge.Bridge;
import leakcanary.AppWatcher;
public abstract class BaseFragment extends Fragment { public abstract class BaseFragment extends Fragment {
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
@ -49,7 +49,7 @@ public abstract class BaseFragment extends Fragment {
+ "savedInstanceState = [" + savedInstanceState + "]"); + "savedInstanceState = [" + savedInstanceState + "]");
} }
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Icepick.restoreInstanceState(this, savedInstanceState); Bridge.restoreInstanceState(this, savedInstanceState);
if (savedInstanceState != null) { if (savedInstanceState != null) {
onRestoreInstanceState(savedInstanceState); onRestoreInstanceState(savedInstanceState);
} }
@ -71,26 +71,39 @@ public abstract class BaseFragment extends Fragment {
@Override @Override
public void onSaveInstanceState(@NonNull final Bundle outState) { public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState); Bridge.saveInstanceState(this, outState);
} }
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
} }
@Override
public void onDestroy() {
super.onDestroy();
AppWatcher.INSTANCE.getObjectWatcher().watch(this);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Init // Init
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
/**
* This method is called in {@link #onViewCreated(View, Bundle)} to initialize the views.
*
* <p>
* {@link #initListeners()} is called after this method to initialize the corresponding
* listeners.
* </p>
* @param rootView The inflated view for this fragment
* (provided by {@link #onViewCreated(View, Bundle)})
* @param savedInstanceState The saved state of this fragment
* (provided by {@link #onViewCreated(View, Bundle)})
*/
protected void initViews(final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
} }
/**
* Initialize the listeners for this fragment.
*
* <p>
* This method is called after {@link #initViews(View, Bundle)}
* in {@link #onViewCreated(View, Bundle)}.
* </p>
*/
protected void initListeners() { protected void initListeners() {
} }
@ -108,9 +121,20 @@ public abstract class BaseFragment extends Fragment {
} }
} }
/**
* Finds the root fragment by looping through all of the parent fragments. The root fragment
* is supposed to be {@link org.schabi.newpipe.fragments.MainFragment}, and is the fragment that
* handles keeping the backstack of opened fragments in NewPipe, and also the player bottom
* sheet. This function therefore returns the fragment manager of said fragment.
*
* @return the fragment manager of the root fragment, i.e.
* {@link org.schabi.newpipe.fragments.MainFragment}
*/
protected FragmentManager getFM() { protected FragmentManager getFM() {
return getParentFragment() == null Fragment current = this;
? getFragmentManager() while (current.getParentFragment() != null) {
: getParentFragment().getFragmentManager(); current = current.getParentFragment();
}
return current.getFragmentManager();
} }
} }

View File

@ -44,6 +44,7 @@ import android.widget.FrameLayout;
import android.widget.Spinner; import android.widget.Spinner;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
@ -51,6 +52,7 @@ import androidx.core.app.ActivityCompat;
import androidx.core.view.GravityCompat; import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout; import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
@ -64,17 +66,20 @@ import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance; import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.local.feed.notifications.NotificationWorker; import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.settings.UpdateSettingsFragment;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.KioskTranslator;
@ -82,6 +87,7 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PeertubeHelper; import org.schabi.newpipe.util.PeertubeHelper;
import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ReleaseVersionUtil;
import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
@ -163,6 +169,11 @@ public class MainActivity extends AppCompatActivity {
// if this is enabled by the user. // if this is enabled by the user.
NotificationWorker.initialize(this); NotificationWorker.initialize(this);
} }
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
&& !App.getApp().isFirstRun()
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
}
} }
@Override @Override
@ -172,7 +183,8 @@ public class MainActivity extends AppCompatActivity {
final App app = App.getApp(); final App app = App.getApp();
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) { if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
&& prefs.getBoolean(app.getString(R.string.update_check_consent_key), false)) {
// Start the worker which is checking all conditions // Start the worker which is checking all conditions
// and eventually searching for a new version. // and eventually searching for a new version.
NewVersionWorker.enqueueNewVersionCheckingWork(app, false); NewVersionWorker.enqueueNewVersionCheckingWork(app, false);
@ -219,14 +231,14 @@ public class MainActivity extends AppCompatActivity {
final int currentServiceId = ServiceHelper.getSelectedServiceId(this); final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
final StreamingService service = NewPipe.getService(currentServiceId); final StreamingService service = NewPipe.getService(currentServiceId);
int kioskId = 0; int kioskMenuItemId = 0;
for (final String ks : service.getKioskList().getAvailableKiosks()) { for (final String ks : service.getKioskList().getAvailableKiosks()) {
drawerLayoutBinding.navigation.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator .add(R.id.menu_tabs_group, kioskMenuItemId, 0, KioskTranslator
.getTranslatedKioskName(ks, this)) .getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcon(ks)); .setIcon(KioskTranslator.getKioskIcon(ks));
kioskId++; kioskMenuItemId++;
} }
drawerLayoutBinding.navigation.getMenu() drawerLayoutBinding.navigation.getMenu()
@ -306,20 +318,16 @@ public class MainActivity extends AppCompatActivity {
NavigationHelper.openStatisticFragment(getSupportFragmentManager()); NavigationHelper.openStatisticFragment(getSupportFragmentManager());
break; break;
default: default:
final int currentServiceId = ServiceHelper.getSelectedServiceId(this); final StreamingService currentService = ServiceHelper.getSelectedService(this);
final StreamingService service = NewPipe.getService(currentServiceId); int kioskMenuItemId = 0;
String serviceName = ""; for (final String kioskId : currentService.getKioskList().getAvailableKiosks()) {
if (kioskMenuItemId == item.getItemId()) {
int kioskId = 0; NavigationHelper.openKioskFragment(getSupportFragmentManager(),
for (final String ks : service.getKioskList().getAvailableKiosks()) { currentService.getServiceId(), kioskId);
if (kioskId == item.getItemId()) { break;
serviceName = ks;
} }
kioskId++; kioskMenuItemId++;
} }
NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId,
serviceName);
break; break;
} }
} }
@ -550,14 +558,21 @@ public class MainActivity extends AppCompatActivity {
// interacts with a fragment inside fragment_holder so all back presses should be // interacts with a fragment inside fragment_holder so all back presses should be
// handled by it // handled by it
if (bottomSheetHiddenOrCollapsed()) { if (bottomSheetHiddenOrCollapsed()) {
final Fragment fragment = getSupportFragmentManager() final FragmentManager fm = getSupportFragmentManager();
.findFragmentById(R.id.fragment_holder); final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
// If current fragment implements BackPressable (i.e. can/wanna handle back press) // If current fragment implements BackPressable (i.e. can/wanna handle back press)
// delegate the back press to it // delegate the back press to it
if (fragment instanceof BackPressable) { if (fragment instanceof BackPressable) {
if (((BackPressable) fragment).onBackPressed()) { if (((BackPressable) fragment).onBackPressed()) {
return; return;
} }
} else if (fragment instanceof CommentRepliesFragment) {
// expand DetailsFragment if CommentRepliesFragment was opened
// to show the top level comments again
// Expand DetailsFragment if CommentRepliesFragment was opened
// and no other CommentRepliesFragments are on top of the back stack
// to show the top level comments again.
openDetailFragmentFromCommentReplies(fm, false);
} }
} else { } else {
@ -633,10 +648,17 @@ public class MainActivity extends AppCompatActivity {
* </pre> * </pre>
*/ */
private void onHomeButtonPressed() { private void onHomeButtonPressed() {
// If search fragment wasn't found in the backstack... final FragmentManager fm = getSupportFragmentManager();
if (!NavigationHelper.tryGotoSearchFragment(getSupportFragmentManager())) { final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
// ...go to the main fragment
NavigationHelper.gotoMainFragment(getSupportFragmentManager()); if (fragment instanceof CommentRepliesFragment) {
// Expand DetailsFragment if CommentRepliesFragment was opened
// and no other CommentRepliesFragments are on top of the back stack
// to show the top level comments again.
openDetailFragmentFromCommentReplies(fm, true);
} else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
// If search fragment wasn't found in the backstack go to the main fragment
NavigationHelper.gotoMainFragment(fm);
} }
} }
@ -832,6 +854,68 @@ public class MainActivity extends AppCompatActivity {
} }
} }
private void openDetailFragmentFromCommentReplies(
@NonNull final FragmentManager fm,
final boolean popBackStack
) {
// obtain the name of the fragment under the replies fragment that's going to be popped
@Nullable final String fragmentUnderEntryName;
if (fm.getBackStackEntryCount() < 2) {
fragmentUnderEntryName = null;
} else {
fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
.getName();
}
// the root comment is the comment for which the user opened the replies page
@Nullable final CommentRepliesFragment repliesFragment =
(CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG);
@Nullable final CommentsInfoItem rootComment =
repliesFragment == null ? null : repliesFragment.getCommentsInfoItem();
// sometimes this function pops the backstack, other times it's handled by the system
if (popBackStack) {
fm.popBackStackImmediate();
}
// only expand the bottom sheet back if there are no more nested comment replies fragments
// stacked under the one that is currently being popped
if (CommentRepliesFragment.TAG.equals(fragmentUnderEntryName)) {
return;
}
final BottomSheetBehavior<FragmentContainerView> behavior = BottomSheetBehavior
.from(mainBinding.fragmentPlayerHolder);
// do not return to the comment if the details fragment was closed
if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
return;
}
// scroll to the root comment once the bottom sheet expansion animation is finished
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull final View bottomSheet,
final int newState) {
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
final Fragment detailFragment = fm.findFragmentById(
R.id.fragment_player_holder);
if (detailFragment instanceof VideoDetailFragment && rootComment != null) {
// should always be the case
((VideoDetailFragment) detailFragment).scrollToComment(rootComment);
}
behavior.removeBottomSheetCallback(this);
}
}
@Override
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
// not needed, listener is removed once the sheet is expanded
}
});
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
private boolean bottomSheetHiddenOrCollapsed() { private boolean bottomSheetHiddenOrCollapsed() {
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior = final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder); BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);

View File

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

View File

@ -6,6 +6,7 @@ import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.net.toUri import androidx.core.net.toUri
@ -19,10 +20,7 @@ import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonParserException import com.grack.nanojson.JsonParserException
import org.schabi.newpipe.extractor.downloader.Response import org.schabi.newpipe.extractor.downloader.Response
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.util.PendingIntentCompat import org.schabi.newpipe.util.ReleaseVersionUtil
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
import java.io.IOException import java.io.IOException
class NewVersionWorker( class NewVersionWorker(
@ -60,7 +58,7 @@ class NewVersionWorker(
val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri()) val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri())
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingIntent = PendingIntentCompat.getActivity( val pendingIntent = PendingIntentCompat.getActivity(
applicationContext, 0, intent, 0 applicationContext, 0, intent, 0, false
) )
val channelId = applicationContext.getString(R.string.app_update_notification_channel_id) val channelId = applicationContext.getString(R.string.app_update_notification_channel_id)
val notificationBuilder = NotificationCompat.Builder(applicationContext, channelId) val notificationBuilder = NotificationCompat.Builder(applicationContext, channelId)
@ -84,7 +82,7 @@ class NewVersionWorker(
@Throws(IOException::class, ReCaptchaException::class) @Throws(IOException::class, ReCaptchaException::class)
private fun checkNewVersion() { private fun checkNewVersion() {
// Check if the current apk is a github one or not. // Check if the current apk is a github one or not.
if (!isReleaseApk()) { if (!ReleaseVersionUtil.isReleaseApk) {
return return
} }
@ -93,7 +91,7 @@ class NewVersionWorker(
// Check if the last request has happened a certain time ago // Check if the last request has happened a certain time ago
// to reduce the number of API requests. // to reduce the number of API requests.
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0) val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
if (!isLastUpdateCheckExpired(expiry)) { if (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) {
return return
} }
} }
@ -108,7 +106,7 @@ class NewVersionWorker(
try { try {
// Store a timestamp which needs to be exceeded, // Store a timestamp which needs to be exceeded,
// before a new request to the API is made. // before a new request to the API is made.
val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires")) val newExpiry = ReleaseVersionUtil.coerceUpdateCheckExpiry(response.getHeader("expires"))
prefs.edit { prefs.edit {
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry) putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
} }
@ -120,13 +118,13 @@ class NewVersionWorker(
// Parse the json from the response. // Parse the json from the response.
try { try {
val githubStableObject = JsonParser.`object`() val newpipeVersionInfo = JsonParser.`object`()
.from(response.responseBody()).getObject("flavors") .from(response.responseBody()).getObject("flavors")
.getObject("github").getObject("stable") .getObject("newpipe")
val versionName = githubStableObject.getString("version") val versionName = newpipeVersionInfo.getString("version")
val versionCode = githubStableObject.getInt("version_code") val versionCode = newpipeVersionInfo.getInt("version_code")
val apkLocationUrl = githubStableObject.getString("apk") val apkLocationUrl = newpipeVersionInfo.getString("apk")
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode) compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
} catch (e: JsonParserException) { } catch (e: JsonParserException) {
// Most likely something is wrong in data received from NEWPIPE_API_URL. // Most likely something is wrong in data received from NEWPIPE_API_URL.

View File

@ -75,7 +75,7 @@ public final class QueueItemMenuUtil {
return true; return true;
case R.id.menu_item_share: case R.id.menu_item_share:
shareText(context, item.getTitle(), item.getUrl(), shareText(context, item.getTitle(), item.getUrl(),
item.getThumbnailUrl()); item.getThumbnails());
return true; return true;
case R.id.menu_item_download: case R.id.menu_item_download:
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(), fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),

View File

@ -41,10 +41,14 @@ import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.evernote.android.state.State;
import com.livefront.bridge.Bridge;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding; import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding; import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.download.LoadingDialog;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.ReCaptchaActivity; import org.schabi.newpipe.error.ReCaptchaActivity;
@ -64,6 +68,7 @@ import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException; import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException; import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
@ -71,10 +76,11 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.ChannelTabHelper;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
@ -95,8 +101,6 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import icepick.Icepick;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
@ -149,7 +153,7 @@ public class RouterActivity extends AppCompatActivity {
getWindow().setAttributes(params); getWindow().setAttributes(params);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Icepick.restoreInstanceState(this, savedInstanceState); Bridge.restoreInstanceState(this, savedInstanceState);
// FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates // FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates
// We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments // We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments
@ -194,7 +198,7 @@ public class RouterActivity extends AppCompatActivity {
@Override @Override
protected void onSaveInstanceState(@NonNull final Bundle outState) { protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState); Bridge.saveInstanceState(this, outState);
} }
@Override @Override
@ -789,10 +793,10 @@ public class RouterActivity extends AppCompatActivity {
} }
} }
}, () -> { }, () ->
// this branch is executed if there is no activity context // this branch is executed if there is no activity context
inFlight(false); inFlight(false)
}); );
} }
<T> Single<T> pleaseWait(final Single<T> single) { <T> Single<T> pleaseWait(final Single<T> single) {
@ -812,19 +816,24 @@ public class RouterActivity extends AppCompatActivity {
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
private void openDownloadDialog(final int currentServiceId, final String currentUrl) { private void openDownloadDialog(final int currentServiceId, final String currentUrl) {
inFlight(true); inFlight(true);
final LoadingDialog loadingDialog = new LoadingDialog(R.string.loading_metadata_title);
loadingDialog.show(getParentFragmentManager(), "loadingDialog");
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true) disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.compose(this::pleaseWait) .compose(this::pleaseWait)
.subscribe(result -> .subscribe(result ->
runOnVisible(ctx -> { runOnVisible(ctx -> {
loadingDialog.dismiss();
final FragmentManager fm = ctx.getSupportFragmentManager(); final FragmentManager fm = ctx.getSupportFragmentManager();
final DownloadDialog downloadDialog = new DownloadDialog(ctx, result); final DownloadDialog downloadDialog = new DownloadDialog(ctx, result);
// dismiss listener to be handled by FragmentManager // dismiss listener to be handled by FragmentManager
downloadDialog.show(fm, "downloadDialog"); downloadDialog.show(fm, "downloadDialog");
} }
), throwable -> runOnVisible(ctx -> ), throwable -> runOnVisible(ctx -> {
((RouterActivity) ctx).showUnsupportedUrlDialog(currentUrl)))); loadingDialog.dismiss();
((RouterActivity) ctx).showUnsupportedUrlDialog(currentUrl);
})));
} }
private void openAddToPlaylistDialog(final int currentServiceId, final String currentUrl) { private void openAddToPlaylistDialog(final int currentServiceId, final String currentUrl) {
@ -1016,7 +1025,16 @@ public class RouterActivity extends AppCompatActivity {
} }
playQueue = new SinglePlayQueue((StreamInfo) info); playQueue = new SinglePlayQueue((StreamInfo) info);
} else if (info instanceof ChannelInfo) { } else if (info instanceof ChannelInfo) {
playQueue = new ChannelPlayQueue((ChannelInfo) info); final Optional<ListLinkHandler> playableTab = ((ChannelInfo) info).getTabs()
.stream()
.filter(ChannelTabHelper::isStreamsTab)
.findFirst();
if (playableTab.isPresent()) {
playQueue = new ChannelTabPlayQueue(info.getServiceId(), playableTab.get());
} else {
return; // there is no playable tab
}
} else if (info instanceof PlaylistInfo) { } else if (info instanceof PlaylistInfo) {
playQueue = new PlaylistPlayQueue((PlaylistInfo) info); playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
} else { } else {

View File

@ -6,6 +6,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
@ -57,13 +58,9 @@ class AboutActivity : AppCompatActivity() {
* A placeholder fragment containing a simple view. * A placeholder fragment containing a simple view.
*/ */
class AboutFragment : Fragment() { class AboutFragment : Fragment() {
private fun Button.openLink(url: Int) { private fun Button.openLink(@StringRes url: Int) {
setOnClickListener { setOnClickListener {
ShareUtils.openUrlInBrowser( ShareUtils.openUrlInApp(context, requireContext().getString(url))
context,
requireContext().getString(url),
false
)
} }
} }
@ -119,7 +116,7 @@ class AboutActivity : AppCompatActivity() {
/** /**
* List of all software components. * List of all software components.
*/ */
private val SOFTWARE_COMPONENTS = arrayOf( private val SOFTWARE_COMPONENTS = arrayListOf(
SoftwareComponent( SoftwareComponent(
"ACRA", "2013", "Kevin Gaudin", "ACRA", "2013", "Kevin Gaudin",
"https://github.com/ACRA/acra", StandardLicenses.APACHE2 "https://github.com/ACRA/acra", StandardLicenses.APACHE2
@ -141,8 +138,12 @@ class AboutActivity : AppCompatActivity() {
"https://github.com/lisawray/groupie", StandardLicenses.MIT "https://github.com/lisawray/groupie", StandardLicenses.MIT
), ),
SoftwareComponent( SoftwareComponent(
"Icepick", "2015", "Frankie Sardo", "Android-State", "2018", "Evernote",
"https://github.com/frankiesardo/icepick", StandardLicenses.EPL1 "https://github.com/Evernote/android-state", StandardLicenses.EPL1
),
SoftwareComponent(
"Bridge", "2021", "Livefront",
"https://github.com/livefront/bridge", StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"Jsoup", "2009 - 2020", "Jonathan Hedley", "Jsoup", "2009 - 2020", "Jonathan Hedley",

View File

@ -1,30 +1,40 @@
package org.schabi.newpipe.about package org.schabi.newpipe.about
import android.os.Bundle import android.os.Bundle
import android.util.Base64
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.WebView
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FragmentLicensesBinding import org.schabi.newpipe.databinding.FragmentLicensesBinding
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
import org.schabi.newpipe.ktx.parcelableArrayList
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.external_communication.ShareUtils
/** /**
* Fragment containing the software licenses. * Fragment containing the software licenses.
*/ */
class LicenseFragment : Fragment() { class LicenseFragment : Fragment() {
private lateinit var softwareComponents: Array<SoftwareComponent> private lateinit var softwareComponents: List<SoftwareComponent>
private var activeLicense: License? = null private var activeSoftwareComponent: SoftwareComponent? = null
private val compositeDisposable = CompositeDisposable() private val compositeDisposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
softwareComponents = arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent> softwareComponents = arguments?.parcelableArrayList<SoftwareComponent>(ARG_COMPONENTS)!!
activeLicense = savedInstanceState?.getSerializable(LICENSE_KEY) as? License .sortedBy { it.name } // Sort components by name
// Sort components by name activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
softwareComponents.sortBy { it.name }
} }
override fun onDestroy() { override fun onDestroy() {
@ -39,9 +49,8 @@ class LicenseFragment : Fragment() {
): View { ): View {
val binding = FragmentLicensesBinding.inflate(inflater, container, false) val binding = FragmentLicensesBinding.inflate(inflater, container, false)
binding.licensesAppReadLicense.setOnClickListener { binding.licensesAppReadLicense.setOnClickListener {
activeLicense = StandardLicenses.GPL3
compositeDisposable.add( compositeDisposable.add(
showLicense(activity, StandardLicenses.GPL3) showLicense(NEWPIPE_SOFTWARE_COMPONENT)
) )
} }
for (component in softwareComponents) { for (component in softwareComponents) {
@ -57,27 +66,72 @@ class LicenseFragment : Fragment() {
val root: View = componentBinding.root val root: View = componentBinding.root
root.tag = component root.tag = component
root.setOnClickListener { root.setOnClickListener {
activeLicense = component.license
compositeDisposable.add( compositeDisposable.add(
showLicense(activity, component) showLicense(component)
) )
} }
binding.licensesSoftwareComponents.addView(root) binding.licensesSoftwareComponents.addView(root)
registerForContextMenu(root) registerForContextMenu(root)
} }
activeLicense?.let { compositeDisposable.add(showLicense(activity, it)) } activeSoftwareComponent?.let { compositeDisposable.add(showLicense(it)) }
return binding.root return binding.root
} }
override fun onSaveInstanceState(savedInstanceState: Bundle) { override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState) super.onSaveInstanceState(savedInstanceState)
activeLicense?.let { savedInstanceState.putSerializable(LICENSE_KEY, it) } activeSoftwareComponent?.let { savedInstanceState.putSerializable(SOFTWARE_COMPONENT_KEY, it) }
}
private fun showLicense(
softwareComponent: SoftwareComponent
): Disposable {
return if (context == null) {
Disposable.empty()
} else {
val context = requireContext()
activeSoftwareComponent = softwareComponent
Observable.fromCallable { getFormattedLicense(context, softwareComponent.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)
val builder = AlertDialog.Builder(requireContext())
.setTitle(softwareComponent.name)
.setView(webView)
.setOnCancelListener { activeSoftwareComponent = null }
.setOnDismissListener { activeSoftwareComponent = null }
.setPositiveButton(R.string.done) { dialog, _ -> dialog.dismiss() }
if (softwareComponent != NEWPIPE_SOFTWARE_COMPONENT) {
builder.setNeutralButton(R.string.open_website_license) { _, _ ->
ShareUtils.openUrlInApp(requireContext(), softwareComponent.link)
}
}
builder.show()
}
}
} }
companion object { companion object {
private const val ARG_COMPONENTS = "components" private const val ARG_COMPONENTS = "components"
private const val LICENSE_KEY = "ACTIVE_LICENSE" private const val SOFTWARE_COMPONENT_KEY = "ACTIVE_SOFTWARE_COMPONENT"
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment { private val NEWPIPE_SOFTWARE_COMPONENT = SoftwareComponent(
"NewPipe",
"2014-2023",
"Team NewPipe",
"https://newpipe.net/",
StandardLicenses.GPL3,
BuildConfig.VERSION_NAME
)
fun newInstance(softwareComponents: ArrayList<SoftwareComponent>): LicenseFragment {
val fragment = LicenseFragment() val fragment = LicenseFragment()
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents) fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
return fragment return fragment

View File

@ -1,17 +1,8 @@
package org.schabi.newpipe.about package org.schabi.newpipe.about
import android.content.Context import android.content.Context
import android.util.Base64
import android.webkit.WebView
import androidx.appcompat.app.AlertDialog
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
import java.io.IOException import java.io.IOException
/** /**
@ -20,7 +11,7 @@ import java.io.IOException
* @return String which contains a HTML formatted license page * @return String which contains a HTML formatted license page
* styled according to the context's theme * styled according to the context's theme
*/ */
private fun getFormattedLicense(context: Context, license: License): String { fun getFormattedLicense(context: Context, license: License): String {
try { try {
return context.assets.open(license.filename).bufferedReader().use { it.readText() } return context.assets.open(license.filename).bufferedReader().use { it.readText() }
// split the HTML file and insert the stylesheet into the HEAD of the file // split the HTML file and insert the stylesheet into the HEAD of the file
@ -34,7 +25,7 @@ private fun getFormattedLicense(context: Context, license: License): String {
* @param context the Android context * @param context the Android context
* @return String which is a CSS stylesheet according to the context's theme * @return String which is a CSS stylesheet according to the context's theme
*/ */
private fun getLicenseStylesheet(context: Context): String { fun getLicenseStylesheet(context: Context): String {
val isLightTheme = ThemeHelper.isLightThemeSelected(context) val isLightTheme = ThemeHelper.isLightThemeSelected(context)
val licenseBackgroundColor = getHexRGBColor( val licenseBackgroundColor = getHexRGBColor(
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
@ -56,48 +47,6 @@ private fun getLicenseStylesheet(context: Context): String {
* @param color the color number from R.color * @param color the color number from R.color
* @return a six characters long String with hexadecimal RGB values * @return a six characters long String with hexadecimal RGB values
*/ */
private fun getHexRGBColor(context: Context, color: Int): String { fun getHexRGBColor(context: Context, color: Int): String {
return context.getString(color).substring(3) 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()
}
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

@ -2,6 +2,7 @@ package org.schabi.newpipe.about
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.io.Serializable
@Parcelize @Parcelize
class SoftwareComponent class SoftwareComponent
@ -13,4 +14,4 @@ constructor(
val link: String, val link: String,
val license: License, val license: License,
val version: String? = null val version: String? = null
) : Parcelable ) : Parcelable, Serializable

View File

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

View File

@ -7,7 +7,7 @@ import java.time.Instant
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
object Converters { class Converters {
/** /**
* Convert a long value to a [OffsetDateTime]. * Convert a long value to a [OffsetDateTime].
* *
@ -47,6 +47,6 @@ object Converters {
@TypeConverter @TypeConverter
fun feedGroupIconOf(id: Int): FeedGroupIcon { fun feedGroupIconOf(id: Int): FeedGroupIcon {
return FeedGroupIcon.values().first { it.id == id } return FeedGroupIcon.entries.first { it.id == id }
} }
} }

View File

@ -24,6 +24,9 @@ public final class Migrations {
public static final int DB_VER_4 = 4; public static final int DB_VER_4 = 4;
public static final int DB_VER_5 = 5; public static final int DB_VER_5 = 5;
public static final int DB_VER_6 = 6; public static final int DB_VER_6 = 6;
public static final int DB_VER_7 = 7;
public static final int DB_VER_8 = 8;
public static final int DB_VER_9 = 9;
private static final String TAG = Migrations.class.getName(); private static final String TAG = Migrations.class.getName();
public static final boolean DEBUG = MainActivity.DEBUG; public static final boolean DEBUG = MainActivity.DEBUG;
@ -185,7 +188,7 @@ public final class Migrations {
@Override @Override
public void migrate(@NonNull final SupportSQLiteDatabase database) { public void migrate(@NonNull final SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` " database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
+ "INTEGER NOT NULL DEFAULT 0"); + "INTEGER NOT NULL DEFAULT 0");
} }
}; };
@ -197,6 +200,108 @@ public final class Migrations {
} }
}; };
public static final Migration MIGRATION_6_7 = new Migration(DB_VER_6, DB_VER_7) {
@Override
public void migrate(@NonNull final SupportSQLiteDatabase database) {
// Create a new column thumbnail_stream_id
database.execSQL("ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` "
+ "INTEGER NOT NULL DEFAULT -1");
// Migrate the thumbnail_url to the thumbnail_stream_id
database.execSQL("UPDATE playlists SET thumbnail_stream_id = ("
+ " SELECT CASE WHEN COUNT(*) != 0 then stream_uid ELSE -1 END"
+ " FROM ("
+ " SELECT p.uid AS playlist_uid, s.uid AS stream_uid"
+ " FROM playlists p"
+ " LEFT JOIN playlist_stream_join ps ON p.uid = ps.playlist_id"
+ " LEFT JOIN streams s ON s.uid = ps.stream_id"
+ " WHERE s.thumbnail_url = p.thumbnail_url) AS temporary_table"
+ " WHERE playlist_uid = playlists.uid)");
// Remove the thumbnail_url field in the playlist table
database.execSQL("CREATE TABLE IF NOT EXISTS `playlists_new`"
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "name TEXT, "
+ "is_thumbnail_permanent INTEGER NOT NULL, "
+ "thumbnail_stream_id INTEGER NOT NULL)");
database.execSQL("INSERT INTO playlists_new"
+ " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id "
+ " FROM playlists");
database.execSQL("DROP TABLE playlists");
database.execSQL("ALTER TABLE playlists_new RENAME TO playlists");
database.execSQL("CREATE INDEX IF NOT EXISTS "
+ "`index_playlists_name` ON `playlists` (`name`)");
}
};
public static final Migration MIGRATION_7_8 = new Migration(DB_VER_7, DB_VER_8) {
@Override
public void migrate(@NonNull final SupportSQLiteDatabase database) {
database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT "
+ "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)");
database.execSQL("UPDATE search_history SET search = trim(search)");
}
};
public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) {
@Override
public void migrate(@NonNull final SupportSQLiteDatabase database) {
try {
database.beginTransaction();
// Update playlists.
// Create a temp table to initialize display_index.
database.execSQL("CREATE TABLE `playlists_tmp` "
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, "
+ "`thumbnail_stream_id` INTEGER NOT NULL, "
+ "`display_index` INTEGER NOT NULL)");
database.execSQL("INSERT INTO `playlists_tmp` "
+ "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
+ "`display_index`) "
+ "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
+ "-1 "
+ "FROM `playlists`");
// Replace the old table, note that this also removes the index on the name which
// we don't need anymore.
database.execSQL("DROP TABLE `playlists`");
database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`");
// Update remote_playlists.
// Create a temp table to initialize display_index.
database.execSQL("CREATE TABLE `remote_playlists_tmp` "
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
+ "`thumbnail_url` TEXT, `uploader` TEXT, "
+ "`display_index` INTEGER NOT NULL,"
+ "`stream_count` INTEGER)");
database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
+ "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, "
+ "`stream_count`)"
+ "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, "
+ "-1, `stream_count` FROM `remote_playlists`");
// Replace the old table, note that this also removes the index on the name which
// we don't need anymore.
database.execSQL("DROP TABLE `remote_playlists`");
database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`");
// Create index on the new table.
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
+ "ON `remote_playlists` (`service_id`, `url`)");
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
}
};
private Migrations() { private Migrations() {
} }
} }

View File

@ -32,6 +32,7 @@ abstract class FeedDAO {
* @return the feed streams filtered according to the conditions provided in the parameters * @return the feed streams filtered according to the conditions provided in the parameters
* @see StreamStateEntity.isFinished() * @see StreamStateEntity.isFinished()
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS * @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
* @see StreamStateEntity.PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
*/ */
@Query( @Query(
""" """
@ -66,6 +67,15 @@ abstract class FeedDAO {
OR s.stream_type = 'LIVE_STREAM' OR s.stream_type = 'LIVE_STREAM'
OR s.stream_type = 'AUDIO_LIVE_STREAM' OR s.stream_type = 'AUDIO_LIVE_STREAM'
) )
AND (
:includePartiallyPlayed
OR sh.stream_id IS NULL
OR sst.stream_id IS NULL
OR (sst.progress_time <= ${StreamStateEntity.PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS}
AND sst.progress_time <= s.duration * 1000 / 4)
OR (sst.progress_time >= s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
AND sst.progress_time >= s.duration * 1000 * 3 / 4)
)
AND ( AND (
:uploadDateBefore IS NULL :uploadDateBefore IS NULL
OR s.upload_date IS NULL OR s.upload_date IS NULL
@ -79,21 +89,34 @@ abstract class FeedDAO {
abstract fun getStreams( abstract fun getStreams(
groupId: Long, groupId: Long,
includePlayed: Boolean, includePlayed: Boolean,
includePartiallyPlayed: Boolean,
uploadDateBefore: OffsetDateTime? uploadDateBefore: OffsetDateTime?
): Maybe<List<StreamWithState>> ): Maybe<List<StreamWithState>>
/**
* Remove links to streams that are older than the given date
* **but keep at least one stream per uploader**.
*
* One stream per uploader is kept because it is needed as reference
* when fetching new streams to check if they are new or not.
* @param offsetDateTime the newest date to keep, older streams are removed
*/
@Query( @Query(
""" """
DELETE FROM feed WHERE DELETE FROM feed
WHERE feed.stream_id IN (SELECT uid from (
SELECT s.uid,
(SELECT MAX(upload_date)
FROM streams s1
INNER JOIN feed f1
ON s1.uid = f1.stream_id
WHERE f1.subscription_id = f.subscription_id) max_upload_date
FROM streams s
INNER JOIN feed f
ON s.uid = f.stream_id
feed.stream_id IN ( WHERE s.upload_date < :offsetDateTime
SELECT s.uid FROM streams s AND s.upload_date <> max_upload_date))
INNER JOIN feed f
ON s.uid = f.stream_id
WHERE s.upload_date < :offsetDateTime
)
""" """
) )
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime) abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)

View File

@ -3,7 +3,6 @@ package org.schabi.newpipe.database.feed.model
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.ForeignKey.CASCADE
import androidx.room.Index import androidx.room.Index
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.FEED_GROUP_SUBSCRIPTION_TABLE import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.FEED_GROUP_SUBSCRIPTION_TABLE
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.GROUP_ID import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.GROUP_ID
@ -19,14 +18,14 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
entity = FeedGroupEntity::class, entity = FeedGroupEntity::class,
parentColumns = [FeedGroupEntity.ID], parentColumns = [FeedGroupEntity.ID],
childColumns = [GROUP_ID], childColumns = [GROUP_ID],
onDelete = CASCADE, onUpdate = CASCADE, deferred = true onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
), ),
ForeignKey( ForeignKey(
entity = SubscriptionEntity::class, entity = SubscriptionEntity::class,
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
childColumns = [SUBSCRIPTION_ID], childColumns = [SUBSCRIPTION_ID],
onDelete = CASCADE, onUpdate = CASCADE, deferred = true onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
) )
] ]
) )

View File

@ -0,0 +1,29 @@
package org.schabi.newpipe.database.playlist;
import androidx.room.ColumnInfo;
/**
* This class adds a field to {@link PlaylistMetadataEntry} that contains an integer representing
* how many times a specific stream is already contained inside a local playlist. Used to be able
* to grey out playlists which already contain the current stream in the playlist append dialog.
* @see org.schabi.newpipe.local.playlist.LocalPlaylistManager#getPlaylistDuplicates(String)
*/
public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry {
public static final String PLAYLIST_TIMES_STREAM_IS_CONTAINED = "timesStreamIsContained";
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
public final long timesStreamIsContained;
@SuppressWarnings("checkstyle:ParameterNumber")
public PlaylistDuplicatesEntry(final long uid,
final String name,
final String thumbnailUrl,
final boolean isThumbnailPermanent,
final long thumbnailStreamId,
final long displayIndex,
final long streamCount,
final long timesStreamIsContained) {
super(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex,
streamCount);
this.timesStreamIsContained = timesStreamIsContained;
}
}

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity import org.schabi.newpipe.database.stream.model.StreamStateEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.image.ImageStrategy
data class PlaylistStreamEntry( data class PlaylistStreamEntry(
@Embedded @Embedded
@ -28,7 +29,7 @@ data class PlaylistStreamEntry(
item.duration = streamEntity.duration item.duration = streamEntity.duration
item.uploaderName = streamEntity.uploader item.uploaderName = streamEntity.uploader
item.uploaderUrl = streamEntity.uploaderUrl item.uploaderUrl = streamEntity.uploaderUrl
item.thumbnailUrl = streamEntity.thumbnailUrl item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
return item return item
} }

View File

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

View File

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

View File

@ -6,18 +6,25 @@ import androidx.room.RewriteQueriesToDropUnusedColumns;
import androidx.room.Transaction; import androidx.room.Transaction;
import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import java.util.List; import java.util.List;
import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT; import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX; import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID;
@ -26,6 +33,7 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PL
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_THUMBNAIL_URL; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_THUMBNAIL_URL;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@ -54,14 +62,15 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
Flowable<Integer> getMaximumIndexOf(long playlistId); Flowable<Integer> getMaximumIndexOf(long playlistId);
@Query("SELECT CASE WHEN COUNT(*) != 0 then " + STREAM_THUMBNAIL_URL + " ELSE :defaultUrl END" @Query("SELECT CASE WHEN COUNT(*) != 0 then " + STREAM_ID
+ " ELSE " + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " END"
+ " FROM " + STREAM_TABLE + " FROM " + STREAM_TABLE
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId " + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId "
+ " LIMIT 1" + " LIMIT 1"
) )
Flowable<String> getAutomaticThumbnailUrl(long playlistId, String defaultUrl); Flowable<Long> getAutomaticThumbnailStreamId(long playlistId);
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
@Transaction @Transaction
@ -84,13 +93,67 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId); Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
@Transaction @Transaction
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_URL + ", " @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
+ PLAYLIST_DISPLAY_INDEX + ", "
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
+ " ELSE (SELECT " + STREAM_THUMBNAIL_URL
+ " FROM " + STREAM_TABLE
+ " WHERE " + STREAM_TABLE + "." + STREAM_ID + " = " + PLAYLIST_THUMBNAIL_STREAM_ID
+ " ) END AS " + PLAYLIST_THUMBNAIL_URL + ", "
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT
+ " FROM " + PLAYLIST_TABLE
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
+ " GROUP BY " + PLAYLIST_ID
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
@RewriteQueriesToDropUnusedColumns
@Transaction
@Query("SELECT *, MIN(" + JOIN_INDEX + ")"
+ " FROM " + STREAM_TABLE + " INNER JOIN"
+ " (SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)"
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
+ " LEFT JOIN "
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
+ STREAM_PROGRESS_MILLIS
+ " FROM " + STREAM_STATE_TABLE + " )"
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
+ " GROUP BY " + STREAM_ID
+ " ORDER BY MIN(" + JOIN_INDEX + ") ASC")
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
@Transaction
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
+ PLAYLIST_DISPLAY_INDEX + ", "
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
+ " ELSE (SELECT " + STREAM_THUMBNAIL_URL
+ " FROM " + STREAM_TABLE
+ " WHERE " + STREAM_TABLE + "." + STREAM_ID + " = " + PLAYLIST_THUMBNAIL_STREAM_ID
+ " ) END AS " + PLAYLIST_THUMBNAIL_URL + ", "
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + ", "
+ "COALESCE(SUM(" + STREAM_URL + " = :streamUrl), 0) AS "
+ PLAYLIST_TIMES_STREAM_IS_CONTAINED
+ " FROM " + PLAYLIST_TABLE + " FROM " + PLAYLIST_TABLE
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
+ " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID + " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
+ " GROUP BY " + PLAYLIST_ID
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC") + " LEFT JOIN " + STREAM_TABLE
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata(); + " ON " + STREAM_TABLE + "." + STREAM_ID + " = " + JOIN_STREAM_ID
+ " AND :streamUrl = :streamUrl"
+ " GROUP BY " + JOIN_PLAYLIST_ID
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX + ", " + PLAYLIST_NAME)
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
} }

View File

@ -2,20 +2,28 @@ package org.schabi.newpipe.database.playlist.model;
import androidx.room.ColumnInfo; import androidx.room.ColumnInfo;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.Index; import androidx.room.Ignore;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
@Entity(tableName = PLAYLIST_TABLE, import org.schabi.newpipe.R;
indices = {@Index(value = {PLAYLIST_NAME})}) import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
@Entity(tableName = PLAYLIST_TABLE)
public class PlaylistEntity { public class PlaylistEntity {
public static final String DEFAULT_THUMBNAIL = "drawable://"
+ R.drawable.placeholder_thumbnail_playlist;
public static final long DEFAULT_THUMBNAIL_ID = -1;
public static final String PLAYLIST_TABLE = "playlists"; public static final String PLAYLIST_TABLE = "playlists";
public static final String PLAYLIST_ID = "uid"; public static final String PLAYLIST_ID = "uid";
public static final String PLAYLIST_NAME = "name"; public static final String PLAYLIST_NAME = "name";
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
public static final String PLAYLIST_DISPLAY_INDEX = "display_index";
public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent"; public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ColumnInfo(name = PLAYLIST_ID) @ColumnInfo(name = PLAYLIST_ID)
@ -24,17 +32,30 @@ public class PlaylistEntity {
@ColumnInfo(name = PLAYLIST_NAME) @ColumnInfo(name = PLAYLIST_NAME)
private String name; private String name;
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
private String thumbnailUrl;
@ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT) @ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
private boolean isThumbnailPermanent; private boolean isThumbnailPermanent;
public PlaylistEntity(final String name, final String thumbnailUrl, @ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
final boolean isThumbnailPermanent) { private long thumbnailStreamId;
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
private long displayIndex;
public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
final long thumbnailStreamId, final long displayIndex) {
this.name = name; this.name = name;
this.thumbnailUrl = thumbnailUrl;
this.isThumbnailPermanent = isThumbnailPermanent; this.isThumbnailPermanent = isThumbnailPermanent;
this.thumbnailStreamId = thumbnailStreamId;
this.displayIndex = displayIndex;
}
@Ignore
public PlaylistEntity(final PlaylistMetadataEntry item) {
this.uid = item.getUid();
this.name = item.name;
this.isThumbnailPermanent = item.isThumbnailPermanent();
this.thumbnailStreamId = item.getThumbnailStreamId();
this.displayIndex = item.getDisplayIndex();
} }
public long getUid() { public long getUid() {
@ -53,12 +74,12 @@ public class PlaylistEntity {
this.name = name; this.name = name;
} }
public String getThumbnailUrl() { public long getThumbnailStreamId() {
return thumbnailUrl; return thumbnailStreamId;
} }
public void setThumbnailUrl(final String thumbnailUrl) { public void setThumbnailStreamId(final long thumbnailStreamId) {
this.thumbnailUrl = thumbnailUrl; this.thumbnailStreamId = thumbnailStreamId;
} }
public boolean getIsThumbnailPermanent() { public boolean getIsThumbnailPermanent() {
@ -69,4 +90,11 @@ public class PlaylistEntity {
this.isThumbnailPermanent = isThumbnailSet; this.isThumbnailPermanent = isThumbnailSet;
} }
public long getDisplayIndex() {
return displayIndex;
}
public void setDisplayIndex(final long displayIndex) {
this.displayIndex = displayIndex;
}
} }

View File

@ -11,6 +11,7 @@ import androidx.room.PrimaryKey;
import org.schabi.newpipe.database.playlist.PlaylistLocalItem; import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.image.ImageStrategy;
import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM; import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME; import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME;
@ -20,7 +21,6 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.RE
@Entity(tableName = REMOTE_PLAYLIST_TABLE, @Entity(tableName = REMOTE_PLAYLIST_TABLE,
indices = { indices = {
@Index(value = {REMOTE_PLAYLIST_NAME}),
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true) @Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
}) })
public class PlaylistRemoteEntity implements PlaylistLocalItem { public class PlaylistRemoteEntity implements PlaylistLocalItem {
@ -31,6 +31,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
public static final String REMOTE_PLAYLIST_URL = "url"; public static final String REMOTE_PLAYLIST_URL = "url";
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader"; public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
public static final String REMOTE_PLAYLIST_DISPLAY_INDEX = "display_index";
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count"; public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ -52,6 +53,9 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME) @ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
private String uploader; private String uploader;
@ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX)
private long displayIndex = -1; // Make sure the new item is on the top
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT) @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
private Long streamCount; private Long streamCount;
@ -66,11 +70,25 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
this.streamCount = streamCount; this.streamCount = streamCount;
} }
@Ignore
public PlaylistRemoteEntity(final int serviceId, final String name, final String url,
final String thumbnailUrl, final String uploader,
final long displayIndex, final Long streamCount) {
this.serviceId = serviceId;
this.name = name;
this.url = url;
this.thumbnailUrl = thumbnailUrl;
this.uploader = uploader;
this.displayIndex = displayIndex;
this.streamCount = streamCount;
}
@Ignore @Ignore
public PlaylistRemoteEntity(final PlaylistInfo info) { public PlaylistRemoteEntity(final PlaylistInfo info) {
this(info.getServiceId(), info.getName(), info.getUrl(), this(info.getServiceId(), info.getName(), info.getUrl(),
info.getThumbnailUrl() == null // use uploader avatar when no thumbnail is available
? info.getUploaderAvatarUrl() : info.getThumbnailUrl(), ImageStrategy.imageListToDbUrl(info.getThumbnails().isEmpty()
? info.getUploaderAvatars() : info.getThumbnails()),
info.getUploaderName(), info.getStreamCount()); info.getUploaderName(), info.getStreamCount());
} }
@ -84,10 +102,14 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
&& getStreamCount() == info.getStreamCount() && getStreamCount() == info.getStreamCount()
&& TextUtils.equals(getName(), info.getName()) && TextUtils.equals(getName(), info.getName())
&& TextUtils.equals(getUrl(), info.getUrl()) && TextUtils.equals(getUrl(), info.getUrl())
&& TextUtils.equals(getThumbnailUrl(), info.getThumbnailUrl()) // we want to update the local playlist data even when either the remote thumbnail
// URL changes, or the preferred image quality setting is changed by the user
&& TextUtils.equals(getThumbnailUrl(),
ImageStrategy.imageListToDbUrl(info.getThumbnails()))
&& TextUtils.equals(getUploader(), info.getUploaderName()); && TextUtils.equals(getUploader(), info.getUploaderName());
} }
@Override
public long getUid() { public long getUid() {
return uid; return uid;
} }
@ -136,6 +158,16 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
this.uploader = uploader; this.uploader = uploader;
} }
@Override
public long getDisplayIndex() {
return displayIndex;
}
@Override
public void setDisplayIndex(final long displayIndex) {
this.displayIndex = displayIndex;
}
public Long getStreamCount() { public Long getStreamCount() {
return streamCount; return streamCount;
} }

View File

@ -7,6 +7,7 @@ import org.schabi.newpipe.database.history.model.StreamHistoryEntity
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.image.ImageStrategy
import java.time.OffsetDateTime import java.time.OffsetDateTime
class StreamStatisticsEntry( class StreamStatisticsEntry(
@ -30,7 +31,7 @@ class StreamStatisticsEntry(
item.duration = streamEntity.duration item.duration = streamEntity.duration
item.uploaderName = streamEntity.uploader item.uploaderName = streamEntity.uploader
item.uploaderUrl = streamEntity.uploaderUrl item.uploaderUrl = streamEntity.uploaderUrl
item.thumbnailUrl = streamEntity.thumbnailUrl item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
return item return item
} }

View File

@ -13,6 +13,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.player.playqueue.PlayQueueItem import org.schabi.newpipe.player.playqueue.PlayQueueItem
import org.schabi.newpipe.util.image.ImageStrategy
import java.io.Serializable import java.io.Serializable
import java.time.OffsetDateTime import java.time.OffsetDateTime
@ -67,7 +68,8 @@ data class StreamEntity(
constructor(item: StreamInfoItem) : this( constructor(item: StreamInfoItem) : this(
serviceId = item.serviceId, url = item.url, title = item.name, serviceId = item.serviceId, url = item.url, title = item.name,
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName, streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount, uploaderUrl = item.uploaderUrl,
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails), viewCount = item.viewCount,
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(), textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
isUploadDateApproximation = item.uploadDate?.isApproximation isUploadDateApproximation = item.uploadDate?.isApproximation
) )
@ -76,7 +78,8 @@ data class StreamEntity(
constructor(info: StreamInfo) : this( constructor(info: StreamInfo) : this(
serviceId = info.serviceId, url = info.url, title = info.name, serviceId = info.serviceId, url = info.url, title = info.name,
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName, streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
uploaderUrl = info.uploaderUrl, thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount, uploaderUrl = info.uploaderUrl,
thumbnailUrl = ImageStrategy.imageListToDbUrl(info.thumbnails), viewCount = info.viewCount,
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(), textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
isUploadDateApproximation = info.uploadDate?.isApproximation isUploadDateApproximation = info.uploadDate?.isApproximation
) )
@ -85,7 +88,8 @@ data class StreamEntity(
constructor(item: PlayQueueItem) : this( constructor(item: PlayQueueItem) : this(
serviceId = item.serviceId, url = item.url, title = item.title, serviceId = item.serviceId, url = item.url, title = item.title,
streamType = item.streamType, duration = item.duration, uploader = item.uploader, streamType = item.streamType, duration = item.duration, uploader = item.uploader,
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl uploaderUrl = item.uploaderUrl,
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails)
) )
fun toStreamInfoItem(): StreamInfoItem { fun toStreamInfoItem(): StreamInfoItem {
@ -93,7 +97,7 @@ data class StreamEntity(
item.duration = duration item.duration = duration
item.uploaderName = uploader item.uploaderName = uploader
item.uploaderUrl = uploaderUrl item.uploaderUrl = uploaderUrl
item.thumbnailUrl = thumbnailUrl item.thumbnails = ImageStrategy.dbUrlToImageList(thumbnailUrl)
if (viewCount != null) item.viewCount = viewCount as Long if (viewCount != null) item.viewCount = viewCount as Long
item.textualUploadDate = textualUploadDate item.textualUploadDate = textualUploadDate

View File

@ -30,7 +30,7 @@ public class StreamStateEntity {
/** /**
* Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s). * Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s).
*/ */
private static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000; public static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000;
/** /**
* Stream will be considered finished if the playback time left exceeds this threshold * Stream will be considered finished if the playback time left exceeds this threshold

View File

@ -1,6 +1,7 @@
package org.schabi.newpipe.database.subscription; package org.schabi.newpipe.database.subscription;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo; import androidx.room.ColumnInfo;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.Ignore; import androidx.room.Ignore;
@ -10,6 +11,7 @@ import androidx.room.PrimaryKey;
import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.image.ImageStrategy;
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID; import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE; import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
@ -57,8 +59,8 @@ public class SubscriptionEntity {
final SubscriptionEntity result = new SubscriptionEntity(); final SubscriptionEntity result = new SubscriptionEntity();
result.setServiceId(info.getServiceId()); result.setServiceId(info.getServiceId());
result.setUrl(info.getUrl()); result.setUrl(info.getUrl());
result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), result.setData(info.getName(), ImageStrategy.imageListToDbUrl(info.getAvatars()),
info.getSubscriberCount()); info.getDescription(), info.getSubscriberCount());
return result; return result;
} }
@ -94,11 +96,12 @@ public class SubscriptionEntity {
this.name = name; this.name = name;
} }
@Nullable
public String getAvatarUrl() { public String getAvatarUrl() {
return avatarUrl; return avatarUrl;
} }
public void setAvatarUrl(final String avatarUrl) { public void setAvatarUrl(@Nullable final String avatarUrl) {
this.avatarUrl = avatarUrl; this.avatarUrl = avatarUrl;
} }
@ -138,7 +141,7 @@ public class SubscriptionEntity {
@Ignore @Ignore
public ChannelInfoItem toChannelInfoItem() { public ChannelInfoItem toChannelInfoItem() {
final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName()); final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
item.setThumbnailUrl(getAvatarUrl()); item.setThumbnails(ImageStrategy.dbUrlToImageList(getAvatarUrl()));
item.setSubscriberCount(getSubscriberCount()); item.setSubscriberCount(getSubscriberCount());
item.setDescription(getDescription()); item.setDescription(getDescription());
return item; return item;

View File

@ -7,8 +7,6 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity; import android.app.Activity;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -16,6 +14,7 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.os.IBinder; import android.os.IBinder;
import android.provider.Settings;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -40,6 +39,8 @@ import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.evernote.android.state.State;
import com.livefront.bridge.Bridge;
import com.nononsenseapps.filepicker.Utils; import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
@ -60,6 +61,8 @@ import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard; import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
import org.schabi.newpipe.streams.io.StoredDirectoryHelper; import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
import org.schabi.newpipe.streams.io.StoredFileHelper; import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.AudioTrackAdapter;
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.FilenameUtils; import org.schabi.newpipe.util.FilenameUtils;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
@ -67,18 +70,17 @@ import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.SecondaryStreamHelper; import org.schabi.newpipe.util.SecondaryStreamHelper;
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener; import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import icepick.Icepick;
import icepick.State;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import us.shandian.giga.get.MissionRecoveryInfo; import us.shandian.giga.get.MissionRecoveryInfo;
import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.postprocessing.Postprocessing;
@ -95,11 +97,13 @@ public class DownloadDialog extends DialogFragment
@State @State
StreamInfo currentInfo; StreamInfo currentInfo;
@State @State
StreamSizeWrapper<AudioStream> wrappedAudioStreams; StreamInfoWrapper<VideoStream> wrappedVideoStreams;
@State @State
StreamSizeWrapper<VideoStream> wrappedVideoStreams; StreamInfoWrapper<SubtitlesStream> wrappedSubtitleStreams;
@State @State
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams; AudioTracksWrapper wrappedAudioTracks;
@State
int selectedAudioTrackIndex;
@State @State
int selectedVideoIndex; // set in the constructor int selectedVideoIndex; // set in the constructor
@State @State
@ -107,16 +111,14 @@ public class DownloadDialog extends DialogFragment
@State @State
int selectedSubtitleIndex = 0; // default to the first item int selectedSubtitleIndex = 0; // default to the first item
@Nullable
private OnDismissListener onDismissListener = null;
private StoredDirectoryHelper mainStorageAudio = null; private StoredDirectoryHelper mainStorageAudio = null;
private StoredDirectoryHelper mainStorageVideo = null; private StoredDirectoryHelper mainStorageVideo = null;
private DownloadManager downloadManager = null; private DownloadManager downloadManager = null;
private ActionMenuItemView okButton = null; private ActionMenuItemView okButton = null;
private Context context; private Context context = null;
private boolean askForSavePath; private boolean askForSavePath;
private AudioTrackAdapter audioTrackAdapter;
private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter; private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter;
private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter; private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter;
private StreamItemAdapter<SubtitlesStream, Stream> subtitleStreamsAdapter; private StreamItemAdapter<SubtitlesStream, Stream> subtitleStreamsAdapter;
@ -141,7 +143,6 @@ public class DownloadDialog extends DialogFragment
registerForActivityResult( registerForActivityResult(
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult); new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Instance creation // Instance creation
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -163,31 +164,32 @@ public class DownloadDialog extends DialogFragment
public DownloadDialog(@NonNull final Context context, @NonNull final StreamInfo info) { public DownloadDialog(@NonNull final Context context, @NonNull final StreamInfo info) {
this.currentInfo = info; this.currentInfo = info;
final List<AudioStream> audioStreams =
getStreamsOfSpecifiedDelivery(info.getAudioStreams(), PROGRESSIVE_HTTP);
final List<List<AudioStream>> groupedAudioStreams =
ListHelper.getGroupedAudioStreams(context, audioStreams);
this.wrappedAudioTracks = new AudioTracksWrapper(groupedAudioStreams, context);
this.selectedAudioTrackIndex =
ListHelper.getDefaultAudioTrackGroup(context, groupedAudioStreams);
// TODO: Adapt this code when the downloader support other types of stream deliveries // TODO: Adapt this code when the downloader support other types of stream deliveries
final List<VideoStream> videoStreams = ListHelper.getSortedStreamVideosList( final List<VideoStream> videoStreams = ListHelper.getSortedStreamVideosList(
context, context,
getStreamsOfSpecifiedDelivery(info.getVideoStreams(), PROGRESSIVE_HTTP), getStreamsOfSpecifiedDelivery(info.getVideoStreams(), PROGRESSIVE_HTTP),
getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(), PROGRESSIVE_HTTP), getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(), PROGRESSIVE_HTTP),
false, false,
false // If there are multiple languages available, prefer streams without audio
// to allow language selection
wrappedAudioTracks.size() > 1
); );
this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, context); this.wrappedVideoStreams = new StreamInfoWrapper<>(videoStreams, context);
this.wrappedAudioStreams = new StreamSizeWrapper<>( this.wrappedSubtitleStreams = new StreamInfoWrapper<>(
getStreamsOfSpecifiedDelivery(info.getAudioStreams(), PROGRESSIVE_HTTP), context);
this.wrappedSubtitleStreams = new StreamSizeWrapper<>(
getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context); getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context);
this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams); this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
} }
/**
* @param onDismissListener the listener to call in {@link #onDismiss(DialogInterface)}
*/
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
this.onDismissListener = onDismissListener;
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Android lifecycle // Android lifecycle
@ -207,38 +209,16 @@ public class DownloadDialog extends DialogFragment
return; return;
} }
// context will remain null if dismiss() was called above, allowing to check whether the
// dialog is being dismissed in onViewCreated()
context = getContext(); context = getContext();
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context)); setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
Icepick.restoreInstanceState(this, savedInstanceState); Bridge.restoreInstanceState(this, savedInstanceState);
final var secondaryStreams = new SparseArrayCompat<SecondaryStreamHelper<AudioStream>>(4); this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks);
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
for (int i = 0; i < videoStreams.size(); i++) {
if (!videoStreams.get(i).isVideoOnly()) {
continue;
}
final AudioStream audioStream = SecondaryStreamHelper
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
if (audioStream != null) {
secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams,
audioStream));
} else if (DEBUG) {
final MediaFormat mediaFormat = videoStreams.get(i).getFormat();
if (mediaFormat != null) {
Log.w(TAG, "No audio stream candidates for video format "
+ mediaFormat.name());
} else {
Log.w(TAG, "No audio stream candidates for unknown video format");
}
}
}
this.videoStreamsAdapter = new StreamItemAdapter<>(wrappedVideoStreams, secondaryStreams);
this.audioStreamsAdapter = new StreamItemAdapter<>(wrappedAudioStreams);
this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams); this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
updateSecondaryStreams();
final Intent intent = new Intent(context, DownloadManagerService.class); final Intent intent = new Intent(context, DownloadManagerService.class);
context.startService(intent); context.startService(intent);
@ -265,6 +245,39 @@ public class DownloadDialog extends DialogFragment
}, Context.BIND_AUTO_CREATE); }, Context.BIND_AUTO_CREATE);
} }
/**
* Update the displayed video streams based on the selected audio track.
*/
private void updateSecondaryStreams() {
final StreamInfoWrapper<AudioStream> audioStreams = getWrappedAudioStreams();
final var secondaryStreams = new SparseArrayCompat<SecondaryStreamHelper<AudioStream>>(4);
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
wrappedVideoStreams.resetInfo();
for (int i = 0; i < videoStreams.size(); i++) {
if (!videoStreams.get(i).isVideoOnly()) {
continue;
}
final AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(
context, audioStreams.getStreamsList(), videoStreams.get(i));
if (audioStream != null) {
secondaryStreams.append(i, new SecondaryStreamHelper<>(audioStreams, audioStream));
} else if (DEBUG) {
final MediaFormat mediaFormat = videoStreams.get(i).getFormat();
if (mediaFormat != null) {
Log.w(TAG, "No audio stream candidates for video format "
+ mediaFormat.name());
} else {
Log.w(TAG, "No audio stream candidates for unknown video format");
}
}
}
this.videoStreamsAdapter = new StreamItemAdapter<>(wrappedVideoStreams, secondaryStreams);
this.audioStreamsAdapter = new StreamItemAdapter<>(audioStreams);
}
@Override @Override
public View onCreateView(@NonNull final LayoutInflater inflater, public View onCreateView(@NonNull final LayoutInflater inflater,
final ViewGroup container, final ViewGroup container,
@ -282,16 +295,20 @@ public class DownloadDialog extends DialogFragment
@Nullable final Bundle savedInstanceState) { @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
dialogBinding = DownloadDialogBinding.bind(view); dialogBinding = DownloadDialogBinding.bind(view);
if (context == null) {
return; // the dialog is being dismissed, see the call to dismiss() in onCreate()
}
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(), dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
currentInfo.getName())); currentInfo.getName()));
selectedAudioIndex = ListHelper selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(),
.getDefaultAudioFormat(getContext(), wrappedAudioStreams.getStreamsList()); getWrappedAudioStreams().getStreamsList());
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll()); selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
dialogBinding.qualitySpinner.setOnItemSelectedListener(this); dialogBinding.qualitySpinner.setOnItemSelectedListener(this);
dialogBinding.audioStreamSpinner.setOnItemSelectedListener(this);
dialogBinding.audioTrackSpinner.setOnItemSelectedListener(this);
dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this); dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this);
initToolbar(dialogBinding.toolbarLayout.toolbar); initToolbar(dialogBinding.toolbarLayout.toolbar);
@ -340,14 +357,6 @@ public class DownloadDialog extends DialogFragment
}); });
} }
@Override
public void onDismiss(@NonNull final DialogInterface dialog) {
super.onDismiss(dialog);
if (onDismissListener != null) {
onDismissListener.onDismiss(dialog);
}
}
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
@ -363,7 +372,7 @@ public class DownloadDialog extends DialogFragment
@Override @Override
public void onSaveInstanceState(@NonNull final Bundle outState) { public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState); Bridge.saveInstanceState(this, outState);
} }
@ -373,7 +382,7 @@ public class DownloadDialog extends DialogFragment
private void fetchStreamsSize() { private void fetchStreamsSize() {
disposables.clear(); disposables.clear();
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams) disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedVideoStreams)
.subscribe(result -> { .subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
== R.id.video_button) { == R.id.video_button) {
@ -383,7 +392,7 @@ public class DownloadDialog extends DialogFragment
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG, new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading video stream size", "Downloading video stream size",
currentInfo.getServiceId())))); currentInfo.getServiceId()))));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams) disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(getWrappedAudioStreams())
.subscribe(result -> { .subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
== R.id.audio_button) { == R.id.audio_button) {
@ -393,7 +402,7 @@ public class DownloadDialog extends DialogFragment
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG, new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading audio stream size", "Downloading audio stream size",
currentInfo.getServiceId())))); currentInfo.getServiceId()))));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams) disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedSubtitleStreams)
.subscribe(result -> { .subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
== R.id.subtitle_button) { == R.id.subtitle_button) {
@ -405,14 +414,28 @@ public class DownloadDialog extends DialogFragment
currentInfo.getServiceId())))); currentInfo.getServiceId()))));
} }
private void setupAudioTrackSpinner() {
if (getContext() == null) {
return;
}
dialogBinding.audioTrackSpinner.setAdapter(audioTrackAdapter);
dialogBinding.audioTrackSpinner.setSelection(selectedAudioTrackIndex);
}
private void setupAudioSpinner() { private void setupAudioSpinner() {
if (getContext() == null) { if (getContext() == null) {
return; return;
} }
dialogBinding.qualitySpinner.setAdapter(audioStreamsAdapter); dialogBinding.qualitySpinner.setVisibility(View.GONE);
dialogBinding.qualitySpinner.setSelection(selectedAudioIndex);
setRadioButtonsState(true); setRadioButtonsState(true);
dialogBinding.audioStreamSpinner.setAdapter(audioStreamsAdapter);
dialogBinding.audioStreamSpinner.setSelection(selectedAudioIndex);
dialogBinding.audioStreamSpinner.setVisibility(View.VISIBLE);
dialogBinding.audioTrackSpinner.setVisibility(
wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE);
dialogBinding.audioTrackPresentInVideoText.setVisibility(View.GONE);
} }
private void setupVideoSpinner() { private void setupVideoSpinner() {
@ -422,7 +445,19 @@ public class DownloadDialog extends DialogFragment
dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter); dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter);
dialogBinding.qualitySpinner.setSelection(selectedVideoIndex); dialogBinding.qualitySpinner.setSelection(selectedVideoIndex);
dialogBinding.qualitySpinner.setVisibility(View.VISIBLE);
setRadioButtonsState(true); setRadioButtonsState(true);
dialogBinding.audioStreamSpinner.setVisibility(View.GONE);
onVideoStreamSelected();
}
private void onVideoStreamSelected() {
final boolean isVideoOnly = videoStreamsAdapter.getItem(selectedVideoIndex).isVideoOnly();
dialogBinding.audioTrackSpinner.setVisibility(
isVideoOnly && wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE);
dialogBinding.audioTrackPresentInVideoText.setVisibility(
!isVideoOnly && wrappedAudioTracks.size() > 1 ? View.VISIBLE : View.GONE);
} }
private void setupSubtitleSpinner() { private void setupSubtitleSpinner() {
@ -432,7 +467,11 @@ public class DownloadDialog extends DialogFragment
dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter); dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter);
dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex); dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex);
dialogBinding.qualitySpinner.setVisibility(View.VISIBLE);
setRadioButtonsState(true); setRadioButtonsState(true);
dialogBinding.audioStreamSpinner.setVisibility(View.GONE);
dialogBinding.audioTrackSpinner.setVisibility(View.GONE);
dialogBinding.audioTrackPresentInVideoText.setVisibility(View.GONE);
} }
@ -511,7 +550,6 @@ public class DownloadDialog extends DialogFragment
} }
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Listeners // Listeners
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -550,18 +588,31 @@ public class DownloadDialog extends DialogFragment
+ "parent = [" + parent + "], view = [" + view + "], " + "parent = [" + parent + "], view = [" + view + "], "
+ "position = [" + position + "], id = [" + id + "]"); + "position = [" + position + "], id = [" + id + "]");
} }
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button: switch (parent.getId()) {
case R.id.quality_spinner:
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.video_button:
selectedVideoIndex = position;
onVideoStreamSelected();
break;
case R.id.subtitle_button:
selectedSubtitleIndex = position;
break;
}
onItemSelectedSetFileName();
break;
case R.id.audio_track_spinner:
final boolean trackChanged = selectedAudioTrackIndex != position;
selectedAudioTrackIndex = position;
if (trackChanged) {
updateSecondaryStreams();
fetchStreamsSize();
}
break;
case R.id.audio_stream_spinner:
selectedAudioIndex = position; selectedAudioIndex = position;
break;
case R.id.video_button:
selectedVideoIndex = position;
break;
case R.id.subtitle_button:
selectedSubtitleIndex = position;
break;
} }
onItemSelectedSetFileName();
} }
private void onItemSelectedSetFileName() { private void onItemSelectedSetFileName() {
@ -607,6 +658,7 @@ public class DownloadDialog extends DialogFragment
protected void setupDownloadOptions() { protected void setupDownloadOptions() {
setRadioButtonsState(false); setRadioButtonsState(false);
setupAudioTrackSpinner();
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0; final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
@ -657,6 +709,13 @@ public class DownloadDialog extends DialogFragment
dialogBinding.subtitleButton.setEnabled(enabled); dialogBinding.subtitleButton.setEnabled(enabled);
} }
private StreamInfoWrapper<AudioStream> getWrappedAudioStreams() {
if (selectedAudioTrackIndex < 0 || selectedAudioTrackIndex > wrappedAudioTracks.size()) {
return StreamInfoWrapper.empty();
}
return wrappedAudioTracks.getTracksList().get(selectedAudioTrackIndex);
}
private int getSubtitleIndexBy(@NonNull final List<SubtitlesStream> streams) { private int getSubtitleIndexBy(@NonNull final List<SubtitlesStream> streams) {
final Localization preferredLocalization = NewPipe.getPreferredLocalization(); final Localization preferredLocalization = NewPipe.getPreferredLocalization();
@ -692,12 +751,11 @@ public class DownloadDialog extends DialogFragment
} }
private void showFailedDialog(@StringRes final int msg) { private void showFailedDialog(@StringRes final int msg) {
assureCorrectAppLanguage(getContext()); assureCorrectAppLanguage(requireContext());
new AlertDialog.Builder(context) new AlertDialog.Builder(context)
.setTitle(R.string.general_error) .setTitle(R.string.general_error)
.setMessage(msg) .setMessage(msg)
.setNegativeButton(getString(R.string.ok), null) .setNegativeButton(getString(R.string.ok), null)
.create()
.show(); .show();
} }
@ -710,6 +768,7 @@ public class DownloadDialog extends DialogFragment
final StoredDirectoryHelper mainStorage; final StoredDirectoryHelper mainStorage;
final MediaFormat format; final MediaFormat format;
final String selectedMediaType; final String selectedMediaType;
final long size;
// first, build the filename and get the output folder (if possible) // first, build the filename and get the output folder (if possible)
// later, run a very very very large file checking logic // later, run a very very very large file checking logic
@ -721,35 +780,38 @@ public class DownloadDialog extends DialogFragment
selectedMediaType = getString(R.string.last_download_type_audio_key); selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio; mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat(); format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
if (format == MediaFormat.WEBMA_OPUS) { if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg"; mimeTmp = "audio/ogg";
filenameTmp += "opus"; filenameTmp += "opus";
} else if (format != null) { } else if (format != null) {
mimeTmp = format.mimeType; mimeTmp = format.mimeType;
filenameTmp += format.suffix; filenameTmp += format.getSuffix();
} }
break; break;
case R.id.video_button: case R.id.video_button:
selectedMediaType = getString(R.string.last_download_type_video_key); selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo; mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat(); format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
if (format != null) { if (format != null) {
mimeTmp = format.mimeType; mimeTmp = format.mimeType;
filenameTmp += format.suffix; filenameTmp += format.getSuffix();
} }
break; break;
case R.id.subtitle_button: case R.id.subtitle_button:
selectedMediaType = getString(R.string.last_download_type_subtitle_key); selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat(); format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
if (format != null) { if (format != null) {
mimeTmp = format.mimeType; mimeTmp = format.mimeType;
} }
if (format == MediaFormat.TTML) { if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.suffix; filenameTmp += MediaFormat.SRT.getSuffix();
} else if (format != null) { } else if (format != null) {
filenameTmp += format.suffix; filenameTmp += format.getSuffix();
} }
break; break;
default: default:
@ -797,6 +859,21 @@ public class DownloadDialog extends DialogFragment
return; return;
} }
// Check for free storage space
final long freeSpace = mainStorage.getFreeStorageSpace();
if (freeSpace <= size) {
Toast.makeText(context, getString(R.
string.error_insufficient_storage), Toast.LENGTH_LONG).show();
// move the user to storage setting tab
final Intent storageSettingsIntent = new Intent(Settings.
ACTION_INTERNAL_STORAGE_SETTINGS);
if (storageSettingsIntent.resolveActivity(context.getPackageManager())
!= null) {
startActivity(storageSettingsIntent);
}
return;
}
// check for existing file with the same name // check for existing file with the same name
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp, checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
mimeTmp); mimeTmp);
@ -910,7 +987,7 @@ public class DownloadDialog extends DialogFragment
break; break;
} }
askDialog.create().show(); askDialog.show();
return; return;
} }
@ -954,7 +1031,7 @@ public class DownloadDialog extends DialogFragment
} }
}); });
askDialog.create().show(); askDialog.show();
} }
private void continueSelectedDownload(@NonNull final StoredFileHelper storage) { private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
@ -979,7 +1056,7 @@ public class DownloadDialog extends DialogFragment
final char kind; final char kind;
int threads = dialogBinding.threads.getProgress() + 1; int threads = dialogBinding.threads.getProgress() + 1;
final String[] urls; final String[] urls;
final MissionRecoveryInfo[] recoveryInfo; final List<MissionRecoveryInfo> recoveryInfo;
String psName = null; String psName = null;
String[] psArgs = null; String[] psArgs = null;
long nearLength = 0; long nearLength = 0;
@ -1013,7 +1090,6 @@ public class DownloadDialog extends DialogFragment
psName = Postprocessing.ALGORITHM_WEBM_MUXER; psName = Postprocessing.ALGORITHM_WEBM_MUXER;
} }
psArgs = null;
final long videoSize = wrappedVideoStreams.getSizeInBytes( final long videoSize = wrappedVideoStreams.getSizeInBytes(
(VideoStream) selectedStream); (VideoStream) selectedStream);
@ -1045,9 +1121,7 @@ public class DownloadDialog extends DialogFragment
urls = new String[] { urls = new String[] {
selectedStream.getContent() selectedStream.getContent()
}; };
recoveryInfo = new MissionRecoveryInfo[] { recoveryInfo = List.of(new MissionRecoveryInfo(selectedStream));
new MissionRecoveryInfo(selectedStream)
};
} else { } else {
if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) { if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) {
throw new IllegalArgumentException("Unsupported stream delivery format" throw new IllegalArgumentException("Unsupported stream delivery format"
@ -1057,12 +1131,14 @@ public class DownloadDialog extends DialogFragment
urls = new String[] { urls = new String[] {
selectedStream.getContent(), secondaryStream.getContent() selectedStream.getContent(), secondaryStream.getContent()
}; };
recoveryInfo = new MissionRecoveryInfo[] {new MissionRecoveryInfo(selectedStream), recoveryInfo = List.of(
new MissionRecoveryInfo(secondaryStream)}; new MissionRecoveryInfo(selectedStream),
new MissionRecoveryInfo(secondaryStream)
);
} }
DownloadManagerService.startMission(context, urls, storage, kind, threads, DownloadManagerService.startMission(context, urls, storage, kind, threads,
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo); currentInfo.getUrl(), psName, psArgs, nearLength, new ArrayList<>(recoveryInfo));
Toast.makeText(context, getString(R.string.download_has_started), Toast.makeText(context, getString(R.string.download_has_started),
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();

View File

@ -0,0 +1,87 @@
package org.schabi.newpipe.download;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.DownloadLoadingDialogBinding;
/**
* This class contains a dialog which shows a loading indicator and has a customizable title.
*/
public class LoadingDialog extends DialogFragment {
private static final String TAG = "LoadingDialog";
private static final boolean DEBUG = MainActivity.DEBUG;
private DownloadLoadingDialogBinding dialogLoadingBinding;
private final @StringRes int title;
/**
* Create a new LoadingDialog.
*
* <p>
* The dialog contains a loading indicator and has a customizable title.
* <br/>
* Use {@code show()} to display the dialog to the user.
* </p>
*
* @param title an informative title shown in the dialog's toolbar
*/
public LoadingDialog(final @StringRes int title) {
this.title = title;
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DEBUG) {
Log.d(TAG, "onCreate() called with: "
+ "savedInstanceState = [" + savedInstanceState + "]");
}
this.setCancelable(false);
}
@Override
public View onCreateView(
@NonNull final LayoutInflater inflater,
final ViewGroup container,
final Bundle savedInstanceState) {
if (DEBUG) {
Log.d(TAG, "onCreateView() called with: "
+ "inflater = [" + inflater + "], container = [" + container + "], "
+ "savedInstanceState = [" + savedInstanceState + "]");
}
return inflater.inflate(R.layout.download_loading_dialog, container);
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
dialogLoadingBinding = DownloadLoadingDialogBinding.bind(view);
initToolbar(dialogLoadingBinding.toolbarLayout.toolbar);
}
private void initToolbar(final Toolbar toolbar) {
if (DEBUG) {
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
}
toolbar.setTitle(requireContext().getString(title));
toolbar.setNavigationOnClickListener(v -> dismiss());
}
@Override
public void onDestroyView() {
dialogLoadingBinding = null;
super.onDestroyView();
}
}

View File

@ -2,7 +2,6 @@ package org.schabi.newpipe.error;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
@ -13,15 +12,14 @@ import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.IntentCompat;
import com.grack.nanojson.JsonWriter; import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityErrorBinding; import org.schabi.newpipe.databinding.ActivityErrorBinding;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
@ -105,7 +103,7 @@ public class ErrorActivity extends AppCompatActivity {
actionBar.setDisplayShowTitleEnabled(true); actionBar.setDisplayShowTitleEnabled(true);
} }
errorInfo = intent.getParcelableExtra(ERROR_INFO); errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo.class);
// important add guru meditation // important add guru meditation
addGuruMeditation(); addGuruMeditation();
@ -160,7 +158,7 @@ public class ErrorActivity extends AppCompatActivity {
.setMessage(R.string.start_accept_privacy_policy) .setMessage(R.string.start_accept_privacy_policy)
.setCancelable(false) .setCancelable(false)
.setNeutralButton(R.string.read_privacy_policy, (dialog, which) -> .setNeutralButton(R.string.read_privacy_policy, (dialog, which) ->
ShareUtils.openUrlInBrowser(context, ShareUtils.openUrlInApp(context,
context.getString(R.string.privacy_policy_url))) context.getString(R.string.privacy_policy_url)))
.setPositiveButton(R.string.accept, (dialog, which) -> { .setPositiveButton(R.string.accept, (dialog, which) -> {
if (action.equals("EMAIL")) { // send on email if (action.equals("EMAIL")) { // send on email
@ -171,14 +169,12 @@ public class ErrorActivity extends AppCompatActivity {
+ getString(R.string.app_name) + " " + getString(R.string.app_name) + " "
+ BuildConfig.VERSION_NAME) + BuildConfig.VERSION_NAME)
.putExtra(Intent.EXTRA_TEXT, buildJson()); .putExtra(Intent.EXTRA_TEXT, buildJson());
ShareUtils.openIntentInApp(context, i, true); ShareUtils.openIntentInApp(context, i);
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub } else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL, false); ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL);
} }
}) })
.setNegativeButton(R.string.decline, (dialog, which) -> { .setNegativeButton(R.string.decline, null)
// do nothing
})
.show(); .show();
} }
@ -188,25 +184,6 @@ public class ErrorActivity extends AppCompatActivity {
.collect(Collectors.joining(separator + "\n", separator + "\n", separator)); .collect(Collectors.joining(separator + "\n", separator + "\n", separator));
} }
/**
* Get the checked activity.
*
* @param returnActivity the activity to return to
* @return the casted return activity or null
*/
@Nullable
static Class<? extends Activity> getReturnActivity(final Class<?> returnActivity) {
Class<? extends Activity> checkedReturnActivity = null;
if (returnActivity != null) {
if (Activity.class.isAssignableFrom(returnActivity)) {
checkedReturnActivity = returnActivity.asSubclass(Activity.class);
} else {
checkedReturnActivity = MainActivity.class;
}
}
return checkedReturnActivity;
}
private void buildInfo(final ErrorInfo info) { private void buildInfo(final ErrorInfo info) {
String text = ""; String text = "";

View File

@ -11,7 +11,6 @@ import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
import org.schabi.newpipe.extractor.exceptions.ExtractionException import org.schabi.newpipe.extractor.exceptions.ExtractionException
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
import org.schabi.newpipe.ktx.isNetworkRelated import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.util.ServiceHelper import org.schabi.newpipe.util.ServiceHelper
@ -96,7 +95,6 @@ class ErrorInfo(
throwable is ContentNotAvailableException -> R.string.content_not_available throwable is ContentNotAvailableException -> R.string.content_not_available
throwable != null && throwable.isNetworkRelated -> R.string.network_error throwable != null && throwable.isNetworkRelated -> R.string.network_error
throwable is ContentNotSupportedException -> R.string.content_not_supported throwable is ContentNotSupportedException -> R.string.content_not_supported
throwable is DeobfuscateException -> R.string.youtube_signature_deobfuscation_error
throwable is ExtractionException -> R.string.parsing_error throwable is ExtractionException -> R.string.parsing_error
throwable is ExoPlaybackException -> { throwable is ExoPlaybackException -> {
when (throwable.type) { when (throwable.type) {

View File

@ -6,7 +6,6 @@ import android.util.Log
import android.view.View import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
import androidx.annotation.Nullable
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -144,7 +143,7 @@ class ErrorPanelHelper(
*/ */
private fun showAndSetErrorButtonAction( private fun showAndSetErrorButtonAction(
@StringRes resid: Int, @StringRes resid: Int,
@Nullable listener: View.OnClickListener listener: View.OnClickListener
) { ) {
errorActionButton.isVisible = true errorActionButton.isVisible = true
errorActionButton.setText(resid) errorActionButton.setText(resid)
@ -156,7 +155,7 @@ class ErrorPanelHelper(
) { ) {
errorOpenInBrowserButton.isVisible = true errorOpenInBrowserButton.isVisible = true
errorOpenInBrowserButton.setOnClickListener { errorOpenInBrowserButton.setOnClickListener {
ShareUtils.openUrlInBrowser(context, errorInfo.request, true) ShareUtils.openUrlInBrowser(context, errorInfo.request)
} }
} }

View File

@ -9,10 +9,10 @@ import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.util.PendingIntentCompat
/** /**
* This class contains all of the methods that should be used to let the user know that an error has * This class contains all of the methods that should be used to let the user know that an error has
@ -54,7 +54,7 @@ class ErrorUtil {
*/ */
@JvmStatic @JvmStatic
fun showSnackbar(context: Context, errorInfo: ErrorInfo) { fun showSnackbar(context: Context, errorInfo: ErrorInfo) {
val rootView = if (context is Activity) context.findViewById<View>(R.id.content) else null val rootView = (context as? Activity)?.findViewById<View>(android.R.id.content)
showSnackbar(context, rootView, errorInfo) showSnackbar(context, rootView, errorInfo)
} }
@ -71,7 +71,7 @@ class ErrorUtil {
fun showSnackbar(fragment: Fragment, errorInfo: ErrorInfo) { fun showSnackbar(fragment: Fragment, errorInfo: ErrorInfo) {
var rootView = fragment.view var rootView = fragment.view
if (rootView == null && fragment.activity != null) { if (rootView == null && fragment.activity != null) {
rootView = fragment.requireActivity().findViewById(R.id.content) rootView = fragment.requireActivity().findViewById(android.R.id.content)
} }
showSnackbar(fragment.requireContext(), rootView, errorInfo) showSnackbar(fragment.requireContext(), rootView, errorInfo)
} }
@ -118,7 +118,8 @@ class ErrorUtil {
context, context,
0, 0,
getErrorActivityIntent(context, errorInfo), getErrorActivityIntent(context, errorInfo),
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT,
false
) )
) )

View File

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

View File

@ -6,6 +6,7 @@ package org.schabi.newpipe.error;
public enum UserAction { public enum UserAction {
USER_REPORT("user report"), USER_REPORT("user report"),
UI_ERROR("ui error"), UI_ERROR("ui error"),
DATABASE_IMPORT_EXPORT("database import or export"),
SUBSCRIPTION_CHANGE("subscription change"), SUBSCRIPTION_CHANGE("subscription change"),
SUBSCRIPTION_UPDATE("subscription update"), SUBSCRIPTION_UPDATE("subscription update"),
SUBSCRIPTION_GET("get subscription"), SUBSCRIPTION_GET("get subscription"),
@ -19,6 +20,7 @@ public enum UserAction {
REQUESTED_PLAYLIST("requested playlist"), REQUESTED_PLAYLIST("requested playlist"),
REQUESTED_KIOSK("requested kiosk"), REQUESTED_KIOSK("requested kiosk"),
REQUESTED_COMMENTS("requested comments"), REQUESTED_COMMENTS("requested comments"),
REQUESTED_COMMENT_REPLIES("requested comment replies"),
REQUESTED_FEED("requested feed"), REQUESTED_FEED("requested feed"),
REQUESTED_BOOKMARK("bookmark"), REQUESTED_BOOKMARK("bookmark"),
DELETE_FROM_HISTORY("delete from history"), DELETE_FROM_HISTORY("delete from history"),

View File

@ -1,14 +1,20 @@
package org.schabi.newpipe.fragments; package org.schabi.newpipe.fragments;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.evernote.android.state.State;
import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
@ -18,17 +24,15 @@ import org.schabi.newpipe.util.InfoCache;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> { public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
@State @State
protected AtomicBoolean wasLoading = new AtomicBoolean(); protected AtomicBoolean wasLoading = new AtomicBoolean();
protected AtomicBoolean isLoading = new AtomicBoolean(); protected AtomicBoolean isLoading = new AtomicBoolean();
@Nullable @Nullable
private View emptyStateView; protected View emptyStateView;
@Nullable
protected TextView emptyStateMessageView;
@Nullable @Nullable
private ProgressBar loadingProgressBar; private ProgressBar loadingProgressBar;
@ -65,6 +69,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
protected void initViews(final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
emptyStateView = rootView.findViewById(R.id.empty_state_view); emptyStateView = rootView.findViewById(R.id.empty_state_view);
emptyStateMessageView = rootView.findViewById(R.id.empty_state_message);
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar); loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar);
errorPanelHelper = new ErrorPanelHelper(this, rootView, this::onRetryButtonClicked); errorPanelHelper = new ErrorPanelHelper(this, rootView, this::onRetryButtonClicked);
} }
@ -75,6 +80,8 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
if (errorPanelHelper != null) { if (errorPanelHelper != null) {
errorPanelHelper.dispose(); errorPanelHelper.dispose();
} }
emptyStateView = null;
emptyStateMessageView = null;
} }
protected void onRetryButtonClicked() { protected void onRetryButtonClicked() {
@ -127,6 +134,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
hideErrorPanel(); hideErrorPanel();
} }
@Override
public void showEmptyState() { public void showEmptyState() {
isLoading.set(false); isLoading.set(false);
if (emptyStateView != null) { if (emptyStateView != null) {
@ -189,6 +197,12 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
errorPanelHelper.showTextError(errorString); errorPanelHelper.showTextError(errorString);
} }
protected void setEmptyStateMessage(@StringRes final int text) {
if (emptyStateMessageView != null) {
emptyStateMessageView.setText(text);
}
}
public final void hideErrorPanel() { public final void hideErrorPanel() {
errorPanelHelper.hide(); errorPanelHelper.hide();
lastPanelError = null; lastPanelError = null;

View File

@ -1,6 +1,16 @@
package org.schabi.newpipe.fragments; package org.schabi.newpipe.fragments;
import static android.widget.RelativeLayout.ABOVE;
import static android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM;
import static android.widget.RelativeLayout.ALIGN_PARENT_TOP;
import static android.widget.RelativeLayout.BELOW;
import static com.google.android.material.tabs.TabLayout.INDICATOR_GRAVITY_BOTTOM;
import static com.google.android.material.tabs.TabLayout.INDICATOR_GRAVITY_TOP;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -9,7 +19,9 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.RelativeLayout;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
@ -17,6 +29,7 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround; import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
@ -25,10 +38,13 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentMainBinding; import org.schabi.newpipe.databinding.FragmentMainBinding;
import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.settings.tabs.Tab; import org.schabi.newpipe.settings.tabs.Tab;
import org.schabi.newpipe.settings.tabs.TabsManager; import org.schabi.newpipe.settings.tabs.TabsManager;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.ScrollableTabLayout;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -42,8 +58,11 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
private boolean hasTabsChanged = false; private boolean hasTabsChanged = false;
private boolean previousYoutubeRestrictedModeEnabled; private SharedPreferences prefs;
private boolean youtubeRestrictedModeEnabled;
private String youtubeRestrictedModeEnabledKey; private String youtubeRestrictedModeEnabledKey;
private boolean mainTabsPositionBottom;
private String mainTabsPositionKey;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Fragment's LifeCycle // Fragment's LifeCycle
@ -66,10 +85,11 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
} }
}); });
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled); youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
previousYoutubeRestrictedModeEnabled = youtubeRestrictedModeEnabled = prefs.getBoolean(youtubeRestrictedModeEnabledKey, false);
PreferenceManager.getDefaultSharedPreferences(requireContext()) mainTabsPositionKey = getString(R.string.main_tabs_position_key);
.getBoolean(youtubeRestrictedModeEnabledKey, false); mainTabsPositionBottom = prefs.getBoolean(mainTabsPositionKey, false);
} }
@Override @Override
@ -87,25 +107,27 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
binding.mainTabLayout.setupWithViewPager(binding.pager); binding.mainTabLayout.setupWithViewPager(binding.pager);
binding.mainTabLayout.addOnTabSelectedListener(this); binding.mainTabLayout.addOnTabSelectedListener(this);
binding.mainTabLayout.setTabRippleColor(binding.mainTabLayout.getTabRippleColor()
.withAlpha(32));
setupTabs(); setupTabs();
updateTabLayoutPosition();
} }
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
final boolean youtubeRestrictedModeEnabled = final boolean newYoutubeRestrictedModeEnabled =
PreferenceManager.getDefaultSharedPreferences(requireContext()) prefs.getBoolean(youtubeRestrictedModeEnabledKey, false);
.getBoolean(youtubeRestrictedModeEnabledKey, false); if (youtubeRestrictedModeEnabled != newYoutubeRestrictedModeEnabled || hasTabsChanged) {
if (previousYoutubeRestrictedModeEnabled != youtubeRestrictedModeEnabled) { youtubeRestrictedModeEnabled = newYoutubeRestrictedModeEnabled;
previousYoutubeRestrictedModeEnabled = youtubeRestrictedModeEnabled;
setupTabs();
} else if (hasTabsChanged) {
setupTabs(); setupTabs();
} }
final boolean newMainTabsPosition = prefs.getBoolean(mainTabsPositionKey, false);
if (mainTabsPositionBottom != newMainTabsPosition) {
mainTabsPositionBottom = newMainTabsPosition;
updateTabLayoutPosition();
}
} }
@Override @Override
@ -118,6 +140,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
} }
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Menu // Menu
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -166,7 +194,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
} }
binding.pager.setAdapter(null); binding.pager.setAdapter(null);
binding.pager.setOffscreenPageLimit(tabsList.size());
binding.pager.setAdapter(pagerAdapter); binding.pager.setAdapter(pagerAdapter);
updateTabsIconAndDescription(); updateTabsIconAndDescription();
@ -190,6 +217,44 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
setTitle(tabsList.get(tabPosition).getTabName(requireContext())); setTitle(tabsList.get(tabPosition).getTabName(requireContext()));
} }
public void commitPlaylistTabs() {
pagerAdapter.getLocalPlaylistFragments()
.stream()
.forEach(LocalPlaylistFragment::saveImmediate);
}
private void updateTabLayoutPosition() {
final ScrollableTabLayout tabLayout = binding.mainTabLayout;
final ViewPager viewPager = binding.pager;
final boolean bottom = mainTabsPositionBottom;
// change layout params to make the tab layout appear either at the top or at the bottom
final var tabParams = (RelativeLayout.LayoutParams) tabLayout.getLayoutParams();
final var pagerParams = (RelativeLayout.LayoutParams) viewPager.getLayoutParams();
tabParams.removeRule(bottom ? ALIGN_PARENT_TOP : ALIGN_PARENT_BOTTOM);
tabParams.addRule(bottom ? ALIGN_PARENT_BOTTOM : ALIGN_PARENT_TOP);
pagerParams.removeRule(bottom ? BELOW : ABOVE);
pagerParams.addRule(bottom ? ABOVE : BELOW, R.id.main_tab_layout);
tabLayout.setSelectedTabIndicatorGravity(
bottom ? INDICATOR_GRAVITY_TOP : INDICATOR_GRAVITY_BOTTOM);
tabLayout.setLayoutParams(tabParams);
viewPager.setLayoutParams(pagerParams);
// change the background and icon color of the tab layout:
// service-colored at the top, app-background-colored at the bottom
tabLayout.setBackgroundColor(ThemeHelper.resolveColorFromAttr(requireContext(),
bottom ? android.R.attr.windowBackground : R.attr.colorPrimary));
@ColorInt final int iconColor = bottom
? ThemeHelper.resolveColorFromAttr(requireContext(), android.R.attr.colorAccent)
: Color.WHITE;
tabLayout.setTabRippleColor(ColorStateList.valueOf(iconColor).withAlpha(32));
tabLayout.setTabIconTint(ColorStateList.valueOf(iconColor));
tabLayout.setSelectedTabIndicatorColor(iconColor);
}
@Override @Override
public void onTabSelected(final TabLayout.Tab selectedTab) { public void onTabSelected(final TabLayout.Tab selectedTab) {
if (DEBUG) { if (DEBUG) {
@ -209,10 +274,18 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
updateTitleForTab(tab.getPosition()); updateTitleForTab(tab.getPosition());
} }
private static final class SelectedTabsPagerAdapter public static final class SelectedTabsPagerAdapter
extends FragmentStatePagerAdapterMenuWorkaround { extends FragmentStatePagerAdapterMenuWorkaround {
private final Context context; private final Context context;
private final List<Tab> internalTabsList; private final List<Tab> internalTabsList;
/**
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
* during runtime and changes are not committed immediately. However, in some cases,
* the changes need to be committed immediately by calling
* {@link LocalPlaylistFragment#saveImmediate()}.
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
*/
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
private SelectedTabsPagerAdapter(final Context context, private SelectedTabsPagerAdapter(final Context context,
final FragmentManager fragmentManager, final FragmentManager fragmentManager,
@ -239,9 +312,17 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
((BaseFragment) fragment).useAsFrontPage(true); ((BaseFragment) fragment).useAsFrontPage(true);
} }
if (fragment instanceof LocalPlaylistFragment) {
localPlaylistFragments.add((LocalPlaylistFragment) fragment);
}
return fragment; return fragment;
} }
public List<LocalPlaylistFragment> getLocalPlaylistFragments() {
return localPlaylistFragments;
}
@Override @Override
public int getItemPosition(@NonNull final Object object) { public int getItemPosition(@NonNull final Object object) {
// Causes adapter to reload all Fragments when // Causes adapter to reload all Fragments when

View File

@ -0,0 +1,281 @@
package org.schabi.newpipe.fragments.detail;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
import android.graphics.Typeface;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.StyleSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.TooltipCompat;
import androidx.core.text.HtmlCompat;
import com.google.android.material.chip.Chip;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
import org.schabi.newpipe.databinding.ItemMetadataBinding;
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.text.TextLinkifier;
import java.util.List;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public abstract class BaseDescriptionFragment extends BaseFragment {
private final CompositeDisposable descriptionDisposables = new CompositeDisposable();
protected FragmentDescriptionBinding binding;
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
binding = FragmentDescriptionBinding.inflate(inflater, container, false);
setupDescription();
setupMetadata(inflater, binding.detailMetadataLayout);
addTagsMetadataItem(inflater, binding.detailMetadataLayout);
return binding.getRoot();
}
@Override
public void onDestroy() {
descriptionDisposables.clear();
super.onDestroy();
}
/**
* Get the description to display.
* @return description object, if available
*/
@Nullable
protected abstract Description getDescription();
/**
* Get the streaming service. Used for generating description links.
* @return streaming service
*/
@NonNull
protected abstract StreamingService getService();
/**
* Get the streaming service ID. Used for tag links.
* @return service ID
*/
protected abstract int getServiceId();
/**
* Get the URL of the described video or audio, used to generate description links.
* @return stream URL
*/
@Nullable
protected abstract String getStreamUrl();
/**
* Get the list of tags to display below the description.
* @return tag list
*/
@NonNull
public abstract List<String> getTags();
/**
* Add additional metadata to display.
* @param inflater LayoutInflater
* @param layout detailMetadataLayout
*/
protected abstract void setupMetadata(LayoutInflater inflater, LinearLayout layout);
private void setupDescription() {
final Description description = getDescription();
if (description == null || isEmpty(description.getContent())
|| description == Description.EMPTY_DESCRIPTION) {
binding.detailDescriptionView.setVisibility(View.GONE);
binding.detailSelectDescriptionButton.setVisibility(View.GONE);
return;
}
// start with disabled state. This also loads description content (!)
disableDescriptionSelection();
binding.detailSelectDescriptionButton.setOnClickListener(v -> {
if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) {
disableDescriptionSelection();
} else {
// enable selection only when button is clicked to prevent flickering
enableDescriptionSelection();
}
});
}
private void enableDescriptionSelection() {
binding.detailDescriptionNoteView.setVisibility(View.VISIBLE);
binding.detailDescriptionView.setTextIsSelectable(true);
final String buttonLabel = getString(R.string.description_select_disable);
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close);
}
private void disableDescriptionSelection() {
// show description content again, otherwise some links are not clickable
final Description description = getDescription();
if (description != null) {
TextLinkifier.fromDescription(binding.detailDescriptionView,
description, HtmlCompat.FROM_HTML_MODE_LEGACY,
getService(), getStreamUrl(),
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
}
binding.detailDescriptionNoteView.setVisibility(View.GONE);
binding.detailDescriptionView.setTextIsSelectable(false);
final String buttonLabel = getString(R.string.description_select_enable);
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all);
}
protected void addMetadataItem(final LayoutInflater inflater,
final LinearLayout layout,
final boolean linkifyContent,
@StringRes final int type,
@NonNull final String content) {
if (isBlank(content)) {
return;
}
final ItemMetadataBinding itemBinding =
ItemMetadataBinding.inflate(inflater, layout, false);
itemBinding.metadataTypeView.setText(type);
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
ShareUtils.copyToClipboard(requireContext(), content);
return true;
});
if (linkifyContent) {
TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null,
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
} else {
itemBinding.metadataContentView.setText(content);
}
itemBinding.metadataContentView.setClickable(true);
layout.addView(itemBinding.getRoot());
}
private String imageSizeToText(final int heightOrWidth) {
if (heightOrWidth < 0) {
return getString(R.string.question_mark);
} else {
return String.valueOf(heightOrWidth);
}
}
protected void addImagesMetadataItem(final LayoutInflater inflater,
final LinearLayout layout,
@StringRes final int type,
final List<Image> images) {
final String preferredImageUrl = ImageStrategy.choosePreferredImage(images);
if (preferredImageUrl == null) {
return; // null will be returned in case there is no image
}
final ItemMetadataBinding itemBinding =
ItemMetadataBinding.inflate(inflater, layout, false);
itemBinding.metadataTypeView.setText(type);
final SpannableStringBuilder urls = new SpannableStringBuilder();
for (final Image image : images) {
if (urls.length() != 0) {
urls.append(", ");
}
final int entryBegin = urls.length();
if (image.getHeight() != Image.HEIGHT_UNKNOWN
|| image.getWidth() != Image.WIDTH_UNKNOWN
// if even the resolution level is unknown, ?x? will be shown
|| image.getEstimatedResolutionLevel() == Image.ResolutionLevel.UNKNOWN) {
urls.append(imageSizeToText(image.getHeight()));
urls.append('x');
urls.append(imageSizeToText(image.getWidth()));
} else {
switch (image.getEstimatedResolutionLevel()) {
case LOW -> urls.append(getString(R.string.image_quality_low));
case MEDIUM -> urls.append(getString(R.string.image_quality_medium));
case HIGH -> urls.append(getString(R.string.image_quality_high));
default -> {
// unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
}
}
}
urls.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull final View widget) {
ShareUtils.openUrlInBrowser(requireContext(), image.getUrl());
}
}, entryBegin, urls.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (preferredImageUrl.equals(image.getUrl())) {
urls.setSpan(new StyleSpan(Typeface.BOLD), entryBegin, urls.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
itemBinding.metadataContentView.setText(urls);
itemBinding.metadataContentView.setMovementMethod(LinkMovementMethod.getInstance());
layout.addView(itemBinding.getRoot());
}
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
final List<String> tags = getTags();
if (!tags.isEmpty()) {
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
tags.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());
}
}
private void onTagClick(final View chip) {
if (getParentFragment() != null) {
NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(),
getServiceId(), ((Chip) chip).getText().toString());
}
}
private boolean onTagLongClick(final View chip) {
ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString());
return true;
}
}

View File

@ -1,134 +1,83 @@
package org.schabi.newpipe.fragments.detail; 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.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.util.Localization.getAppLocale; import static org.schabi.newpipe.util.Localization.getAppLocale;
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.widget.TooltipCompat;
import androidx.core.text.HtmlCompat;
import com.google.android.material.chip.Chip; import com.evernote.android.state.State;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentDescriptionBinding; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.databinding.ItemMetadataBinding;
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.text.TextLinkifier;
import icepick.State; import java.util.List;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class DescriptionFragment extends BaseFragment { public class DescriptionFragment extends BaseDescriptionFragment {
@State @State
StreamInfo streamInfo = null; StreamInfo streamInfo;
final CompositeDisposable descriptionDisposables = new CompositeDisposable();
FragmentDescriptionBinding binding;
public DescriptionFragment() {
}
public DescriptionFragment(final StreamInfo streamInfo) { public DescriptionFragment(final StreamInfo streamInfo) {
this.streamInfo = streamInfo; this.streamInfo = streamInfo;
} }
@Override public DescriptionFragment() {
public View onCreateView(@NonNull final LayoutInflater inflater, // keep empty constructor for State when resuming fragment from memory
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
binding = FragmentDescriptionBinding.inflate(inflater, container, false);
if (streamInfo != null) {
setupUploadDate();
setupDescription();
setupMetadata(inflater, binding.detailMetadataLayout);
}
return binding.getRoot();
}
@Override
public void onDestroy() {
descriptionDisposables.clear();
super.onDestroy();
} }
private void setupUploadDate() { @Nullable
if (streamInfo.getUploadDate() != null) { @Override
protected Description getDescription() {
return streamInfo.getDescription();
}
@NonNull
@Override
protected StreamingService getService() {
return streamInfo.getService();
}
@Override
protected int getServiceId() {
return streamInfo.getServiceId();
}
@NonNull
@Override
protected String getStreamUrl() {
return streamInfo.getUrl();
}
@NonNull
@Override
public List<String> getTags() {
return streamInfo.getTags();
}
@Override
protected void setupMetadata(final LayoutInflater inflater,
final LinearLayout layout) {
if (streamInfo != null && streamInfo.getUploadDate() != null) {
binding.detailUploadDateView.setText(Localization binding.detailUploadDateView.setText(Localization
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime())); .localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
} else { } else {
binding.detailUploadDateView.setVisibility(View.GONE); binding.detailUploadDateView.setVisibility(View.GONE);
} }
}
if (streamInfo == null) {
private void setupDescription() {
final Description description = streamInfo.getDescription();
if (description == null || isEmpty(description.getContent())
|| description == Description.EMPTY_DESCRIPTION) {
binding.detailDescriptionView.setVisibility(View.GONE);
binding.detailSelectDescriptionButton.setVisibility(View.GONE);
return; return;
} }
// start with disabled state. This also loads description content (!)
disableDescriptionSelection();
binding.detailSelectDescriptionButton.setOnClickListener(v -> {
if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) {
disableDescriptionSelection();
} else {
// enable selection only when button is clicked to prevent flickering
enableDescriptionSelection();
}
});
}
private void enableDescriptionSelection() {
binding.detailDescriptionNoteView.setVisibility(View.VISIBLE);
binding.detailDescriptionView.setTextIsSelectable(true);
final String buttonLabel = getString(R.string.description_select_disable);
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close);
}
private void disableDescriptionSelection() {
// show description content again, otherwise some links are not clickable
TextLinkifier.fromDescription(binding.detailDescriptionView,
streamInfo.getDescription(), HtmlCompat.FROM_HTML_MODE_LEGACY,
streamInfo.getService(), streamInfo.getUrl(),
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
binding.detailDescriptionNoteView.setVisibility(View.GONE);
binding.detailDescriptionView.setTextIsSelectable(false);
final String buttonLabel = getString(R.string.description_select_enable);
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all);
}
private void setupMetadata(final LayoutInflater inflater,
final LinearLayout layout) {
addMetadataItem(inflater, layout, false, R.string.metadata_category, addMetadataItem(inflater, layout, false, R.string.metadata_category,
streamInfo.getCategory()); streamInfo.getCategory());
@ -151,69 +100,13 @@ public class DescriptionFragment extends BaseFragment {
streamInfo.getSupportInfo()); streamInfo.getSupportInfo());
addMetadataItem(inflater, layout, true, R.string.metadata_host, addMetadataItem(inflater, layout, true, R.string.metadata_host,
streamInfo.getHost()); streamInfo.getHost());
addMetadataItem(inflater, layout, true, R.string.metadata_thumbnail_url,
streamInfo.getThumbnailUrl());
addTagsMetadataItem(inflater, layout); addImagesMetadataItem(inflater, layout, R.string.metadata_thumbnails,
} streamInfo.getThumbnails());
addImagesMetadataItem(inflater, layout, R.string.metadata_uploader_avatars,
private void addMetadataItem(final LayoutInflater inflater, streamInfo.getUploaderAvatars());
final LinearLayout layout, addImagesMetadataItem(inflater, layout, R.string.metadata_subchannel_avatars,
final boolean linkifyContent, streamInfo.getSubChannelAvatars());
@StringRes final int type,
@Nullable final String content) {
if (isBlank(content)) {
return;
}
final ItemMetadataBinding itemBinding =
ItemMetadataBinding.inflate(inflater, layout, false);
itemBinding.metadataTypeView.setText(type);
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
ShareUtils.copyToClipboard(requireContext(), content);
return true;
});
if (linkifyContent) {
TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null,
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
} else {
itemBinding.metadataContentView.setText(content);
}
itemBinding.metadataContentView.setClickable(true);
layout.addView(itemBinding.getRoot());
}
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) {
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
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());
}
}
private void onTagClick(final View chip) {
if (getParentFragment() != null) {
NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(),
streamInfo.getServiceId(), ((Chip) chip).getText().toString());
}
}
private boolean onTagLongClick(final View chip) {
ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString());
return true;
} }
private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) { private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {

View File

@ -7,11 +7,10 @@ import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation; 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.globalScreenOrientationLocked;
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired; 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.DependentPreferenceHelper.getResumePlaybackEnabled;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView; import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams; import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
import static org.schabi.newpipe.util.NavigationHelper.openPlayQueue; import static org.schabi.newpipe.util.NavigationHelper.openPlayQueue;
import static org.schabi.newpipe.util.NavigationHelper.playWithKore;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
@ -25,8 +24,8 @@ import android.content.pm.ActivityInfo;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
@ -54,17 +53,15 @@ import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.view.WindowCompat; import androidx.fragment.app.Fragment;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.evernote.android.state.State;
import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
import com.squareup.picasso.Callback;
import org.schabi.newpipe.App; import org.schabi.newpipe.App;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -75,8 +72,9 @@ import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.ReCaptchaActivity; import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.AudioStream;
@ -87,11 +85,13 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.EmptyFragment; import org.schabi.newpipe.fragments.EmptyFragment;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment; import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment; import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.PlayerType;
@ -107,15 +107,17 @@ import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.InfoCache;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
@ -126,7 +128,6 @@ import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Consumer;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
@ -165,8 +166,12 @@ public final class VideoDetailFragment
private boolean showRelatedItems; private boolean showRelatedItems;
private boolean showDescription; private boolean showDescription;
private String selectedTabTag; private String selectedTabTag;
@AttrRes @NonNull final List<Integer> tabIcons = new ArrayList<>(); @AttrRes
@StringRes @NonNull final List<Integer> tabContentDescriptions = new ArrayList<>(); @NonNull
final List<Integer> tabIcons = new ArrayList<>();
@StringRes
@NonNull
final List<Integer> tabContentDescriptions = new ArrayList<>();
private boolean tabSettingsChanged = false; private boolean tabSettingsChanged = false;
private int lastAppBarVerticalOffset = Integer.MAX_VALUE; // prevents useless updates private int lastAppBarVerticalOffset = Integer.MAX_VALUE; // prevents useless updates
@ -470,10 +475,23 @@ public final class VideoDetailFragment
binding.detailControlsBackground.setOnClickListener(v -> openBackgroundPlayer(false)); binding.detailControlsBackground.setOnClickListener(v -> openBackgroundPlayer(false));
binding.detailControlsPopup.setOnClickListener(v -> openPopupPlayer(false)); binding.detailControlsPopup.setOnClickListener(v -> openPopupPlayer(false));
binding.detailControlsPlaylistAppend.setOnClickListener(makeOnClickListener(info -> binding.detailControlsPlaylistAppend.setOnClickListener(makeOnClickListener(info -> {
if (getFM() != null && currentInfo != null) {
final Fragment fragment = getParentFragmentManager().
findFragmentById(R.id.fragment_holder);
// commit previous pending changes to database
if (fragment instanceof LocalPlaylistFragment) {
((LocalPlaylistFragment) fragment).saveImmediate();
} else if (fragment instanceof MainFragment) {
((MainFragment) fragment).commitPlaylistTabs();
}
disposables.add(PlaylistDialog.createCorrespondingDialog(requireContext(), disposables.add(PlaylistDialog.createCorrespondingDialog(requireContext(),
List.of(new StreamEntity(info)), List.of(new StreamEntity(info)),
dialog -> dialog.show(getParentFragmentManager(), TAG))))); dialog -> dialog.show(getParentFragmentManager(), TAG)));
}
}));
binding.detailControlsDownload.setOnClickListener(v -> { binding.detailControlsDownload.setOnClickListener(v -> {
if (PermissionHelper.checkStoragePermissions(activity, if (PermissionHelper.checkStoragePermissions(activity,
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
@ -482,19 +500,11 @@ public final class VideoDetailFragment
}); });
binding.detailControlsShare.setOnClickListener(makeOnClickListener(info -> binding.detailControlsShare.setOnClickListener(makeOnClickListener(info ->
ShareUtils.shareText(requireContext(), info.getName(), info.getUrl(), ShareUtils.shareText(requireContext(), info.getName(), info.getUrl(),
info.getThumbnailUrl()))); info.getThumbnails())));
binding.detailControlsOpenInBrowser.setOnClickListener(makeOnClickListener(info -> binding.detailControlsOpenInBrowser.setOnClickListener(makeOnClickListener(info ->
ShareUtils.openUrlInBrowser(requireContext(), info.getUrl()))); ShareUtils.openUrlInBrowser(requireContext(), info.getUrl())));
binding.detailControlsPlayWithKodi.setOnClickListener(makeOnClickListener(info -> { binding.detailControlsPlayWithKodi.setOnClickListener(makeOnClickListener(info ->
try { KoreUtils.playWithKore(requireContext(), Uri.parse(info.getUrl()))));
playWithKore(requireContext(), Uri.parse(info.getUrl()));
} catch (final Exception e) {
if (DEBUG) {
Log.i(TAG, "Failed to start kore", e);
}
KoreUtils.showInstallKoreDialog(requireContext());
}
}));
if (DEBUG) { if (DEBUG) {
binding.detailControlsCrashThePlayer.setOnClickListener(v -> binding.detailControlsCrashThePlayer.setOnClickListener(v ->
VideoDetailPlayerCrasher.onCrashThePlayer(requireContext(), player)); VideoDetailPlayerCrasher.onCrashThePlayer(requireContext(), player));
@ -543,9 +553,11 @@ public final class VideoDetailFragment
})); }));
binding.detailControlsBackground.setOnLongClickListener(makeOnLongClickListener(info -> binding.detailControlsBackground.setOnLongClickListener(makeOnLongClickListener(info ->
openBackgroundPlayer(true))); openBackgroundPlayer(true)
));
binding.detailControlsPopup.setOnLongClickListener(makeOnLongClickListener(info -> binding.detailControlsPopup.setOnLongClickListener(makeOnLongClickListener(info ->
openPopupPlayer(true))); openPopupPlayer(true)
));
binding.detailControlsDownload.setOnLongClickListener(makeOnLongClickListener(info -> binding.detailControlsDownload.setOnLongClickListener(makeOnLongClickListener(info ->
NavigationHelper.openDownloads(activity))); NavigationHelper.openDownloads(activity)));
@ -628,8 +640,7 @@ public final class VideoDetailFragment
final View.OnTouchListener controlsTouchListener = (view, motionEvent) -> { final View.OnTouchListener controlsTouchListener = (view, motionEvent) -> {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN if (motionEvent.getAction() == MotionEvent.ACTION_DOWN
&& PreferenceManager.getDefaultSharedPreferences(activity) && PlayButtonHelper.shouldShowHoldToAppendTip(activity)) {
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
animate(binding.touchAppendDetail, true, 250, AnimationType.ALPHA, 0, () -> animate(binding.touchAppendDetail, true, 250, AnimationType.ALPHA, 0, () ->
animate(binding.touchAppendDetail, false, 1500, AnimationType.ALPHA, 1000)); animate(binding.touchAppendDetail, false, 1500, AnimationType.ALPHA, 1000));
@ -656,27 +667,6 @@ public final class VideoDetailFragment
} }
} }
private void initThumbnailViews(@NonNull final StreamInfo info) {
PicassoHelper.loadDetailsThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailThumbnailImageView, new Callback() {
@Override
public void onSuccess() {
// nothing to do, the image was loaded correctly into the thumbnail
}
@Override
public void onError(final Exception e) {
showSnackBarError(new ErrorInfo(e, UserAction.LOAD_IMAGE,
info.getThumbnailUrl(), info));
}
});
PicassoHelper.loadAvatar(info.getSubChannelAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
PicassoHelper.loadAvatar(info.getUploaderAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailUploaderThumbnailView);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// OwnStack // OwnStack
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -750,7 +740,7 @@ public final class VideoDetailFragment
final boolean isPlayerStopped = !isPlayerAvailable() || player.isStopped(); final boolean isPlayerStopped = !isPlayerAvailable() || player.isStopped();
if (playQueueItem != null && isPlayerStopped) { if (playQueueItem != null && isPlayerStopped) {
updateOverlayData(playQueueItem.getTitle(), updateOverlayData(playQueueItem.getTitle(),
playQueueItem.getUploader(), playQueueItem.getThumbnailUrl()); playQueueItem.getUploader(), playQueueItem.getThumbnails());
} }
} }
@ -1023,6 +1013,20 @@ public final class VideoDetailFragment
updateTabLayoutVisibility(); updateTabLayoutVisibility();
} }
public void scrollToComment(final CommentsInfoItem comment) {
final int commentsTabPos = pageAdapter.getItemPositionByTitle(COMMENTS_TAB_TAG);
final Fragment fragment = pageAdapter.getItem(commentsTabPos);
if (!(fragment instanceof CommentsFragment)) {
return;
}
// unexpand the app bar only if scrolling to the comment succeeded
if (((CommentsFragment) fragment).scrollToComment(comment)) {
binding.appBarLayout.setExpanded(false, false);
binding.viewPager.setCurrentItem(commentsTabPos, false);
}
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Play Utils // Play Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -1051,20 +1055,10 @@ public final class VideoDetailFragment
player.setRecovery(); player.setRecovery();
} }
if (!useExternalAudioPlayer) { if (useExternalAudioPlayer) {
openNormalBackgroundPlayer(append); showExternalAudioPlaybackDialog();
} else { } else {
final List<AudioStream> audioStreams = getUrlAndNonTorrentStreams( openNormalBackgroundPlayer(append);
currentInfo.getAudioStreams());
final int index = ListHelper.getDefaultAudioFormat(activity, audioStreams);
if (index == -1) {
Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players,
Toast.LENGTH_SHORT).show();
return;
}
startOnExternalPlayer(activity, currentInfo, audioStreams.get(index));
} }
} }
@ -1117,7 +1111,7 @@ public final class VideoDetailFragment
if (PreferenceManager.getDefaultSharedPreferences(activity) if (PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) { .getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
showExternalPlaybackDialog(); showExternalVideoPlaybackDialog();
} else { } else {
replaceQueueIfUserConfirms(this::openMainPlayer); replaceQueueIfUserConfirms(this::openMainPlayer);
} }
@ -1451,14 +1445,14 @@ public final class VideoDetailFragment
super.showLoading(); super.showLoading();
//if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required //if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required
if (!ExtractorHelper.isCached(serviceId, url, InfoItem.InfoType.STREAM)) { if (!ExtractorHelper.isCached(serviceId, url, InfoCache.Type.STREAM)) {
binding.detailContentRootHiding.setVisibility(View.INVISIBLE); binding.detailContentRootHiding.setVisibility(View.INVISIBLE);
} }
animate(binding.detailThumbnailPlayButton, false, 50); animate(binding.detailThumbnailPlayButton, false, 50);
animate(binding.detailDurationView, false, 100); animate(binding.detailDurationView, false, 100);
animate(binding.detailPositionView, false, 100); binding.detailPositionView.setVisibility(View.GONE);
animate(binding.positionView, false, 50); binding.positionView.setVisibility(View.GONE);
binding.detailVideoTitleView.setText(title); binding.detailVideoTitleView.setText(title);
binding.detailVideoTitleView.setMaxLines(1); binding.detailVideoTitleView.setMaxLines(1);
@ -1497,19 +1491,11 @@ public final class VideoDetailFragment
binding.detailSubChannelThumbnailView.setVisibility(View.GONE); binding.detailSubChannelThumbnailView.setVisibility(View.GONE);
if (!isEmpty(info.getSubChannelName())) { if (!isEmpty(info.getSubChannelName())) {
displayBothUploaderAndSubChannel(info, activity); displayBothUploaderAndSubChannel(info);
} else if (!isEmpty(info.getUploaderName())) {
displayUploaderAsSubChannel(info, activity);
} else { } else {
binding.detailUploaderTextView.setVisibility(View.GONE); displayUploaderAsSubChannel(info);
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
} }
final Drawable buddyDrawable =
AppCompatResources.getDrawable(activity, R.drawable.placeholder_person);
binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable);
binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable);
if (info.getViewCount() >= 0) { if (info.getViewCount() >= 0) {
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) { if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
binding.detailViewCountView.setText(Localization.listeningCount(activity, binding.detailViewCountView.setText(Localization.listeningCount(activity,
@ -1575,13 +1561,14 @@ public final class VideoDetailFragment
binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE); binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE);
binding.detailSecondaryControlPanel.setVisibility(View.GONE); binding.detailSecondaryControlPanel.setVisibility(View.GONE);
updateProgressInfo(info); checkUpdateProgressInfo(info);
initThumbnailViews(info); PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailThumbnailImageView);
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView, showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator, disposables); binding.detailMetaInfoSeparator, disposables);
if (!isPlayerAvailable() || player.isStopped()) { if (!isPlayerAvailable() || player.isStopped()) {
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl()); updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnails());
} }
if (!info.getErrors().isEmpty()) { if (!info.getErrors().isEmpty()) {
@ -1613,27 +1600,30 @@ public final class VideoDetailFragment
noVideoStreams ? R.drawable.ic_headset_shadow : R.drawable.ic_play_arrow_shadow); noVideoStreams ? R.drawable.ic_headset_shadow : R.drawable.ic_play_arrow_shadow);
} }
private void displayUploaderAsSubChannel(final StreamInfo info, final Context context) { private void displayUploaderAsSubChannel(final StreamInfo info) {
binding.detailSubChannelTextView.setText(info.getUploaderName()); binding.detailSubChannelTextView.setText(info.getUploaderName());
binding.detailSubChannelTextView.setVisibility(View.VISIBLE); binding.detailSubChannelTextView.setVisibility(View.VISIBLE);
binding.detailSubChannelTextView.setSelected(true); binding.detailSubChannelTextView.setSelected(true);
if (info.getUploaderSubscriberCount() > -1) { if (info.getUploaderSubscriberCount() > -1) {
binding.detailUploaderTextView.setText( binding.detailUploaderTextView.setText(
Localization.shortSubscriberCount(context, info.getUploaderSubscriberCount())); Localization.shortSubscriberCount(activity, info.getUploaderSubscriberCount()));
binding.detailUploaderTextView.setVisibility(View.VISIBLE); binding.detailUploaderTextView.setVisibility(View.VISIBLE);
} else { } else {
binding.detailUploaderTextView.setVisibility(View.GONE); binding.detailUploaderTextView.setVisibility(View.GONE);
} }
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
} }
private void displayBothUploaderAndSubChannel(final StreamInfo info, final Context context) { private void displayBothUploaderAndSubChannel(final StreamInfo info) {
binding.detailSubChannelTextView.setText(info.getSubChannelName()); binding.detailSubChannelTextView.setText(info.getSubChannelName());
binding.detailSubChannelTextView.setVisibility(View.VISIBLE); binding.detailSubChannelTextView.setVisibility(View.VISIBLE);
binding.detailSubChannelTextView.setSelected(true); binding.detailSubChannelTextView.setSelected(true);
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
final StringBuilder subText = new StringBuilder(); final StringBuilder subText = new StringBuilder();
if (!isEmpty(info.getUploaderName())) { if (!isEmpty(info.getUploaderName())) {
subText.append( subText.append(
@ -1644,7 +1634,7 @@ public final class VideoDetailFragment
subText.append(Localization.DOT_SEPARATOR); subText.append(Localization.DOT_SEPARATOR);
} }
subText.append( subText.append(
Localization.shortSubscriberCount(context, info.getUploaderSubscriberCount())); Localization.shortSubscriberCount(activity, info.getUploaderSubscriberCount()));
} }
if (subText.length() > 0) { if (subText.length() > 0) {
@ -1654,6 +1644,13 @@ public final class VideoDetailFragment
} else { } else {
binding.detailUploaderTextView.setVisibility(View.GONE); binding.detailUploaderTextView.setVisibility(View.GONE);
} }
PicassoHelper.loadAvatar(info.getSubChannelAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailUploaderThumbnailView);
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
} }
public void openDownloadDialog() { public void openDownloadDialog() {
@ -1674,67 +1671,43 @@ public final class VideoDetailFragment
// Stream Results // Stream Results
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void updateProgressInfo(@NonNull final StreamInfo info) { private void checkUpdateProgressInfo(@NonNull final StreamInfo info) {
if (positionSubscriber != null) { if (positionSubscriber != null) {
positionSubscriber.dispose(); positionSubscriber.dispose();
} }
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); if (!getResumePlaybackEnabled(activity)) {
final boolean playbackResumeEnabled = prefs binding.positionView.setVisibility(View.GONE);
.getBoolean(activity.getString(R.string.enable_watch_history_key), true) binding.detailPositionView.setVisibility(View.GONE);
&& prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true);
final boolean showPlaybackPosition = prefs.getBoolean(
activity.getString(R.string.enable_playback_state_lists_key), true);
if (!playbackResumeEnabled) {
if (playQueue == null || playQueue.getStreams().isEmpty()
|| playQueue.getItem().getRecoveryPosition() == RECOVERY_UNSET
|| !showPlaybackPosition) {
binding.positionView.setVisibility(View.INVISIBLE);
binding.detailPositionView.setVisibility(View.GONE);
// TODO: Remove this check when separation of concerns is done.
// (live streams weren't getting updated because they are mixed)
if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
return;
}
} else {
// Show saved position from backStack if user allows it
showPlaybackProgress(playQueue.getItem().getRecoveryPosition(),
playQueue.getItem().getDuration() * 1000);
animate(binding.positionView, true, 500);
animate(binding.detailPositionView, true, 500);
}
return; return;
} }
final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext()); final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
// TODO: Separate concerns when updating database data.
// (move the updating part to when the loading happens)
positionSubscriber = recordManager.loadStreamState(info) positionSubscriber = recordManager.loadStreamState(info)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.onErrorComplete() .onErrorComplete()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(state -> { .subscribe(state -> {
showPlaybackProgress(state.getProgressMillis(), info.getDuration() * 1000); updatePlaybackProgress(
animate(binding.positionView, true, 500); state.getProgressMillis(), info.getDuration() * 1000);
animate(binding.detailPositionView, true, 500);
}, e -> { }, e -> {
if (DEBUG) { // impossible since the onErrorComplete()
e.printStackTrace();
}
}, () -> { }, () -> {
binding.positionView.setVisibility(View.GONE); binding.positionView.setVisibility(View.GONE);
binding.detailPositionView.setVisibility(View.GONE); binding.detailPositionView.setVisibility(View.GONE);
}); });
} }
private void showPlaybackProgress(final long progress, final long duration) { private void updatePlaybackProgress(final long progress, final long duration) {
if (!getResumePlaybackEnabled(activity)) {
return;
}
final int progressSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(progress); final int progressSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(progress);
final int durationSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(duration); final int durationSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(duration);
// If the old and the new progress values have a big difference then use // If the old and the new progress values have a big difference then use animation.
// animation. Otherwise don't because it affects CPU // Otherwise don't because it affects CPU
final boolean shouldAnimate = Math.abs(binding.positionView.getProgress() final int progressDifference = Math.abs(binding.positionView.getProgress()
- progressSeconds) > 2; - progressSeconds);
binding.positionView.setMax(durationSeconds); binding.positionView.setMax(durationSeconds);
if (shouldAnimate) { if (progressDifference > 2) {
binding.positionView.setProgressAnimated(progressSeconds); binding.positionView.setProgressAnimated(progressSeconds);
} else { } else {
binding.positionView.setProgress(progressSeconds); binding.positionView.setProgress(progressSeconds);
@ -1829,7 +1802,7 @@ public final class VideoDetailFragment
} }
if (player.getPlayQueue().getItem().getUrl().equals(url)) { if (player.getPlayQueue().getItem().getUrl().equals(url)) {
showPlaybackProgress(currentProgress, duration); updatePlaybackProgress(currentProgress, duration);
} }
} }
@ -1850,7 +1823,7 @@ public final class VideoDetailFragment
return; return;
} }
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl()); updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnails());
if (currentInfo != null && info.getUrl().equals(currentInfo.getUrl())) { if (currentInfo != null && info.getUrl().equals(currentInfo.getUrl())) {
return; return;
} }
@ -1879,7 +1852,7 @@ public final class VideoDetailFragment
if (currentInfo != null) { if (currentInfo != null) {
updateOverlayData(currentInfo.getName(), updateOverlayData(currentInfo.getName(),
currentInfo.getUploaderName(), currentInfo.getUploaderName(),
currentInfo.getThumbnailUrl()); currentInfo.getThumbnails());
} }
updateOverlayPlayQueueButtonVisibility(); updateOverlayPlayQueueButtonVisibility();
} }
@ -1961,17 +1934,15 @@ public final class VideoDetailFragment
return; return;
} }
final var window = activity.getWindow(); // Prevent jumping of the player on devices with cutout
final var windowInsetsController = WindowCompat.getInsetsController(window, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.getDecorView()); activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
WindowCompat.setDecorFitsSystemWindows(window, true); }
windowInsetsController.setSystemBarsBehavior(WindowInsetsControllerCompat activity.getWindow().getDecorView().setSystemUiVisibility(0);
.BEHAVIOR_SHOW_BARS_BY_TOUCH); activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
windowInsetsController.show(WindowInsetsCompat.Type.systemBars()); activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr(
requireContext(), android.R.attr.colorPrimary));
window.setStatusBarColor(ThemeHelper.resolveColorFromAttr(requireContext(),
android.R.attr.colorPrimary));
} }
private void hideSystemUi() { private void hideSystemUi() {
@ -1983,19 +1954,30 @@ public final class VideoDetailFragment
return; return;
} }
final var window = activity.getWindow(); // Prevent jumping of the player on devices with cutout
final var windowInsetsController = WindowCompat.getInsetsController(window, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.getDecorView()); activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
WindowCompat.setDecorFitsSystemWindows(window, false);
windowInsetsController.setSystemBarsBehavior(WindowInsetsControllerCompat
.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());
if (DeviceUtils.isInMultiWindow(activity) || isFullscreen()) {
window.setStatusBarColor(Color.TRANSPARENT);
window.setNavigationBarColor(Color.TRANSPARENT);
} }
int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
// In multiWindow mode status bar is not transparent for devices with cutout
// if I include this flag. So without it is better in this case
final boolean isInMultiWindow = DeviceUtils.isInMultiWindow(activity);
if (!isInMultiWindow) {
visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
}
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
if (isInMultiWindow || isFullscreen()) {
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
}
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
} }
// Listener implementation // Listener implementation
@ -2039,7 +2021,10 @@ public final class VideoDetailFragment
restoreDefaultBrightness(); restoreDefaultBrightness();
} else { } else {
// Do not restore if user has disabled brightness gesture // Do not restore if user has disabled brightness gesture
if (!PlayerHelper.isBrightnessGestureEnabled(activity)) { if (!PlayerHelper.getActionForRightGestureSide(activity)
.equals(getString(R.string.brightness_control_key))
&& !PlayerHelper.getActionForLeftGestureSide(activity)
.equals(getString(R.string.brightness_control_key))) {
return; return;
} }
// Restore already saved brightness level // Restore already saved brightness level
@ -2132,10 +2117,11 @@ public final class VideoDetailFragment
.setPositiveButton(R.string.ok, (dialog, which) -> { .setPositiveButton(R.string.ok, (dialog, which) -> {
onAllow.run(); onAllow.run();
dialog.dismiss(); dialog.dismiss();
}).show(); })
.show();
} }
private void showExternalPlaybackDialog() { private void showExternalVideoPlaybackDialog() {
if (currentInfo == null) { if (currentInfo == null) {
return; return;
} }
@ -2182,6 +2168,43 @@ public final class VideoDetailFragment
builder.show(); builder.show();
} }
private void showExternalAudioPlaybackDialog() {
if (currentInfo == null) {
return;
}
final List<AudioStream> audioStreams = getUrlAndNonTorrentStreams(
currentInfo.getAudioStreams());
final List<AudioStream> audioTracks =
ListHelper.getFilteredAudioStreams(activity, audioStreams);
if (audioTracks.isEmpty()) {
Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players,
Toast.LENGTH_SHORT).show();
} else if (audioTracks.size() == 1) {
startOnExternalPlayer(activity, currentInfo, audioTracks.get(0));
} else {
final int selectedAudioStream =
ListHelper.getDefaultAudioFormat(activity, audioTracks);
final CharSequence[] trackNames = audioTracks.stream()
.map(audioStream -> Localization.audioTrackName(activity, audioStream))
.toArray(CharSequence[]::new);
new AlertDialog.Builder(activity)
.setTitle(R.string.select_audio_track_external_players)
.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
ShareUtils.openUrlInBrowser(requireActivity(), url))
.setSingleChoiceItems(trackNames, selectedAudioStream, null)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok, (dialog, i) -> {
final int index = ((AlertDialog) dialog).getListView()
.getCheckedItemPosition();
startOnExternalPlayer(activity, currentInfo, audioTracks.get(index));
})
.show();
}
}
/* /*
* Remove unneeded information while waiting for a next task * Remove unneeded information while waiting for a next task
* */ * */
@ -2194,7 +2217,7 @@ public final class VideoDetailFragment
playerHolder.stopService(); playerHolder.stopService();
setInitialData(0, null, "", null); setInitialData(0, null, "", null);
currentInfo = null; currentInfo = null;
updateOverlayData(null, null, null); updateOverlayData(null, null, List.of());
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -2376,11 +2399,11 @@ public final class VideoDetailFragment
private void updateOverlayData(@Nullable final String overlayTitle, private void updateOverlayData(@Nullable final String overlayTitle,
@Nullable final String uploader, @Nullable final String uploader,
@Nullable final String thumbnailUrl) { @NonNull final List<Image> thumbnails) {
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle); binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader); binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
binding.overlayThumbnail.setImageDrawable(null); binding.overlayThumbnail.setImageDrawable(null);
PicassoHelper.loadDetailsThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG) PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.overlayThumbnail); .into(binding.overlayThumbnail);
} }

View File

@ -1,5 +1,7 @@
package org.schabi.newpipe.fragments.list; package org.schabi.newpipe.fragments.list;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
@ -7,13 +9,15 @@ import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.evernote.android.state.State;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo; import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.views.NewPipeRecyclerView; import org.schabi.newpipe.views.NewPipeRecyclerView;
@ -22,7 +26,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
@ -141,7 +144,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
currentWorker = loadResult(forceLoad) currentWorker = loadResult(forceLoad)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe((@NonNull L result) -> { .subscribe((@NonNull final L result) -> {
isLoading.set(false); isLoading.set(false);
currentInfo = result; currentInfo = result;
currentNextPage = result.getNextPage(); currentNextPage = result.getNextPage();
@ -229,13 +232,11 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
if (!result.getRelatedItems().isEmpty()) { if (!result.getRelatedItems().isEmpty()) {
infoListAdapter.addInfoItemList(result.getRelatedItems()); infoListAdapter.addInfoItemList(result.getRelatedItems());
showListFooter(hasMoreItems()); showListFooter(hasMoreItems());
} else if (hasMoreItems()) {
loadMoreItems();
} else { } else {
infoListAdapter.clearStreamItemList(); infoListAdapter.clearStreamItemList();
// showEmptyState should be called only if there is no item as showEmptyState();
// well as no header in infoListAdapter
if (!(result instanceof ChannelInfo && infoListAdapter.getItemCount() == 1)) {
showEmptyState();
}
} }
} }
@ -252,6 +253,20 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
} }
} }
@Override
public void showEmptyState() {
// show "no streams" for SoundCloud; otherwise "no videos"
// showing "no live streams" is handled in KioskFragment
if (emptyStateView != null) {
if (currentInfo.getService() == SoundCloud) {
setEmptyStateMessage(R.string.no_streams);
} else {
setEmptyStateMessage(R.string.no_videos);
}
}
super.showEmptyState();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Utils // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View File

@ -0,0 +1,94 @@
package org.schabi.newpipe.fragments.list.channel;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.evernote.android.state.State;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.fragments.detail.BaseDescriptionFragment;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import java.util.List;
public class ChannelAboutFragment extends BaseDescriptionFragment {
@State
protected ChannelInfo channelInfo;
ChannelAboutFragment(@NonNull final ChannelInfo channelInfo) {
this.channelInfo = channelInfo;
}
public ChannelAboutFragment() {
// keep empty constructor for State when resuming fragment from memory
}
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
binding.constraintLayout.setPadding(0, DeviceUtils.dpToPx(8, requireContext()), 0, 0);
}
@Nullable
@Override
protected Description getDescription() {
return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT);
}
@NonNull
@Override
protected StreamingService getService() {
return channelInfo.getService();
}
@Override
protected int getServiceId() {
return channelInfo.getServiceId();
}
@Nullable
@Override
protected String getStreamUrl() {
return null;
}
@NonNull
@Override
public List<String> getTags() {
return channelInfo.getTags();
}
@Override
protected void setupMetadata(final LayoutInflater inflater,
final LinearLayout layout) {
// There is no upload date available for channels, so hide the relevant UI element
binding.detailUploadDateView.setVisibility(View.GONE);
if (channelInfo == null) {
return;
}
if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) {
addMetadataItem(inflater, layout, false, R.string.metadata_subscribers,
Localization.localizeNumber(
requireContext(),
channelInfo.getSubscriberCount()));
}
addImagesMetadataItem(inflater, layout, R.string.metadata_avatars,
channelInfo.getAvatars());
addImagesMetadataItem(inflater, layout, R.string.metadata_banners,
channelInfo.getBanners());
}
}

View File

@ -5,6 +5,7 @@ import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor; import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color; import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
@ -16,51 +17,51 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.core.view.MenuProvider;
import androidx.preference.PreferenceManager;
import com.evernote.android.state.State;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.tabs.TabLayout;
import com.jakewharton.rxbinding4.view.RxView; import com.jakewharton.rxbinding4.view.RxView;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.NotificationMode; import org.schabi.newpipe.database.subscription.NotificationMode;
import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
import org.schabi.newpipe.databinding.FragmentChannelBinding; import org.schabi.newpipe.databinding.FragmentChannelBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.detail.TabAdapter;
import org.schabi.newpipe.ktx.AnimationType; 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.local.feed.notifications.NotificationHelper;
import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.util.ChannelTabHelper;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.List; import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.functions.Action; import io.reactivex.rxjava3.functions.Action;
@ -68,29 +69,38 @@ import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.functions.Function; import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, ChannelInfo> public class ChannelFragment extends BaseStateFragment<ChannelInfo>
implements View.OnClickListener { implements StateSaver.WriteRead {
private static final int BUTTON_DEBOUNCE_INTERVAL = 100; private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG"; private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@State
protected String name;
@State
protected String url;
private ChannelInfo currentInfo;
private Disposable currentWorker;
private final CompositeDisposable disposables = new CompositeDisposable(); private final CompositeDisposable disposables = new CompositeDisposable();
private Disposable subscribeButtonMonitor; private Disposable subscribeButtonMonitor;
private SubscriptionManager subscriptionManager;
private int lastTab;
private boolean channelContentNotSupported = false; private boolean channelContentNotSupported = false;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Views // Views
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private SubscriptionManager subscriptionManager; private FragmentChannelBinding binding;
private TabAdapter tabAdapter;
private FragmentChannelBinding channelBinding;
private ChannelHeaderBinding headerBinding;
private PlaylistControlBinding playlistControlBinding;
private MenuItem menuRssButton; private MenuItem menuRssButton;
private MenuItem menuNotifyButton; private MenuItem menuNotifyButton;
private SubscriptionEntity channelSubscription;
private MenuProvider menuProvider;
public static ChannelFragment getInstance(final int serviceId, final String url, public static ChannelFragment getInstance(final int serviceId, final String url,
final String name) { final String name) {
@ -99,22 +109,78 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
return instance; return instance;
} }
public ChannelFragment() { private void setInitialData(final int sid, final String u, final String title) {
super(UserAction.REQUESTED_CHANNEL); this.serviceId = sid;
this.url = u;
this.name = !TextUtils.isEmpty(title) ? title : "";
} }
@Override
public void onResume() {
super.onResume();
if (activity != null && useAsFrontPage) {
setTitle(currentInfo != null ? currentInfo.getName() : name);
}
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// LifeCycle // LifeCycle
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
menuProvider = new MenuProvider() {
@Override
public void onCreateMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
inflater.inflate(R.menu.menu_channel, menu);
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
}
@Override
public void onPrepareMenu(@NonNull final Menu menu) {
menuRssButton = menu.findItem(R.id.menu_item_rss);
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
updateRssButton();
updateNotifyButton(channelSubscription);
}
@Override
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_notify:
final boolean value = !item.isChecked();
item.setEnabled(false);
setNotify(value);
break;
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_rss:
if (currentInfo != null) {
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
}
break;
case R.id.menu_item_openInBrowser:
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(requireContext(),
currentInfo.getOriginalUrl());
}
break;
case R.id.menu_item_share:
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name,
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
}
break;
default:
return false;
}
return true;
}
};
activity.addMenuProvider(menuProvider);
}
@Override @Override
public void onAttach(@NonNull final Context context) { public void onAttach(@NonNull final Context context) {
super.onAttach(context); super.onAttach(context);
@ -125,104 +191,57 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
public View onCreateView(@NonNull final LayoutInflater inflater, public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) { @Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_channel, container, false); binding = FragmentChannelBinding.inflate(inflater, container, false);
return binding.getRoot();
} }
@Override @Override // called from onViewCreated in BaseFragment.onViewCreated
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
channelBinding = FragmentChannelBinding.bind(rootView);
showContentNotSupportedIfNeeded();
}
@Override tabAdapter = new TabAdapter(getChildFragmentManager());
public void onDestroy() { binding.viewPager.setAdapter(tabAdapter);
super.onDestroy(); binding.tabLayout.setupWithViewPager(binding.viewPager);
disposables.clear();
if (subscribeButtonMonitor != null) { setTitle(name);
subscribeButtonMonitor.dispose(); binding.channelTitleView.setText(name);
if (!ImageStrategy.shouldLoadImages()) {
// do not waste space for the banner if it is not going to be loaded
binding.channelBannerImage.setImageDrawable(null);
} }
channelBinding = null;
headerBinding = null;
playlistControlBinding = null;
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Supplier<View> getListHeaderSupplier() {
headerBinding = ChannelHeaderBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
playlistControlBinding = headerBinding.playlistControl;
return headerBinding::getRoot;
} }
@Override @Override
protected void initListeners() { protected void initListeners() {
super.initListeners(); super.initListeners();
headerBinding.subChannelTitleView.setOnClickListener(this); final View.OnClickListener openSubChannel = v -> {
headerBinding.subChannelAvatarView.setOnClickListener(this); if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) {
} try {
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
/*////////////////////////////////////////////////////////////////////////// currentInfo.getParentChannelUrl(),
// Menu currentInfo.getParentChannelName());
//////////////////////////////////////////////////////////////////////////*/ } catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
@Override }
public void onCreateOptionsMenu(@NonNull final Menu menu, } else if (DEBUG) {
@NonNull final MenuInflater inflater) { Log.i(TAG, "Can't open parent channel because we got no channel URL");
super.onCreateOptionsMenu(menu, inflater);
final ActionBar supportActionBar = activity.getSupportActionBar();
if (useAsFrontPage && supportActionBar != null) {
supportActionBar.setDisplayHomeAsUpEnabled(false);
} else {
inflater.inflate(R.menu.menu_channel, menu);
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
} }
menuRssButton = menu.findItem(R.id.menu_item_rss); };
menuNotifyButton = menu.findItem(R.id.menu_item_notify); binding.subChannelAvatarView.setOnClickListener(openSubChannel);
} binding.subChannelTitleView.setOnClickListener(openSubChannel);
} }
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public void onDestroy() {
switch (item.getItemId()) { super.onDestroy();
case R.id.action_settings: if (currentWorker != null) {
NavigationHelper.openSettings(requireContext()); currentWorker.dispose();
break;
case R.id.menu_item_notify:
final boolean value = !item.isChecked();
item.setEnabled(false);
setNotify(value);
break;
case R.id.menu_item_rss:
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(
requireContext(), currentInfo.getFeedUrl(), false);
}
break;
case R.id.menu_item_openInBrowser:
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl());
}
break;
case R.id.menu_item_share:
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
currentInfo.getAvatarUrl());
}
break;
default:
return super.onOptionsItemSelected(item);
} }
return true; disposables.clear();
binding = null;
activity.removeMenuProvider(menuProvider);
menuProvider = null;
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -230,8 +249,8 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void monitorSubscription(final ChannelInfo info) { private void monitorSubscription(final ChannelInfo info) {
final Consumer<Throwable> onError = (Throwable throwable) -> { final Consumer<Throwable> onError = (final Throwable throwable) -> {
animate(headerBinding.channelSubscribeButton, false, 100); animate(binding.channelSubscribeButton, false, 100);
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET, showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
"Get subscription status", currentInfo)); "Get subscription status", currentInfo));
}; };
@ -264,16 +283,15 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
}, onError)); }, onError));
} }
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription, private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
final ChannelInfo info) { return (@NonNull final Object o) -> {
return (@NonNull Object o) -> { subscriptionManager.insertSubscription(subscription);
subscriptionManager.insertSubscription(subscription, info);
return o; return o;
}; };
} }
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) { private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
return (@NonNull Object o) -> { return (@NonNull final Object o) -> {
subscriptionManager.deleteSubscription(subscription); subscriptionManager.deleteSubscription(subscription);
return o; return o;
}; };
@ -299,9 +317,8 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
.subscribe(onComplete, onError)); .subscribe(onComplete, onError));
} }
private Disposable monitorSubscribeButton(final Button subscribeButton, private Disposable monitorSubscribeButton(final Function<Object, Object> action) {
final Function<Object, Object> action) { final Consumer<Object> onNext = (@NonNull final Object o) -> {
final Consumer<Object> onNext = (@NonNull Object o) -> {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Changed subscription status to this channel!"); Log.d(TAG, "Changed subscription status to this channel!");
} }
@ -312,7 +329,7 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
"Changing subscription for " + currentInfo.getUrl(), currentInfo)); "Changing subscription for " + currentInfo.getUrl(), currentInfo));
/* Emit clicks from main thread unto io thread */ /* Emit clicks from main thread unto io thread */
return RxView.clicks(subscribeButton) return RxView.clicks(binding.channelSubscribeButton)
.subscribeOn(AndroidSchedulers.mainThread()) .subscribeOn(AndroidSchedulers.mainThread())
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks .debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks
@ -321,7 +338,7 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
} }
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) { private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
return (List<SubscriptionEntity> subscriptionEntities) -> { return (final List<SubscriptionEntity> subscriptionEntities) -> {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: " Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
+ "subscriptionEntities = [" + subscriptionEntities + "]"); + "subscriptionEntities = [" + subscriptionEntities + "]");
@ -338,20 +355,20 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
channel.setServiceId(info.getServiceId()); channel.setServiceId(info.getServiceId());
channel.setUrl(info.getUrl()); channel.setUrl(info.getUrl());
channel.setData(info.getName(), channel.setData(info.getName(),
info.getAvatarUrl(), ImageStrategy.imageListToDbUrl(info.getAvatars()),
info.getDescription(), info.getDescription(),
info.getSubscriberCount()); info.getSubscriberCount());
channelSubscription = null;
updateNotifyButton(null); updateNotifyButton(null);
subscribeButtonMonitor = monitorSubscribeButton( subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel));
headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info));
} else { } else {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Found subscription to this channel!"); Log.d(TAG, "Found subscription to this channel!");
} }
final SubscriptionEntity subscription = subscriptionEntities.get(0); channelSubscription = subscriptionEntities.get(0);
updateNotifyButton(subscription); updateNotifyButton(channelSubscription);
subscribeButtonMonitor = monitorSubscribeButton( subscribeButtonMonitor =
headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription)); monitorSubscribeButton(mapOnUnsubscribe(channelSubscription));
} }
}; };
} }
@ -362,34 +379,40 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
+ "isSubscribed = [" + isSubscribed + "]"); + "isSubscribed = [" + isSubscribed + "]");
} }
final boolean isButtonVisible = headerBinding.channelSubscribeButton.getVisibility() final boolean isButtonVisible = binding.channelSubscribeButton.getVisibility()
== View.VISIBLE; == View.VISIBLE;
final int backgroundDuration = isButtonVisible ? 300 : 0; final int backgroundDuration = isButtonVisible ? 300 : 0;
final int textDuration = isButtonVisible ? 200 : 0; final int textDuration = isButtonVisible ? 200 : 0;
final int subscribeBackground = ThemeHelper
.resolveColorFromAttr(activity, R.attr.colorPrimary);
final int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
final int subscribedBackground = ContextCompat final int subscribedBackground = ContextCompat
.getColor(activity, R.color.subscribed_background_color); .getColor(activity, R.color.subscribed_background_color);
final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color); final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
final int subscribeBackground = ColorUtils.blendARGB(ThemeHelper
.resolveColorFromAttr(activity, R.attr.colorPrimary), subscribedBackground, 0.35f);
final int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
if (!isSubscribed) { if (isSubscribed) {
headerBinding.channelSubscribeButton.setText(R.string.subscribe_button_title); binding.channelSubscribeButton.setText(R.string.subscribed_button_title);
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration, animateBackgroundColor(binding.channelSubscribeButton, backgroundDuration,
subscribedBackground, subscribeBackground);
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribedText,
subscribeText);
} else {
headerBinding.channelSubscribeButton.setText(R.string.subscribed_button_title);
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
subscribeBackground, subscribedBackground); subscribeBackground, subscribedBackground);
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribeText, animateTextColor(binding.channelSubscribeButton, textDuration, subscribeText,
subscribedText); subscribedText);
} else {
binding.channelSubscribeButton.setText(R.string.subscribe_button_title);
animateBackgroundColor(binding.channelSubscribeButton, backgroundDuration,
subscribedBackground, subscribeBackground);
animateTextColor(binding.channelSubscribeButton, textDuration, subscribedText,
subscribeText);
} }
animate(headerBinding.channelSubscribeButton, true, 100, animate(binding.channelSubscribeButton, true, 100, AnimationType.LIGHT_SCALE_AND_ALPHA);
AnimationType.LIGHT_SCALE_AND_ALPHA); }
private void updateRssButton() {
if (menuRssButton == null || currentInfo == null) {
return;
}
menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl()));
} }
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) { private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
@ -425,107 +448,176 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
* Show a snackbar with the option to enable notifications on new streams for this channel. * Show a snackbar with the option to enable notifications on new streams for this channel.
*/ */
private void showNotifySnackbar() { private void showNotifySnackbar() {
Snackbar.make(itemsList, R.string.you_successfully_subscribed, Snackbar.LENGTH_LONG) Snackbar.make(binding.getRoot(), R.string.you_successfully_subscribed, Snackbar.LENGTH_LONG)
.setAction(R.string.get_notified, v -> setNotify(true)) .setAction(R.string.get_notified, v -> setNotify(true))
.setActionTextColor(Color.YELLOW) .setActionTextColor(Color.YELLOW)
.show(); .show();
} }
/*//////////////////////////////////////////////////////////////////////////
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.InfoItemsPage<StreamInfoItem>> loadMoreItemsLogic() {
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage);
}
@Override
protected Single<ChannelInfo> loadResult(final boolean forceLoad) {
return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// OnClick // Init
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override private void updateTabs() {
public void onClick(final View v) { tabAdapter.clearAllItems();
if (isLoading.get() || currentInfo == null) {
return;
}
switch (v.getId()) { if (currentInfo != null && !channelContentNotSupported) {
case R.id.sub_channel_avatar_view: final Context context = requireContext();
case R.id.sub_channel_title_view: final SharedPreferences preferences = PreferenceManager
if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) { .getDefaultSharedPreferences(context);
try {
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(), for (final ListLinkHandler linkHandler : currentInfo.getTabs()) {
currentInfo.getParentChannelUrl(), final String tab = linkHandler.getContentFilters().get(0);
currentInfo.getParentChannelName()); if (ChannelTabHelper.showChannelTab(context, preferences, tab)) {
} catch (final Exception e) { final ChannelTabFragment channelTabFragment =
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e); ChannelTabFragment.getInstance(serviceId, linkHandler, name);
} channelTabFragment.useAsFrontPage(useAsFrontPage);
} else if (DEBUG) { tabAdapter.addFragment(channelTabFragment,
Log.i(TAG, "Can't open parent channel because we got no channel URL"); context.getString(ChannelTabHelper.getTranslationKey(tab)));
} }
break; }
if (ChannelTabHelper.showChannelTab(
context, preferences, R.string.show_channel_tabs_about)) {
tabAdapter.addFragment(
new ChannelAboutFragment(currentInfo),
context.getString(R.string.channel_tab_about));
}
}
tabAdapter.notifyDataSetUpdate();
for (int i = 0; i < tabAdapter.getCount(); i++) {
binding.tabLayout.getTabAt(i).setText(tabAdapter.getItemTitle(i));
}
// Restore previously selected tab
final TabLayout.Tab ltab = binding.tabLayout.getTabAt(lastTab);
if (ltab != null) {
binding.tabLayout.selectTab(ltab);
} }
} }
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/
@Override
public String generateSuffix() {
return null;
}
@Override
public void writeTo(final Queue<Object> objectsToSave) {
objectsToSave.add(currentInfo);
objectsToSave.add(binding == null ? 0 : binding.tabLayout.getSelectedTabPosition());
}
@Override
public void readFrom(@NonNull final Queue<Object> savedObjects) {
currentInfo = (ChannelInfo) savedObjects.poll();
lastTab = (Integer) savedObjects.poll();
}
@Override
public void onSaveInstanceState(final @NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (binding != null) {
outState.putInt("LastTab", binding.tabLayout.getSelectedTabPosition());
}
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
lastTab = savedInstanceState.getInt("LastTab", 0);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Contract // Contract
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override
protected void doInitialLoadLogic() {
if (currentInfo == null) {
startLoading(false);
} else {
handleResult(currentInfo);
}
}
@Override
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);
currentInfo = null;
updateTabs();
if (currentWorker != null) {
currentWorker.dispose();
}
runWorker(forceLoad);
}
private void runWorker(final boolean forceLoad) {
currentWorker = ExtractorHelper.getChannelInfo(serviceId, url, forceLoad)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
isLoading.set(false);
handleResult(result);
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL,
url == null ? "No URL" : url, serviceId)));
}
@Override @Override
public void showLoading() { public void showLoading() {
super.showLoading(); super.showLoading();
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG); PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
animate(headerBinding.channelSubscribeButton, false, 100); animate(binding.channelSubscribeButton, false, 100);
} }
@Override @Override
public void handleResult(@NonNull final ChannelInfo result) { public void handleResult(@NonNull final ChannelInfo result) {
super.handleResult(result); super.handleResult(result);
currentInfo = result;
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
headerBinding.getRoot().setVisibility(View.VISIBLE); if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG) PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
.into(headerBinding.channelBannerImage); .into(binding.channelBannerImage);
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG) } else {
.into(headerBinding.channelAvatarView); // do not waste space for the banner, if the user disabled images or there is not one
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG) binding.channelBannerImage.setImageDrawable(null);
.into(headerBinding.subChannelAvatarView); }
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE); PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelAvatarView);
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG)
.into(binding.subChannelAvatarView);
binding.channelTitleView.setText(result.getName());
binding.channelSubscriberView.setVisibility(View.VISIBLE);
if (result.getSubscriberCount() >= 0) { if (result.getSubscriberCount() >= 0) {
headerBinding.channelSubscriberView.setText(Localization binding.channelSubscriberView.setText(Localization
.shortSubscriberCount(activity, result.getSubscriberCount())); .shortSubscriberCount(activity, result.getSubscriberCount()));
} else { } else {
headerBinding.channelSubscriberView.setText(R.string.subscribers_count_not_available); binding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
} }
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) { if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
headerBinding.subChannelTitleView.setText(String.format( binding.subChannelTitleView.setText(String.format(
getString(R.string.channel_created_by), getString(R.string.channel_created_by),
currentInfo.getParentChannelName()) currentInfo.getParentChannelName())
); );
headerBinding.subChannelTitleView.setVisibility(View.VISIBLE); binding.subChannelTitleView.setVisibility(View.VISIBLE);
headerBinding.subChannelAvatarView.setVisibility(View.VISIBLE); binding.subChannelAvatarView.setVisibility(View.VISIBLE);
} else {
headerBinding.subChannelTitleView.setVisibility(View.GONE);
} }
if (menuRssButton != null) { updateRssButton();
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
}
// PlaylistControls should be visible only if there is some item in
// infoListAdapter other than header
if (infoListAdapter.getItemCount() != 1) {
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
} else {
playlistControlBinding.getRoot().setVisibility(View.GONE);
}
channelContentNotSupported = false; channelContentNotSupported = false;
for (final Throwable throwable : result.getErrors()) { for (final Throwable throwable : result.getErrors()) {
@ -540,62 +632,21 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
if (subscribeButtonMonitor != null) { if (subscribeButtonMonitor != null) {
subscribeButtonMonitor.dispose(); subscribeButtonMonitor.dispose();
} }
updateTabs();
updateSubscription(result); updateSubscription(result);
monitorSubscription(result); monitorSubscription(result);
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.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
return true;
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
return true;
});
} }
private void showContentNotSupportedIfNeeded() { private void showContentNotSupportedIfNeeded() {
// channelBinding might not be initialized when handleResult() is called // channelBinding might not be initialized when handleResult() is called
// (e.g. after rotating the screen, #6696) // (e.g. after rotating the screen, #6696)
if (!channelContentNotSupported || channelBinding == null) { if (!channelContentNotSupported || binding == null) {
return; return;
} }
channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE); binding.errorContentNotSupported.setVisibility(View.VISIBLE);
channelBinding.channelKaomoji.setText("(︶︹︺)"); binding.channelKaomoji.setText("(︶︹︺)");
channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f); binding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
channelBinding.channelNoVideos.setVisibility(View.GONE);
}
private PlayQueue getPlayQueue() {
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, 0);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setTitle(final String title) {
super.setTitle(title);
if (!useAsFrontPage) {
headerBinding.channelTitleView.setText(title);
}
} }
} }

View File

@ -0,0 +1,170 @@
package org.schabi.newpipe.fragments.list.channel;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.evernote.android.state.State;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.linkhandler.ReadyChannelTabListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.util.ChannelTabHelper;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.PlayButtonHelper;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.reactivex.rxjava3.core.Single;
public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo>
implements PlaylistControlViewHolder {
// states must be protected and not private for State being able to access them
@State
protected ListLinkHandler tabHandler;
@State
protected String channelName;
private PlaylistControlBinding playlistControlBinding;
@NonNull
public static ChannelTabFragment getInstance(final int serviceId,
final ListLinkHandler tabHandler,
final String channelName) {
final ChannelTabFragment instance = new ChannelTabFragment();
instance.serviceId = serviceId;
instance.tabHandler = tabHandler;
instance.channelName = channelName;
return instance;
}
public ChannelTabFragment() {
super(UserAction.REQUESTED_CHANNEL);
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(false);
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_channel_tab, container, false);
}
@Override
public void onDestroyView() {
super.onDestroyView();
playlistControlBinding = null;
}
@Override
protected Supplier<View> getListHeaderSupplier() {
if (ChannelTabHelper.isStreamsTab(tabHandler)) {
playlistControlBinding = PlaylistControlBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
return playlistControlBinding::getRoot;
}
return null;
}
@Override
protected Single<ChannelTabInfo> loadResult(final boolean forceLoad) {
return ExtractorHelper.getChannelTab(serviceId, tabHandler, forceLoad);
}
@Override
protected Single<ListExtractor.InfoItemsPage<InfoItem>> loadMoreItemsLogic() {
return ExtractorHelper.getMoreChannelTabItems(serviceId, tabHandler, currentNextPage);
}
@Override
public void setTitle(final String title) {
// The channel name is displayed as title in the toolbar.
// The title is always a description of the content of the tab fragment.
// It should be unique for each channel because multiple channel tabs
// can be added to the main page. Therefore, the channel name is used.
// Using the title variable would cause the title to be the same for all channel tabs.
super.setTitle(channelName);
}
@Override
public void handleResult(@NonNull final ChannelTabInfo result) {
super.handleResult(result);
// FIXME this is a really hacky workaround, to avoid storing useless data in the fragment
// state. The problem is, `ReadyChannelTabListLinkHandler` might contain raw JSON data that
// uses a lot of memory (e.g. ~800KB for YouTube). While 800KB doesn't seem much, if
// you combine just a couple of channel tab fragments you easily go over the 1MB
// save&restore transaction limit, and get `TransactionTooLargeException`s. A proper
// solution would require rethinking about `ReadyChannelTabListLinkHandler`s.
if (tabHandler instanceof ReadyChannelTabListLinkHandler) {
try {
// once `handleResult` is called, the parsed data was already saved to cache, so
// we can discard any raw data in ReadyChannelTabListLinkHandler and create a
// link handler with identical properties, but without any raw data
final ListLinkHandlerFactory channelTabLHFactory = result.getService()
.getChannelTabLHFactory();
if (channelTabLHFactory != null) {
// some services do not not have a ChannelTabLHFactory
tabHandler = channelTabLHFactory.fromQuery(tabHandler.getId(),
tabHandler.getContentFilters(), tabHandler.getSortFilter());
}
} catch (final ParsingException e) {
// silently ignore the error, as the app can continue to function normally
Log.w(TAG, "Could not recreate channel tab handler", e);
}
}
if (playlistControlBinding != null) {
// PlaylistControls should be visible only if there is some item in
// infoListAdapter other than header
if (infoListAdapter.getItemCount() > 1) {
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
} else {
playlistControlBinding.getRoot().setVisibility(View.GONE);
}
PlayButtonHelper.initPlaylistControlClickListener(
activity, playlistControlBinding, this);
}
}
@Override
public PlayQueue getPlayQueue() {
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
.filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast)
.collect(Collectors.toList());
return new ChannelTabPlayQueue(currentInfo.getServiceId(), tabHandler,
currentInfo.getNextPage(), streamItems, 0);
}
}

View File

@ -0,0 +1,171 @@
package org.schabi.newpipe.fragments.list.comments;
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.text.HtmlCompat;
import com.evernote.android.state.State;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.CommentRepliesHeaderBinding;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.ItemViewMode;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextLinkifier;
import java.util.Queue;
import java.util.function.Supplier;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public final class CommentRepliesFragment
extends BaseListInfoFragment<CommentsInfoItem, CommentRepliesInfo> {
public static final String TAG = CommentRepliesFragment.class.getSimpleName();
@State
CommentsInfoItem commentsInfoItem; // the comment to show replies of
private final CompositeDisposable disposables = new CompositeDisposable();
/*//////////////////////////////////////////////////////////////////////////
// Constructors and lifecycle
//////////////////////////////////////////////////////////////////////////*/
// only called by the Android framework, after which readFrom is called and restores all data
public CommentRepliesFragment() {
super(UserAction.REQUESTED_COMMENT_REPLIES);
}
public CommentRepliesFragment(@NonNull final CommentsInfoItem commentsInfoItem) {
this();
this.commentsInfoItem = commentsInfoItem;
// setting "" as title since the title will be properly set right after
setInitialData(commentsInfoItem.getServiceId(), commentsInfoItem.getUrl(), "");
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_comments, container, false);
}
@Override
public void onDestroyView() {
disposables.clear();
super.onDestroyView();
}
@Override
protected Supplier<View> getListHeaderSupplier() {
return () -> {
final CommentRepliesHeaderBinding binding = CommentRepliesHeaderBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
final CommentsInfoItem item = commentsInfoItem;
// load the author avatar
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar);
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
? View.VISIBLE : View.GONE);
// setup author name and comment date
binding.authorName.setText(item.getUploaderName());
binding.uploadDate.setText(Localization.relativeTimeOrTextual(
getContext(), item.getUploadDate(), item.getTextualUploadDate()));
binding.authorTouchArea.setOnClickListener(
v -> NavigationHelper.openCommentAuthorIfPresent(requireActivity(), item));
// setup like count, hearted and pinned
binding.thumbsUpCount.setText(
Localization.likeCount(requireContext(), item.getLikeCount()));
// for heartImage goneMarginEnd was used, but there is no way to tell ConstraintLayout
// not to use a different margin only when both the next two views are gone
((ConstraintLayout.LayoutParams) binding.thumbsUpCount.getLayoutParams())
.setMarginEnd(DeviceUtils.dpToPx(
(item.isHeartedByUploader() || item.isPinned() ? 8 : 16),
requireContext()));
binding.heartImage.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
binding.pinnedImage.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
// setup comment content
TextLinkifier.fromDescription(binding.commentContent, item.getCommentText(),
HtmlCompat.FROM_HTML_MODE_LEGACY, getServiceById(item.getServiceId()),
item.getUrl(), disposables, null);
return binding.getRoot();
};
}
/*//////////////////////////////////////////////////////////////////////////
// State saving
//////////////////////////////////////////////////////////////////////////*/
@Override
public void writeTo(final Queue<Object> objectsToSave) {
super.writeTo(objectsToSave);
objectsToSave.add(commentsInfoItem);
}
@Override
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
super.readFrom(savedObjects);
commentsInfoItem = (CommentsInfoItem) savedObjects.poll();
}
/*//////////////////////////////////////////////////////////////////////////
// Data loading
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<CommentRepliesInfo> loadResult(final boolean forceLoad) {
return Single.fromCallable(() -> new CommentRepliesInfo(commentsInfoItem,
// the reply count string will be shown as the activity title
Localization.replyCount(requireContext(), commentsInfoItem.getReplyCount())));
}
@Override
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
// commentsInfoItem.getUrl() should contain the url of the original
// ListInfo<CommentsInfoItem>, which should be the stream url
return ExtractorHelper.getMoreCommentItems(
serviceId, commentsInfoItem.getUrl(), currentNextPage);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Override
protected ItemViewMode getItemViewMode() {
return ItemViewMode.LIST;
}
/**
* @return the comment to which the replies are shown
*/
public CommentsInfoItem getCommentsInfoItem() {
return commentsInfoItem;
}
}

View File

@ -0,0 +1,22 @@
package org.schabi.newpipe.fragments.list.comments;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import java.util.Collections;
public final class CommentRepliesInfo extends ListInfo<CommentsInfoItem> {
/**
* This class is used to wrap the comment replies page into a ListInfo object.
*
* @param comment the comment from which to get replies
* @param name will be shown as the fragment title
*/
public CommentRepliesInfo(final CommentsInfoItem comment, final String name) {
super(comment.getServiceId(),
new ListLinkHandler("", "", "", Collections.emptyList(), null), name);
setNextPage(comment.getReplies());
setRelatedItems(Collections.emptyList()); // since it must be non-null
}
}

View File

@ -110,4 +110,14 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, Com
protected ItemViewMode getItemViewMode() { protected ItemViewMode getItemViewMode() {
return ItemViewMode.LIST; return ItemViewMode.LIST;
} }
public boolean scrollToComment(final CommentsInfoItem comment) {
final int position = infoListAdapter.getItemsList().indexOf(comment);
if (position < 0) {
return false;
}
itemsList.scrollToPosition(position);
return true;
}
} }

View File

@ -11,23 +11,26 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import com.evernote.android.state.State;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.kiosk.KioskInfo; import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCLiveStreamKiosk;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import icepick.State;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
/** /**
@ -161,4 +164,14 @@ public class KioskFragment extends BaseListInfoFragment<StreamInfoItem, KioskInf
name = kioskTranslatedName; name = kioskTranslatedName;
setTitle(kioskTranslatedName); setTitle(kioskTranslatedName);
} }
@Override
public void showEmptyState() {
// show "no live streams" for live stream kiosk
super.showEmptyState();
if (MediaCCCLiveStreamKiosk.KIOSK_ID.equals(currentInfo.getId())
&& ServiceList.MediaCCC.getServiceId() == currentInfo.getServiceId()) {
setEmptyStateMessage(R.string.no_live_streams);
}
}
} }

View File

@ -0,0 +1,11 @@
package org.schabi.newpipe.fragments.list.playlist;
import org.schabi.newpipe.player.playqueue.PlayQueue;
/**
* Interface for {@code R.layout.playlist_control} view holders
* to give access to the play queue.
*/
public interface PlaylistControlViewHolder {
PlayQueue getPlayQueue();
}

View File

@ -1,7 +1,9 @@
package org.schabi.newpipe.fragments.list.playlist; package org.schabi.newpipe.fragments.list.playlist;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.ktx.ViewUtils.animate; import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
@ -37,20 +39,22 @@ import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper; import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextEllipsizer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -64,7 +68,8 @@ import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo> { public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
implements PlaylistControlViewHolder {
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG"; private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
@ -84,6 +89,9 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
private MenuItem playlistBookmarkButton; private MenuItem playlistBookmarkButton;
private long streamCount;
private long playlistOverallDurationSeconds;
public static PlaylistFragment getInstance(final int serviceId, final String url, public static PlaylistFragment getInstance(final int serviceId, final String url,
final String name) { final String name) {
final PlaylistFragment instance = new PlaylistFragment(); final PlaylistFragment instance = new PlaylistFragment();
@ -233,7 +241,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
break; break;
case R.id.menu_item_share: case R.id.menu_item_share:
ShareUtils.shareText(requireContext(), name, url, ShareUtils.shareText(requireContext(), name, url,
currentInfo == null ? null : currentInfo.getThumbnailUrl()); currentInfo == null ? List.of() : currentInfo.getThumbnails());
break; break;
case R.id.menu_item_bookmark: case R.id.menu_item_bookmark:
onBookmarkClicked(); onBookmarkClicked();
@ -272,6 +280,12 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
animate(headerBinding.uploaderLayout, false, 200); animate(headerBinding.uploaderLayout, false, 200);
} }
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
setStreamCountAndOverallDuration(result.getItems(), !result.hasNextPage());
}
@Override @Override
public void handleResult(@NonNull final PlaylistInfo result) { public void handleResult(@NonNull final PlaylistInfo result) {
super.handleResult(result); super.handleResult(result);
@ -298,7 +312,6 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
playlistControlBinding.getRoot().setVisibility(View.VISIBLE); playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
final String avatarUrl = result.getUploaderAvatarUrl();
if (result.getServiceId() == ServiceList.YouTube.getServiceId() if (result.getServiceId() == ServiceList.YouTube.getServiceId()
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId()) && (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) { || YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
@ -314,12 +327,36 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
R.drawable.ic_radio) R.drawable.ic_radio)
); );
} else { } else {
PicassoHelper.loadAvatar(avatarUrl).tag(PICASSO_PLAYLIST_TAG) PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG)
.into(headerBinding.uploaderAvatarView); .into(headerBinding.uploaderAvatarView);
} }
headerBinding.playlistStreamCount.setText(Localization streamCount = result.getStreamCount();
.localizeStreamCount(getContext(), result.getStreamCount())); setStreamCountAndOverallDuration(result.getRelatedItems(), !result.hasNextPage());
final Description description = result.getDescription();
if (description != null && description != Description.EMPTY_DESCRIPTION
&& !isBlank(description.getContent())) {
final TextEllipsizer ellipsizer = new TextEllipsizer(
headerBinding.playlistDescription, 5, getServiceById(result.getServiceId()));
ellipsizer.setStateChangeListener(isEllipsized ->
headerBinding.playlistDescriptionReadMore.setText(
Boolean.TRUE.equals(isEllipsized) ? R.string.show_more : R.string.show_less
));
ellipsizer.setOnContentChanged(canBeEllipsized -> {
headerBinding.playlistDescriptionReadMore.setVisibility(
Boolean.TRUE.equals(canBeEllipsized) ? View.VISIBLE : View.GONE);
if (Boolean.TRUE.equals(canBeEllipsized)) {
ellipsizer.ellipsize();
}
});
ellipsizer.setContent(description);
headerBinding.playlistDescriptionReadMore.setOnClickListener(v -> ellipsizer.toggle());
headerBinding.playlistDescription.setOnClickListener(v -> ellipsizer.toggle());
} else {
headerBinding.playlistDescription.setVisibility(View.GONE);
headerBinding.playlistDescriptionReadMore.setVisibility(View.GONE);
}
if (!result.getErrors().isEmpty()) { if (!result.getErrors().isEmpty()) {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST, showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
@ -332,25 +369,10 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistBookmarkSubscriber()); .subscribe(getPlaylistBookmarkSubscriber());
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
return true;
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
return true;
});
} }
private PlayQueue getPlayQueue() { public PlayQueue getPlayQueue() {
return getPlayQueue(0); return getPlayQueue(0);
} }
@ -474,4 +496,20 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
playlistBookmarkButton.setIcon(drawable); playlistBookmarkButton.setIcon(drawable);
playlistBookmarkButton.setTitle(titleRes); playlistBookmarkButton.setTitle(titleRes);
} }
private void setStreamCountAndOverallDuration(final List<StreamInfoItem> list,
final boolean isDurationComplete) {
if (activity != null && headerBinding != null) {
playlistOverallDurationSeconds += list.stream()
.mapToLong(x -> x.getDuration())
.sum();
headerBinding.playlistStreamCount.setText(
Localization.concatenateStrings(
Localization.localizeStreamCount(activity, streamCount),
Localization.getDurationString(playlistOverallDurationSeconds,
isDurationComplete, true))
);
}
}
} }

View File

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

View File

@ -10,6 +10,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -18,21 +19,22 @@ import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.ItemViewMode; import org.schabi.newpipe.info_list.ItemViewMode;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.util.RelatedItemInfo;
import java.io.Serializable; import java.io.Serializable;
import java.util.function.Supplier; import java.util.function.Supplier;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemInfo> public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemsInfo>
implements SharedPreferences.OnSharedPreferenceChangeListener { implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String INFO_KEY = "related_info_key"; private static final String INFO_KEY = "related_info_key";
private RelatedItemInfo relatedItemInfo; private RelatedItemsInfo relatedItemsInfo;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Views // Views
@ -69,7 +71,7 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
@Override @Override
protected Supplier<View> getListHeaderSupplier() { protected Supplier<View> getListHeaderSupplier() {
if (relatedItemInfo == null || relatedItemInfo.getRelatedItems() == null) { if (relatedItemsInfo == null || relatedItemsInfo.getRelatedItems() == null) {
return null; return null;
} }
@ -97,8 +99,8 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override @Override
protected Single<RelatedItemInfo> loadResult(final boolean forceLoad) { protected Single<RelatedItemsInfo> loadResult(final boolean forceLoad) {
return Single.fromCallable(() -> relatedItemInfo); return Single.fromCallable(() -> relatedItemsInfo);
} }
@Override @Override
@ -110,7 +112,7 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
} }
@Override @Override
public void handleResult(@NonNull final RelatedItemInfo result) { public void handleResult(@NonNull final RelatedItemsInfo result) {
super.handleResult(result); super.handleResult(result);
if (headerBinding != null) { if (headerBinding != null) {
@ -137,23 +139,23 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
private void setInitialData(final StreamInfo info) { private void setInitialData(final StreamInfo info) {
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName()); super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
if (this.relatedItemInfo == null) { if (this.relatedItemsInfo == null) {
this.relatedItemInfo = RelatedItemInfo.getInfo(info); this.relatedItemsInfo = new RelatedItemsInfo(info);
} }
} }
@Override @Override
public void onSaveInstanceState(@NonNull final Bundle outState) { public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putSerializable(INFO_KEY, relatedItemInfo); outState.putSerializable(INFO_KEY, relatedItemsInfo);
} }
@Override @Override
protected void onRestoreInstanceState(@NonNull final Bundle savedState) { protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
super.onRestoreInstanceState(savedState); super.onRestoreInstanceState(savedState);
final Serializable serializable = savedState.getSerializable(INFO_KEY); final Serializable serializable = savedState.getSerializable(INFO_KEY);
if (serializable instanceof RelatedItemInfo) { if (serializable instanceof RelatedItemsInfo) {
this.relatedItemInfo = (RelatedItemInfo) serializable; this.relatedItemsInfo = (RelatedItemsInfo) serializable;
} }
} }
@ -174,4 +176,27 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
} }
return mode; return mode;
} }
@Override
protected void showInfoItemDialog(final StreamInfoItem item) {
// Try and attach the InfoItemDialog to the parent fragment of the RelatedItemsFragment
// so that its context is not lost when the RelatedItemsFragment is reinitialized,
// e.g. when a new stream is loaded in a parent VideoDetailFragment.
final Fragment parentFragment = getParentFragment();
if (parentFragment != null) {
try {
new InfoItemDialog.Builder(
parentFragment.getActivity(),
parentFragment.getContext(),
parentFragment,
item
).create().show();
} catch (final IllegalArgumentException e) {
InfoItemDialog.Builder.reportErrorDuringInitialization(e, item);
}
} else {
super.showInfoItemDialog(item);
}
}
} }

View File

@ -0,0 +1,22 @@
package org.schabi.newpipe.fragments.list.videos;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import java.util.ArrayList;
import java.util.Collections;
public final class RelatedItemsInfo extends ListInfo<InfoItem> {
/**
* This class is used to wrap the related items of a StreamInfo into a ListInfo object.
*
* @param info the stream info from which to get related items
*/
public RelatedItemsInfo(final StreamInfo info) {
super(info.getServiceId(), new ListLinkHandler(info.getOriginalUrl(), info.getUrl(),
info.getId(), Collections.emptyList(), null), info.getName());
setRelatedItems(new ArrayList<>(info.getRelatedItems()));
}
}

View File

@ -13,8 +13,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder; import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
@ -87,8 +86,7 @@ public class InfoItemBuilder {
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
: new PlaylistInfoItemHolder(this, parent); : new PlaylistInfoItemHolder(this, parent);
case COMMENT: case COMMENT:
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent) return new CommentInfoItemHolder(this, parent);
: new CommentsInfoItemHolder(this, parent);
default: default:
throw new RuntimeException("InfoType not expected = " + infoType.name()); throw new RuntimeException("InfoType not expected = " + infoType.name());
} }

View File

@ -17,11 +17,11 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.holder.ChannelCardInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder; import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
@ -73,12 +73,12 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
private static final int MINI_CHANNEL_HOLDER_TYPE = 0x200; private static final int MINI_CHANNEL_HOLDER_TYPE = 0x200;
private static final int CHANNEL_HOLDER_TYPE = 0x201; private static final int CHANNEL_HOLDER_TYPE = 0x201;
private static final int GRID_CHANNEL_HOLDER_TYPE = 0x202; private static final int GRID_CHANNEL_HOLDER_TYPE = 0x202;
private static final int CARD_CHANNEL_HOLDER_TYPE = 0x203;
private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300; private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300;
private static final int PLAYLIST_HOLDER_TYPE = 0x301; private static final int PLAYLIST_HOLDER_TYPE = 0x301;
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302; private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
private static final int CARD_PLAYLIST_HOLDER_TYPE = 0x303; private static final int CARD_PLAYLIST_HOLDER_TYPE = 0x303;
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400; private static final int COMMENT_HOLDER_TYPE = 0x400;
private static final int COMMENT_HOLDER_TYPE = 0x401;
private final LayoutInflater layoutInflater; private final LayoutInflater layoutInflater;
private final InfoItemBuilder infoItemBuilder; private final InfoItemBuilder infoItemBuilder;
@ -249,7 +249,9 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return STREAM_HOLDER_TYPE; return STREAM_HOLDER_TYPE;
} }
case CHANNEL: case CHANNEL:
if (itemMode == ItemViewMode.GRID) { if (itemMode == ItemViewMode.CARD) {
return CARD_CHANNEL_HOLDER_TYPE;
} else if (itemMode == ItemViewMode.GRID) {
return GRID_CHANNEL_HOLDER_TYPE; return GRID_CHANNEL_HOLDER_TYPE;
} else if (useMiniVariant) { } else if (useMiniVariant) {
return MINI_CHANNEL_HOLDER_TYPE; return MINI_CHANNEL_HOLDER_TYPE;
@ -267,7 +269,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return PLAYLIST_HOLDER_TYPE; return PLAYLIST_HOLDER_TYPE;
} }
case COMMENT: case COMMENT:
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE; return COMMENT_HOLDER_TYPE;
default: default:
return -1; return -1;
} }
@ -304,6 +306,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent); return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
case CHANNEL_HOLDER_TYPE: case CHANNEL_HOLDER_TYPE:
return new ChannelInfoItemHolder(infoItemBuilder, parent); return new ChannelInfoItemHolder(infoItemBuilder, parent);
case CARD_CHANNEL_HOLDER_TYPE:
return new ChannelCardInfoItemHolder(infoItemBuilder, parent);
case GRID_CHANNEL_HOLDER_TYPE: case GRID_CHANNEL_HOLDER_TYPE:
return new ChannelGridInfoItemHolder(infoItemBuilder, parent); return new ChannelGridInfoItemHolder(infoItemBuilder, parent);
case MINI_PLAYLIST_HOLDER_TYPE: case MINI_PLAYLIST_HOLDER_TYPE:
@ -314,10 +318,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent); return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
case CARD_PLAYLIST_HOLDER_TYPE: case CARD_PLAYLIST_HOLDER_TYPE:
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent); return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
case MINI_COMMENT_HOLDER_TYPE:
return new CommentsMiniInfoItemHolder(infoItemBuilder, parent);
case COMMENT_HOLDER_TYPE: case COMMENT_HOLDER_TYPE:
return new CommentsInfoItemHolder(infoItemBuilder, parent); return new CommentInfoItemHolder(infoItemBuilder, parent);
default: default:
return new FallbackViewHolder(new View(parent.getContext())); return new FallbackViewHolder(new View(parent.getContext()));
} }

View File

@ -8,7 +8,7 @@ import com.xwray.groupie.Item
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamSegment import org.schabi.newpipe.extractor.stream.StreamSegment
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.PicassoHelper import org.schabi.newpipe.util.image.PicassoHelper
class StreamSegmentItem( class StreamSegmentItem(
private val item: StreamSegment, private val item: StreamSegment,

View File

@ -99,18 +99,12 @@ public enum StreamDialogDefaultEntry {
) )
), ),
PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragment, item) -> { PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragment, item) ->
final Uri videoUrl = Uri.parse(item.getUrl()); KoreUtils.playWithKore(fragment.requireContext(), Uri.parse(item.getUrl()))),
try {
NavigationHelper.playWithKore(fragment.requireContext(), videoUrl);
} catch (final Exception e) {
KoreUtils.showInstallKoreDialog(fragment.requireActivity());
}
}),
SHARE(R.string.share, (fragment, item) -> SHARE(R.string.share, (fragment, item) ->
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(), ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
item.getThumbnailUrl())), item.getThumbnails())),
/** /**
* Opens a {@link DownloadDialog} after fetching some stream info. * Opens a {@link DownloadDialog} after fetching some stream info.
@ -119,7 +113,10 @@ public enum StreamDialogDefaultEntry {
DOWNLOAD(R.string.download, (fragment, item) -> DOWNLOAD(R.string.download, (fragment, item) ->
fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(), fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(),
item.getUrl(), info -> { item.getUrl(), info -> {
if (fragment.getContext() != null) { // Ensure the fragment is attached and its state hasn't been saved to avoid
// showing dialog during lifecycle changes or when the activity is paused,
// e.g. by selecting the download option and opening a different fragment.
if (fragment.isAdded() && !fragment.isStateSaved()) {
final DownloadDialog downloadDialog = final DownloadDialog downloadDialog =
new DownloadDialog(fragment.requireContext(), info); new DownloadDialog(fragment.requireContext(), info);
downloadDialog.show(fragment.getChildFragmentManager(), downloadDialog.show(fragment.getChildFragmentManager(),

View File

@ -0,0 +1,22 @@
package org.schabi.newpipe.info_list.holder;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import org.schabi.newpipe.R;
import org.schabi.newpipe.info_list.InfoItemBuilder;
public class ChannelCardInfoItemHolder extends ChannelMiniInfoItemHolder {
public ChannelCardInfoItemHolder(final InfoItemBuilder infoItemBuilder,
final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_channel_card_item, parent);
}
@Override
protected int getDescriptionMaxLineCount(@Nullable final String content) {
// Based on `list_channel_card_item` left side content (thumbnail 100dp
// + additional details), Right side description can grow up to 8 lines.
return 8;
}
}

View File

@ -13,7 +13,7 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.utils.Utils; import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
public class ChannelMiniInfoItemHolder extends InfoItemHolder { public class ChannelMiniInfoItemHolder extends InfoItemHolder {
@ -46,6 +46,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
final ChannelInfoItem item = (ChannelInfoItem) infoItem; final ChannelInfoItem item = (ChannelInfoItem) infoItem;
itemTitleView.setText(item.getName()); itemTitleView.setText(item.getName());
itemTitleView.setSelected(true);
final String detailLine = getDetailLine(item); final String detailLine = getDetailLine(item);
if (detailLine == null) { if (detailLine == null) {
@ -55,7 +56,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemAdditionalDetailView.setText(getDetailLine(item)); itemAdditionalDetailView.setText(getDetailLine(item));
} }
PicassoHelper.loadAvatar(item.getThumbnailUrl()).into(itemThumbnailView); PicassoHelper.loadAvatar(item.getThumbnails()).into(itemThumbnailView);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnChannelSelectedListener() != null) { if (itemBuilder.getOnChannelSelectedListener() != null) {
@ -77,11 +78,24 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
} else { } else {
itemChannelDescriptionView.setVisibility(View.VISIBLE); itemChannelDescriptionView.setVisibility(View.VISIBLE);
itemChannelDescriptionView.setText(item.getDescription()); itemChannelDescriptionView.setText(item.getDescription());
itemChannelDescriptionView.setMaxLines(detailLine == null ? 3 : 2); // setMaxLines utilize the line space for description if the additional details
// (sub / video count) are not present.
// Case1: 2 lines of description + 1 line additional details
// Case2: 3 lines of description (additionalDetails is GONE)
itemChannelDescriptionView.setMaxLines(getDescriptionMaxLineCount(detailLine));
} }
} }
} }
/**
* Returns max number of allowed lines for the description field.
* @param content additional detail content (video / sub count)
* @return max line count
*/
protected int getDescriptionMaxLineCount(@Nullable final String content) {
return content == null ? 3 : 2;
}
@Nullable @Nullable
private String getDetailLine(final ChannelInfoItem item) { private String getDetailLine(final ChannelInfoItem item) {
if (item.getStreamCount() >= 0 && item.getSubscriberCount() >= 0) { if (item.getStreamCount() >= 0 && item.getSubscriberCount() >= 0) {

View File

@ -0,0 +1,210 @@
package org.schabi.newpipe.info_list.holder;
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextEllipsizer;
public class CommentInfoItemHolder extends InfoItemHolder {
private static final int COMMENT_DEFAULT_LINES = 2;
private final int commentHorizontalPadding;
private final int commentVerticalPadding;
private final RelativeLayout itemRoot;
private final ImageView itemThumbnailView;
private final TextView itemContentView;
private final ImageView itemThumbsUpView;
private final TextView itemLikesCountView;
private final TextView itemTitleView;
private final ImageView itemHeartView;
private final ImageView itemPinnedView;
private final Button repliesButton;
@NonNull
private final TextEllipsizer textEllipsizer;
public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_comment_item, parent);
itemRoot = itemView.findViewById(R.id.itemRoot);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
itemThumbsUpView = itemView.findViewById(R.id.detail_thumbs_up_img_view);
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
itemTitleView = itemView.findViewById(R.id.itemTitleView);
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
repliesButton = itemView.findViewById(R.id.replies_button);
commentHorizontalPadding = (int) infoItemBuilder.getContext()
.getResources().getDimension(R.dimen.comments_horizontal_padding);
commentVerticalPadding = (int) infoItemBuilder.getContext()
.getResources().getDimension(R.dimen.comments_vertical_padding);
textEllipsizer = new TextEllipsizer(itemContentView, COMMENT_DEFAULT_LINES, null);
textEllipsizer.setStateChangeListener(isEllipsized -> {
if (Boolean.TRUE.equals(isEllipsized)) {
denyLinkFocus();
} else {
determineMovementMethod();
}
});
}
@Override
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem)) {
return;
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
// load the author avatar
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
if (ImageStrategy.shouldLoadImages()) {
itemThumbnailView.setVisibility(View.VISIBLE);
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
commentVerticalPadding, commentVerticalPadding);
} else {
itemThumbnailView.setVisibility(View.GONE);
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
commentHorizontalPadding, commentVerticalPadding);
}
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
// setup the top row, with pinned icon, author name and comment date
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
itemTitleView.setText(Localization.concatenateStrings(item.getUploaderName(),
Localization.relativeTimeOrTextual(itemBuilder.getContext(), item.getUploadDate(),
item.getTextualUploadDate())));
// setup bottom row, with likes, heart and replies button
itemLikesCountView.setText(
Localization.likeCount(itemBuilder.getContext(), item.getLikeCount()));
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
final boolean hasReplies = item.getReplies() != null;
repliesButton.setOnClickListener(hasReplies ? v -> openCommentReplies(item) : null);
repliesButton.setVisibility(hasReplies ? View.VISIBLE : View.GONE);
repliesButton.setText(hasReplies
? Localization.replyCount(itemBuilder.getContext(), item.getReplyCount()) : "");
((RelativeLayout.LayoutParams) itemThumbsUpView.getLayoutParams()).topMargin =
hasReplies ? 0 : DeviceUtils.dpToPx(6, itemBuilder.getContext());
// setup comment content and click listeners to expand/ellipsize it
textEllipsizer.setStreamingService(getServiceById(item.getServiceId()));
textEllipsizer.setStreamUrl(item.getUrl());
textEllipsizer.setContent(item.getCommentText());
textEllipsizer.ellipsize();
//noinspection ClickableViewAccessibility
itemContentView.setOnTouchListener((v, event) -> {
final CharSequence text = itemContentView.getText();
if (text instanceof Spanned buffer) {
final int action = event.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
final int offset = getOffsetForHorizontalLine(itemContentView, event);
final var links = buffer.getSpans(offset, offset, ClickableSpan.class);
if (links.length != 0) {
if (action == MotionEvent.ACTION_UP) {
links[0].onClick(itemContentView);
}
// we handle events that intersect links, so return true
return true;
}
}
}
return false;
});
itemView.setOnClickListener(view -> {
textEllipsizer.toggle();
if (itemBuilder.getOnCommentsSelectedListener() != null) {
itemBuilder.getOnCommentsSelectedListener().selected(item);
}
});
itemView.setOnLongClickListener(view -> {
if (DeviceUtils.isTv(itemBuilder.getContext())) {
openCommentAuthor(item);
} else {
final CharSequence text = itemContentView.getText();
if (text != null) {
ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString());
}
}
return true;
});
}
private void openCommentAuthor(@NonNull final CommentsInfoItem item) {
NavigationHelper.openCommentAuthorIfPresent((FragmentActivity) itemBuilder.getContext(),
item);
}
private void openCommentReplies(@NonNull final CommentsInfoItem item) {
NavigationHelper.openCommentRepliesFragment((FragmentActivity) itemBuilder.getContext(),
item);
}
private void allowLinkFocus() {
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
}
private void denyLinkFocus() {
itemContentView.setMovementMethod(null);
}
private boolean shouldFocusLinks() {
if (itemView.isInTouchMode()) {
return false;
}
final URLSpan[] urls = itemContentView.getUrls();
return urls != null && urls.length != 0;
}
private void determineMovementMethod() {
if (shouldFocusLinks()) {
allowLinkFocus();
} else {
denyLinkFocus();
}
}
}

View File

@ -1,63 +0,0 @@
package org.schabi.newpipe.info_list.holder;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
/*
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ChannelInfoItemHolder .java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
public final TextView itemTitleView;
private final ImageView itemHeartView;
private final ImageView itemPinnedView;
public CommentsInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_comments_item, parent);
itemTitleView = itemView.findViewById(R.id.itemTitleView);
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
}
@Override
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
super.updateFromItem(infoItem, historyRecordManager);
if (!(infoItem instanceof CommentsInfoItem)) {
return;
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
itemTitleView.setText(item.getUploaderName());
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
}
}

View File

@ -1,275 +0,0 @@
package org.schabi.newpipe.info_list.holder;
import android.graphics.Paint;
import android.text.Layout;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.text.HtmlCompat;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
import org.schabi.newpipe.util.text.TextLinkifier;
import java.util.function.Consumer;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private static final String TAG = "CommentsMiniIIHolder";
private static final String ELLIPSIS = "";
private static final int COMMENT_DEFAULT_LINES = 2;
private static final int COMMENT_EXPANDED_LINES = 1000;
private final int commentHorizontalPadding;
private final int commentVerticalPadding;
private final Paint paintAtContentSize;
private final float ellipsisWidthPx;
private final RelativeLayout itemRoot;
private final ImageView itemThumbnailView;
private final TextView itemContentView;
private final TextView itemLikesCountView;
private final TextView itemPublishedTime;
private final CompositeDisposable disposables = new CompositeDisposable();
private Description commentText;
private StreamingService streamService;
private String streamUrl;
CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
final ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemRoot = itemView.findViewById(R.id.itemRoot);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
commentHorizontalPadding = (int) infoItemBuilder.getContext()
.getResources().getDimension(R.dimen.comments_horizontal_padding);
commentVerticalPadding = (int) infoItemBuilder.getContext()
.getResources().getDimension(R.dimen.comments_vertical_padding);
paintAtContentSize = new Paint();
paintAtContentSize.setTextSize(itemContentView.getTextSize());
ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS);
}
public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder,
final ViewGroup parent) {
this(infoItemBuilder, R.layout.list_comments_mini_item, parent);
}
@Override
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem)) {
return;
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
PicassoHelper.loadAvatar(item.getUploaderAvatarUrl()).into(itemThumbnailView);
if (PicassoHelper.getShouldLoadImages()) {
itemThumbnailView.setVisibility(View.VISIBLE);
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
commentVerticalPadding, commentVerticalPadding);
} else {
itemThumbnailView.setVisibility(View.GONE);
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
commentHorizontalPadding, commentVerticalPadding);
}
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
try {
streamService = NewPipe.getService(item.getServiceId());
} catch (final ExtractionException e) {
// should never happen
ErrorUtil.showUiErrorSnackbar(itemBuilder.getContext(), "Getting StreamingService", e);
Log.w(TAG, "Cannot obtain service from comment service id, defaulting to YouTube", e);
streamService = ServiceList.YouTube;
}
streamUrl = item.getUrl();
commentText = item.getCommentText();
ellipsize();
//noinspection ClickableViewAccessibility
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
if (item.getLikeCount() >= 0) {
itemLikesCountView.setText(
Localization.shortCount(
itemBuilder.getContext(),
item.getLikeCount()));
} else {
itemLikesCountView.setText("-");
}
if (item.getUploadDate() != null) {
itemPublishedTime.setText(Localization.relativeTime(item.getUploadDate()
.offsetDateTime()));
} else {
itemPublishedTime.setText(item.getTextualUploadDate());
}
itemView.setOnClickListener(view -> {
toggleEllipsize();
if (itemBuilder.getOnCommentsSelectedListener() != null) {
itemBuilder.getOnCommentsSelectedListener().selected(item);
}
});
itemView.setOnLongClickListener(view -> {
if (DeviceUtils.isTv(itemBuilder.getContext())) {
openCommentAuthor(item);
} else {
ShareUtils.copyToClipboard(itemBuilder.getContext(),
itemContentView.getText().toString());
}
return true;
});
}
private void openCommentAuthor(final CommentsInfoItem item) {
if (TextUtils.isEmpty(item.getUploaderUrl())) {
return;
}
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
try {
NavigationHelper.openChannelFragment(
activity.getSupportFragmentManager(),
item.getServiceId(),
item.getUploaderUrl(),
item.getUploaderName());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(activity, "Opening channel fragment", e);
}
}
private void allowLinkFocus() {
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
}
private void denyLinkFocus() {
itemContentView.setMovementMethod(null);
}
private boolean shouldFocusLinks() {
if (itemView.isInTouchMode()) {
return false;
}
final URLSpan[] urls = itemContentView.getUrls();
return urls != null && urls.length != 0;
}
private void determineMovementMethod() {
if (shouldFocusLinks()) {
allowLinkFocus();
} else {
denyLinkFocus();
}
}
private void ellipsize() {
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
linkifyCommentContentView(v -> {
boolean hasEllipsis = false;
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
// Note that converting to String removes spans (i.e. links), but that's something
// we actually want since when the text is ellipsized we want all clicks on the
// comment to expand the comment, not to open links.
final String text = itemContentView.getText().toString();
final Layout layout = itemContentView.getLayout();
final float lineWidth = layout.getLineWidth(COMMENT_DEFAULT_LINES - 1);
final float layoutWidth = layout.getWidth();
final int lineStart = layout.getLineStart(COMMENT_DEFAULT_LINES - 1);
final int lineEnd = layout.getLineEnd(COMMENT_DEFAULT_LINES - 1);
// remove characters up until there is enough space for the ellipsis
// (also summing 2 more pixels, just to be sure to avoid float rounding errors)
int end = lineEnd;
float removedCharactersWidth = 0.0f;
while (lineWidth - removedCharactersWidth + ellipsisWidthPx + 2.0f > layoutWidth
&& end >= lineStart) {
end -= 1;
// recalculate each time to account for ligatures or other similar things
removedCharactersWidth = paintAtContentSize.measureText(
text.substring(end, lineEnd));
}
// remove trailing spaces and newlines
while (end > 0 && Character.isWhitespace(text.charAt(end - 1))) {
end -= 1;
}
final String newVal = text.substring(0, end) + ELLIPSIS;
itemContentView.setText(newVal);
hasEllipsis = true;
}
itemContentView.setMaxLines(COMMENT_DEFAULT_LINES);
if (hasEllipsis) {
denyLinkFocus();
} else {
determineMovementMethod();
}
});
}
private void toggleEllipsize() {
final CharSequence text = itemContentView.getText();
if (text.charAt(text.length() - 1) == ELLIPSIS.charAt(0)) {
expand();
} else if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
ellipsize();
}
}
private void expand() {
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
linkifyCommentContentView(v -> determineMovementMethod());
}
private void linkifyCommentContentView(@Nullable final Consumer<TextView> onCompletion) {
disposables.clear();
if (commentText != null) {
TextLinkifier.fromDescription(itemContentView, commentText,
HtmlCompat.FROM_HTML_MODE_LEGACY, streamService, streamUrl, disposables,
onCompletion);
}
}
}

View File

@ -9,7 +9,7 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
public class PlaylistMiniInfoItemHolder extends InfoItemHolder { public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
@ -46,7 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount())); .localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
itemUploaderView.setText(item.getUploaderName()); itemUploaderView.setText(item.getUploaderName());
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView); PicassoHelper.loadPlaylistThumbnail(item.getThumbnails()).into(itemThumbnailView);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnPlaylistSelectedListener() != null) { if (itemBuilder.getOnPlaylistSelectedListener() != null) {

View File

@ -12,10 +12,6 @@ import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import androidx.preference.PreferenceManager;
import static org.schabi.newpipe.MainActivity.DEBUG;
/* /*
* Created by Christian Schabesberger on 01.08.16. * Created by Christian Schabesberger on 01.08.16.
* <p> * <p>
@ -81,7 +77,9 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
} }
} }
final String uploadDate = getFormattedRelativeUploadDate(infoItem); final String uploadDate = Localization.relativeTimeOrTextual(itemBuilder.getContext(),
infoItem.getUploadDate(),
infoItem.getTextualUploadDate());
if (!TextUtils.isEmpty(uploadDate)) { if (!TextUtils.isEmpty(uploadDate)) {
if (viewsAndDate.isEmpty()) { if (viewsAndDate.isEmpty()) {
return uploadDate; return uploadDate;
@ -92,20 +90,4 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
return viewsAndDate; return viewsAndDate;
} }
private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) {
if (infoItem.getUploadDate() != null) {
String formattedRelativeTime = Localization
.relativeTime(infoItem.getUploadDate().offsetDateTime());
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext())
.getBoolean(itemBuilder.getContext()
.getString(R.string.show_original_time_ago_key), false)) {
formattedRelativeTime += " (" + infoItem.getTextualUploadDate() + ")";
}
return formattedRelativeTime;
} else {
return infoItem.getTextualUploadDate();
}
}
} }

View File

@ -14,8 +14,9 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.AnimatedProgressBar;
@ -60,8 +61,12 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
R.color.duration_background_color)); R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE); itemDurationView.setVisibility(View.VISIBLE);
final StreamStateEntity state2 = historyRecordManager.loadStreamState(infoItem) StreamStateEntity state2 = null;
.blockingGet()[0]; if (DependentPreferenceHelper
.getPositionsInListsEnabled(itemProgressView.getContext())) {
state2 = historyRecordManager.loadStreamState(infoItem)
.blockingGet()[0];
}
if (state2 != null) { if (state2 != null) {
itemProgressView.setVisibility(View.VISIBLE); itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.getDuration()); itemProgressView.setMax((int) item.getDuration());
@ -82,7 +87,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
} }
// Default thumbnail is shown on error, while loading and if the url is empty // Default thumbnail is shown on error, while loading and if the url is empty
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView); PicassoHelper.loadThumbnail(item.getThumbnails()).into(itemThumbnailView);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) { if (itemBuilder.getOnStreamSelectedListener() != null) {
@ -111,9 +116,12 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
final HistoryRecordManager historyRecordManager) { final HistoryRecordManager historyRecordManager) {
final StreamInfoItem item = (StreamInfoItem) infoItem; final StreamInfoItem item = (StreamInfoItem) infoItem;
final StreamStateEntity state = historyRecordManager StreamStateEntity state = null;
.loadStreamState(infoItem) if (DependentPreferenceHelper.getPositionsInListsEnabled(itemProgressView.getContext())) {
.blockingGet()[0]; state = historyRecordManager
.loadStreamState(infoItem)
.blockingGet()[0];
}
if (state != null && item.getDuration() > 0 if (state != null && item.getDuration() > 0
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) { && !StreamTypeUtil.isLiveStream(item.getStreamType())) {
itemProgressView.setMax((int) item.getDuration()); itemProgressView.setMax((int) item.getDuration());

View File

@ -0,0 +1,9 @@
package org.schabi.newpipe.ktx
import android.os.Bundle
import android.os.Parcelable
import androidx.core.os.BundleCompat
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -13,7 +14,8 @@ import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.local.LocalItemListAdapter; import org.schabi.newpipe.local.LocalItemListAdapter;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
@ -28,6 +30,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
private RecyclerView playlistRecyclerView; private RecyclerView playlistRecyclerView;
private LocalItemListAdapter playlistAdapter; private LocalItemListAdapter playlistAdapter;
private TextView playlistDuplicateIndicator;
private final CompositeDisposable playlistDisposables = new CompositeDisposable(); private final CompositeDisposable playlistDisposables = new CompositeDisposable();
@ -63,8 +66,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
playlistAdapter = new LocalItemListAdapter(getActivity()); playlistAdapter = new LocalItemListAdapter(getActivity());
playlistAdapter.setSelectedListener(selectedItem -> { playlistAdapter.setSelectedListener(selectedItem -> {
final List<StreamEntity> entities = getStreamEntities(); final List<StreamEntity> entities = getStreamEntities();
if (selectedItem instanceof PlaylistMetadataEntry && entities != null) { if (selectedItem instanceof PlaylistDuplicatesEntry && entities != null) {
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, entities); onPlaylistSelected(playlistManager,
(PlaylistDuplicatesEntry) selectedItem, entities);
} }
}); });
@ -72,10 +76,13 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
playlistRecyclerView.setAdapter(playlistAdapter); playlistRecyclerView.setAdapter(playlistAdapter);
playlistDuplicateIndicator = view.findViewById(R.id.playlist_duplicate);
final View newPlaylistButton = view.findViewById(R.id.newPlaylist); final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog());
playlistDisposables.add(playlistManager.getPlaylists() playlistDisposables.add(playlistManager
.getPlaylistDuplicates(getStreamEntities().get(0).getUrl())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onPlaylistsReceived)); .subscribe(this::onPlaylistsReceived));
} }
@ -117,31 +124,51 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
requireDialog().dismiss(); requireDialog().dismiss();
} }
private void onPlaylistsReceived(@NonNull final List<PlaylistMetadataEntry> playlists) { private void onPlaylistsReceived(@NonNull final List<PlaylistDuplicatesEntry> playlists) {
if (playlistAdapter != null && playlistRecyclerView != null) { if (playlistAdapter != null
&& playlistRecyclerView != null
&& playlistDuplicateIndicator != null) {
playlistAdapter.clearStreamItemList(); playlistAdapter.clearStreamItemList();
playlistAdapter.addItems(playlists); playlistAdapter.addItems(playlists);
playlistRecyclerView.setVisibility(View.VISIBLE); playlistRecyclerView.setVisibility(View.VISIBLE);
playlistDuplicateIndicator.setVisibility(
anyPlaylistContainsDuplicates(playlists) ? View.VISIBLE : View.GONE);
} }
} }
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager, private boolean anyPlaylistContainsDuplicates(final List<PlaylistDuplicatesEntry> playlists) {
@NonNull final PlaylistMetadataEntry playlist, return playlists.stream()
@NonNull final List<StreamEntity> streams) { .anyMatch(playlist -> playlist.timesStreamIsContained > 0);
final Toast successToast = Toast.makeText(getContext(), }
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
if (playlist.thumbnailUrl private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
.equals("drawable://" + R.drawable.placeholder_thumbnail_playlist)) { @NonNull final PlaylistDuplicatesEntry playlist,
playlistDisposables.add(manager @NonNull final List<StreamEntity> streams) {
.changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl(), false)
.observeOn(AndroidSchedulers.mainThread()) final String toastText;
.subscribe(ignored -> successToast.show())); if (playlist.timesStreamIsContained > 0) {
toastText = getString(R.string.playlist_add_stream_success_duplicate,
playlist.timesStreamIsContained);
} else {
toastText = getString(R.string.playlist_add_stream_success);
} }
playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams) final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
playlistDisposables.add(manager.appendToPlaylist(playlist.getUid(), streams)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> successToast.show())); .subscribe(ignored -> {
successToast.show();
if (playlist.thumbnailUrl != null
&& playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
playlistDisposables.add(manager
.changePlaylistThumbnail(playlist.getUid(), streams.get(0).getUid(),
false)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignore -> successToast.show()));
}
}));
requireDialog().dismiss(); requireDialog().dismiss();
} }

View File

@ -43,11 +43,13 @@ class FeedDatabaseManager(context: Context) {
fun getStreams( fun getStreams(
groupId: Long, groupId: Long,
includePlayedStreams: Boolean, includePlayedStreams: Boolean,
includePartiallyPlayedStreams: Boolean,
includeFutureStreams: Boolean includeFutureStreams: Boolean
): Maybe<List<StreamWithState>> { ): Maybe<List<StreamWithState>> {
return feedTable.getStreams( return feedTable.getStreams(
groupId, groupId,
includePlayedStreams, includePlayedStreams,
includePartiallyPlayedStreams,
if (includeFutureStreams) null else OffsetDateTime.now() if (includeFutureStreams) null else OffsetDateTime.now()
) )
} }

View File

@ -37,21 +37,18 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.math.MathUtils
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.MenuItemCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.evernote.android.state.State
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Item import com.xwray.groupie.Item
import com.xwray.groupie.OnItemClickListener import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.OnItemLongClickListener import com.xwray.groupie.OnItemLongClickListener
import icepick.State
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
@ -62,6 +59,7 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.databinding.FragmentFeedBinding import org.schabi.newpipe.databinding.FragmentFeedBinding
import org.schabi.newpipe.error.ErrorInfo import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.ErrorUtil
import org.schabi.newpipe.error.UserAction import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
@ -100,8 +98,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
private var oldestSubscriptionUpdate: OffsetDateTime? = null private var oldestSubscriptionUpdate: OffsetDateTime? = null
private lateinit var groupAdapter: GroupieAdapter 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 onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
private var updateListViewModeOnResume = false private var updateListViewModeOnResume = false
@ -140,8 +136,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
val factory = FeedViewModel.getFactory(requireContext(), groupId) val factory = FeedViewModel.getFactory(requireContext(), groupId)
viewModel = ViewModelProvider(this, factory)[FeedViewModel::class.java] viewModel = ViewModelProvider(this, factory)[FeedViewModel::class.java]
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
showFutureItems = viewModel.getShowFutureItemsFromPreferences()
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) } viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
groupAdapter = GroupieAdapter().apply { groupAdapter = GroupieAdapter().apply {
@ -216,8 +210,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
activity.supportActionBar?.subtitle = groupName activity.supportActionBar?.subtitle = groupName
inflater.inflate(R.menu.menu_feed_fragment, menu) 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -239,24 +231,42 @@ class FeedFragment : BaseStateFragment<FeedState>() {
} }
} }
.setPositiveButton(resources.getString(R.string.ok), null) .setPositiveButton(resources.getString(R.string.ok), null)
.create()
.show() .show()
return true return true
} else if (item.itemId == R.id.menu_item_feed_toggle_played_items) { } else if (item.itemId == R.id.menu_item_feed_toggle_played_items) {
showPlayedItems = !item.isChecked showStreamVisibilityDialog()
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) return super.onOptionsItemSelected(item)
} }
private fun showStreamVisibilityDialog() {
val dialogItems = arrayOf(
getString(R.string.feed_show_watched),
getString(R.string.feed_show_partially_watched),
getString(R.string.feed_show_upcoming)
)
val checkedDialogItems = booleanArrayOf(
viewModel.getShowPlayedItemsFromPreferences(),
viewModel.getShowPartiallyPlayedItemsFromPreferences(),
viewModel.getShowFutureItemsFromPreferences()
)
AlertDialog.Builder(context!!)
.setTitle(R.string.feed_hide_streams_title)
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
checkedDialogItems[which] = isChecked
}
.setPositiveButton(R.string.ok) { _, _ ->
viewModel.setSaveShowPlayedItems(checkedDialogItems[0])
viewModel.setSaveShowPartiallyPlayedItems(checkedDialogItems[1])
viewModel.setSaveShowFutureItems(checkedDialogItems[2])
}
.setNegativeButton(R.string.cancel, null)
.show()
}
override fun onDestroyOptionsMenu() { override fun onDestroyOptionsMenu() {
super.onDestroyOptionsMenu() super.onDestroyOptionsMenu()
activity?.supportActionBar?.subtitle = null activity?.supportActionBar?.subtitle = null
@ -283,40 +293,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
super.onDestroyView() super.onDestroyView()
} }
private fun updateTogglePlayedItemsButton(menuItem: MenuItem) {
menuItem.isChecked = showPlayedItems
menuItem.icon = AppCompatResources.getDrawable(
requireContext(),
if (showPlayedItems) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off
)
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
)
)
}
// ////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////
// Handling // Handling
// ////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////
@ -477,24 +453,33 @@ class FeedFragment : BaseStateFragment<FeedState>() {
if (t is FeedLoadService.RequestException && if (t is FeedLoadService.RequestException &&
t.cause is ContentNotAvailableException t.cause is ContentNotAvailableException
) { ) {
Single.fromCallable { disposables.add(
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO() Single.fromCallable {
.getSubscription(t.subscriptionId) NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
}.subscribeOn(Schedulers.io()) .getSubscription(t.subscriptionId)
.observeOn(AndroidSchedulers.mainThread()) }
.subscribe( .subscribeOn(Schedulers.io())
{ subscriptionEntity -> .observeOn(AndroidSchedulers.mainThread())
handleFeedNotAvailable( .subscribe(
subscriptionEntity, { subscriptionEntity ->
t.cause, handleFeedNotAvailable(
errors.subList(i + 1, errors.size) subscriptionEntity,
) t.cause,
}, errors.subList(i + 1, errors.size)
{ throwable -> Log.e(TAG, "Unable to process", throwable) } )
) },
return // this will be called on the remaining errors by handleFeedNotAvailable() { throwable -> Log.e(TAG, "Unable to process", throwable) }
)
)
// this will be called on the remaining errors by handleFeedNotAvailable()
return@handleItemsErrors
} }
} }
if (errors.isNotEmpty()) {
// if no error was a ContentNotAvailableException, show a general error snackbar
ErrorUtil.showSnackbar(this, ErrorInfo(errors, UserAction.REQUESTED_FEED, ""))
}
} }
private fun handleFeedNotAvailable( private fun handleFeedNotAvailable(
@ -509,15 +494,13 @@ class FeedFragment : BaseStateFragment<FeedState>() {
val builder = AlertDialog.Builder(requireContext()) val builder = AlertDialog.Builder(requireContext())
.setTitle(R.string.feed_load_error) .setTitle(R.string.feed_load_error)
.setPositiveButton( .setPositiveButton(R.string.unsubscribe) { _, _ ->
R.string.unsubscribe SubscriptionManager(requireContext())
) { _, _ -> .deleteSubscription(subscriptionEntity.serviceId, subscriptionEntity.url)
SubscriptionManager(requireContext()).deleteSubscription( .subscribe()
subscriptionEntity.serviceId, subscriptionEntity.url
).subscribe()
handleItemsErrors(nextItemsErrors) handleItemsErrors(nextItemsErrors)
} }
.setNegativeButton(R.string.cancel) { _, _ -> } .setNegativeButton(R.string.cancel, null)
var message = getString(R.string.feed_load_error_account_info, subscriptionEntity.name) var message = getString(R.string.feed_load_error_account_info, subscriptionEntity.name)
if (cause is AccountTerminatedException) { if (cause is AccountTerminatedException) {
@ -534,7 +517,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
message += "\n" + cause.message message += "\n" + cause.message
} }
} }
builder.setMessage(message).create().show() builder.setMessage(message)
.show()
} }
private fun updateRelativeTimeViews() { private fun updateRelativeTimeViews() {
@ -565,7 +549,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
var typeface = Typeface.DEFAULT var typeface = Typeface.DEFAULT
var backgroundSupplier = { ctx: Context -> var backgroundSupplier = { ctx: Context ->
resolveDrawable(ctx, R.attr.selectableItemBackground) resolveDrawable(ctx, android.R.attr.selectableItemBackground)
} }
if (doCheck) { if (doCheck) {
// If the uploadDate is null or true we should highlight the item // If the uploadDate is null or true we should highlight the item
@ -578,7 +562,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
LayerDrawable( LayerDrawable(
arrayOf( arrayOf(
resolveDrawable(ctx, R.attr.dashed_border), resolveDrawable(ctx, R.attr.dashed_border),
resolveDrawable(ctx, R.attr.selectableItemBackground) resolveDrawable(ctx, android.R.attr.selectableItemBackground)
) )
) )
} }
@ -604,7 +588,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
// state until the user scrolls them out of the visible area which causes a update/bind-call // state until the user scrolls them out of the visible area which causes a update/bind-call
groupAdapter.notifyItemRangeChanged( groupAdapter.notifyItemRangeChanged(
0, 0,
MathUtils.clamp(highlightCount, lastNewItemsCount, groupAdapter.itemCount) highlightCount.coerceIn(lastNewItemsCount, groupAdapter.itemCount)
) )
if (highlightCount > 0) { if (highlightCount > 0) {
@ -623,9 +607,13 @@ class FeedFragment : BaseStateFragment<FeedState>() {
execOnEnd = { execOnEnd = {
// Disabled animations would result in immediately hiding the button // Disabled animations would result in immediately hiding the button
// after it showed up // after it showed up
if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(context)) { // Context can be null in some cases, so we have to make sure it is not null in
// Hide the new items-"popup" after 10s // order to avoid a NullPointerException
hideNewItemsLoaded(true, 10000) context?.let {
if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(it)) {
// Hide the new items button after 10s
hideNewItemsLoaded(true, 10000)
}
} }
} }
) )

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