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

Merge pull request #5456 from TeamNewPipe/release_0.20.9

Release 0.20.10
This commit is contained in:
Tobi 2021-02-14 22:24:48 +01:00 committed by GitHub
commit 5383a0af0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
301 changed files with 11136 additions and 8652 deletions

View File

@ -15,7 +15,7 @@ Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
### Checklist
<!-- The first box has been checked for you to show you how it is done. -->
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
- [x] I am using the latest version - x.xx.x <!-- Check https://github.com/TeamNewPipe/NewPipe/releases -->
- [ ] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->

View File

@ -11,7 +11,7 @@ assignees: ''
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
### Checklist
<!-- The first box has been checked for you to show you how it is done. -->
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
- [x] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.

View File

@ -22,7 +22,8 @@
#### 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".) -->
debug.zip
<!-- Remove the following line if you directly link the APK created by the CI pipeline. Directly linking is preferred if you need to let users test.-->
On the website the APK can be found by going to the "Checks" tab below the title and then on "artifacts" on the right.
#### Due diligence
- [ ] I read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md).

View File

@ -8,6 +8,11 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: create and checkout branch
# push events already checked out the branch
if: github.event_name == 'pull_request'
run: git checkout -B ${{ github.head_ref }}
- name: set up JDK 1.8
uses: actions/setup-java@v1.4.3
with:

149
README.ja.md Normal file
View File

@ -0,0 +1,149 @@
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
<h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">自由で軽量な Android 向けストリーミングフロントエンド</h4>
<!-- F-Droid is extremely out of date, so we hide this for now. -->
<!-- <p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-ja.svg"></a></p> -->
<p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub リリース"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg"></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="ライセンス: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="ビルド状態"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="翻訳状態"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC チャンネル: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource 寄付"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
<p align="center"><a href="#screenshots">スクリーンショット</a> &bull; <a href="#description">説明</a> &bull; <a href="#features">機能</a> &bull; <a href="#installation-and-updates">インストールと更新</a> &bull; <a href="#contribution">貢献</a> &bull; <a href="#donate">寄付</a> &bull; <a href="#license">ライセンス</a></p>
<p align="center"><a href="https://newpipe.net">ウェブサイト</a> &bull; <a href="https://newpipe.net/blog/">ブログ</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <a href="https://newpipe.net/press/">ニュース</a></p>
<hr>
*他の言語で読む: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt.br.md), [日本語](README.ja.md), [Română](README.ro.md) 。*
<b>注意: これはベータ版のため、バグが発生する可能性があります。もしバグが発生した場合、GitHub のリポジトリで Issue を開いてください。</b>
<b>NewPipe 及びいずれのフォークを Google Play ストアに公開すると、Google の取引条件の違反になります。</b>
<span id="screenshots"></span>
## スクリーンショット
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
<span id="description"></span>
## 説明
自由なコピーレフトソフトウェアの NewPipe は一切の Google フレームワークライブラリ及び、YouTube API を使用しません。ウェブサイトは必要な情報のためだけに読み込まれるため、このアプリは Google のサービスがインストールされていない端末で使用ができます。また、NewPipe の使用に YouTube アカウントは必要となりません。
<span id="features"></span>
### 機能
* 動画の検索
* 動画の基本情報の表示
* YouTube の動画の視聴
* YouTube の動画のバックグラウンド再生
* ポップアップモード (フローティングプレイヤー)
* 動画を視聴するストリーミングプレイヤーの選択
* 動画のダウンロード
* 音声のみのダウンロード
* Kodi での動画再生
* 次の動画/関連動画の表示
* 特定の言語の YouTube の検索
* 年齢制限のあるコンテンツの視聴/ブロック
* チャンネルの基本情報の表示
* チャンネルの検索
* チャンネルからの動画の視聴
* Orbot/Tor 対応 (直接的なものは未実装)
* 1080p/2K/4K 対応
* 履歴の表示
* チャンネルの登録
* 履歴の検索
* 再生リストの検索/視聴
* 再生リストをキューに追加して再生
* 動画のキューへの追加
* 端末内の再生リスト
* 字幕
* ライブ配信の対応
* コメントの表示
### 対応しているサービス
NewPipe は複数のサービスに対応しています。[ドキュメント](https://teamnewpipe.github.io/documentation/)は、どのようにしてアプリと NewPipe Extractor にサービスを追加できるかについて詳細な情報を提供しています。もし、新しいサービスを追加するならば、是非私たちに連絡をお願いします。現在対応しているサービスは:
* YouTube
* SoundCloud \[ベータ\]
* media.ccc.de \[ベータ\]
* PeerTube インスタンス \[ベータ\]
<!-- Hidden span to keep old links compatible. -->
<span id="updates"></span>
<span id="installation-and-updates"></span>
## インストールと更新
以下の方法のいずれかに従うことによって NewPipe をインストールできます。
1. カスタムリポジトリを F-Droid に追加してリリースが公開され次第インストールする。この方法の説明はこちら: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
2. リリースが公開され次第[GitHub のリリース](https://github.com/TeamNewPipe/NewPipe/releases)から APK をダウンロードしてインストールする。
3. F-Droid から更新する。これは更新を手にする上で最も遅い方法です。F-Droid が変更を検知して、APK をビルドし、署名、そしてユーザーに更新を届ける必要があるためです。
4. 自分でデバッグ APK をビルドする。これは新しい機能を使用する上で最も早い方法ですが、他と比べてとても複雑なので、他の方法の使用を推奨します。
私たちはほとんどのユーザーに方法1を推奨します。方法1と2でインストールされた APK は互換性がありますが、方法3でインストールされたものにはありません。これは方法1と2では、同じ署名鍵 (私たちが使用するもの)が使用されますが、方法3では異なった署名鍵 (F-Droidが使用するもの)が使用されるためです。方法4を使ったデバッグ APK のビルドは根本的に署名鍵の問題を除きます。署名鍵はユーザーが騙されて悪意のある更新がアプリにインストールされないことを助けるためにあります。
もし、何かしらの理由によりソースを切り替えたい場合 (例: NewPipe のコア機能が壊れてしまったが F-Droid はまだ更新をしていない) は、この手順を推奨します。
1. 履歴や登録チャンネル、再生リストを保つために 設定 > コンテンツ > データベースをエクスポート からデータをバックアップ
2. NewPipe をアンインストール
3. 新しいソースから APK をダウンロードしてインストール
4. 設定 > コンテンツ > データベースをインポート からステップ1で作ったデータベースをインポート
<span id="contribution"></span>
## 貢献
翻訳、デザインの変更、コードの整理、大規模なコードの変更などの助けはいつでも歓迎します。
より良いものを一緒に作り上げましょう!
もし貢献をしたい場合、[貢献ノート](.github/CONTRIBUTING.md)をご確認ください。
<a href="https://hosted.weblate.org/engage/newpipe/">
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="翻訳状態" />
</a>
<span id="donate"></span>
## 寄付
もし、NewPipe を気に入っていただけたら、寄付をしていただけると嬉しいです。Bitcoin または Bountysource, Liberapay から寄付をすることができます。NewPipe への寄付については、[ウェブサイト](https://newpipe.net/donate)からお願いします。
<table>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR コード" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="liberapay.com で NewPipe を訪れる" width="100px"></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Liberapay で寄付" height="35px"></a></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="bountysource.com で NewPipe を訪れる" 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="あなたがどれほどの寄付を得られるのか確認しましょう。"></a></td>
</tr>
</table>
## プライバシーポリシー
NewPipe プロジェクトはメディアウェブサービスを使用する上でのプライベートで匿名の体験を提供することを目的としています。
そのため、アプリはあなたの同意なしで一切のデータを収集しません。NewPipe のプライバシーポリシーはあなたがクラッシュレポートまたは、私たちのブログでコメントを送信した場合にどのようなデータが送信され、保存されるのかを詳細に説明しています。そのドキュメントは[こちら](https://newpipe.net/legal/privacy/)から見つけることができます。
<span id="license"></span>
## ライセンス
[![GNU GPLv3 のロゴ](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe はフリーソフトウェアなので、あなたはあなたの望むように使用、習得、共有、改善を行えます。
具体的には、フリーソフトウェア財団により公開された [GNU General Public License](https://www.gnu.org/licenses/gpl.html) のバージョン3のライセンスもしくは、(あなたの選択で) いずれかの後継バージョンの規約の元で配布または改変を行うことができます。

View File

@ -16,7 +16,7 @@
<p align="center"><a href="https://newpipe.net">Website</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <a href="https://newpipe.net/press/">Press</a></p>
<hr>
*Read this in other languages: [English](README.md), [한국어](README.ko.md).*
*Read this in other languages: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md).*
<b>경고: 이 버전은 베타 버전이므로, 버그가 발생할 수도 있습니다. 만약 버그가 발생하였다면, 우리의 GITHUB 저장소에서 ISSUE를 열람하여 주십시오.</b>
@ -98,7 +98,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로
1. 당신의 기록, 구독, 그리고 재생목록을 유지할 수 있도록 Settings > Content > Export Database 를 통해 데이터를 백업하십시오.
2. NewPipe를 삭제하십시오.
3. 새로운 소스에서 APK를 다운로드하고 이것을 설치하십시오.
4. Step 1의 Settings > Content > Export Database 을 통해 데이터를 불러오십시오.
4. Step 1의 Settings > Content > Import Database 을 통해 데이터를 불러오십시오.
## Contribution
당신이 아이디어, 번역, 디자인 변경, 코드 정리, 또는 정말 큰 코드 수정에 대한 의견이 있다면, 도움은 항상 환영합니다.

View File

@ -1,7 +1,9 @@
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
<h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">A libre lightweight streaming frontend for Android.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
<!-- F-Droid is extremely out of date, so we hide this for now. -->
<!-- <p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p> -->
<p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
@ -12,11 +14,11 @@
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#updates">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="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#installation-and-updates">Installation and updates</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p>
<p align="center"><a href="https://newpipe.net">Website</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <a href="https://newpipe.net/press/">Press</a></p>
<hr>
*Read this in other languages: [English](README.md), [한국어](README.ko.md).*
*Read this in other languages: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md),[Română](README.ro.md) .*
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
@ -80,14 +82,17 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc
* media.ccc.de \[beta\]
* PeerTube instances \[beta\]
## Updates
When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can:
1. 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.
2. Add our custom repo to F-Droid and install it from there as soon as we publish a release. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
3. Download the APK from [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it as soon as we publish a release.
4. Update via F-droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users.
<!-- Hidden span to keep old links compatible. -->
<span id="updates"></span>
We recommend method 2 for most users. APKs installed using method 2 or 3 are compatible with each other, but not with those installed using method 4. This is due to the same signing key (ours) being used for 2 and 3, but a different signing key (F-Droid's) being used for 4. Building a debug APK using method 1 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app.
## Installation and updates
You can install NewPipe using one of the following methods:
1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.schabi.org/FAQ/tutorials/install-add-fdroid-repo/
2. Download the APK from [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users. (**IMPORTANT**: as of the time of writing, an issue is preventing releases later than 0.20.1 from being published. Thus, till this issue is solved, if you want to use F-Droid, we recommend method 1.)
4. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other, but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app.
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure:
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlists

140
README.pt_BR.md Normal file
View File

@ -0,0 +1,140 @@
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
<h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">Uma interface de streaming leve e gratuita para Android.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
<p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#description">Descrição</a> &bull; <a href="#features">Características</a> &bull; <a href="#updates">Atualizações</a> &bull; <a href="#contribution">Contribuição</a> &bull; <a href="#donate">Doar</a> &bull; <a href="#license">Licença</a></p>
<p align="center"><a href="https://newpipe.net">Site</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <a href="https://newpipe.net/press/">Press</a></p>
<hr>
*Read this in other languages: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md).*
<b>AVISO: ESTA É UMA VERSÃO BETA, PORTANTO, VOCÊ PODE ENCONTRAR BUGS. ENCONTROU ALGUM, ABRA UM ISSUE ATRAVÉS DO NOSSO REPOSITÓRIO GITHUB.</b>
<b>COLOCAR NEWPIPE OU QUALQUER FORK DELE NA GOOGLE PLAY STORE VIOLA SEUS TERMOS E CONDIÇÕES.</b>
## 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/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
## Descrição
O NewPipe não usa nenhuma biblioteca de framework do Google, nem a API do YouTube. Os sites são apenas analisados para obter informações necessárias, para que este aplicativo possa ser usado em dispositivos sem os serviços do Google instalados. Além disso, você não precisa de uma conta no YouTube para usar o NewPipe, que é um software livre com copyleft.
### Características
* Procurar vídeos
* Exibir informações gerais sobre vídeos
* Assista aos vídeos do YouTube
* Ouça vídeos do YouTube
* Modo popup (player flutuante)
* Selecione o player para assistir streaming
* Baixar vídeos
* Baixar somente áudio
* Abrir vídeo no Kodi
* Mostrar vídeos próximos/relacionados
* Pesquise no YouTube em um idioma específico
* Assistir/Bloquear material restrito
* Exibir informações gerais sobre canais
* Pesquisar canais
* Assista a vídeos de um canal
* Suporte Orbot/Tor (ainda não diretamente)
* Suporte 1080p/2K/4K
* Ver histórico
* Inscreva-se nos canais
* Procurar histórico
* Porcurar/Assistir playlists
* Assistir playlists em fila
* Vídeos em fila
* Playlists Local
* Legenda
* Suporte a live
* Mostrar comentários
### Serviços Suportados
O NewPipe suporta vários serviços. Nosso [documentação](https://teamnewpipe.github.io/documentation/) fornecer mais informações sobre como um novo serviço pode ser adicionado ao aplicativo e ao extrator. Por favor, entre em contato conosco se você pretende adicionar um novo. Atualmente, os serviços suportados são:
* YouTube
* SoundCloud \[beta\]
* media.ccc.de \[beta\]
* PeerTube instances \[beta\]
## Atualizações
Quando uma alteração no código NewPipe (devido à adição de recursos ou fixação de bugs), eventualmente ocorrerá uma versão. Estes estão no formato x.xx.x . A fim de obter esta nova versão, você pode:
1. Construa um APK de depuração você mesmo. Esta é a maneira mais rápida de obter novos recursos em seu dispositivo, mas é muito mais complicado, por isso recomendamos usar um dos outros métodos.
2. Adicione nosso repo personalizado ao F-Droid e instale-o a partir daí assim que publicarmos um lançamento. As instruções estão aqui.: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
3. Baixe o APK do [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) e instalá-lo assim que publicarmos um lançamento.
4. Atualização via F-droid. Este é o método mais lento para obter atualizações, pois o F-Droid deve reconhecer alterações, construir o próprio APK, assiná-lo e, em seguida, enviar a atualização para os usuários.
Recomendamos o método 2 para a maioria dos usuários. Os APKs instalados usando o método 2 ou 3 são compatíveis entre si, mas não com aqueles instalados usando o método 4. Isso se deve à mesma chave de assinatura (nossa) sendo usada para 2 e 3, mas uma chave de assinatura diferente (F-Droid's) está sendo usada para 4. Construir um APK depuração usando o método 1 exclui totalmente uma chave. Assinar chaves ajudam a garantir que um usuário não seja enganado para instalar uma atualização maliciosa em um aplicativo.
Enquanto isso, se você quiser trocar de fontes por algum motivo (por exemplo, a funcionalidade principal do NewPipe foi quebrada e o F-Droid ainda não tem a atualização), recomendamos seguir este procedimento:
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlistsFaça backup de seus dados através de Configurações > Conteúdo > Exportar Base de Dados para que você mantenha seu histórico, inscrições e playlists
2. Desinstale o NewPipe
3. Baixe o APK da nova fonte e instale-o
4. Importe os dados da etapa 1 via Configurações > Conteúdo > Inportar Banco de Dados
## Contribuição
Se você tem ideias, traduções, alterações de design, limpeza de códigos ou mudanças reais de código, a ajuda é sempre bem-vinda.
Quanto mais for feito, melhor fica!
Se você quiser se envolver, verifique nossa [notas de contribuição](.github/CONTRIBUTING.md).
<a href="https://hosted.weblate.org/engage/newpipe/">
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
</a>
## Doar
Se você gosta de NewPipe, ficaríamos felizes com uma doação. Você pode enviar bitcoin ou doar via Bountysource ou Liberapay. Para obter mais informações sobre como doar para a NewPipe, visite nosso [site](https://newpipe.net/donate).
<table>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
</tr>
</table>
## Política de Privacidade
O projeto NewPipe tem como objetivo proporcionar uma experiência privada e anônima para o uso de serviços web de mídia.
Portanto, o aplicativo não coleta nenhum dado sem o seu consentimento. A política de privacidade da NewPipe explica em detalhes quais dados são enviados e armazenados quando você envia um relatório de erro ou comenta em nosso blog. Você pode encontrar o documento [aqui](https://newpipe.net/legal/privacy/).
## Licença
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe é Software Livre: Você pode usar, estudar compartilhamento e melhorá-lo à sua vontade.
Especificamente, você pode redistribuir e/ou modificá-lo sob os termos do
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) publicado pela Free Software Foundation, seja a versão 3 da Licença, ou
(a sua opção) qualquer versão posterior.

144
README.ro.md Normal file
View File

@ -0,0 +1,144 @@
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
<h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">Un front-end de streaming „uşor” liber, pentru Android.</h4>
<!-- F-Droid is extremely out of date, so we hide this for now. -->
<!-- <p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p> -->
<p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
<p align="center"><a href="#screenshots">Capturi de ecran</a> &bull; <a href="#description">Descriere</a> &bull; <a href="#features">Funcţii</a> &bull; <a href="#installation-and-updates">Instalare şi actualizări</a> &bull; <a href="#contribution">Contribuţie</a> &bull; <a href="#donate">Donaţi</a> &bull; <a href="#license">Licenţă</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/">Presă</a></p>
<hr>
*Citiţi în alte limbi: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md)*
<b>Atenţionare: ACEASTA ESTE O VERSIUNE BETA, AŞA CĂ S-AR PUTE SĂ ÎNTÂLNIŢI ERORI. DACĂ SE ÎNTÂMPLĂ ACEST LUCRU, DESCHIDEŢI UN ISSUE PRIN REPSITORY-UL NOSTRU GITHUB.</b>
<b>PUNERA NEWPIPE SAU ORICĂRUI FORK AL ACESTUIA ÎN MAGAZINUL GOOGLE PLAY LE ÎNCALCĂ TERMENII ŞI CONDIŢIILE.</b>
## Capturi de ecran
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
## Descriere
NewPipe nu foloseşte nici-o bibliotecă Google framework sau API-ul Youtube. Website-urile sunt doar analizate pentru a prelua informaţia necesară, aşa că această aplicaţie poate fi folosită pe telefoane fără Serviciile Google instalate. De asemenea, nu aveţi nevoie de un cont Youtube pentru a folosi Newpipe, care este sofware liber şi copylefted.
### Funcţii
* Căutarea videoclipurilor
* Afişarea informaţiilor generale despre videoclipuri
* Urmărirea videoclipurilor Youtube
* Ascultarea videoclipurilor Youtube
* Modul popup (player plutitor)
* Selectarea playerului de streaming pentru vizionarea videoclipului
* Descărcarea videoclipurilor
* Doar descărcarea sunetului
* Deschiderea videoclipurilor cu Kodi
* Expunerea videoclipurilor următoare/asociate
* Căutarea YouTube într-o limbă specifică
* Vizionarea/Blocarea materialului restricţionat în funcţie de vârstă
* Afişarea informaţiilor generale despre canale
* Căutarea canalelor
* Vizionarea videoclipurilor dintr-un canal
* Suport Orbot/Tor (încă nu direct)
* Suport 1080p/2K/4K
* Vizionarea istoricului
* Abonarea la canale
* Căutarea în istoric
* Căutarea/vizionarea playlisturilor
* Vizionarea ca playlisturi puse în coadă
* Punerea în coadă a videoclipurilor
* Playlisturi locale
* Subtitrări
* Suport al transmiterilor live
* Afişarea comentariilor
### Servicii întreţinute
NewPipe suportă servicii multiple. [Documentele](https://teamnewpipe.github.io/documentation/) noastre furnizează mai multe informaţii în legătură cu modalităţile prin care un nou serviciu poate fi adăugat aplicaţiei şi extractorului. Vă rugăm să ne contactaţi dacă doriţi să adăugaţi unul nou. Serviciile întreţinute acum sunt:
* YouTube
* SoundCloud \[beta\]
* media.ccc.de \[beta\]
* Instanţe PeerTube \[beta\]
<!-- Hidden span to keep old links compatible. -->
<span id="updates"></span>
## Instalare şi actualizări
Puteţi instala NewPipe folosind una dintre următoarele metode:
1. Adăugaţi depozitul nostru F-droid personalizat. Instrucţiunile sunt aici: https://newpipe.schabi.org/FAQ/tutorials/install-add-fdroid-repo/
2. Descărcaţi APK-ul din [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) şi instalaţi-l.
3. Actualizaţi via F-Droid. Aceasta este cea mai lentă metodă de a obţine actualizări, deoarece F-Droid trebuie să recunoască schimbările, să constriască APK-ul, să îl semneze, iar apoi să îl trimită utilizatorilor. (**IMPORTANT**: în momentul scrierii, o problemă împiedică versiunile mai noi de 0.20.1 să fie publicate. Aşa că, dacă doriţi să folosiţi F-droid, până această problemă este rezolvată, vă recomandăm metoda 1.)
4. Construiţi un APK de depanare. Aceasta este cea mai rapidă metodă de a primi funcţii noi, dar este mult mai complicată, aşa că vă recomandăm să folosiţi una dintre celelalte metode.
Recomandăm metoda 1 pentru majoritatea utilizatorilor. APK-urile din metodele 1 şi 2 suntcompatibile una cu cealaltă, dar nu cu cele din metoda 3. Acest lucru se datorează faptului că aceeași cheie de semnare (a noastră) este utilizată pentru 1 și 2, dar o altă cheie de semnare (F-Droid) este utilizată pentru 3. Construirea unui APK de depanare folosind metoda 4 exclude o cheie în întregime. Cheile de semnare vă asigură că un utilizator nu este păcălit să instaleze o actualizare rău intenționată a unei aplicații.
Între timp, dacă doriți să schimbați sursa dintr-un anumit motiv (de exemplu, funcționalitatea de bază a NewPipe a fost întreruptă și F-Droid nu are încă actualizarea), vă recomandăm să folosiţi următoarea procedură:
1. Faceți o copie de rezervă a datelor prin Setări> Conținut> Exportați baza de date, astfel încât să vă păstrați istoricul, abonamentele și playlisturile
2. Dezinstalaţi NewPipe
3. Descărcaţi APK-ul din noua sursă şi instalaţi-l
4. Importați datele de la pasul 1 prin Setări> Conținut> Importare bază de date
## Contribuţie
Dacă aveţi idei, traduceri, schimbări de design, curaţarea codului, sau schimbări majore ale codului, ajutorul este întotdeauna binevenit.
Cu cât se face mai mult cu atât mai bună devine aplicaţia!
Dacă doriţi să vă implicaţi, accesaţi [notele noastre de contribuţie](.github/CONTRIBUTING.md).
<a href="https://hosted.weblate.org/engage/newpipe/">
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
</a>
## Donaţii
Dacă vă place NewPipe, am fi bucuroşi să primim o donaţie. Puteţi să ne trimiteţi bitcoin sau să ne donaţi cu Bountysource sau Liberapay. Pentru mai multe informaţii în legătură cu donaţiile către NewPipe, vă rugăm vizitaţi [website-ul nostru](https://newpipe.net/donate).
<table>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
</tr>
</table>
## Politica de Confidenţialitate
Proiectul NewPipe îşi propune să furnizeze o experienţă privată şi anonimă pentru utilizarea serviciilor web media.
Prin urmare, aplicaţia nu colectează niciun fel de informaţii fără acordul dumneavoastră. Politica de confidențialitate a NewPipe explică în detaliu ce date sunt trimise și stocate atunci când trimiteți un raport de blocare sau comentați pe blogul nostru. Puteți găsi documentul [aici](https://newpipe.net/legal/privacy/).
## Licenţă
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe este Software Gratuit: Îl puteţi folosi şi împărtăşi cum doriţi. Mai exact, îl puteți redistribui și / sau modifica în conformitate cu termenii
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) aşa cum a fost publicat de Free Software Foundation, fie versiunea 3 a Licenței, fie
(la alegerea dvs.) orice versiune ulterioară.

136
README.so.md Normal file
View File

@ -0,0 +1,136 @@
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
<h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">App bilaash ah oo fudud looguna talagalay in Android-ka wax loogu daawado.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
<p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="Siidaynta GitHub "><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="Laysinka: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Darajada Dhismaha"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Heerka Turjimaada"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="Kanaalka IRC: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Kuwa Bountysource "><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
<p align="center"><a href="#sawir-shaashadeed">Sawir-shaashadeed</a> &bull; <a href="#faahfaahin">Faahfaahin</a> &bull; <a href="#waxqabadka">Waxqabadka</a> &bull; <a href="#kushubida-iyo-cusboonaysiinta">Kushubida iyo cusboonaysiinta</a> &bull; <a href="#kusoo-kordhin">Kusoo Kordhin</a> &bull; <a href="#ugu-deeq">Ugu Deeq</a> &bull; <a href="#laysinka">Laysinka</a></p>
<p align="center"><a href="https://newpipe.net">Website-ka</a> &bull; <a href="https://newpipe.net/blog/">Maqaalada</a> &bull; <a href="https://newpipe.net/FAQ/">Su'aalaha Aalaa La-iswaydiiyo</a> &bull; <a href="https://newpipe.net/press/">Warbaahinta</a></p>
<hr>
*Ku akhri luuqad kale: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt_BR.md), [日本語](README.ja.md), [Română](README.ro.md).*
<b>DIGNIIN: MIDKAN, NOOCA APP-KA EE HADDA WALI TIJAABO AYUU KU JIRAA, SIDAA DARTEED CILLADO AYAAD LA KULMI KARTAA. HADAAD LA KULANTO, KA FUR ARIN SHARAXAYA QAYBTANADA ARRIMAHA EE GITHUB-KA.</b>
<b>NEWPIPE AMA KUWA KU SALAYSAN IN PLAYSTORE-KA LA GALIYO WAXAY KA HOR IMANAYSAA SHARCIGA IYO SHURUUDAHA AY LEEYIHIIN.</b>
## Sawir-shaashadeed
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
## Faahfaahin
NewPipe ma isticmalo nidaamka wada shaqaynta Google, ama API-ga YouTube. Kaliya website-yada ayaa la furaa si xogta loo baahanyahay loogala soo dhex baxo, App-kan waxaa lagu isticmaali karaa aaladaha aysa ku jirin Adeegyada Google. Sidoo kale, uma baahnid akoon YouTube ah si aad u isticmaasho NewPipe, kaasoo ah barnaamij bilaash ah.
### Waxqabadka
* Raadi muuqaalo
* Soo bandhiga faahfaahin guud oo muuqaalada ku saabsan
* Ku daawo muuqaalada YouTube
* Dhagayso muuqaalada YouTube
* Qaab daaqad ah (muuqaal daare yar oo application-nada dul fuula)
* Dooro muuqaal daareha aad rabto inaad wax ku daawato
* Daji muuqaalada
* Daji dhagaysiga kaliya (cod)
* Ku fur muuqaal Kodi
* Tus muuqaalada ka xiga/kuwa lamidka ah
* Inaad luuqada aad rabto wax kaga dhex raadiso YouTube
* Daawo/xanib muuqaalada da'da ku xidhan
* Soo bandhig xog guud oo ku saabsan kanaalada
* Raadi kanaalo
* Daawo muuqaalada kanaal
* Taageerida Orbot/Tor (wali toos ma aha)
* Taageerida muuqaalada 1080p/2K/4K
* Kaydka wixii hore [aad u daawatay]
* Inaad rukumato kanaalada
* Kaydinta waxaad raadisay
* Raadi/daawo xulalka
* U daawo sidii xulal la horay
* Hormo gali muuqaalada
* Xulal gudaha [aalada] ah
* Qoraal-hooseed
* Taageerida waxyaabaha tooska ah
* Soo bandhiga faalooyinka
### Adeegyada la Taageero
NewPipe wuxuu taageeraa adeegyo badan. [warqadan](https://teamnewpipe.github.io/documentation/) ayaa si faahfaahsan u sharaxaysa sida adeeg cusub loogu soo dari lahaa iyo kala fur-furaha. Fadlan nala soo xidhiidh hadaad rabto inaad mid cusub kusoo darto. Adeegyada aan hadda taageero waxaa kamid ah:
* YouTube
* SoundCloud \[tijaabo\]
* media.ccc.de \[tijaabo\]
* PeerTube instances \[tijaabo\]
## Kushubida iyo cusboonaysiinta
Marka koodhka NewPipe isbadal ku dhaco (wax cusub oo lagusoo kordhiyay ama cilad bixin), ugu dambayn waxaa lasii daayaa mid cusub (Siidayn). Siidaynta qaabkeedu waa x.xx.x . Si aad midka cusub u hesho, waxaad samayn kartaa:
1. Inaad mid cusub (APK) adigu dhisato. Tani waa mida ugu dagdag badan eed waxyaabaha cusub ku heli karto, laakiin way adagtahay, sidaa darteed waxaan soojeedinaynaa inaad isticmaasho qababka kale.
2. Ku dar qayb gaar ah xaganaga F-Droid oo xagaas kaga shub isla markay siidayn soobaxdo. Hagitaanka xagan ka eeg: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
3. Kasoo dajiso APK-ga xaga [Siidaynta Github](https://github.com/TeamNewPipe/NewPipe/releases) oo ku shubo isla markay siidayn soobaxdo.
4. Ka cusboonaysii xaga F-Droid. Tani waa mida ugu daahitaanka badan, sababtoo ah F-Droid waxay fiirin isbadalka waxayna iyadu dhisi mid (app), sixiixi, kadibna ay cusboonaysiinta usiidayn isticmaalayaasha.
Waxaan usoojeedinaynaa isticmaaalka qaabka 2 dadka badankood. APK-yada loogu shubo qaabka 2 ama 3 way isqaadan karaan, laakiin isma qaadan karaan kuwa loogu shubay qaabka 4. Sababtuna waxaa weeye furaha sixiixa oo iskumid ah (kaanaga weeye) oo loo isticmaalay 2 iyo 3, laakiin furo sixiixeed ka duwan (midka F-Droid) oo loo isticmaalay 4. Dhisida APK ayadoo la isticmaalayo qaabka 1 waxay gabi ahaanba ka reebtaa wax fure ah. Furayaasha sixiixa waxay xaqiijiyaan in isticmaalaha aan lagu khaldin inuu ku shubto cusboonaysiin khalad ah (wax lasoo dhexraaciyay) app-ka.
Waxaa kale, hadaad rabto inaad tixraacayada kala badasho sabab jirta awgeed (tusaale shaqaynta aasaasiga ah ee NewPipe ayaa khalkhashay F-Droid-na wali cusboonysiin ma hayo), waxaan soojeedinaynaa isticmaalka qaabkan:
1. Xogtaada koobi ka samee adoo raacaya Fadhiga > Luuqada & Fadhiga Kale > Gudbi Xog Diyaaran si aysa kaaga bixin kaydka wixii hore, rukunka, iyo xulalka
2. Saar NewPipe
3. Kasoo daji APK-ga tixraaca cusub oo ku shub
4. Kasoo gali xogta talaabada 1 xaga Fadhiga > Luuqada & Fadhiga Kale > Soo Gali Xog Kaydsan
## Kusoo Kordhin
Hadaad hayso fikrado; rogid, qaab badal, nadiifin koodh, ama koodhka ood si wayn wax oga badashaa—caawinta marwalba waa lasoo dhawaynayaa. Waxbadan hadii la qabto waxbadan ayaa fiicnaan!
Hadaad jeceshahay inaad qayb ka noqoto, fiiri [ogaysiisyada kusoo kordhinta](.github/CONTRIBUTING.md).
<a href="https://hosted.weblate.org/engage/newpipe/">
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Heerka turjimaada" />
</a>
## Ugu Deeq
Hadaad jeceshahay NewPipe waan ku faraxsanaan lahayn deeq. Waxaad soo diri kartaa bitcoin ama sidoo kale waxaad deeqda kusoo diri kartaa xaga Bountysource ama Liberapay. Faahfaahin dheeraad ah oo kusaabsan ugu deeqida NewPipe, fadlan booqo [website-kanaga](https://newpipe.net/donate).
<table>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
</tr>
</table>
## Siyaasada Sirdhawrka
Mashruuca NewPipe waxay ujeedadiisu tahay inuu bixiyo wax kuu gaar ah, oo adoon shaqsi ahaan laguu aqoonsan aad isticmaasho website-yada wax laga daawado/dhagaysto.
Sidaa darteed, app-ku wax xog ah ma uruuriyo fasaxaaga la'aantii. Siyaasada Sirdhawrka NewPipe ayaa si faahfaahsan u sharaxda waxii xog ah ee la diro markaad cillad wariso, ama aad bogganaga faallo ka dhiibato. Warqada waxaad ka heli kartaa [halkan](https://newpipe.net/legal/privacy/).
## Laysinka
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe waa barnaamij bilaash ah oon lahayn xuquuqda daabacaada: Waad isticmaali kartaa, waad wadaagi kartaa waadna hormarin kartaa hadaad rabto. Gaar ahaan waad sii daabici kartaa ama wax baad ka badali kartaa ayadoo la raacayo shuruudaha sharciga guud ee [GNU](https://www.gnu.org/licenses/gpl.html) sida ay soosaareen Ururka Barnaamijyada Bilaashka ah, soosaarista 3aad ee laysinka, ama (hadaad doonto) nooc walba oo kasii dambeeyay laysinkii 3aad.

View File

@ -13,8 +13,8 @@ android {
resValue "string", "app_name", "NewPipe"
minSdkVersion 19
targetSdkVersion 29
versionCode 963
versionName "0.20.9"
versionCode 964
versionName "0.20.10"
multiDexEnabled true
@ -102,6 +102,7 @@ ext {
groupieVersion = '2.8.1'
markwonVersion = '4.6.0'
googleAutoServiceVersion = '1.0-rc7'
mockitoVersion = '3.6.0'
}
configurations {
@ -179,7 +180,7 @@ dependencies {
// NewPipe dependencies
// You can use a local version by uncommenting a few lines in settings.gradle
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.20.9'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.20.10'
implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
implementation "org.jsoup:jsoup:1.13.1"
@ -203,6 +204,7 @@ dependencies {
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.media:media:1.2.1'
implementation 'androidx.webkit:webkit:1.4.0'
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
@ -215,7 +217,7 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "com.xwray:groupie:${groupieVersion}"
implementation "com.xwray:groupie-kotlin-android-extensions:${groupieVersion}"
implementation "com.xwray:groupie-viewbinding:${groupieVersion}"
implementation "de.hdodenhof:circleimageview:3.1.0"
implementation "com.nostra13.universalimageloader:universal-image-loader:1.9.5"
@ -234,7 +236,8 @@ dependencies {
implementation "org.ocpsoft.prettytime:prettytime:4.0.6.Final"
testImplementation 'junit:junit:4.13.1'
testImplementation 'org.mockito:mockito-core:3.6.0'
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
androidTestImplementation "androidx.test.ext:junit:1.1.2"
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"

View File

@ -53,7 +53,7 @@
</service>
<activity
android:name=".player.BackgroundPlayerActivity"
android:name=".player.PlayQueueActivity"
android:label="@string/title_activity_play_queue"
android:launchMode="singleTask" />
@ -227,7 +227,7 @@
<data android:host="invidio.us" />
<data android:host="dev.invidio.us" />
<data android:host="www.invidio.us" />
<data android:host="vid.encryptionin.space" />
<data android:host="redirect.invidious.io" />
<data android:host="invidious.snopyta.org" />
<data android:host="yewtu.be" />
<data android:host="tube.connect.cafe" />
@ -239,6 +239,10 @@
<data android:host="vid.mint.lgbt" />
<data android:host="invidiou.site" />
<data android:host="invidious.fdn.fr" />
<data android:host="invidious.048596.xyz" />
<data android:host="invidious.zee.li" />
<data android:host="vid.puffyan.us" />
<data android:host="ytprivate.com" />
<data android:pathPrefix="/" />
</intent-filter>
@ -318,6 +322,8 @@
android:name=".RouterActivity$FetcherService"
android:exported="false" />
<!-- opting out of sending metrics to Google in Android System WebView -->
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
<!-- see https://github.com/TeamNewPipe/NewPipe/issues/3947 -->
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
<meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/>

View File

@ -0,0 +1,245 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- saved from url=(0050)https://www.eclipse.org/org/documents/epl-v10.html -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>Eclipse Public License - Version 1.0</title>
</head>
<body cz-shortcut-listen="true" lang="EN-US">
<h2>Eclipse Public License - v 1.0</h2>
<p>THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR
DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS
AGREEMENT.</p>
<p><b>1. DEFINITIONS</b></p>
<p>"Contribution" means:</p>
<p class="list">a) in the case of the initial Contributor, the initial
code and documentation distributed under this Agreement, and</p>
<p class="list">b) in the case of each subsequent Contributor:</p>
<p class="list">i) changes to the Program, and</p>
<p class="list">ii) additions to the Program;</p>
<p class="list">where such changes and/or additions to the Program
originate from and are distributed by that particular Contributor. A
Contribution 'originates' from a Contributor if it was added to the
Program by such Contributor itself or anyone acting on such
Contributor's behalf. Contributions do not include additions to the
Program which: (i) are separate modules of software distributed in
conjunction with the Program under their own license agreement, and (ii)
are not derivative works of the Program.</p>
<p>"Contributor" means any person or entity that distributes
the Program.</p>
<p>"Licensed Patents" mean patent claims licensable by a
Contributor which are necessarily infringed by the use or sale of its
Contribution alone or when combined with the Program.</p>
<p>"Program" means the Contributions distributed in accordance
with this Agreement.</p>
<p>"Recipient" means anyone who receives the Program under
this Agreement, including all Contributors.</p>
<p><b>2. GRANT OF RIGHTS</b></p>
<p class="list">a) Subject to the terms of this Agreement, each
Contributor hereby grants Recipient a non-exclusive, worldwide,
royalty-free copyright license to reproduce, prepare derivative works
of, publicly display, publicly perform, distribute and sublicense the
Contribution of such Contributor, if any, and such derivative works, in
source code and object code form.</p>
<p class="list">b) Subject to the terms of this Agreement, each
Contributor hereby grants Recipient a non-exclusive, worldwide,
royalty-free patent license under Licensed Patents to make, use, sell,
offer to sell, import and otherwise transfer the Contribution of such
Contributor, if any, in source code and object code form. This patent
license shall apply to the combination of the Contribution and the
Program if, at the time the Contribution is added by the Contributor,
such addition of the Contribution causes such combination to be covered
by the Licensed Patents. The patent license shall not apply to any other
combinations which include the Contribution. No hardware per se is
licensed hereunder.</p>
<p class="list">c) Recipient understands that although each Contributor
grants the licenses to its Contributions set forth herein, no assurances
are provided by any Contributor that the Program does not infringe the
patent or other intellectual property rights of any other entity. Each
Contributor disclaims any liability to Recipient for claims brought by
any other entity based on infringement of intellectual property rights
or otherwise. As a condition to exercising the rights and licenses
granted hereunder, each Recipient hereby assumes sole responsibility to
secure any other intellectual property rights needed, if any. For
example, if a third party patent license is required to allow Recipient
to distribute the Program, it is Recipient's responsibility to acquire
that license before distributing the Program.</p>
<p class="list">d) Each Contributor represents that to its knowledge it
has sufficient copyright rights in its Contribution, if any, to grant
the copyright license set forth in this Agreement.</p>
<p><b>3. REQUIREMENTS</b></p>
<p>A Contributor may choose to distribute the Program in object code
form under its own license agreement, provided that:</p>
<p class="list">a) it complies with the terms and conditions of this
Agreement; and</p>
<p class="list">b) its license agreement:</p>
<p class="list">i) effectively disclaims on behalf of all Contributors
all warranties and conditions, express and implied, including warranties
or conditions of title and non-infringement, and implied warranties or
conditions of merchantability and fitness for a particular purpose;</p>
<p class="list">ii) effectively excludes on behalf of all Contributors
all liability for damages, including direct, indirect, special,
incidental and consequential damages, such as lost profits;</p>
<p class="list">iii) states that any provisions which differ from this
Agreement are offered by that Contributor alone and not by any other
party; and</p>
<p class="list">iv) states that source code for the Program is available
from such Contributor, and informs licensees how to obtain it in a
reasonable manner on or through a medium customarily used for software
exchange.</p>
<p>When the Program is made available in source code form:</p>
<p class="list">a) it must be made available under this Agreement; and</p>
<p class="list">b) a copy of this Agreement must be included with each
copy of the Program.</p>
<p>Contributors may not remove or alter any copyright notices contained
within the Program.</p>
<p>Each Contributor must identify itself as the originator of its
Contribution, if any, in a manner that reasonably allows subsequent
Recipients to identify the originator of the Contribution.</p>
<p><b>4. COMMERCIAL DISTRIBUTION</b></p>
<p>Commercial distributors of software may accept certain
responsibilities with respect to end users, business partners and the
like. While this license is intended to facilitate the commercial use of
the Program, the Contributor who includes the Program in a commercial
product offering should do so in a manner which does not create
potential liability for other Contributors. Therefore, if a Contributor
includes the Program in a commercial product offering, such Contributor
("Commercial Contributor") hereby agrees to defend and
indemnify every other Contributor ("Indemnified Contributor")
against any losses, damages and costs (collectively "Losses")
arising from claims, lawsuits and other legal actions brought by a third
party against the Indemnified Contributor to the extent caused by the
acts or omissions of such Commercial Contributor in connection with its
distribution of the Program in a commercial product offering. The
obligations in this section do not apply to any claims or Losses
relating to any actual or alleged intellectual property infringement. In
order to qualify, an Indemnified Contributor must: a) promptly notify
the Commercial Contributor in writing of such claim, and b) allow the
Commercial Contributor to control, and cooperate with the Commercial
Contributor in, the defense and any related settlement negotiations. The
Indemnified Contributor may participate in any such claim at its own
expense.</p>
<p>For example, a Contributor might include the Program in a commercial
product offering, Product X. That Contributor is then a Commercial
Contributor. If that Commercial Contributor then makes performance
claims, or offers warranties related to Product X, those performance
claims and warranties are such Commercial Contributor's responsibility
alone. Under this section, the Commercial Contributor would have to
defend claims against the other Contributors related to those
performance claims and warranties, and if a court requires any other
Contributor to pay any damages as a result, the Commercial Contributor
must pay those damages.</p>
<p><b>5. NO WARRANTY</b></p>
<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION,
ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
responsible for determining the appropriateness of using and
distributing the Program and assumes all risks associated with its
exercise of rights under this Agreement , including but not limited to
the risks and costs of program errors, compliance with applicable laws,
damage to or loss of data, programs or equipment, and unavailability or
interruption of operations.</p>
<p><b>6. DISCLAIMER OF LIABILITY</b></p>
<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.</p>
<p><b>7. GENERAL</b></p>
<p>If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of
the remainder of the terms of this Agreement, and without further action
by the parties hereto, such provision shall be reformed to the minimum
extent necessary to make such provision valid and enforceable.</p>
<p>If Recipient institutes patent litigation against any entity
(including a cross-claim or counterclaim in a lawsuit) alleging that the
Program itself (excluding combinations of the Program with other
software or hardware) infringes such Recipient's patent(s), then such
Recipient's rights granted under Section 2(b) shall terminate as of the
date such litigation is filed.</p>
<p>All Recipient's rights under this Agreement shall terminate if it
fails to comply with any of the material terms or conditions of this
Agreement and does not cure such failure in a reasonable period of time
after becoming aware of such noncompliance. If all Recipient's rights
under this Agreement terminate, Recipient agrees to cease use and
distribution of the Program as soon as reasonably practicable. However,
Recipient's obligations under this Agreement and any licenses granted by
Recipient relating to the Program shall continue and survive.</p>
<p>Everyone is permitted to copy and distribute copies of this
Agreement, but in order to avoid inconsistency the Agreement is
copyrighted and may only be modified in the following manner. The
Agreement Steward reserves the right to publish new versions (including
revisions) of this Agreement from time to time. No one other than the
Agreement Steward has the right to modify this Agreement. The Eclipse
Foundation is the initial Agreement Steward. The Eclipse Foundation may
assign the responsibility to serve as the Agreement Steward to a
suitable separate entity. Each new version of the Agreement will be
given a distinguishing version number. The Program (including
Contributions) may always be distributed subject to the version of the
Agreement under which it was received. In addition, after a new version
of the Agreement is published, Contributor may elect to distribute the
Program (including its Contributions) under the new version. Except as
expressly stated in Sections 2(a) and 2(b) above, Recipient receives no
rights or licenses to the intellectual property of any Contributor under
this Agreement, whether expressly, by implication, estoppel or
otherwise. All rights in the Program not expressly granted under this
Agreement are reserved.</p>
<p>This Agreement is governed by the laws of the State of New York and
the intellectual property laws of the United States of America. No party
to this Agreement will bring a legal action under this Agreement more
than one year after the cause of action arose. Each party waives its
rights to a jury trial in any resulting litigation.</p>
</body>
</html>

View File

@ -10,6 +10,7 @@ import android.widget.OverScroller;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import org.schabi.newpipe.R;
import java.lang.reflect.Field;
@ -27,7 +28,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
private boolean allowScroll = true;
private final Rect globalRect = new Rect();
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
R.id.playQueuePanel, R.id.playbackSeekBar,
R.id.itemsListPanel, R.id.playbackSeekBar,
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
@Override

View File

@ -22,11 +22,11 @@ import org.acra.config.CoreConfiguration;
import org.acra.config.CoreConfigurationBuilder;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
@ -67,8 +67,10 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
public class App extends MultiDexApplication {
protected static final String TAG = App.class.toString();
private static App app;
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
@Nullable private Disposable disposable = null;
@Nullable
private Disposable disposable = null;
@NonNull
public static App getApp() {
@ -91,9 +93,9 @@ public class App extends MultiDexApplication {
SettingsActivity.initSettings(this);
NewPipe.init(getDownloader(),
Localization.getPreferredLocalization(this),
Localization.getPreferredContentCountry(this));
Localization.init(getApplicationContext());
Localization.getPreferredLocalization(this),
Localization.getPreferredContentCountry(this));
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
StateSaver.init(this);
initNotificationChannels();

View File

@ -35,7 +35,7 @@ public abstract class BaseFragment extends Fragment {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(final Context context) {
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
activity = (AppCompatActivity) context;
}
@ -61,7 +61,7 @@ public abstract class BaseFragment extends Fragment {
@Override
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
if (DEBUG) {
Log.d(TAG, "onViewCreated() called with: "
@ -73,7 +73,7 @@ public abstract class BaseFragment extends Fragment {
}
@Override
public void onSaveInstanceState(final Bundle outState) {
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}

View File

@ -10,22 +10,19 @@ import android.content.pm.Signature;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.MessageDigest;
@ -34,11 +31,9 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
public final class CheckForNewAppVersion {
private CheckForNewAppVersion() { }
@ -132,7 +127,13 @@ public final class CheckForNewAppVersion {
if (BuildConfig.VERSION_CODE < versionCode) {
// A pending intent to open the apk location url in the browser.
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
final Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
final Intent intent = new Intent(Intent.ACTION_CHOOSER);
intent.putExtra(Intent.EXTRA_INTENT, viewIntent);
intent.putExtra(Intent.EXTRA_TITLE, R.string.open_with);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final PendingIntent pendingIntent
= PendingIntent.getActivity(application, 0, intent, 0);
@ -170,6 +171,7 @@ public final class CheckForNewAppVersion {
@Nullable
public static Disposable checkNewVersion(@NonNull final App app) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
final NewVersionManager manager = new NewVersionManager();
// Check if user has enabled/disabled update checking
// and if the current apk is a github one or not.
@ -177,31 +179,52 @@ public final class CheckForNewAppVersion {
return null;
}
return Maybe
.fromCallable(() -> {
if (!isConnected(app)) {
return null;
}
// Check if the last request has happened a certain time ago
// to reduce the number of API requests.
final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0);
if (!manager.isExpired(expiry)) {
return null;
}
// Make a network request to get latest NewPipe data.
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
})
return Maybe
.fromCallable(() -> {
if (!isConnected(app)) {
return null;
}
// Make a network request to get latest NewPipe data.
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
response -> {
try {
// Store a timestamp which needs to be exceeded,
// before a new request to the API is made.
final long newExpiry = manager
.coerceExpiry(response.getHeader("expires"));
prefs.edit()
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
.apply();
} catch (final Exception e) {
if (DEBUG) {
Log.w(TAG, "Could not extract and save new expiry date", e);
}
}
// Parse the json from the response.
try {
final JsonObject githubStableObject = JsonParser.object()
.from(response).getObject("flavors").getObject("github")
.getObject("stable");
.from(response.responseBody()).getObject("flavors")
.getObject("github").getObject("stable");
final String versionName = githubStableObject
.getString("version");
.getString("version");
final int versionCode = githubStableObject
.getInt("version_code");
.getInt("version_code");
final String apkLocationUrl = githubStableObject
.getString("apk");
.getString("apk");
compareAppVersionAndShowNotification(app, versionName,
apkLocationUrl, versionCode);

View File

@ -2,10 +2,10 @@ package org.schabi.newpipe;
import android.content.Context;
import android.os.Build;
import androidx.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.downloader.Request;

View File

@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import androidx.preference.PreferenceManager;
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;

View File

@ -68,7 +68,7 @@ import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.player.VideoPlayer;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
@ -468,7 +468,7 @@ public class MainActivity extends AppCompatActivity {
protected void onResume() {
assureCorrectAppLanguage(this);
// Change the date format to match the selected language on resume
Localization.init(getApplicationContext());
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
super.onResume();
// Close drawer on return, and don't show animation,
@ -763,7 +763,7 @@ public class MainActivity extends AppCompatActivity {
switch (linkType) {
case STREAM:
final String intentCacheKey = intent.getStringExtra(
VideoPlayer.PLAY_QUEUE_KEY);
Player.PLAY_QUEUE_KEY);
final PlayQueue playQueue = intentCacheKey != null
? SerializedCache.getInstance()
.take(intentCacheKey, PlayQueue.class)

View File

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

View File

@ -188,7 +188,7 @@ public class RouterActivity extends AppCompatActivity {
.setPositiveButton(R.string.open_in_browser,
(dialog, which) -> ShareUtils.openUrlInBrowser(this, url))
.setNegativeButton(R.string.share,
(dialog, which) -> ShareUtils.shareUrl(this, "", url)) // no subject
(dialog, which) -> ShareUtils.shareText(this, "", url)) // no subject
.setNeutralButton(R.string.cancel, null)
.setOnDismissListener(dialog -> finish())
.show();

View File

@ -29,37 +29,46 @@ public class AboutActivity extends AppCompatActivity {
* List of all software components.
*/
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = {
new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai",
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin",
"https://github.com/ACRA/acra", StandardLicenses.APACHE2),
new SoftwareComponent("AndroidX", "2005 - 2011", "The Android Open Source Project",
"https://developer.android.com/jetpack", StandardLicenses.APACHE2),
new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof",
"https://github.com/hdodenhof/CircleImageView",
StandardLicenses.APACHE2),
new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google, Inc.",
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
new SoftwareComponent("GigaGet", "2014 - 2015", "Peter Cai",
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3),
new SoftwareComponent("Groupie", "2016", "Lisa Wray",
"https://github.com/lisawray/groupie", StandardLicenses.MIT),
new SoftwareComponent("Icepick", "2015", "Frankie Sardo",
"https://github.com/frankiesardo/icepick", StandardLicenses.EPL1),
new SoftwareComponent("Jsoup", "2009 - 2020", "Jonathan Hedley",
"https://github.com/jhy/jsoup", StandardLicenses.MIT),
new SoftwareComponent("Markwon", "2019", "Dimitry Ivanov",
"https://github.com/noties/Markwon", StandardLicenses.APACHE2),
new SoftwareComponent("Material Components for Android", "2016 - 2020", "Google, Inc.",
"https://github.com/material-components/material-components-android",
StandardLicenses.APACHE2),
new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley",
"https://github.com/jhy/jsoup", StandardLicenses.MIT),
new SoftwareComponent("Rhino", "2015", "Mozilla",
"https://www.mozilla.org/rhino/", StandardLicenses.MPL2),
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin",
"http://www.acra.ch", StandardLicenses.APACHE2),
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
"https://github.com/spacecowboy/NoNonsense-FilePicker",
StandardLicenses.MPL2),
new SoftwareComponent("OkHttp", "2019", "Square, Inc.",
"https://square.github.io/okhttp/", StandardLicenses.APACHE2),
new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2),
new SoftwareComponent("RxAndroid", "2015", "The RxAndroid authors",
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
new SoftwareComponent("RxBinding", "2015", "Jake Wharton",
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2),
new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors",
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
"https://github.com/nostra13/Android-Universal-Image-Loader",
StandardLicenses.APACHE2),
new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof",
"https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google Inc",
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
new SoftwareComponent("RxAndroid", "2015 - 2018", "The RxAndroid authors",
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors",
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
new SoftwareComponent("RxBinding", "2015 - 2018", "Jake Wharton",
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2),
new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2),
new SoftwareComponent("Markwon", "2017 - 2020", "Noties",
"https://github.com/noties/Markwon", StandardLicenses.APACHE2),
new SoftwareComponent("Groupie", "2016", "Lisa Wray",
"https://github.com/lisawray/groupie", StandardLicenses.MIT)
};
private static final int POS_ABOUT = 0;
@ -115,7 +124,8 @@ public class AboutActivity extends AppCompatActivity {
* A placeholder fragment containing a simple view.
*/
public static class AboutFragment extends Fragment {
public AboutFragment() { }
public AboutFragment() {
}
/**
* Created a new instance of this fragment for the given section number.
@ -136,16 +146,17 @@ public class AboutActivity extends AppCompatActivity {
aboutBinding.appVersion.setText(BuildConfig.VERSION_NAME);
aboutBinding.githubLink.setOnClickListener(nv ->
openUrlInBrowser(context, context.getString(R.string.github_url)));
openUrlInBrowser(context, context.getString(R.string.github_url), false));
aboutBinding.donationLink.setOnClickListener(v ->
openUrlInBrowser(context, context.getString(R.string.donation_url)));
openUrlInBrowser(context, context.getString(R.string.donation_url), false));
aboutBinding.websiteLink.setOnClickListener(nv ->
openUrlInBrowser(context, context.getString(R.string.website_url)));
openUrlInBrowser(context, context.getString(R.string.website_url), false));
aboutBinding.privacyPolicyLink.setOnClickListener(v ->
openUrlInBrowser(context, context.getString(R.string.privacy_policy_url)));
openUrlInBrowser(context, context.getString(R.string.privacy_policy_url),
false));
return aboutBinding.getRoot();
}

View File

@ -7,18 +7,20 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentLicensesBinding;
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding;
import org.schabi.newpipe.util.ShareUtils;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
@ -35,12 +37,9 @@ public class LicenseFragment extends Fragment {
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
public static LicenseFragment newInstance(final SoftwareComponent[] softwareComponents) {
if (softwareComponents == null) {
throw new NullPointerException("softwareComponents is null");
}
final LicenseFragment fragment = new LicenseFragment();
final Bundle bundle = new Bundle();
bundle.putParcelableArray(ARG_COMPONENTS, softwareComponents);
bundle.putParcelableArray(ARG_COMPONENTS, Objects.requireNonNull(softwareComponents));
final LicenseFragment fragment = new LicenseFragment();
fragment.setArguments(bundle);
return fragment;
}
@ -69,43 +68,42 @@ public class LicenseFragment extends Fragment {
@Nullable
@Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
final View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
final ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
final FragmentLicensesBinding binding = FragmentLicensesBinding
.inflate(inflater, container, false);
final View licenseLink = rootView.findViewById(R.id.app_read_license);
licenseLink.setOnClickListener(v -> {
binding.appReadLicense.setOnClickListener(v -> {
activeLicense = StandardLicenses.GPL3;
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
StandardLicenses.GPL3));
});
for (final SoftwareComponent component : softwareComponents) {
final View componentView = inflater
.inflate(R.layout.item_software_component, container, false);
final TextView softwareName = componentView.findViewById(R.id.name);
final TextView copyright = componentView.findViewById(R.id.copyright);
softwareName.setText(component.getName());
copyright.setText(getString(R.string.copyright,
final ItemSoftwareComponentBinding componentBinding = ItemSoftwareComponentBinding
.inflate(inflater, container, false);
componentBinding.name.setText(component.getName());
componentBinding.copyright.setText(getString(R.string.copyright,
component.getYears(),
component.getCopyrightOwner(),
component.getLicense().getAbbreviation()));
componentView.setTag(component);
componentView.setOnClickListener(v -> {
final View root = componentBinding.getRoot();
root.setTag(component);
root.setOnClickListener(v -> {
activeLicense = component.getLicense();
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
component.getLicense()));
});
softwareComponentsView.addView(componentView);
registerForContextMenu(componentView);
binding.softwareComponents.addView(root);
registerForContextMenu(root);
}
if (activeLicense != null) {
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
activeLicense));
}
return rootView;
return binding.getRoot();
}
@Override

View File

@ -12,6 +12,8 @@ public final class StandardLicenses {
= new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html");
public static final License MIT
= new License("MIT License", "MIT", "mit.html");
public static final License EPL1
= new License("Eclipse Public License, Version 1.0", "EPL 1.0", "epl1.html");
private StandardLicenses() { }
}

View File

@ -16,12 +16,8 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.IdRes;
@ -40,6 +36,7 @@ import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.RouterActivity;
import org.schabi.newpipe.databinding.DownloadDialogBinding;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.Localization;
@ -116,11 +113,7 @@ public class DownloadDialog extends DialogFragment
private final CompositeDisposable disposables = new CompositeDisposable();
private EditText nameEditText;
private Spinner streamsSpinner;
private RadioGroup radioStreamsGroup;
private TextView threadsCountTextView;
private SeekBar threadsSeekBar;
private DownloadDialogBinding dialogBinding;
private SharedPreferences prefs;
@ -277,38 +270,35 @@ public class DownloadDialog extends DialogFragment
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
nameEditText = view.findViewById(R.id.file_name);
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
dialogBinding = DownloadDialogBinding.bind(view);
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
currentInfo.getName()));
selectedAudioIndex = ListHelper
.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
streamsSpinner = view.findViewById(R.id.quality_spinner);
streamsSpinner.setOnItemSelectedListener(this);
dialogBinding.qualitySpinner.setOnItemSelectedListener(this);
threadsCountTextView = view.findViewById(R.id.threads_count);
threadsSeekBar = view.findViewById(R.id.threads);
dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this);
radioStreamsGroup = view.findViewById(R.id.video_audio_group);
radioStreamsGroup.setOnCheckedChangeListener(this);
initToolbar(view.findViewById(R.id.toolbar));
initToolbar(dialogBinding.toolbarLayout.toolbar);
setupDownloadOptions();
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
final int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
threadsCountTextView.setText(String.valueOf(threads));
threadsSeekBar.setProgress(threads - 1);
threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
dialogBinding.threadsCount.setText(String.valueOf(threads));
dialogBinding.threads.setProgress(threads - 1);
dialogBinding.threads.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(final SeekBar seekbar, final int progress,
final boolean fromUser) {
final int newProgress = progress + 1;
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
.apply();
threadsCountTextView.setText(String.valueOf(newProgress));
dialogBinding.threadsCount.setText(String.valueOf(newProgress));
}
@Override
@ -326,19 +316,19 @@ public class DownloadDialog extends DialogFragment
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
.subscribe(result -> {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) {
setupVideoSpinner();
}
}));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams)
.subscribe(result -> {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
setupAudioSpinner();
}
}));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
.subscribe(result -> {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
setupSubtitleSpinner();
}
}));
@ -350,6 +340,12 @@ public class DownloadDialog extends DialogFragment
disposables.clear();
}
@Override
public void onDestroyView() {
dialogBinding = null;
super.onDestroyView();
}
/*//////////////////////////////////////////////////////////////////////////
// Radio group Video&Audio options - Listener
//////////////////////////////////////////////////////////////////////////*/
@ -429,8 +425,8 @@ public class DownloadDialog extends DialogFragment
return;
}
streamsSpinner.setAdapter(audioStreamsAdapter);
streamsSpinner.setSelection(selectedAudioIndex);
dialogBinding.qualitySpinner.setAdapter(audioStreamsAdapter);
dialogBinding.qualitySpinner.setSelection(selectedAudioIndex);
setRadioButtonsState(true);
}
@ -439,8 +435,8 @@ public class DownloadDialog extends DialogFragment
return;
}
streamsSpinner.setAdapter(videoStreamsAdapter);
streamsSpinner.setSelection(selectedVideoIndex);
dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter);
dialogBinding.qualitySpinner.setSelection(selectedVideoIndex);
setRadioButtonsState(true);
}
@ -449,8 +445,8 @@ public class DownloadDialog extends DialogFragment
return;
}
streamsSpinner.setAdapter(subtitleStreamsAdapter);
streamsSpinner.setSelection(selectedSubtitleIndex);
dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter);
dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex);
setRadioButtonsState(true);
}
@ -475,7 +471,7 @@ public class DownloadDialog extends DialogFragment
break;
}
threadsSeekBar.setEnabled(flag);
dialogBinding.threads.setEnabled(flag);
}
@Override
@ -486,7 +482,7 @@ public class DownloadDialog extends DialogFragment
+ "parent = [" + parent + "], view = [" + view + "], "
+ "position = [" + position + "], id = [" + id + "]");
}
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
selectedAudioIndex = position;
break;
@ -506,16 +502,14 @@ public class DownloadDialog extends DialogFragment
protected void setupDownloadOptions() {
setRadioButtonsState(false);
final RadioButton audioButton = radioStreamsGroup.findViewById(R.id.audio_button);
final RadioButton videoButton = radioStreamsGroup.findViewById(R.id.video_button);
final RadioButton subtitleButton = radioStreamsGroup.findViewById(R.id.subtitle_button);
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
subtitleButton.setVisibility(isSubtitleStreamsAvailable ? View.VISIBLE : View.GONE);
dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
? View.VISIBLE : View.GONE);
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
@ -523,24 +517,24 @@ public class DownloadDialog extends DialogFragment
if (isVideoStreamsAvailable
&& (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) {
videoButton.setChecked(true);
dialogBinding.videoButton.setChecked(true);
setupVideoSpinner();
} else if (isAudioStreamsAvailable
&& (defaultMedia.equals(getString(R.string.last_download_type_audio_key)))) {
audioButton.setChecked(true);
dialogBinding.audioButton.setChecked(true);
setupAudioSpinner();
} else if (isSubtitleStreamsAvailable
&& (defaultMedia.equals(getString(R.string.last_download_type_subtitle_key)))) {
subtitleButton.setChecked(true);
dialogBinding.subtitleButton.setChecked(true);
setupSubtitleSpinner();
} else if (isVideoStreamsAvailable) {
videoButton.setChecked(true);
dialogBinding.videoButton.setChecked(true);
setupVideoSpinner();
} else if (isAudioStreamsAvailable) {
audioButton.setChecked(true);
dialogBinding.audioButton.setChecked(true);
setupAudioSpinner();
} else if (isSubtitleStreamsAvailable) {
subtitleButton.setChecked(true);
dialogBinding.subtitleButton.setChecked(true);
setupSubtitleSpinner();
} else {
Toast.makeText(getContext(), R.string.no_streams_available_download,
@ -550,9 +544,9 @@ public class DownloadDialog extends DialogFragment
}
private void setRadioButtonsState(final boolean enabled) {
radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled);
radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled);
radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
dialogBinding.audioButton.setEnabled(enabled);
dialogBinding.videoButton.setEnabled(enabled);
dialogBinding.subtitleButton.setEnabled(enabled);
}
private int getSubtitleIndexBy(final List<SubtitlesStream> streams) {
@ -582,7 +576,7 @@ public class DownloadDialog extends DialogFragment
}
private String getNameEditText() {
final String str = nameEditText.getText().toString().trim();
final String str = dialogBinding.fileName.getText().toString().trim();
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
}
@ -619,7 +613,7 @@ public class DownloadDialog extends DialogFragment
String filename = getNameEditText().concat(".");
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio;
@ -669,7 +663,7 @@ public class DownloadDialog extends DialogFragment
filename, mime);
} else {
File initialSavePath;
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
} else {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
@ -862,7 +856,7 @@ public class DownloadDialog extends DialogFragment
final Stream selectedStream;
Stream secondaryStream = null;
final char kind;
int threads = threadsSeekBar.getProgress() + 1;
int threads = dialogBinding.threads.getProgress() + 1;
final String[] urls;
final MissionRecoveryInfo[] recoveryInfo;
String psName = null;
@ -870,7 +864,7 @@ public class DownloadDialog extends DialogFragment
long nearLength = 0;
// more download logic: select muxer, subtitle converter, etc.
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
kind = 'a';
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);

View File

@ -10,6 +10,7 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
@ -22,10 +23,10 @@ import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.InfoCache;
import java.util.Collections;
@ -37,7 +38,7 @@ import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
@State
@ -56,7 +57,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
private TextView errorTextView;
@Override
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
doInitialLoadLogic();
}
@ -131,35 +132,35 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
@Override
public void showLoading() {
if (emptyStateView != null) {
animateView(emptyStateView, false, 150);
animate(emptyStateView, false, 150);
}
if (loadingProgressBar != null) {
animateView(loadingProgressBar, true, 400);
animate(loadingProgressBar, true, 400);
}
animateView(errorPanelRoot, false, 150);
animate(errorPanelRoot, false, 150);
}
@Override
public void hideLoading() {
if (emptyStateView != null) {
animateView(emptyStateView, false, 150);
animate(emptyStateView, false, 150);
}
if (loadingProgressBar != null) {
animateView(loadingProgressBar, false, 0);
animate(loadingProgressBar, false, 0);
}
animateView(errorPanelRoot, false, 150);
animate(errorPanelRoot, false, 150);
}
@Override
public void showEmptyState() {
isLoading.set(false);
if (emptyStateView != null) {
animateView(emptyStateView, true, 200);
animate(emptyStateView, true, 200);
}
if (loadingProgressBar != null) {
animateView(loadingProgressBar, false, 0);
animate(loadingProgressBar, false, 0);
}
animateView(errorPanelRoot, false, 150);
animate(errorPanelRoot, false, 150);
}
@Override
@ -174,11 +175,11 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
errorTextView.setText(message);
if (showRetryButton) {
animateView(errorButtonRetry, true, 600);
animate(errorButtonRetry, true, 600);
} else {
animateView(errorButtonRetry, false, 0);
animate(errorButtonRetry, false, 0);
}
animateView(errorPanelRoot, true, 300);
animate(errorPanelRoot, true, 300);
}
@Override

View File

@ -19,12 +19,12 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround;
import androidx.preference.PreferenceManager;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentMainBinding;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
@ -34,15 +34,13 @@ import org.schabi.newpipe.settings.tabs.TabsManager;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.ScrollableTabLayout;
import java.util.ArrayList;
import java.util.List;
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
private ViewPager viewPager;
private FragmentMainBinding binding;
private SelectedTabsPagerAdapter pagerAdapter;
private ScrollableTabLayout tabLayout;
private final List<Tab> tabsList = new ArrayList<>();
private TabsManager tabsManager;
@ -90,13 +88,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
tabLayout = rootView.findViewById(R.id.main_tab_layout);
viewPager = rootView.findViewById(R.id.pager);
binding = FragmentMainBinding.bind(rootView);
tabLayout.setTabIconTint(ColorStateList.valueOf(
binding.mainTabLayout.setTabIconTint(ColorStateList.valueOf(
ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent)));
tabLayout.setupWithViewPager(viewPager);
tabLayout.addOnTabSelectedListener(this);
binding.mainTabLayout.setupWithViewPager(binding.pager);
binding.mainTabLayout.addOnTabSelectedListener(this);
setupTabs();
}
@ -120,8 +117,9 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
public void onDestroy() {
super.onDestroy();
tabsManager.unsetSavedTabsListener();
if (viewPager != null) {
viewPager.setAdapter(null);
if (binding != null) {
binding.pager.setAdapter(null);
binding = null;
}
}
@ -172,19 +170,19 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
getChildFragmentManager(), tabsList);
}
viewPager.setAdapter(null);
viewPager.setOffscreenPageLimit(tabsList.size());
viewPager.setAdapter(pagerAdapter);
binding.pager.setAdapter(null);
binding.pager.setOffscreenPageLimit(tabsList.size());
binding.pager.setAdapter(pagerAdapter);
updateTabsIconAndDescription();
updateTitleForTab(viewPager.getCurrentItem());
updateTitleForTab(binding.pager.getCurrentItem());
hasTabsChanged = false;
}
private void updateTabsIconAndDescription() {
for (int i = 0; i < tabsList.size(); i++) {
final TabLayout.Tab tabToSet = tabLayout.getTabAt(i);
final TabLayout.Tab tabToSet = binding.mainTabLayout.getTabAt(i);
if (tabToSet != null) {
final Tab tab = tabsList.get(i);
tabToSet.setIcon(tab.getTabIconRes(requireContext()));

View File

@ -0,0 +1,93 @@
package org.schabi.newpipe.fragments.detail;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.TextLinkifier;
import icepick.State;
import io.reactivex.rxjava3.disposables.Disposable;
import static android.text.TextUtils.isEmpty;
public class DescriptionFragment extends BaseFragment {
@State
StreamInfo streamInfo = null;
@Nullable
Disposable descriptionDisposable = null;
public DescriptionFragment() {
}
public DescriptionFragment(final StreamInfo streamInfo) {
this.streamInfo = streamInfo;
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
final FragmentDescriptionBinding binding =
FragmentDescriptionBinding.inflate(inflater, container, false);
if (streamInfo != null) {
setupUploadDate(binding.detailUploadDateView);
setupDescription(binding.detailDescriptionView);
}
return binding.getRoot();
}
@Override
public void onDestroy() {
super.onDestroy();
if (descriptionDisposable != null) {
descriptionDisposable.dispose();
}
}
private void setupUploadDate(final TextView uploadDateTextView) {
if (streamInfo.getUploadDate() != null) {
uploadDateTextView.setText(Localization
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
} else {
uploadDateTextView.setVisibility(View.GONE);
}
}
private void setupDescription(final TextView descriptionTextView) {
final Description description = streamInfo.getDescription();
if (description == null || isEmpty(description.getContent())
|| description == Description.emptyDescription) {
descriptionTextView.setText("");
return;
}
switch (description.getType()) {
case Description.HTML:
descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock(requireContext(),
description.getContent(), descriptionTextView,
HtmlCompat.FROM_HTML_MODE_LEGACY);
break;
case Description.MARKDOWN:
descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(requireContext(),
description.getContent(), descriptionTextView);
break;
case Description.PLAIN_TEXT: default:
descriptionDisposable = TextLinkifier.createLinksFromPlainText(requireContext(),
description.getContent(), descriptionTextView);
break;
}
}
}

View File

@ -12,13 +12,16 @@ import android.view.MenuInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PignateFooterBinding;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
@ -31,6 +34,7 @@ import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StateSaver;
@ -42,7 +46,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.Queue;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
implements ListViewContract<I, N>, StateSaver.WriteRead,
@ -66,7 +70,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(final Context context) {
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
if (infoListAdapter == null) {
@ -182,7 +186,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
}
@Override
public void onSaveInstanceState(final Bundle bundle) {
public void onSaveInstanceState(@NonNull final Bundle bundle) {
super.onSaveInstanceState(bundle);
if (useDefaultStateSaving) {
savedState = StateSaver
@ -214,12 +218,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
// Init
//////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() {
@Nullable
protected ViewBinding getListHeader() {
return null;
}
protected View getListFooter() {
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false);
protected ViewBinding getListFooter() {
return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false);
}
protected RecyclerView.LayoutManager getListLayoutManager() {
@ -246,8 +251,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
infoListAdapter.setUseGridVariant(useGrid);
infoListAdapter.setFooter(getListFooter());
infoListAdapter.setHeader(getListHeader());
infoListAdapter.setFooter(getListFooter().getRoot());
final ViewBinding listHeader = getListHeader();
if (listHeader != null) {
infoListAdapter.setHeader(listHeader.getRoot());
}
itemsList.setAdapter(infoListAdapter);
}
@ -332,7 +341,6 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
}
}
protected void showStreamDialog(final StreamInfoItem item) {
final Context context = getContext();
final Activity activity = getActivity();
@ -359,6 +367,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
StreamDialogEntry.share
));
}
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
StreamDialogEntry.setEnabledEntries(entries);
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
@ -395,23 +406,17 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
// animateView(itemsList, false, 400);
}
@Override
public void hideLoading() {
super.hideLoading();
animateView(itemsList, true, 300);
animate(itemsList, true, 300);
}
@Override
public void showError(final String message, final boolean showRetryButton) {
super.showError(message, showRetryButton);
showListFooter(false);
animateView(itemsList, false, 200);
animate(itemsList, false, 200);
}
@Override

View File

@ -1,8 +1,6 @@
package org.schabi.newpipe.fragments.list.channel;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
@ -14,20 +12,21 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.viewbinding.ViewBinding;
import com.jakewharton.rxbinding4.view.RxView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
import org.schabi.newpipe.databinding.FragmentChannelBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
@ -36,12 +35,12 @@ import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
@ -63,9 +62,9 @@ import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor;
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
implements View.OnClickListener {
@ -78,22 +77,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
//////////////////////////////////////////////////////////////////////////*/
private SubscriptionManager subscriptionManager;
private View headerRootLayout;
private ImageView headerChannelBanner;
private ImageView headerAvatarView;
private TextView headerTitleView;
private ImageView headerSubChannelAvatarView;
private TextView headerSubChannelTitleView;
private TextView headerSubscribersTextView;
private Button headerSubscribeButton;
private View playlistCtrl;
private LinearLayout headerPlayAllButton;
private LinearLayout headerPopupButton;
private LinearLayout headerBackgroundButton;
private FragmentChannelBinding channelBinding;
private ChannelHeaderBinding headerBinding;
private PlaylistControlBinding playlistControlBinding;
private MenuItem menuRssButton;
private TextView contentNotSupportedTextView;
private TextView kaomojiTextView;
private TextView noVideosTextView;
public static ChannelFragment getInstance(final int serviceId, final String url,
final String name) {
@ -117,7 +106,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(final Context context) {
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
subscriptionManager = new SubscriptionManager(activity);
}
@ -130,55 +119,42 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
}
@Override
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
contentNotSupportedTextView = rootView.findViewById(R.id.error_content_not_supported);
kaomojiTextView = rootView.findViewById(R.id.channel_kaomoji);
noVideosTextView = rootView.findViewById(R.id.channel_no_videos);
channelBinding = FragmentChannelBinding.bind(rootView);
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) {
disposables.clear();
}
disposables.clear();
if (subscribeButtonMonitor != null) {
subscribeButtonMonitor.dispose();
}
channelBinding = null;
headerBinding = null;
playlistControlBinding = null;
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() {
headerRootLayout = activity.getLayoutInflater()
.inflate(R.layout.channel_header, itemsList, false);
headerChannelBanner = headerRootLayout.findViewById(R.id.channel_banner_image);
headerAvatarView = headerRootLayout.findViewById(R.id.channel_avatar_view);
headerTitleView = headerRootLayout.findViewById(R.id.channel_title_view);
headerSubscribersTextView = headerRootLayout.findViewById(R.id.channel_subscriber_view);
headerSubscribeButton = headerRootLayout.findViewById(R.id.channel_subscribe_button);
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
headerSubChannelAvatarView =
headerRootLayout.findViewById(R.id.sub_channel_avatar_view);
headerSubChannelTitleView =
headerRootLayout.findViewById(R.id.sub_channel_title_view);
@Override
protected ViewBinding getListHeader() {
headerBinding = ChannelHeaderBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
playlistControlBinding = headerBinding.playlistControl;
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
return headerRootLayout;
return headerBinding;
}
@Override
protected void initListeners() {
super.initListeners();
headerSubChannelTitleView.setOnClickListener(this);
headerSubChannelAvatarView.setOnClickListener(this);
headerBinding.subChannelTitleView.setOnClickListener(this);
headerBinding.subChannelAvatarView.setOnClickListener(this);
}
/*//////////////////////////////////////////////////////////////////////////
@ -205,8 +181,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
private void openRssFeed() {
final ChannelInfo info = currentInfo;
if (info != null) {
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl()));
startActivity(intent);
ShareUtils.openUrlInBrowser(requireContext(), info.getFeedUrl(), false);
}
}
@ -226,7 +201,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
break;
case R.id.menu_item_share:
if (currentInfo != null) {
ShareUtils.shareUrl(requireContext(), name, currentInfo.getOriginalUrl());
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl());
}
break;
default:
@ -241,7 +216,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
private void monitorSubscription(final ChannelInfo info) {
final Consumer<Throwable> onError = (Throwable throwable) -> {
animateView(headerSubscribeButton, false, 100);
animate(headerBinding.channelSubscribeButton, false, 100);
showSnackBarError(throwable, UserAction.SUBSCRIPTION,
NewPipe.getNameOfService(currentInfo.getServiceId()),
"Get subscription status", 0);
@ -351,15 +326,15 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
info.getAvatarUrl(),
info.getDescription(),
info.getSubscriberCount());
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton,
mapOnSubscribe(channel, info));
subscribeButtonMonitor = monitorSubscribeButton(
headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info));
} else {
if (DEBUG) {
Log.d(TAG, "Found subscription to this channel!");
}
final SubscriptionEntity subscription = subscriptionEntities.get(0);
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton,
mapOnUnsubscribe(subscription));
subscribeButtonMonitor = monitorSubscribeButton(
headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription));
}
};
}
@ -370,7 +345,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
+ "isSubscribed = [" + isSubscribed + "]");
}
final boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE;
final boolean isButtonVisible = headerBinding.channelSubscribeButton.getVisibility()
== View.VISIBLE;
final int backgroundDuration = isButtonVisible ? 300 : 0;
final int textDuration = isButtonVisible ? 200 : 0;
@ -382,18 +358,21 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
if (!isSubscribed) {
headerSubscribeButton.setText(R.string.subscribe_button_title);
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground,
subscribeBackground);
animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText);
headerBinding.channelSubscribeButton.setText(R.string.subscribe_button_title);
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
subscribedBackground, subscribeBackground);
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribedText,
subscribeText);
} else {
headerSubscribeButton.setText(R.string.subscribed_button_title);
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground,
subscribedBackground);
animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText);
headerBinding.channelSubscribeButton.setText(R.string.subscribed_button_title);
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
subscribeBackground, subscribedBackground);
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribeText,
subscribedText);
}
animateView(headerSubscribeButton, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, true, 100);
animate(headerBinding.channelSubscribeButton, true, 100,
AnimationType.LIGHT_SCALE_AND_ALPHA);
}
/*//////////////////////////////////////////////////////////////////////////
@ -446,48 +425,49 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
public void showLoading() {
super.showLoading();
IMAGE_LOADER.cancelDisplayTask(headerChannelBanner);
IMAGE_LOADER.cancelDisplayTask(headerAvatarView);
IMAGE_LOADER.cancelDisplayTask(headerSubChannelAvatarView);
animateView(headerSubscribeButton, false, 100);
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelBannerImage);
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelAvatarView);
IMAGE_LOADER.cancelDisplayTask(headerBinding.subChannelAvatarView);
animate(headerBinding.channelSubscribeButton, false, 100);
}
@Override
public void handleResult(@NonNull final ChannelInfo result) {
super.handleResult(result);
headerRootLayout.setVisibility(View.VISIBLE);
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerChannelBanner,
headerBinding.getRoot().setVisibility(View.VISIBLE);
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerBinding.channelBannerImage,
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerAvatarView,
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerBinding.channelAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(), headerSubChannelAvatarView,
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(),
headerBinding.subChannelAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
headerSubscribersTextView.setVisibility(View.VISIBLE);
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
if (result.getSubscriberCount() >= 0) {
headerSubscribersTextView.setText(Localization
headerBinding.channelSubscriberView.setText(Localization
.shortSubscriberCount(activity, result.getSubscriberCount()));
} else {
headerSubscribersTextView.setText(R.string.subscribers_count_not_available);
headerBinding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
}
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
headerSubChannelTitleView.setText(String.format(
headerBinding.subChannelTitleView.setText(String.format(
getString(R.string.channel_created_by),
currentInfo.getParentChannelName())
);
headerSubChannelTitleView.setVisibility(View.VISIBLE);
headerSubChannelAvatarView.setVisibility(View.VISIBLE);
headerBinding.subChannelTitleView.setVisibility(View.VISIBLE);
headerBinding.subChannelAvatarView.setVisibility(View.VISIBLE);
} else {
headerSubChannelTitleView.setVisibility(View.GONE);
headerBinding.subChannelTitleView.setVisibility(View.GONE);
}
if (menuRssButton != null) {
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
}
playlistCtrl.setVisibility(View.VISIBLE);
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
final List<Throwable> errors = new ArrayList<>(result.getErrors());
if (!errors.isEmpty()) {
@ -516,29 +496,32 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
updateSubscription(result);
monitorSubscription(result);
headerPlayAllButton.setOnClickListener(view -> NavigationHelper
.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view -> NavigationHelper
.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view -> NavigationHelper
.playOnBackgroundPlayer(activity, getPlayQueue(), false));
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));
headerPopupButton.setOnLongClickListener(view -> {
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
return true;
});
headerBackgroundButton.setOnLongClickListener(view -> {
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
return true;
});
}
private void showContentNotSupported() {
contentNotSupportedTextView.setVisibility(View.VISIBLE);
kaomojiTextView.setText("(︶︹︺)");
kaomojiTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
noVideosTextView.setVisibility(View.GONE);
channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE);
channelBinding.channelKaomoji.setText("(︶︹︺)");
channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
channelBinding.channelNoVideos.setVisibility(View.GONE);
}
private PlayQueue getPlayQueue() {
@ -596,7 +579,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
public void setTitle(final String title) {
super.setTitle(title);
if (!useAsFrontPage) {
headerTitleView.setText(title);
headerBinding.channelTitleView.setText(title);
}
}
}

View File

@ -1,6 +1,5 @@
package org.schabi.newpipe.fragments.list.comments;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
@ -16,8 +15,8 @@ import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import io.reactivex.rxjava3.core.Single;
@ -37,11 +36,6 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(final Context context) {
super.onAttach(context);
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@ -52,9 +46,7 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) {
disposables.clear();
}
disposables.clear();
}
/*//////////////////////////////////////////////////////////////////////////
@ -84,16 +76,14 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
public void handleResult(@NonNull final CommentsInfo result) {
super.handleResult(result);
AnimationUtils.slideUp(requireView(), 120, 150, 0.06f);
ViewUtils.slideUp(requireView(), 120, 150, 0.06f);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
if (disposables != null) {
disposables.clear();
}
disposables.clear();
}
@Override

View File

@ -28,7 +28,7 @@ import org.schabi.newpipe.util.Localization;
import icepick.State;
import io.reactivex.rxjava3.core.Single;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
/**
* Created by Christian Schabesberger on 23.09.17.
@ -160,7 +160,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
@Override
public void showLoading() {
super.showLoading();
animateView(itemsList, false, 100);
animate(itemsList, false, 100);
}
@Override

View File

@ -11,18 +11,20 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.viewbinding.ViewBinding;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.databinding.PlaylistHeaderBinding;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
@ -42,6 +44,7 @@ import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
@ -52,14 +55,13 @@ import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import de.hdodenhof.circleimageview.CircleImageView;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
@ -73,17 +75,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
// Views
//////////////////////////////////////////////////////////////////////////*/
private View headerRootLayout;
private TextView headerTitleView;
private View headerUploaderLayout;
private TextView headerUploaderName;
private CircleImageView headerUploaderAvatar;
private TextView headerStreamCount;
private View playlistCtrl;
private View headerPlayAllButton;
private View headerPopupButton;
private View headerBackgroundButton;
private PlaylistHeaderBinding headerBinding;
private PlaylistControlBinding playlistControlBinding;
private MenuItem playlistBookmarkButton;
@ -118,22 +111,13 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
// Init
//////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() {
headerRootLayout = activity.getLayoutInflater()
.inflate(R.layout.playlist_header, itemsList, false);
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
headerUploaderLayout = headerRootLayout.findViewById(R.id.uploader_layout);
headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name);
headerUploaderAvatar = headerRootLayout.findViewById(R.id.uploader_avatar_view);
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
@Override
protected ViewBinding getListHeader() {
headerBinding = PlaylistHeaderBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
playlistControlBinding = headerBinding.playlistControl;
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
return headerRootLayout;
return headerBinding;
}
@Override
@ -174,6 +158,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
StreamDialogEntry.share
));
}
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
StreamDialogEntry.setEnabledEntries(entries);
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) ->
@ -199,6 +186,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
@Override
public void onDestroyView() {
headerBinding = null;
playlistControlBinding = null;
super.onDestroyView();
if (isBookmarkButtonReady != null) {
isBookmarkButtonReady.set(false);
@ -252,7 +242,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
ShareUtils.openUrlInBrowser(requireContext(), url);
break;
case R.id.menu_item_share:
ShareUtils.shareUrl(requireContext(), name, url);
ShareUtils.shareText(requireContext(), name, url);
break;
case R.id.menu_item_bookmark:
onBookmarkClicked();
@ -271,25 +261,25 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
@Override
public void showLoading() {
super.showLoading();
animateView(headerRootLayout, false, 200);
animateView(itemsList, false, 100);
animate(headerBinding.getRoot(), false, 200);
animate(itemsList, false, 100);
IMAGE_LOADER.cancelDisplayTask(headerUploaderAvatar);
animateView(headerUploaderLayout, false, 200);
IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView);
animate(headerBinding.uploaderLayout, false, 200);
}
@Override
public void handleResult(@NonNull final PlaylistInfo result) {
super.handleResult(result);
animateView(headerRootLayout, true, 100);
animateView(headerUploaderLayout, true, 300);
headerUploaderLayout.setOnClickListener(null);
animate(headerBinding.getRoot(), true, 100);
animate(headerBinding.uploaderLayout, true, 300);
headerBinding.uploaderLayout.setOnClickListener(null);
// If we have an uploader put them into the UI
if (!TextUtils.isEmpty(result.getUploaderName())) {
headerUploaderName.setText(result.getUploaderName());
headerBinding.uploaderName.setText(result.getUploaderName());
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
headerUploaderLayout.setOnClickListener(v -> {
headerBinding.uploaderLayout.setOnClickListener(v -> {
try {
NavigationHelper.openChannelFragment(getFM(), result.getServiceId(),
result.getUploaderUrl(), result.getUploaderName());
@ -299,28 +289,29 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
});
}
} else { // Otherwise say we have no uploader
headerUploaderName.setText(R.string.playlist_no_uploader);
headerBinding.uploaderName.setText(R.string.playlist_no_uploader);
}
playlistCtrl.setVisibility(View.VISIBLE);
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
final String avatarUrl = result.getUploaderAvatarUrl();
if (result.getServiceId() == ServiceList.YouTube.getServiceId()
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
// this is an auto-generated playlist (e.g. Youtube mix), so a radio is shown
headerUploaderAvatar.setDisableCircularTransformation(true);
headerUploaderAvatar.setBorderColor(
headerBinding.uploaderAvatarView.setDisableCircularTransformation(true);
headerBinding.uploaderAvatarView.setBorderColor(
getResources().getColor(R.color.transparent_background_color));
headerUploaderAvatar.setImageDrawable(AppCompatResources.getDrawable(requireContext(),
resolveResourceIdFromAttr(requireContext(), R.attr.ic_radio)));
headerBinding.uploaderAvatarView.setImageDrawable(
AppCompatResources.getDrawable(requireContext(),
resolveResourceIdFromAttr(requireContext(), R.attr.ic_radio))
);
} else {
IMAGE_LOADER.displayImage(avatarUrl, headerUploaderAvatar,
IMAGE_LOADER.displayImage(avatarUrl, headerBinding.uploaderAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
}
headerStreamCount.setText(Localization
headerBinding.playlistStreamCount.setText(Localization
.localizeStreamCount(getContext(), result.getStreamCount()));
if (!result.getErrors().isEmpty()) {
@ -334,19 +325,19 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistBookmarkSubscriber());
headerPlayAllButton.setOnClickListener(view ->
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view ->
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view ->
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
headerPopupButton.setOnLongClickListener(view -> {
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
return true;
});
headerBackgroundButton.setOnLongClickListener(view -> {
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
return true;
});
@ -455,7 +446,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
@Override
public void setTitle(final String title) {
super.setTitle(title);
headerTitleView.setText(title);
headerBinding.playlistTitleView.setText(title);
}
private void onBookmarkClicked() {

View File

@ -37,6 +37,7 @@ import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
import org.schabi.newpipe.databinding.FragmentSearchBinding;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.MetaInfo;
@ -46,16 +47,18 @@ import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
@ -79,7 +82,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject;
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
import static java.util.Arrays.asList;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage<?>>
@ -153,6 +156,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
// Views
//////////////////////////////////////////////////////////////////////////*/
private FragmentSearchBinding searchBinding;
private View searchToolbarContainer;
private EditText searchEditText;
private View searchClear;
@ -163,7 +168,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
private View suggestionsPanel;
private boolean suggestionsPanelVisible = false;
private RecyclerView suggestionsRecyclerView;
/*////////////////////////////////////////////////////////////////////////*/
@ -192,7 +196,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(final Context context) {
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
suggestionListAdapter = new SuggestionListAdapter(activity);
@ -222,7 +226,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
}
@Override
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
showSearchOnStart();
initSearchListeners();
@ -276,8 +280,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
handleSearchSuggestion();
showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
metaInfoTextView, metaInfoSeparator);
disposables.add(showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
metaInfoTextView, metaInfoSeparator));
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
initSuggestionObserver();
@ -299,6 +303,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "onDestroyView() called");
}
unsetSearchListeners();
searchBinding = null;
super.onDestroyView();
}
@ -335,9 +341,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
suggestionsPanel = rootView.findViewById(R.id.suggestions_panel);
suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list);
suggestionsRecyclerView.setAdapter(suggestionListAdapter);
searchBinding = FragmentSearchBinding.bind(rootView);
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
new ItemTouchHelper(new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(@NonNull final RecyclerView recyclerView,
@ -356,7 +362,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, final int i) {
onSuggestionItemSwiped(viewHolder);
}
}).attachToRecyclerView(suggestionsRecyclerView);
}).attachToRecyclerView(searchBinding.suggestionsList);
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
@ -384,7 +390,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
}
@Override
public void onSaveInstanceState(final Bundle bundle) {
public void onSaveInstanceState(@NonNull final Bundle bundle) {
searchString = searchEditText != null
? searchEditText.getText().toString()
: searchString;
@ -407,7 +413,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
searchEditText.setText("");
showKeyboardSearch();
}
animateView(errorPanelRoot, false, 200);
animate(errorPanelRoot, false, 200);
}
}
@ -431,12 +437,18 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
boolean isFirstItem = true;
final Context c = getContext();
for (final String filter : service.getSearchQHFactory().getAvailableContentFilter()) {
if (filter.equals("music_songs")) {
if (filter.equals(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS)) {
final MenuItem musicItem = menu.add(2,
itemId++,
0,
"YouTube Music");
musicItem.setEnabled(false);
} else if (filter.equals(PeertubeSearchQueryHandlerFactory.SEPIA_VIDEOS)) {
final MenuItem sepiaItem = menu.add(2,
itemId++,
0,
"Sepia Search");
sepiaItem.setEnabled(false);
}
menuItemToFilterName.put(itemId, filter);
final MenuItem item = menu.add(1,
@ -515,7 +527,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
return;
}
correctSuggestion.setVisibility(View.GONE);
searchBinding.correctSuggestion.setVisibility(View.GONE);
searchEditText.setText("");
suggestionListAdapter.setItems(new ArrayList<>());
@ -632,7 +644,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "showSuggestionsPanel() called");
}
suggestionsPanelVisible = true;
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, true, 200);
animate(searchBinding.suggestionsPanel, true, 200,
AnimationType.LIGHT_SLIDE_AND_ALPHA);
}
private void hideSuggestionsPanel() {
@ -640,7 +653,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "hideSuggestionsPanel() called");
}
suggestionsPanelVisible = false;
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, false, 200);
animate(searchBinding.suggestionsPanel, false, 200,
AnimationType.LIGHT_SLIDE_AND_ALPHA);
}
private void showKeyboardSearch() {
@ -928,8 +942,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (DEBUG) {
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
}
suggestionsRecyclerView.smoothScrollToPosition(0);
suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions));
searchBinding.suggestionsList.smoothScrollToPosition(0);
searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions));
if (suggestionsPanelVisible && errorPanelRoot.getVisibility() == View.VISIBLE) {
hideLoading();
@ -988,11 +1002,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
// List<MetaInfo> cannot be bundled without creating some containers
metaInfo = new MetaInfo[result.getMetaInfo().size()];
metaInfo = result.getMetaInfo().toArray(metaInfo);
disposables.add(showMetaInfoInTextView(result.getMetaInfo(), metaInfoTextView,
metaInfoSeparator));
handleSearchSuggestion();
showMetaInfoInTextView(result.getMetaInfo(), metaInfoTextView, metaInfoSeparator);
lastSearchedString = searchString;
nextPage = result.getNextPage();
@ -1011,7 +1025,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
private void handleSearchSuggestion() {
if (TextUtils.isEmpty(searchSuggestion)) {
correctSuggestion.setVisibility(View.GONE);
searchBinding.correctSuggestion.setVisibility(View.GONE);
} else {
final String helperText = getString(isCorrectedSearch
? R.string.search_showing_result_for
@ -1020,22 +1034,23 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
final String highlightedSearchSuggestion =
"<b><i>" + Html.escapeHtml(searchSuggestion) + "</i></b>";
final String text = String.format(helperText, highlightedSearchSuggestion);
correctSuggestion.setText(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY));
searchBinding.correctSuggestion.setText(HtmlCompat.fromHtml(text,
HtmlCompat.FROM_HTML_MODE_LEGACY));
correctSuggestion.setOnClickListener(v -> {
correctSuggestion.setVisibility(View.GONE);
searchBinding.correctSuggestion.setOnClickListener(v -> {
searchBinding.correctSuggestion.setVisibility(View.GONE);
search(searchSuggestion, contentFilter, sortFilter);
searchEditText.setText(searchSuggestion);
});
correctSuggestion.setOnLongClickListener(v -> {
searchBinding.correctSuggestion.setOnLongClickListener(v -> {
searchEditText.setText(searchSuggestion);
searchEditText.setSelection(searchSuggestion.length());
showKeyboardSearch();
return true;
});
correctSuggestion.setVisibility(View.VISIBLE);
searchBinding.correctSuggestion.setVisibility(View.VISIBLE);
}
}

View File

@ -8,19 +8,20 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Switch;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.RelatedStreamsHeaderBinding;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.RelatedStreamInfo;
import java.io.Serializable;
@ -38,8 +39,7 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
// Views
//////////////////////////////////////////////////////////////////////////*/
private View headerRootLayout;
private Switch autoplaySwitch;
private RelatedStreamsHeaderBinding headerBinding;
public static RelatedVideosFragment getInstance(final StreamInfo info) {
final RelatedVideosFragment instance = new RelatedVideosFragment();
@ -52,7 +52,7 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(final Context context) {
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
}
@ -66,25 +66,29 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) {
disposables.clear();
}
disposables.clear();
}
protected View getListHeader() {
@Override
public void onDestroyView() {
headerBinding = null;
super.onDestroyView();
}
@Override
protected ViewBinding getListHeader() {
if (relatedStreamInfo != null && relatedStreamInfo.getRelatedItems() != null) {
headerRootLayout = activity.getLayoutInflater()
.inflate(R.layout.related_streams_header, itemsList, false);
autoplaySwitch = headerRootLayout.findViewById(R.id.autoplay_switch);
headerBinding = RelatedStreamsHeaderBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
final SharedPreferences pref = PreferenceManager
.getDefaultSharedPreferences(requireContext());
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
autoplaySwitch.setChecked(autoplay);
autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
headerBinding.autoplaySwitch.setChecked(autoplay);
headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
.putBoolean(getString(R.string.auto_queue_key), b).apply());
return headerRootLayout;
return headerBinding;
} else {
return null;
}
@ -107,8 +111,8 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
@Override
public void showLoading() {
super.showLoading();
if (headerRootLayout != null) {
headerRootLayout.setVisibility(View.INVISIBLE);
if (headerBinding != null) {
headerBinding.getRoot().setVisibility(View.INVISIBLE);
}
}
@ -116,19 +120,17 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
public void handleResult(@NonNull final RelatedStreamInfo result) {
super.handleResult(result);
if (headerRootLayout != null) {
headerRootLayout.setVisibility(View.VISIBLE);
if (headerBinding != null) {
headerBinding.getRoot().setVisibility(View.VISIBLE);
}
AnimationUtils.slideUp(getView(), 120, 96, 0.06f);
ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
if (disposables != null) {
disposables.clear();
}
disposables.clear();
}
@Override
@ -180,7 +182,7 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
}
@Override
public void onSaveInstanceState(final Bundle outState) {
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(INFO_KEY, relatedStreamInfo);
}
@ -202,8 +204,8 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
final SharedPreferences pref =
PreferenceManager.getDefaultSharedPreferences(requireContext());
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
if (autoplaySwitch != null) {
autoplaySwitch.setChecked(autoplay);
if (headerBinding != null) {
headerBinding.autoplaySwitch.setChecked(autoplay);
}
}

View File

@ -0,0 +1,66 @@
package org.schabi.newpipe.info_list
import android.util.Log
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.GroupieViewHolder
import org.schabi.newpipe.extractor.stream.StreamInfo
import kotlin.math.max
/**
* Custom RecyclerView.Adapter/GroupieAdapter for [StreamSegmentItem] for handling selection state.
*/
class StreamSegmentAdapter(
private val listener: StreamSegmentListener
) : GroupAdapter<GroupieViewHolder>() {
var currentIndex: Int = 0
private set
/**
* Returns `true` if the provided [StreamInfo] contains segments, `false` otherwise.
*/
fun setItems(info: StreamInfo): Boolean {
if (info.streamSegments.isNotEmpty()) {
clear()
addAll(info.streamSegments.map { StreamSegmentItem(it, listener) })
return true
}
return false
}
fun selectSegment(segment: StreamSegmentItem) {
unSelectCurrentSegment()
currentIndex = max(0, getAdapterPosition(segment))
segment.isSelected = true
segment.notifyChanged(StreamSegmentItem.PAYLOAD_SELECT)
}
fun selectSegmentAt(position: Int) {
try {
selectSegment(getGroupAtAdapterPosition(position) as StreamSegmentItem)
} catch (e: IndexOutOfBoundsException) {
// Just to make sure that getGroupAtAdapterPosition doesn't close the app
// Shouldn't happen since setItems is always called before select-methods but just in case
currentIndex = 0
Log.e("StreamSegmentAdapter", "selectSegmentAt: ${e.message}")
}
}
private fun unSelectCurrentSegment() {
try {
val segmentItem = getGroupAtAdapterPosition(currentIndex) as StreamSegmentItem
currentIndex = 0
segmentItem.isSelected = false
segmentItem.notifyChanged(StreamSegmentItem.PAYLOAD_SELECT)
} catch (e: IndexOutOfBoundsException) {
// Just to make sure that getGroupAtAdapterPosition doesn't close the app
// Shouldn't happen since setItems is always called before select-methods but just in case
currentIndex = 0
Log.e("StreamSegmentAdapter", "unSelectCurrentSegment: ${e.message}")
}
}
interface StreamSegmentListener {
fun onItemClick(item: StreamSegmentItem, seconds: Int)
}
}

View File

@ -0,0 +1,47 @@
package org.schabi.newpipe.info_list
import android.widget.ImageView
import android.widget.TextView
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamSegment
import org.schabi.newpipe.util.ImageDisplayConstants
import org.schabi.newpipe.util.Localization
class StreamSegmentItem(
private val item: StreamSegment,
private val onClick: StreamSegmentAdapter.StreamSegmentListener
) : Item<GroupieViewHolder>() {
companion object {
const val PAYLOAD_SELECT = 1
}
var isSelected = false
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
item.previewUrl?.let {
ImageLoader.getInstance().displayImage(
it, viewHolder.root.findViewById<ImageView>(R.id.previewImage),
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
)
}
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
Localization.getDurationString(item.startTimeSeconds.toLong())
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewHolder.root.isSelected = isSelected
}
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.contains(PAYLOAD_SELECT)) {
viewHolder.root.isSelected = isSelected
return
}
super.bind(viewHolder, position, payloads)
}
override fun getLayout() = R.layout.item_stream_segment
}

View File

@ -13,15 +13,14 @@ import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
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.report.ErrorActivity;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.CommentTextOnTouchListener;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;

View File

@ -1,10 +1,11 @@
package org.schabi.newpipe.info_list.holder;
import androidx.preference.PreferenceManager;
import android.text.TextUtils;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;

View File

@ -13,8 +13,8 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.views.AnimatedProgressBar;
@ -125,10 +125,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
} else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime()));
AnimationUtils.animateView(itemProgressView, true, 500);
ViewUtils.animate(itemProgressView, true, 500);
}
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
AnimationUtils.animateView(itemProgressView, false, 500);
ViewUtils.animate(itemProgressView, false, 500);
}
}

View File

@ -0,0 +1,47 @@
@file:JvmName("TextViewUtils")
package org.schabi.newpipe.ktx
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.util.Log
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import org.schabi.newpipe.MainActivity
private const val TAG = "TextViewUtils"
/**
* Animate the text color of any view that extends [TextView] (Buttons, EditText...).
*
* @param duration the duration of the animation
* @param colorStart the text color to start with
* @param colorEnd the text color to end with
*/
fun TextView.animateTextColor(duration: Long, @ColorInt colorStart: Int, @ColorInt colorEnd: Int) {
if (MainActivity.DEBUG) {
Log.d(
TAG,
"animateTextColor() called with: " +
"view = [" + this + "], duration = [" + duration + "], " +
"colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"
)
}
val viewPropertyAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorStart, colorEnd)
viewPropertyAnimator.interpolator = FastOutSlowInInterpolator()
viewPropertyAnimator.duration = duration
viewPropertyAnimator.addUpdateListener { setTextColor(it.animatedValue as Int) }
viewPropertyAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
setTextColor(colorEnd)
}
override fun onAnimationCancel(animation: Animator) {
setTextColor(colorEnd)
}
})
viewPropertyAnimator.start()
}

View File

@ -0,0 +1,75 @@
@file:JvmName("ExceptionUtils")
package org.schabi.newpipe.ktx
import java.io.IOException
import java.io.InterruptedIOException
/**
* @return if throwable is related to Interrupted exceptions, or one of its causes is.
*/
val Throwable.isInterruptedCaused: Boolean
get() = hasExactCause(InterruptedIOException::class.java, InterruptedException::class.java)
/**
* @return if throwable is related to network issues, or one of its causes is.
*/
val Throwable.isNetworkRelated: Boolean
get() = hasAssignableCause<IOException>()
/**
* Calls [hasCause] with the `checkSubtypes` parameter set to false.
*/
fun Throwable.hasExactCause(vararg causesToCheck: Class<*>) = hasCause(false, *causesToCheck)
/**
* Calls [hasCause] with a reified [Throwable] type.
*/
inline fun <reified T : Throwable> Throwable.hasExactCause() = hasExactCause(T::class.java)
/**
* Calls [hasCause] with the `checkSubtypes` parameter set to true.
*/
fun Throwable?.hasAssignableCause(vararg causesToCheck: Class<*>) = hasCause(true, *causesToCheck)
/**
* Calls [hasCause] with a reified [Throwable] type.
*/
inline fun <reified T : Throwable> Throwable?.hasAssignableCause() = hasAssignableCause(T::class.java)
/**
* Check if the throwable has some cause from the causes to check, or is itself in it.
*
* If `checkIfAssignable` is true, not only the exact type will be considered equals, but also its subtypes.
*
* @param checkSubtypes if subtypes are also checked.
* @param causesToCheck an array of causes to check.
*
* @see Class.isAssignableFrom
*/
tailrec fun Throwable?.hasCause(checkSubtypes: Boolean, vararg causesToCheck: Class<*>): Boolean {
if (this == null) {
return false
}
// Check if throwable is a subtype of any of the causes to check
causesToCheck.forEach { causeClass ->
if (checkSubtypes) {
if (causeClass.isAssignableFrom(this.javaClass)) {
return true
}
} else {
if (causeClass == this.javaClass) {
return true
}
}
}
val currentCause: Throwable? = cause
// Check if cause is not pointing to the same instance, to avoid infinite loops.
if (this !== currentCause) {
return currentCause.hasCause(checkSubtypes, *causesToCheck)
}
return false
}

View File

@ -0,0 +1,324 @@
@file:JvmName("ViewUtils")
package org.schabi.newpipe.ktx
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.res.ColorStateList
import android.util.Log
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.FloatRange
import androidx.core.view.ViewCompat
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import org.schabi.newpipe.MainActivity
private const val TAG = "ViewUtils"
inline var View.backgroundTintListCompat: ColorStateList?
get() = ViewCompat.getBackgroundTintList(this)
set(value) = ViewCompat.setBackgroundTintList(this, value)
/**
* Animate the view.
*
* @param enterOrExit true to enter, false to exit
* @param duration how long the animation will take, in milliseconds
* @param animationType Type of the animation
* @param delay how long the animation will wait to start, in milliseconds
* @param execOnEnd runnable that will be executed when the animation ends
*/
@JvmOverloads
fun View.animate(
enterOrExit: Boolean,
duration: Long,
animationType: AnimationType = AnimationType.ALPHA,
delay: Long = 0,
execOnEnd: Runnable? = null
) {
if (MainActivity.DEBUG) {
val id = try {
resources.getResourceEntryName(id)
} catch (e: Exception) {
id.toString() + ""
}
val msg = String.format(
"%8s → [%s:%s] [%s %s:%s] execOnEnd=%s", enterOrExit,
javaClass.simpleName, id, animationType, duration, delay, execOnEnd
)
Log.d(TAG, "animate(): $msg")
}
if (isVisible && enterOrExit) {
if (MainActivity.DEBUG) {
Log.d(TAG, "animate(): view was already visible > view = [$this]")
}
animate().setListener(null).cancel()
isVisible = true
alpha = 1f
execOnEnd?.run()
return
} else if ((isGone || isInvisible) && !enterOrExit) {
if (MainActivity.DEBUG) {
Log.d(TAG, "animate(): view was already gone > view = [$this]")
}
animate().setListener(null).cancel()
isGone = true
alpha = 0f
execOnEnd?.run()
return
}
animate().setListener(null).cancel()
isVisible = true
when (animationType) {
AnimationType.ALPHA -> animateAlpha(enterOrExit, duration, delay, execOnEnd)
AnimationType.SCALE_AND_ALPHA -> animateScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
AnimationType.LIGHT_SCALE_AND_ALPHA -> animateLightScaleAndAlpha(enterOrExit, duration, delay, execOnEnd)
AnimationType.SLIDE_AND_ALPHA -> animateSlideAndAlpha(enterOrExit, duration, delay, execOnEnd)
AnimationType.LIGHT_SLIDE_AND_ALPHA -> animateLightSlideAndAlpha(enterOrExit, duration, delay, execOnEnd)
}
}
/**
* Animate the background color of a view.
*
* @param duration the duration of the animation
* @param colorStart the background color to start with
* @param colorEnd the background color to end with
*/
fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @ColorInt colorEnd: Int) {
if (MainActivity.DEBUG) {
Log.d(
TAG,
"animateBackgroundColor() called with: " +
"view = [" + this + "], duration = [" + duration + "], " +
"colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"
)
}
val empty = arrayOf(IntArray(0))
val viewPropertyAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorStart, colorEnd)
viewPropertyAnimator.interpolator = FastOutSlowInInterpolator()
viewPropertyAnimator.duration = duration
viewPropertyAnimator.addUpdateListener { animation: ValueAnimator ->
backgroundTintListCompat = ColorStateList(empty, intArrayOf(animation.animatedValue as Int))
}
viewPropertyAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd))
}
override fun onAnimationCancel(animation: Animator) {
onAnimationEnd(animation)
}
})
viewPropertyAnimator.start()
}
fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
if (MainActivity.DEBUG) {
Log.d(
TAG,
"animateHeight: duration = [" + duration + "], " +
"from " + height + " to → " + targetHeight + " in: " + this
)
}
val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat())
animator.interpolator = FastOutSlowInInterpolator()
animator.duration = duration
animator.addUpdateListener { animation: ValueAnimator ->
val value = animation.animatedValue as Float
layoutParams.height = value.toInt()
requestLayout()
}
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
layoutParams.height = targetHeight
requestLayout()
}
override fun onAnimationCancel(animation: Animator) {
layoutParams.height = targetHeight
requestLayout()
}
})
animator.start()
return animator
}
fun View.animateRotation(duration: Long, targetRotation: Int) {
if (MainActivity.DEBUG) {
Log.d(
TAG,
"animateRotation: duration = [" + duration + "], " +
"from " + rotation + " to → " + targetRotation + " in: " + this
)
}
animate().setListener(null).cancel()
animate()
.rotation(targetRotation.toFloat()).setDuration(duration)
.setInterpolator(FastOutSlowInInterpolator())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationCancel(animation: Animator) {
rotation = targetRotation.toFloat()
}
override fun onAnimationEnd(animation: Animator) {
rotation = targetRotation.toFloat()
}
}).start()
}
private fun View.animateAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
if (enterOrExit) {
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(1f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
}).start()
} else {
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(0f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
isGone = true
execOnEnd?.run()
}
}).start()
}
}
private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
if (enterOrExit) {
scaleX = .8f
scaleY = .8f
animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
}).start()
} else {
scaleX = 1f
scaleY = 1f
animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).scaleX(.8f).scaleY(.8f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
isGone = true
execOnEnd?.run()
}
}).start()
}
}
private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
if (enterOrExit) {
alpha = .5f
scaleX = .95f
scaleY = .95f
animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
}).start()
} else {
alpha = 1f
scaleX = 1f
scaleY = 1f
animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).scaleX(.95f).scaleY(.95f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
isGone = true
execOnEnd?.run()
}
}).start()
}
}
private fun View.animateSlideAndAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
if (enterOrExit) {
translationY = -height.toFloat()
alpha = 0f
animate()
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
}).start()
} else {
animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).translationY(-height.toFloat())
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
isGone = true
execOnEnd?.run()
}
}).start()
}
}
private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long, delay: Long, execOnEnd: Runnable?) {
if (enterOrExit) {
translationY = -height / 2.0f
alpha = 0f
animate()
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
}).start()
} else {
animate().setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).translationY(-height / 2.0f)
.setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
isGone = true
execOnEnd?.run()
}
}).start()
}
}
fun View.slideUp(duration: Long, delay: Long, @FloatRange(from = 0.0, to = 1.0) translationPercent: Float) {
val newTranslationY = (resources.displayMetrics.heightPixels * translationPercent).toInt()
animate().setListener(null).cancel()
alpha = 0f
translationY = newTranslationY.toFloat()
visibility = View.VISIBLE
animate()
.alpha(1f)
.translationY(0f)
.setStartDelay(delay)
.setDuration(duration)
.setInterpolator(FastOutSlowInInterpolator())
.start()
}
enum class AnimationType {
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
}

View File

@ -4,23 +4,26 @@ import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import androidx.preference.PreferenceManager;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PignateFooterBinding;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.list.ListViewContract;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
/**
* This fragment is design to be used with persistent data such as
@ -42,8 +45,8 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
//////////////////////////////////////////////////////////////////////////*/
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
private View headerRootView;
private View footerRootView;
private ViewBinding headerRootBinding;
private ViewBinding footerRootBinding;
protected LocalItemListAdapter itemListAdapter;
protected RecyclerView itemsList;
private int updateFlags = 0;
@ -86,12 +89,13 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
// Lifecycle - View
//////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() {
@Nullable
protected ViewBinding getListHeader() {
return null;
}
protected View getListFooter() {
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false);
protected ViewBinding getListFooter() {
return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false);
}
protected RecyclerView.LayoutManager getGridLayoutManager() {
@ -120,10 +124,12 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
itemListAdapter.setUseGridVariant(useGrid);
headerRootView = getListHeader();
itemListAdapter.setHeader(headerRootView);
footerRootView = getListFooter();
itemListAdapter.setFooter(footerRootView);
headerRootBinding = getListHeader();
if (headerRootBinding != null) {
itemListAdapter.setHeader(headerRootBinding.getRoot());
}
footerRootBinding = getListFooter();
itemListAdapter.setFooter(footerRootBinding.getRoot());
itemsList.setAdapter(itemListAdapter);
}
@ -178,10 +184,10 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
public void showLoading() {
super.showLoading();
if (itemsList != null) {
animateView(itemsList, false, 200);
animate(itemsList, false, 200);
}
if (headerRootView != null) {
animateView(headerRootView, false, 200);
if (headerRootBinding != null) {
animate(headerRootBinding.getRoot(), false, 200);
}
}
@ -189,10 +195,10 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
public void hideLoading() {
super.hideLoading();
if (itemsList != null) {
animateView(itemsList, true, 200);
animate(itemsList, true, 200);
}
if (headerRootView != null) {
animateView(headerRootView, true, 200);
if (headerRootBinding != null) {
animate(headerRootBinding.getRoot(), true, 200);
}
}
@ -202,10 +208,10 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
showListFooter(false);
if (itemsList != null) {
animateView(itemsList, false, 200);
animate(itemsList, false, 200);
}
if (headerRootView != null) {
animateView(headerRootView, false, 200);
if (headerRootBinding != null) {
animate(headerRootBinding.getRoot(), false, 200);
}
}

View File

@ -33,9 +33,9 @@ import org.schabi.newpipe.util.OnClickGesture;
import java.util.List;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;

View File

@ -32,33 +32,25 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import icepick.State
import kotlinx.android.synthetic.main.error_retry.error_button_retry
import kotlinx.android.synthetic.main.error_retry.error_message_view
import kotlinx.android.synthetic.main.fragment_feed.empty_state_view
import kotlinx.android.synthetic.main.fragment_feed.error_panel
import kotlinx.android.synthetic.main.fragment_feed.items_list
import kotlinx.android.synthetic.main.fragment_feed.loading_progress_bar
import kotlinx.android.synthetic.main.fragment_feed.loading_progress_text
import kotlinx.android.synthetic.main.fragment_feed.refresh_root_view
import kotlinx.android.synthetic.main.fragment_feed.refresh_subtitle_text
import kotlinx.android.synthetic.main.fragment_feed.refresh_text
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.FragmentFeedBinding
import org.schabi.newpipe.fragments.list.BaseListFragment
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.report.UserAction
import org.schabi.newpipe.util.AnimationUtils.animateView
import org.schabi.newpipe.util.Localization
import java.util.Calendar
class FeedFragment : BaseListFragment<FeedState, Unit>() {
private var _feedBinding: FragmentFeedBinding? = null
private val feedBinding get() = _feedBinding!!
private val errorBinding get() = _feedBinding!!.errorPanel
private lateinit var viewModel: FeedViewModel
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
@State
@JvmField
var listState: Parcelable? = null
@ -85,16 +77,17 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
}
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
// super.onViewCreated() calls initListeners() which require the binding to be initialized
_feedBinding = FragmentFeedBinding.bind(rootView)
super.onViewCreated(rootView, savedInstanceState)
swipeRefreshLayout = requireView().findViewById(R.id.swiperefresh)
swipeRefreshLayout.setOnRefreshListener { reloadContent() }
viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(::handleResult) })
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
}
override fun onPause() {
super.onPause()
listState = items_list?.layoutManager?.onSaveInstanceState()
listState = feedBinding.itemsList.layoutManager?.onSaveInstanceState()
}
override fun onResume() {
@ -112,9 +105,8 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun initListeners() {
super.initListeners()
refresh_root_view.setOnClickListener {
triggerUpdate()
}
feedBinding.refreshRootView.setOnClickListener { reloadContent() }
feedBinding.swiperefresh.setOnRefreshListener { reloadContent() }
}
// /////////////////////////////////////////////////////////////////////////
@ -169,55 +161,60 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
activity?.supportActionBar?.subtitle = null
}
override fun onDestroyView() {
_feedBinding = null
super.onDestroyView()
}
// /////////////////////////////////////////////////////////////////////////
// Handling
// /////////////////////////////////////////////////////////////////////////
override fun showLoading() {
animateView(refresh_root_view, false, 0)
animateView(items_list, false, 0)
feedBinding.refreshRootView.animate(false, 0)
feedBinding.itemsList.animate(false, 0)
animateView(loading_progress_bar, true, 200)
animateView(loading_progress_text, true, 200)
feedBinding.loadingProgressBar.animate(true, 200)
feedBinding.loadingProgressText.animate(true, 200)
empty_state_view?.let { animateView(it, false, 0) }
animateView(error_panel, false, 0)
feedBinding.emptyStateView.root.animate(false, 0)
errorBinding.root.animate(false, 0)
}
override fun hideLoading() {
animateView(refresh_root_view, true, 200)
animateView(items_list, true, 300)
feedBinding.refreshRootView.animate(true, 200)
feedBinding.itemsList.animate(true, 300)
animateView(loading_progress_bar, false, 0)
animateView(loading_progress_text, false, 0)
feedBinding.loadingProgressBar.animate(false, 0)
feedBinding.loadingProgressText.animate(false, 0)
empty_state_view?.let { animateView(it, false, 0) }
animateView(error_panel, false, 0)
swipeRefreshLayout.isRefreshing = false
feedBinding.emptyStateView.root.animate(false, 0)
errorBinding.root.animate(false, 0)
feedBinding.swiperefresh.isRefreshing = false
}
override fun showEmptyState() {
animateView(refresh_root_view, true, 200)
animateView(items_list, false, 0)
feedBinding.refreshRootView.animate(true, 200)
feedBinding.itemsList.animate(false, 0)
animateView(loading_progress_bar, false, 0)
animateView(loading_progress_text, false, 0)
feedBinding.loadingProgressBar.animate(false, 0)
feedBinding.loadingProgressText.animate(false, 0)
empty_state_view?.let { animateView(it, true, 800) }
animateView(error_panel, false, 0)
feedBinding.emptyStateView.root.animate(true, 800)
errorBinding.root.animate(false, 0)
}
override fun showError(message: String, showRetryButton: Boolean) {
infoListAdapter.clearStreamItemList()
animateView(refresh_root_view, false, 120)
animateView(items_list, false, 120)
feedBinding.refreshRootView.animate(false, 120)
feedBinding.itemsList.animate(false, 120)
animateView(loading_progress_bar, false, 120)
animateView(loading_progress_text, false, 120)
feedBinding.loadingProgressBar.animate(false, 120)
feedBinding.loadingProgressText.animate(false, 120)
error_message_view.text = message
animateView(error_button_retry, showRetryButton, if (showRetryButton) 600 else 0)
animateView(error_panel, true, 300)
errorBinding.errorMessageView.text = message
errorBinding.errorButtonRetry.animate(showRetryButton, if (showRetryButton) 600 else 0)
errorBinding.root.animate(true, 300)
}
override fun handleResult(result: FeedState) {
@ -236,34 +233,37 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
val isIndeterminate = progressState.currentProgress == -1 &&
progressState.maxProgress == -1
if (!isIndeterminate) {
loading_progress_text.text = "${progressState.currentProgress}/${progressState.maxProgress}"
feedBinding.loadingProgressText.text = if (!isIndeterminate) {
"${progressState.currentProgress}/${progressState.maxProgress}"
} else if (progressState.progressMessage > 0) {
loading_progress_text?.setText(progressState.progressMessage)
getString(progressState.progressMessage)
} else {
loading_progress_text?.text = "∞/∞"
"∞/∞"
}
loading_progress_bar.isIndeterminate = isIndeterminate ||
feedBinding.loadingProgressBar.isIndeterminate = isIndeterminate ||
(progressState.maxProgress > 0 && progressState.currentProgress == 0)
loading_progress_bar.progress = progressState.currentProgress
feedBinding.loadingProgressBar.progress = progressState.currentProgress
loading_progress_bar.max = progressState.maxProgress
feedBinding.loadingProgressBar.max = progressState.maxProgress
}
private fun handleLoadedState(loadedState: FeedState.LoadedState) {
infoListAdapter.setInfoItemList(loadedState.items)
listState?.run {
items_list.layoutManager?.onRestoreInstanceState(listState)
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
listState = null
}
oldestSubscriptionUpdate = loadedState.oldestUpdate
val loadedCount = loadedState.notLoadedCount > 0
refresh_subtitle_text.isVisible = loadedCount
feedBinding.refreshSubtitleText.isVisible = loadedCount
if (loadedCount) {
refresh_subtitle_text.text = getString(R.string.feed_subscription_not_loaded_count, loadedState.notLoadedCount)
feedBinding.refreshSubtitleText.text = getString(
R.string.feed_subscription_not_loaded_count,
loadedState.notLoadedCount
)
}
if (loadedState.itemsErrors.isNotEmpty()) {
@ -300,7 +300,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
else -> ""
}
refresh_text?.text = getString(R.string.feed_oldest_subscription_update, oldestSubscriptionUpdateText)
feedBinding.refreshText.text = getString(R.string.feed_oldest_subscription_update, oldestSubscriptionUpdateText)
}
// /////////////////////////////////////////////////////////////////////////
@ -308,11 +308,10 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
// /////////////////////////////////////////////////////////////////////////
override fun doInitialLoadLogic() {}
override fun reloadContent() = triggerUpdate()
override fun loadMoreItems() {}
override fun hasMoreItems() = false
private fun triggerUpdate() {
override fun reloadContent() {
getActivity()?.startService(
Intent(requireContext(), FeedLoadService::class.java).apply {
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)

View File

@ -43,19 +43,20 @@ import io.reactivex.rxjava3.processors.PublishProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription
import org.schabi.newpipe.App
import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.ExceptionUtils
import org.schabi.newpipe.util.ExtractorHelper
import java.io.IOException
import java.time.OffsetDateTime
@ -68,7 +69,7 @@ class FeedLoadService : Service() {
companion object {
private val TAG = FeedLoadService::class.java.simpleName
private const val NOTIFICATION_ID = 7293450
private const val ACTION_CANCEL = "org.schabi.newpipe.local.feed.service.FeedLoadService.CANCEL"
private const val ACTION_CANCEL = App.PACKAGE_NAME + ".local.feed.service.FeedLoadService.CANCEL"
/**
* How often the notification will be updated.
@ -343,7 +344,7 @@ class FeedLoadService : Service() {
error is IOException -> throw error
cause is IOException -> throw cause
ExceptionUtils.isNetworkRelated(error) -> throw IOException(error)
error.isNetworkRelated -> throw IOException(error)
}
}
}

View File

@ -10,13 +10,12 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.viewbinding.ViewBinding;
import com.google.android.material.snackbar.Snackbar;
@ -26,6 +25,8 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemDialog;
@ -37,6 +38,7 @@ import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StreamDialogEntry;
@ -59,13 +61,10 @@ public class StatisticsPlaylistFragment
@State
Parcelable itemsListState;
private StatisticSortMode sortMode = StatisticSortMode.LAST_PLAYED;
private View headerPlayAllButton;
private View headerPopupButton;
private View headerBackgroundButton;
private View playlistCtrl;
private View sortButton;
private ImageView sortButtonIcon;
private TextView sortButtonText;
private StatisticPlaylistControlBinding headerBinding;
private PlaylistControlBinding playlistControlBinding;
/* Used for independent events */
private Subscription databaseSubscription;
private HistoryRecordManager recordManager;
@ -130,17 +129,12 @@ public class StatisticsPlaylistFragment
}
@Override
protected View getListHeader() {
final View headerRootLayout = activity.getLayoutInflater()
.inflate(R.layout.statistic_playlist_control, itemsList, false);
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
sortButton = headerRootLayout.findViewById(R.id.sortButton);
sortButtonIcon = headerRootLayout.findViewById(R.id.sortButtonIcon);
sortButtonText = headerRootLayout.findViewById(R.id.sortButtonText);
return headerRootLayout;
protected ViewBinding getListHeader() {
headerBinding = StatisticPlaylistControlBinding.inflate(activity.getLayoutInflater(),
itemsList, false);
playlistControlBinding = headerBinding.playlistControl;
return headerBinding;
}
@Override
@ -244,14 +238,13 @@ public class StatisticsPlaylistFragment
if (itemListAdapter != null) {
itemListAdapter.unsetSelectedListener();
}
if (headerBackgroundButton != null) {
headerBackgroundButton.setOnClickListener(null);
}
if (headerPlayAllButton != null) {
headerPlayAllButton.setOnClickListener(null);
}
if (headerPopupButton != null) {
headerPopupButton.setOnClickListener(null);
if (playlistControlBinding != null) {
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null);
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null);
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null);
headerBinding = null;
playlistControlBinding = null;
}
if (databaseSubscription != null) {
@ -310,7 +303,7 @@ public class StatisticsPlaylistFragment
return;
}
playlistCtrl.setVisibility(View.VISIBLE);
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
itemListAdapter.clearStreamItemList();
@ -325,13 +318,13 @@ public class StatisticsPlaylistFragment
itemsListState = null;
}
headerPlayAllButton.setOnClickListener(view ->
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view ->
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view ->
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
sortButton.setOnClickListener(view -> toggleSortMode());
headerBinding.sortButton.setOnClickListener(view -> toggleSortMode());
hideLoading();
}
@ -367,15 +360,15 @@ public class StatisticsPlaylistFragment
if (sortMode == StatisticSortMode.LAST_PLAYED) {
sortMode = StatisticSortMode.MOST_PLAYED;
setTitle(getString(R.string.title_most_played));
sortButtonIcon.setImageResource(
headerBinding.sortButtonIcon.setImageResource(
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_history));
sortButtonText.setText(R.string.title_last_played);
headerBinding.sortButtonText.setText(R.string.title_last_played);
} else {
sortMode = StatisticSortMode.LAST_PLAYED;
setTitle(getString(R.string.title_last_played));
sortButtonIcon.setImageResource(
headerBinding.sortButtonIcon.setImageResource(
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_filter_list));
sortButtonText.setText(R.string.title_most_played);
headerBinding.sortButtonText.setText(R.string.title_most_played);
}
startLoading(true);
}
@ -413,6 +406,9 @@ public class StatisticsPlaylistFragment
StreamDialogEntry.share
));
}
if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
StreamDialogEntry.setEnabledEntries(entries);
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) ->

View File

@ -12,9 +12,9 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.views.AnimatedProgressBar;
@ -117,10 +117,10 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
} else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime()));
AnimationUtils.animateView(itemProgressView, true, 500);
ViewUtils.animate(itemProgressView, true, 500);
}
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
AnimationUtils.animateView(itemProgressView, false, 500);
ViewUtils.animate(itemProgressView, false, 500);
}
}

View File

@ -12,9 +12,9 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.views.AnimatedProgressBar;
@ -148,10 +148,10 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
} else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime()));
AnimationUtils.animateView(itemProgressView, true, 500);
ViewUtils.animate(itemProgressView, true, 500);
}
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
AnimationUtils.animateView(itemProgressView, false, 500);
ViewUtils.animate(itemProgressView, false, 500);
}
}
}

View File

@ -14,7 +14,6 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@ -22,6 +21,7 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewbinding.ViewBinding;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
@ -32,6 +32,8 @@ import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.databinding.LocalPlaylistHeaderBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemDialog;
@ -41,6 +43,7 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
@ -55,14 +58,14 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> {
// Save the list 10 seconds after the last change occurred
@ -76,13 +79,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@State
Parcelable itemsListState;
private View headerRootLayout;
private TextView headerTitleView;
private TextView headerStreamCount;
private View playlistControl;
private View headerPlayAllButton;
private View headerPopupButton;
private View headerBackgroundButton;
private LocalPlaylistHeaderBinding headerBinding;
private PlaylistControlBinding playlistControlBinding;
private ItemTouchHelper itemTouchHelper;
@ -136,8 +134,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
public void setTitle(final String title) {
super.setTitle(title);
if (headerTitleView != null) {
headerTitleView.setText(title);
if (headerBinding != null) {
headerBinding.playlistTitleView.setText(title);
}
}
@ -148,28 +146,21 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
@Override
protected View getListHeader() {
headerRootLayout = activity.getLayoutInflater()
.inflate(R.layout.local_playlist_header, itemsList, false);
protected ViewBinding getListHeader() {
headerBinding = LocalPlaylistHeaderBinding.inflate(activity.getLayoutInflater(), itemsList,
false);
playlistControlBinding = headerBinding.playlistControl;
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
headerTitleView.setSelected(true);
headerBinding.playlistTitleView.setSelected(true);
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
playlistControl = headerRootLayout.findViewById(R.id.playlist_control);
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
return headerRootLayout;
return headerBinding;
}
@Override
protected void initListeners() {
super.initListeners();
headerTitleView.setOnClickListener(view -> createRenameDialog());
headerBinding.playlistTitleView.setOnClickListener(view -> createRenameDialog());
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(itemsList);
@ -209,22 +200,18 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public void showLoading() {
super.showLoading();
if (headerRootLayout != null) {
animateView(headerRootLayout, false, 200);
}
if (playlistControl != null) {
animateView(playlistControl, false, 200);
if (headerBinding != null) {
animate(headerBinding.getRoot(), false, 200);
animate(playlistControlBinding.getRoot(), false, 200);
}
}
@Override
public void hideLoading() {
super.hideLoading();
if (headerRootLayout != null) {
animateView(headerRootLayout, true, 200);
}
if (playlistControl != null) {
animateView(playlistControl, true, 200);
if (headerBinding != null) {
animate(headerBinding.getRoot(), true, 200);
animate(playlistControlBinding.getRoot(), true, 200);
}
}
@ -276,14 +263,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
if (itemListAdapter != null) {
itemListAdapter.unsetSelectedListener();
}
if (headerBackgroundButton != null) {
headerBackgroundButton.setOnClickListener(null);
}
if (headerPlayAllButton != null) {
headerPlayAllButton.setOnClickListener(null);
}
if (headerPopupButton != null) {
headerPopupButton.setOnClickListener(null);
if (playlistControlBinding != null) {
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null);
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null);
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null);
headerBinding = null;
playlistControlBinding = null;
}
if (databaseSubscription != null) {
@ -493,19 +479,19 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
setVideoCount(itemListAdapter.getItemsList().size());
headerPlayAllButton.setOnClickListener(view ->
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view ->
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view ->
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
headerPopupButton.setOnLongClickListener(view -> {
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
return true;
});
headerBackgroundButton.setOnLongClickListener(view -> {
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
return true;
});
@ -781,6 +767,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
StreamDialogEntry.share
));
}
if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
StreamDialogEntry.setEnabledEntries(entries);
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) ->
@ -802,8 +791,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
private void setVideoCount(final long count) {
if (activity != null && headerStreamCount != null) {
headerStreamCount.setText(Localization.localizeStreamCount(activity, count));
if (activity != null && headerBinding != null) {
headerBinding.playlistStreamCount.setText(Localization
.localizeStreamCount(activity, count));
}
}

View File

@ -26,16 +26,17 @@ import com.xwray.groupie.Group
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Item
import com.xwray.groupie.Section
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.viewbinding.GroupieViewHolder
import icepick.State
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.dialog_title.view.itemAdditionalDetails
import kotlinx.android.synthetic.main.dialog_title.view.itemTitleView
import kotlinx.android.synthetic.main.fragment_subscription.items_list
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogTitleBinding
import org.schabi.newpipe.databinding.FeedItemCarouselBinding
import org.schabi.newpipe.databinding.FragmentSubscriptionBinding
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.local.subscription.SubscriptionViewModel.SubscriptionState
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialog
@ -56,7 +57,6 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
import org.schabi.newpipe.report.UserAction
import org.schabi.newpipe.util.AnimationUtils.animateView
import org.schabi.newpipe.util.FilePickerActivityHelper
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture
@ -70,13 +70,16 @@ import kotlin.math.floor
import kotlin.math.max
class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private var _binding: FragmentSubscriptionBinding? = null
private val binding get() = _binding!!
private lateinit var viewModel: SubscriptionViewModel
private lateinit var subscriptionManager: SubscriptionManager
private val disposables: CompositeDisposable = CompositeDisposable()
private var subscriptionBroadcastReceiver: BroadcastReceiver? = null
private val groupAdapter = GroupAdapter<GroupieViewHolder>()
private val groupAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
private val feedGroupsSection = Section()
private var feedGroupsCarousel: FeedGroupCarouselItem? = null
private lateinit var importExportItem: FeedImportExportItem
@ -129,7 +132,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun onPause() {
super.onPause()
itemsListState = items_list.layoutManager?.onSaveInstanceState()
itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState()
feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState()
importExportItemExpandedState = importExportItem.isExpanded
@ -169,7 +172,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
filters.addAction(IMPORT_COMPLETE_ACTION)
subscriptionBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
items_list?.post {
_binding?.itemsList?.post {
importExportItem.isExpanded = false
importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
}
@ -231,7 +234,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private fun setupInitialLayout() {
Section().apply {
val carouselAdapter = GroupAdapter<GroupieViewHolder>()
val carouselAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
carouselAdapter.add(FeedGroupCardItem(-1, getString(R.string.all), FeedGroupIcon.RSS))
carouselAdapter.add(feedGroupsSection)
@ -275,39 +278,37 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun initViews(rootView: View, savedInstanceState: Bundle?) {
super.initViews(rootView, savedInstanceState)
_binding = FragmentSubscriptionBinding.bind(rootView)
val shouldUseGridLayout = shouldUseGridLayout()
groupAdapter.spanCount = if (shouldUseGridLayout) getGridSpanCount() else 1
items_list.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
binding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
spanSizeLookup = groupAdapter.spanSizeLookup
}
items_list.adapter = groupAdapter
binding.itemsList.adapter = groupAdapter
viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer { it?.let(this::handleResult) })
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer { it?.let(this::handleFeedGroups) })
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(this::handleResult) })
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, { it?.let(this::handleFeedGroups) })
}
private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
val commands = arrayOf(
getString(R.string.share),
getString(R.string.unsubscribe)
)
val commands = arrayOf(getString(R.string.share), getString(R.string.unsubscribe))
val actions = DialogInterface.OnClickListener { _, i ->
when (i) {
0 -> ShareUtils.shareUrl(requireContext(), selectedItem.name, selectedItem.url)
0 -> ShareUtils.shareText(requireContext(), selectedItem.name, selectedItem.url)
1 -> deleteChannel(selectedItem)
}
}
val bannerView = View.inflate(requireContext(), R.layout.dialog_title, null)
bannerView.isSelected = true
bannerView.itemTitleView.text = selectedItem.name
bannerView.itemAdditionalDetails.visibility = View.GONE
val dialogTitleBinding = DialogTitleBinding.inflate(LayoutInflater.from(requireContext()))
dialogTitleBinding.root.isSelected = true
dialogTitleBinding.itemTitleView.text = selectedItem.name
dialogTitleBinding.itemAdditionalDetails.visibility = View.GONE
AlertDialog.Builder(requireContext())
.setCustomTitle(bannerView)
.setCustomTitle(dialogTitleBinding.root)
.setItems(commands, actions)
.create()
.show()
@ -368,14 +369,14 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
subscriptionsSection.setHideWhenEmpty(false)
if (result.subscriptions.isEmpty() && importExportItemExpandedState == null) {
items_list.post {
binding.itemsList.post {
importExportItem.isExpanded = true
importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
}
}
if (itemsListState != null) {
items_list.layoutManager?.onRestoreInstanceState(itemsListState)
binding.itemsList.layoutManager?.onRestoreInstanceState(itemsListState)
itemsListState = null
}
}
@ -394,7 +395,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
feedGroupsSortMenuItem.showMenuItem = groups.size > 1
items_list.post { feedGroupsSortMenuItem.notifyChanged(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM) }
binding.itemsList.post { feedGroupsSortMenuItem.notifyChanged(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM) }
}
// /////////////////////////////////////////////////////////////////////////
@ -403,12 +404,12 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun showLoading() {
super.showLoading()
animateView(items_list, false, 100)
binding.itemsList.animate(false, 100)
}
override fun hideLoading() {
super.hideLoading()
animateView(items_list, true, 200)
binding.itemsList.animate(true, 200)
}
// /////////////////////////////////////////////////////////////////////////
@ -435,11 +436,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
return when (listMode) {
getString(R.string.list_view_mode_auto_key) -> {
val configuration = resources.configuration
(
configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
)
configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
}
getString(R.string.list_view_mode_grid_key) -> true
else -> false

View File

@ -19,15 +19,15 @@ import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.Section
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import icepick.Icepick
import icepick.State
import kotlinx.android.synthetic.main.dialog_feed_group_create.*
import kotlinx.android.synthetic.main.toolbar_search_layout.*
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding
import org.schabi.newpipe.databinding.ToolbarSearchLayoutBinding
import org.schabi.newpipe.fragments.BackPressable
import org.schabi.newpipe.local.subscription.FeedGroupIcon
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.DeleteScreen
@ -45,6 +45,12 @@ import java.io.Serializable
import kotlin.collections.contains
class FeedGroupDialog : DialogFragment(), BackPressable {
private var _feedGroupCreateBinding: DialogFeedGroupCreateBinding? = null
private val feedGroupCreateBinding get() = _feedGroupCreateBinding!!
private var _searchLayoutBinding: ToolbarSearchLayoutBinding? = null
private val searchLayoutBinding get() = _searchLayoutBinding!!
private lateinit var viewModel: FeedGroupDialogViewModel
private var groupId: Long = NO_GROUP_SELECTED
private var groupIcon: FeedGroupIcon? = null
@ -107,14 +113,16 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
iconsListState = icon_selector.layoutManager?.onSaveInstanceState()
subscriptionsListState = subscriptions_selector_list.layoutManager?.onSaveInstanceState()
iconsListState = feedGroupCreateBinding.iconSelector.layoutManager?.onSaveInstanceState()
subscriptionsListState = feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onSaveInstanceState()
Icepick.saveInstanceState(this, outState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_feedGroupCreateBinding = DialogFeedGroupCreateBinding.bind(view)
_searchLayoutBinding = feedGroupCreateBinding.subscriptionsHeaderSearchContainer
viewModel = ViewModelProvider(
this,
@ -146,7 +154,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
add(subscriptionEmptyFooter)
spanCount = 4
}
subscriptions_selector_list.apply {
feedGroupCreateBinding.subscriptionsSelectorList.apply {
// Disable animations, too distracting.
itemAnimator = null
adapter = subscriptionGroupAdapter
@ -172,8 +180,11 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
override fun onDestroyView() {
super.onDestroyView()
subscriptions_selector_list?.adapter = null
icon_selector?.adapter = null
feedGroupCreateBinding.subscriptionsSelectorList.adapter = null
feedGroupCreateBinding.iconSelector.adapter = null
_feedGroupCreateBinding = null
_searchLayoutBinding = null
}
/*///////////////////////////////////////////////////////////////////////////
@ -193,30 +204,30 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
}
private fun setupListeners() {
delete_button.setOnClickListener { showScreen(DeleteScreen) }
feedGroupCreateBinding.deleteButton.setOnClickListener { showScreen(DeleteScreen) }
cancel_button.setOnClickListener {
feedGroupCreateBinding.cancelButton.setOnClickListener {
when (currentScreen) {
InitialScreen -> dismiss()
else -> showScreen(InitialScreen)
}
}
group_name_input_container.error = null
group_name_input.doOnTextChanged { text, _, _, _ ->
if (group_name_input_container.isErrorEnabled && !text.isNullOrBlank()) {
group_name_input_container.error = null
feedGroupCreateBinding.groupNameInputContainer.error = null
feedGroupCreateBinding.groupNameInput.doOnTextChanged { text, _, _, _ ->
if (feedGroupCreateBinding.groupNameInputContainer.isErrorEnabled && !text.isNullOrBlank()) {
feedGroupCreateBinding.groupNameInputContainer.error = null
}
}
confirm_button.setOnClickListener { handlePositiveButton() }
feedGroupCreateBinding.confirmButton.setOnClickListener { handlePositiveButton() }
select_channel_button.setOnClickListener {
subscriptions_selector_list.scrollToPosition(0)
feedGroupCreateBinding.selectChannelButton.setOnClickListener {
feedGroupCreateBinding.subscriptionsSelectorList.scrollToPosition(0)
showScreen(SubscriptionsPickerScreen)
}
val headerMenu = subscriptions_header_toolbar.menu
val headerMenu = feedGroupCreateBinding.subscriptionsHeaderToolbar.menu
requireActivity().menuInflater.inflate(R.menu.menu_feed_group_dialog, headerMenu)
headerMenu.findItem(R.id.action_search).setOnMenuItemClickListener {
@ -234,8 +245,8 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
}
}
toolbar_search_clear.setOnClickListener {
if (toolbar_search_edit_text.text.isEmpty()) {
searchLayoutBinding.toolbarSearchClear.setOnClickListener {
if (searchLayoutBinding.toolbarSearchEditText.text.isNullOrEmpty()) {
hideSearch()
return@setOnClickListener
}
@ -243,14 +254,14 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
showKeyboardSearch()
}
toolbar_search_edit_text.setOnClickListener {
searchLayoutBinding.toolbarSearchEditText.setOnClickListener {
if (DeviceUtils.isTv(context)) {
showKeyboardSearch()
}
}
toolbar_search_edit_text.doOnTextChanged { _, _, _, _ ->
val newQuery: String = toolbar_search_edit_text.text.toString()
searchLayoutBinding.toolbarSearchEditText.doOnTextChanged { _, _, _, _ ->
val newQuery: String = searchLayoutBinding.toolbarSearchEditText.text.toString()
subscriptionsCurrentSearchQuery = newQuery
viewModel.filterSubscriptionsBy(newQuery)
}
@ -266,16 +277,16 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
}
private fun handlePositiveButtonInitialScreen() {
val name = group_name_input.text.toString().trim()
val name = feedGroupCreateBinding.groupNameInput.text.toString().trim()
val icon = selectedIcon ?: groupIcon ?: FeedGroupIcon.ALL
if (name.isBlank()) {
group_name_input_container.error = getString(R.string.feed_group_dialog_empty_name)
group_name_input.text = null
group_name_input.requestFocus()
feedGroupCreateBinding.groupNameInputContainer.error = getString(R.string.feed_group_dialog_empty_name)
feedGroupCreateBinding.groupNameInput.text = null
feedGroupCreateBinding.groupNameInput.requestFocus()
return
} else {
group_name_input_container.error = null
feedGroupCreateBinding.groupNameInputContainer.error = null
}
if (selectedSubscriptions.isEmpty()) {
@ -296,10 +307,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!!
icon_preview.setImageResource(feedGroupIcon.getDrawableRes(requireContext()))
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes(requireContext()))
if (group_name_input.text.isNullOrBlank()) {
group_name_input.setText(name)
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
feedGroupCreateBinding.groupNameInput.setText(name)
}
}
@ -346,10 +357,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
subscriptionMainSection.update(subscriptions, false)
if (subscriptionsListState != null) {
subscriptions_selector_list.layoutManager?.onRestoreInstanceState(subscriptionsListState)
feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onRestoreInstanceState(subscriptionsListState)
subscriptionsListState = null
} else {
subscriptions_selector_list.scrollToPosition(0)
feedGroupCreateBinding.subscriptionsSelectorList.scrollToPosition(0)
}
}
@ -359,15 +370,15 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
R.plurals.feed_group_dialog_selection_count,
selectedCount, selectedCount
)
selected_subscription_count_view.text = selectedCountText
subscriptions_header_info.text = selectedCountText
feedGroupCreateBinding.selectedSubscriptionCountView.text = selectedCountText
feedGroupCreateBinding.subscriptionsHeaderInfo.text = selectedCountText
}
private fun setupIconPicker() {
val groupAdapter = GroupAdapter<GroupieViewHolder>()
groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(requireContext(), it) })
icon_selector.apply {
feedGroupCreateBinding.iconSelector.apply {
layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false)
adapter = groupAdapter
@ -381,20 +392,20 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
when (item) {
is PickerIconItem -> {
selectedIcon = item.icon
icon_preview.setImageResource(item.iconRes)
feedGroupCreateBinding.iconPreview.setImageResource(item.iconRes)
showScreen(InitialScreen)
}
}
}
icon_preview.setOnClickListener {
icon_selector.scrollToPosition(0)
feedGroupCreateBinding.iconPreview.setOnClickListener {
feedGroupCreateBinding.iconSelector.scrollToPosition(0)
showScreen(IconPickerScreen)
}
if (groupId == NO_GROUP_SELECTED) {
val icon = selectedIcon ?: FeedGroupIcon.ALL
icon_preview.setImageResource(icon.getDrawableRes(requireContext()))
feedGroupCreateBinding.iconPreview.setImageResource(icon.getDrawableRes(requireContext()))
}
}
@ -405,22 +416,22 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
private fun showScreen(screen: ScreenState) {
currentScreen = screen
options_root.onlyVisibleIn(InitialScreen)
icon_selector.onlyVisibleIn(IconPickerScreen)
subscriptions_selector.onlyVisibleIn(SubscriptionsPickerScreen)
delete_screen_message.onlyVisibleIn(DeleteScreen)
feedGroupCreateBinding.optionsRoot.onlyVisibleIn(InitialScreen)
feedGroupCreateBinding.iconSelector.onlyVisibleIn(IconPickerScreen)
feedGroupCreateBinding.subscriptionsSelector.onlyVisibleIn(SubscriptionsPickerScreen)
feedGroupCreateBinding.deleteScreenMessage.onlyVisibleIn(DeleteScreen)
separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen)
cancel_button.onlyVisibleIn(InitialScreen, DeleteScreen)
feedGroupCreateBinding.separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen)
feedGroupCreateBinding.cancelButton.onlyVisibleIn(InitialScreen, DeleteScreen)
confirm_button.setText(
feedGroupCreateBinding.confirmButton.setText(
when {
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
else -> android.R.string.ok
}
)
delete_button.isGone = currentScreen != InitialScreen || groupId == NO_GROUP_SELECTED
feedGroupCreateBinding.deleteButton.isGone = currentScreen != InitialScreen || groupId == NO_GROUP_SELECTED
hideKeyboard()
hideSearch()
@ -434,26 +445,26 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
// Utils
////////////////////////////////////////////////////////////////////////// */
private fun isSearchVisible() = subscriptions_header_search_container?.visibility == View.VISIBLE
private fun isSearchVisible() = _searchLayoutBinding?.root?.visibility == View.VISIBLE
private fun resetSearch() {
toolbar_search_edit_text.setText("")
searchLayoutBinding.toolbarSearchEditText.setText("")
subscriptionsCurrentSearchQuery = ""
viewModel.clearSubscriptionsFilter()
}
private fun hideSearch() {
resetSearch()
subscriptions_header_search_container.visibility = View.GONE
subscriptions_header_info_container.visibility = View.VISIBLE
subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = true
searchLayoutBinding.root.visibility = View.GONE
feedGroupCreateBinding.subscriptionsHeaderInfoContainer.visibility = View.VISIBLE
feedGroupCreateBinding.subscriptionsHeaderToolbar.menu.findItem(R.id.action_search).isVisible = true
hideKeyboardSearch()
}
private fun showSearch() {
subscriptions_header_search_container.visibility = View.VISIBLE
subscriptions_header_info_container.visibility = View.GONE
subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = false
searchLayoutBinding.root.visibility = View.VISIBLE
feedGroupCreateBinding.subscriptionsHeaderInfoContainer.visibility = View.GONE
feedGroupCreateBinding.subscriptionsHeaderToolbar.menu.findItem(R.id.action_search).isVisible = false
showKeyboardSearch()
}
@ -462,37 +473,43 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
}
private fun showKeyboardSearch() {
if (toolbar_search_edit_text.requestFocus()) {
inputMethodManager.showSoftInput(toolbar_search_edit_text, InputMethodManager.SHOW_IMPLICIT)
if (searchLayoutBinding.toolbarSearchEditText.requestFocus()) {
inputMethodManager.showSoftInput(
searchLayoutBinding.toolbarSearchEditText,
InputMethodManager.SHOW_IMPLICIT
)
}
}
private fun hideKeyboardSearch() {
inputMethodManager.hideSoftInputFromWindow(
toolbar_search_edit_text.windowToken,
searchLayoutBinding.toolbarSearchEditText.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN
)
toolbar_search_edit_text.clearFocus()
searchLayoutBinding.toolbarSearchEditText.clearFocus()
}
private fun showKeyboard() {
if (group_name_input.requestFocus()) {
inputMethodManager.showSoftInput(group_name_input, InputMethodManager.SHOW_IMPLICIT)
if (feedGroupCreateBinding.groupNameInput.requestFocus()) {
inputMethodManager.showSoftInput(
feedGroupCreateBinding.groupNameInput,
InputMethodManager.SHOW_IMPLICIT
)
}
}
private fun hideKeyboard() {
inputMethodManager.hideSoftInputFromWindow(
group_name_input.windowToken,
feedGroupCreateBinding.groupNameInput.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN
)
group_name_input.clearFocus()
feedGroupCreateBinding.groupNameInput.clearFocus()
}
private fun disableInput() {
delete_button?.isEnabled = false
confirm_button?.isEnabled = false
cancel_button?.isEnabled = false
_feedGroupCreateBinding?.deleteButton?.isEnabled = false
_feedGroupCreateBinding?.confirmButton?.isEnabled = false
_feedGroupCreateBinding?.cancelButton?.isEnabled = false
isCancelable = false
hideKeyboard()

View File

@ -12,14 +12,13 @@ import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.TouchCallback
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import icepick.Icepick
import icepick.State
import kotlinx.android.synthetic.main.dialog_feed_group_reorder.confirm_button
import kotlinx.android.synthetic.main.dialog_feed_group_reorder.feed_groups_list
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogFeedGroupReorderBinding
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.ProcessingEvent
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
@ -27,6 +26,9 @@ import org.schabi.newpipe.util.ThemeHelper
import java.util.Collections
class FeedGroupReorderDialog : DialogFragment() {
private var _binding: DialogFeedGroupReorderBinding? = null
private val binding get() = _binding!!
private lateinit var viewModel: FeedGroupReorderDialogViewModel
@State
@ -48,6 +50,7 @@ class FeedGroupReorderDialog : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = DialogFeedGroupReorderBinding.bind(view)
viewModel = ViewModelProvider(this).get(FeedGroupReorderDialogViewModel::class.java)
viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups))
@ -61,15 +64,20 @@ class FeedGroupReorderDialog : DialogFragment() {
}
)
feed_groups_list.layoutManager = LinearLayoutManager(requireContext())
feed_groups_list.adapter = groupAdapter
itemTouchHelper.attachToRecyclerView(feed_groups_list)
binding.feedGroupsList.layoutManager = LinearLayoutManager(requireContext())
binding.feedGroupsList.adapter = groupAdapter
itemTouchHelper.attachToRecyclerView(binding.feedGroupsList)
confirm_button.setOnClickListener {
binding.confirmButton.setOnClickListener {
viewModel.updateOrder(groupOrderedIdList)
}
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Icepick.saveInstanceState(this, outState)
@ -89,7 +97,7 @@ class FeedGroupReorderDialog : DialogFragment() {
}
private fun disableInput() {
confirm_button?.isEnabled = false
_binding?.confirmButton?.isEnabled = false
isCancelable = false
}

View File

@ -1,13 +1,11 @@
package org.schabi.newpipe.local.subscription.item
import android.content.Context
import android.widget.ImageView
import android.widget.TextView
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import kotlinx.android.synthetic.main.list_channel_item.itemAdditionalDetails
import kotlinx.android.synthetic.main.list_channel_item.itemChannelDescriptionView
import kotlinx.android.synthetic.main.list_channel_item.itemThumbnailView
import kotlinx.android.synthetic.main.list_channel_item.itemTitleView
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.util.ImageDisplayConstants
@ -19,8 +17,7 @@ class ChannelItem(
private val subscriptionId: Long = -1L,
var itemVersion: ItemVersion = ItemVersion.NORMAL,
var gesturesListener: OnClickGesture<ChannelInfoItem>? = null
) : Item() {
) : Item<GroupieViewHolder>() {
override fun getId(): Long = if (subscriptionId == -1L) super.getId() else subscriptionId
enum class ItemVersion { NORMAL, MINI, GRID }
@ -32,18 +29,25 @@ class ChannelItem(
}
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.itemTitleView.text = infoItem.name
viewHolder.itemAdditionalDetails.text = getDetailLine(viewHolder.root.context)
if (itemVersion == ItemVersion.NORMAL) viewHolder.itemChannelDescriptionView.text = infoItem.description
val itemTitleView = viewHolder.root.findViewById<TextView>(R.id.itemTitleView)
val itemAdditionalDetails = viewHolder.root.findViewById<TextView>(R.id.itemAdditionalDetails)
val itemChannelDescriptionView = viewHolder.root.findViewById<TextView>(R.id.itemChannelDescriptionView)
val itemThumbnailView = viewHolder.root.findViewById<ImageView>(R.id.itemThumbnailView)
itemTitleView.text = infoItem.name
itemAdditionalDetails.text = getDetailLine(viewHolder.root.context)
if (itemVersion == ItemVersion.NORMAL) {
itemChannelDescriptionView.text = infoItem.description
}
ImageLoader.getInstance().displayImage(
infoItem.thumbnailUrl, viewHolder.itemThumbnailView,
infoItem.thumbnailUrl, itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
)
gesturesListener?.run {
viewHolder.containerView.setOnClickListener { selected(infoItem) }
viewHolder.containerView.setOnLongClickListener { held(infoItem); true }
viewHolder.root.setOnClickListener { selected(infoItem) }
viewHolder.root.setOnLongClickListener { held(infoItem); true }
}
}

View File

@ -1,11 +1,13 @@
package org.schabi.newpipe.local.subscription.item
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import android.view.View
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ListEmptyViewBinding
class EmptyPlaceholderItem : Item() {
class EmptyPlaceholderItem : BindableItem<ListEmptyViewBinding>() {
override fun getLayout(): Int = R.layout.list_empty_view
override fun bind(viewHolder: GroupieViewHolder, position: Int) {}
override fun bind(viewBinding: ListEmptyViewBinding, position: Int) {}
override fun getSpanSize(spanCount: Int, position: Int): Int = spanCount
override fun initializeViewBinding(view: View) = ListEmptyViewBinding.bind(view)
}

View File

@ -1,10 +1,12 @@
package org.schabi.newpipe.local.subscription.item
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import android.view.View
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FeedGroupAddNewItemBinding
class FeedGroupAddItem : Item() {
class FeedGroupAddItem : BindableItem<FeedGroupAddNewItemBinding>() {
override fun getLayout(): Int = R.layout.feed_group_add_new_item
override fun bind(viewHolder: GroupieViewHolder, position: Int) {}
override fun bind(viewBinding: FeedGroupAddNewItemBinding, position: Int) {}
override fun initializeViewBinding(view: View) = FeedGroupAddNewItemBinding.bind(view)
}

View File

@ -1,18 +1,17 @@
package org.schabi.newpipe.local.subscription.item
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import kotlinx.android.synthetic.main.feed_group_card_item.icon
import kotlinx.android.synthetic.main.feed_group_card_item.title
import android.view.View
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.FeedGroupCardItemBinding
import org.schabi.newpipe.local.subscription.FeedGroupIcon
data class FeedGroupCardItem(
val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
val name: String,
val icon: FeedGroupIcon
) : Item() {
) : BindableItem<FeedGroupCardItemBinding>() {
constructor (feedGroupEntity: FeedGroupEntity) : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon)
override fun getId(): Long {
@ -24,8 +23,10 @@ data class FeedGroupCardItem(
override fun getLayout(): Int = R.layout.feed_group_card_item
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.title.text = name
viewHolder.icon.setImageResource(icon.getDrawableRes(viewHolder.containerView.context))
override fun bind(viewBinding: FeedGroupCardItemBinding, position: Int) {
viewBinding.title.text = name
viewBinding.icon.setImageResource(icon.getDrawableRes(viewBinding.root.context))
}
override fun initializeViewBinding(view: View) = FeedGroupCardItemBinding.bind(view)
}

View File

@ -6,13 +6,16 @@ import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import kotlinx.android.synthetic.main.feed_item_carousel.recycler_view
import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FeedItemCarouselBinding
import org.schabi.newpipe.local.subscription.decoration.FeedGroupCarouselDecoration
class FeedGroupCarouselItem(context: Context, private val carouselAdapter: GroupAdapter<GroupieViewHolder>) : Item() {
class FeedGroupCarouselItem(
context: Context,
private val carouselAdapter: GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>
) : BindableItem<FeedItemCarouselBinding>() {
private val feedGroupCarouselDecoration = FeedGroupCarouselDecoration(context)
private var linearLayoutManager: LinearLayoutManager? = null
@ -30,12 +33,12 @@ class FeedGroupCarouselItem(context: Context, private val carouselAdapter: Group
listState = state
}
override fun createViewHolder(itemView: View): GroupieViewHolder {
val viewHolder = super.createViewHolder(itemView)
override fun initializeViewBinding(view: View): FeedItemCarouselBinding {
val viewHolder = FeedItemCarouselBinding.bind(view)
linearLayoutManager = LinearLayoutManager(itemView.context, RecyclerView.HORIZONTAL, false)
linearLayoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
viewHolder.recycler_view.apply {
viewHolder.recyclerView.apply {
layoutManager = linearLayoutManager
adapter = carouselAdapter
addItemDecoration(feedGroupCarouselDecoration)
@ -44,12 +47,12 @@ class FeedGroupCarouselItem(context: Context, private val carouselAdapter: Group
return viewHolder
}
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.recycler_view.apply { adapter = carouselAdapter }
override fun bind(viewBinding: FeedItemCarouselBinding, position: Int) {
viewBinding.recyclerView.apply { adapter = carouselAdapter }
linearLayoutManager?.onRestoreInstanceState(listState)
}
override fun unbind(viewHolder: GroupieViewHolder) {
override fun unbind(viewHolder: GroupieViewHolder<FeedItemCarouselBinding>) {
super.unbind(viewHolder)
listState = linearLayoutManager?.onSaveInstanceState()

View File

@ -1,16 +1,15 @@
package org.schabi.newpipe.local.subscription.item
import android.view.MotionEvent
import android.view.View
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
import androidx.recyclerview.widget.ItemTouchHelper.UP
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import kotlinx.android.synthetic.main.feed_group_reorder_item.group_icon
import kotlinx.android.synthetic.main.feed_group_reorder_item.group_name
import kotlinx.android.synthetic.main.feed_group_reorder_item.handle
import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.FeedGroupReorderItemBinding
import org.schabi.newpipe.local.subscription.FeedGroupIcon
data class FeedGroupReorderItem(
@ -18,7 +17,7 @@ data class FeedGroupReorderItem(
val name: String,
val icon: FeedGroupIcon,
val dragCallback: ItemTouchHelper
) : Item() {
) : BindableItem<FeedGroupReorderItemBinding>() {
constructor (feedGroupEntity: FeedGroupEntity, dragCallback: ItemTouchHelper) :
this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon, dragCallback)
@ -31,12 +30,12 @@ data class FeedGroupReorderItem(
override fun getLayout(): Int = R.layout.feed_group_reorder_item
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.group_name.text = name
viewHolder.group_icon.setImageResource(icon.getDrawableRes(viewHolder.containerView.context))
viewHolder.handle.setOnTouchListener { _, event ->
override fun bind(viewBinding: FeedGroupReorderItemBinding, position: Int) {
viewBinding.groupName.text = name
viewBinding.groupIcon.setImageResource(icon.getDrawableRes(viewBinding.root.context))
viewBinding.handle.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
dragCallback.startDrag(viewHolder)
dragCallback.startDrag(GroupieViewHolder(viewBinding))
return@setOnTouchListener true
}
@ -47,4 +46,6 @@ data class FeedGroupReorderItem(
override fun getDragDirs(): Int {
return UP or DOWN
}
override fun initializeViewBinding(view: View) = FeedGroupReorderItemBinding.bind(view)
}

View File

@ -7,17 +7,13 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import kotlinx.android.synthetic.main.feed_import_export_group.export_to_options
import kotlinx.android.synthetic.main.feed_import_export_group.import_export
import kotlinx.android.synthetic.main.feed_import_export_group.import_export_expand_icon
import kotlinx.android.synthetic.main.feed_import_export_group.import_export_options
import kotlinx.android.synthetic.main.feed_import_export_group.import_from_options
import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FeedImportExportGroupBinding
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.exceptions.ExtractionException
import org.schabi.newpipe.util.AnimationUtils
import org.schabi.newpipe.ktx.animateRotation
import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.views.CollapsibleView
@ -27,51 +23,52 @@ class FeedImportExportItem(
val onImportFromServiceSelected: (Int) -> Unit,
val onExportSelected: () -> Unit,
var isExpanded: Boolean = false
) : Item() {
) : BindableItem<FeedImportExportGroupBinding>() {
companion object {
const val REFRESH_EXPANDED_STATUS = 123
}
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
override fun bind(viewBinding: FeedImportExportGroupBinding, position: Int, payloads: MutableList<Any>) {
if (payloads.contains(REFRESH_EXPANDED_STATUS)) {
viewHolder.import_export_options.apply { if (isExpanded) expand() else collapse() }
viewBinding.importExportOptions.apply { if (isExpanded) expand() else collapse() }
return
}
super.bind(viewHolder, position, payloads)
super.bind(viewBinding, position, payloads)
}
override fun getLayout(): Int = R.layout.feed_import_export_group
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
if (viewHolder.import_from_options.childCount == 0) setupImportFromItems(viewHolder.import_from_options)
if (viewHolder.export_to_options.childCount == 0) setupExportToItems(viewHolder.export_to_options)
override fun bind(viewBinding: FeedImportExportGroupBinding, position: Int) {
if (viewBinding.importFromOptions.childCount == 0) setupImportFromItems(viewBinding.importFromOptions)
if (viewBinding.exportToOptions.childCount == 0) setupExportToItems(viewBinding.exportToOptions)
expandIconListener?.let { viewHolder.import_export_options.removeListener(it) }
expandIconListener?.let { viewBinding.importExportOptions.removeListener(it) }
expandIconListener = CollapsibleView.StateListener { newState ->
AnimationUtils.animateRotation(
viewHolder.import_export_expand_icon,
viewBinding.importExportExpandIcon.animateRotation(
250, if (newState == CollapsibleView.COLLAPSED) 0 else 180
)
}
viewHolder.import_export_options.currentState = if (isExpanded) CollapsibleView.EXPANDED else CollapsibleView.COLLAPSED
viewHolder.import_export_expand_icon.rotation = if (isExpanded) 180F else 0F
viewHolder.import_export_options.ready()
viewBinding.importExportOptions.currentState = if (isExpanded) CollapsibleView.EXPANDED else CollapsibleView.COLLAPSED
viewBinding.importExportExpandIcon.rotation = if (isExpanded) 180F else 0F
viewBinding.importExportOptions.ready()
viewHolder.import_export_options.addListener(expandIconListener)
viewHolder.import_export.setOnClickListener {
viewHolder.import_export_options.switchState()
isExpanded = viewHolder.import_export_options.currentState == CollapsibleView.EXPANDED
viewBinding.importExportOptions.addListener(expandIconListener)
viewBinding.importExport.setOnClickListener {
viewBinding.importExportOptions.switchState()
isExpanded = viewBinding.importExportOptions.currentState == CollapsibleView.EXPANDED
}
}
override fun unbind(viewHolder: GroupieViewHolder) {
override fun unbind(viewHolder: GroupieViewHolder<FeedImportExportGroupBinding>) {
super.unbind(viewHolder)
expandIconListener?.let { viewHolder.import_export_options.removeListener(it) }
expandIconListener?.let { viewHolder.binding.importExportOptions.removeListener(it) }
expandIconListener = null
}
override fun initializeViewBinding(view: View) = FeedImportExportGroupBinding.bind(view)
private var expandIconListener: CollapsibleView.StateListener? = null
private fun addItemView(title: String, @DrawableRes icon: Int, container: ViewGroup): View {
@ -118,7 +115,8 @@ class FeedImportExportItem(
private fun setupExportToItems(listHolder: ViewGroup) {
val previousBackupItem = addItemView(
listHolder.context.getString(R.string.file),
ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_save), listHolder
ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_save),
listHolder
)
previousBackupItem.setOnClickListener { onExportSelected() }
}

View File

@ -1,19 +1,23 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import android.view.View.OnClickListener
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import kotlinx.android.synthetic.main.header_item.header_title
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.HeaderItemBinding
class HeaderItem(val title: String, private val onClickListener: (() -> Unit)? = null) : Item() {
class HeaderItem(
val title: String,
private val onClickListener: (() -> Unit)? = null
) : BindableItem<HeaderItemBinding>() {
override fun getLayout(): Int = R.layout.header_item
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.header_title.text = title
override fun bind(viewBinding: HeaderItemBinding, position: Int) {
viewBinding.headerTitle.text = title
val listener: OnClickListener? = if (onClickListener != null) OnClickListener { onClickListener.invoke() } else null
viewHolder.root.setOnClickListener(listener)
viewBinding.root.setOnClickListener(listener)
}
override fun initializeViewBinding(view: View) = HeaderItemBinding.bind(view)
}

View File

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

View File

@ -1,20 +1,25 @@
package org.schabi.newpipe.local.subscription.item
import android.content.Context
import android.view.View
import androidx.annotation.DrawableRes
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import kotlinx.android.synthetic.main.picker_icon_item.icon_view
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.PickerIconItemBinding
import org.schabi.newpipe.local.subscription.FeedGroupIcon
class PickerIconItem(context: Context, val icon: FeedGroupIcon) : Item() {
class PickerIconItem(
context: Context,
val icon: FeedGroupIcon
) : BindableItem<PickerIconItemBinding>() {
@DrawableRes
val iconRes: Int = icon.getDrawableRes(context)
override fun getLayout(): Int = R.layout.picker_icon_item
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.icon_view.setImageResource(iconRes)
override fun bind(viewBinding: PickerIconItemBinding, position: Int) {
viewBinding.iconView.setImageResource(iconRes)
}
override fun initializeViewBinding(view: View) = PickerIconItemBinding.bind(view)
}

View File

@ -1,49 +1,51 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import kotlinx.android.synthetic.main.picker_subscription_item.*
import kotlinx.android.synthetic.main.picker_subscription_item.view.*
import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder
import org.schabi.newpipe.R
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.util.AnimationUtils
import org.schabi.newpipe.util.AnimationUtils.animateView
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.util.ImageDisplayConstants
data class PickerSubscriptionItem(
val subscriptionEntity: SubscriptionEntity,
var isSelected: Boolean = false
) : Item() {
) : BindableItem<PickerSubscriptionItemBinding>() {
override fun getId(): Long = subscriptionEntity.uid
override fun getLayout(): Int = R.layout.picker_subscription_item
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
ImageLoader.getInstance().displayImage(
subscriptionEntity.avatarUrl,
viewHolder.thumbnail_view, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS
viewBinding.thumbnailView, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS
)
viewHolder.title_view.text = subscriptionEntity.name
viewHolder.selected_highlight.isVisible = isSelected
viewBinding.titleView.text = subscriptionEntity.name
viewBinding.selectedHighlight.isVisible = isSelected
}
override fun unbind(viewHolder: GroupieViewHolder) {
override fun unbind(viewHolder: GroupieViewHolder<PickerSubscriptionItemBinding>) {
super.unbind(viewHolder)
viewHolder.selected_highlight.animate().setListener(null).cancel()
viewHolder.selected_highlight.visibility = View.GONE
viewHolder.selected_highlight.alpha = 1F
viewHolder.binding.selectedHighlight.apply {
animate().setListener(null).cancel()
isGone = true
alpha = 1F
}
}
override fun initializeViewBinding(view: View) = PickerSubscriptionItemBinding.bind(view)
fun updateSelected(containerView: View, isSelected: Boolean) {
this.isSelected = isSelected
animateView(
containerView.selected_highlight,
AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150
)
PickerSubscriptionItemBinding.bind(containerView).selectedHighlight
.animate(isSelected, 150, AnimationType.LIGHT_SCALE_AND_ALPHA)
}
}

View File

@ -36,11 +36,11 @@ import androidx.core.app.ServiceCompat;
import org.reactivestreams.Publisher;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExceptionUtils;
import java.io.FileNotFoundException;
import java.util.Collections;

View File

@ -27,6 +27,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
@ -50,7 +51,7 @@ public class SubscriptionsExportService extends BaseImportExportService {
* A {@link LocalBroadcastManager local broadcast} will be made with this action
* when the export is successfully completed.
*/
public static final String EXPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription"
public static final String EXPORT_COMPLETE_ACTION = App.PACKAGE_NAME + ".local.subscription"
+ ".services.SubscriptionsExportService.EXPORT_COMPLETE";
private Subscription subscription;

View File

@ -29,13 +29,14 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import java.io.File;
@ -66,7 +67,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
* A {@link LocalBroadcastManager local broadcast} will be made with this action
* when the import is successfully completed.
*/
public static final String IMPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription"
public static final String IMPORT_COMPLETE_ACTION = App.PACKAGE_NAME + ".local.subscription"
+ ".services.SubscriptionsImportService.IMPORT_COMPLETE";
/**

View File

@ -1,57 +0,0 @@
package org.schabi.newpipe.player;
import android.content.Intent;
import android.view.Menu;
import org.schabi.newpipe.R;
public final class BackgroundPlayerActivity extends ServicePlayerActivity {
private static final String TAG = "BackgroundPlayerActivity";
@Override
public String getTag() {
return TAG;
}
@Override
public String getSupportActionTitle() {
return getResources().getString(R.string.title_activity_play_queue);
}
@Override
public Intent getBindIntent() {
return new Intent(this, MainPlayer.class);
}
@Override
public void startPlayerListener() {
if (player instanceof VideoPlayerImpl) {
((VideoPlayerImpl) player).setActivityListener(this);
}
}
@Override
public void stopPlayerListener() {
if (player instanceof VideoPlayerImpl) {
((VideoPlayerImpl) player).removeActivityListener(this);
}
}
@Override
public int getPlayerOptionMenuResource() {
return R.menu.menu_play_queue_bg;
}
@Override
public void setupMenu(final Menu menu) {
if (player == null) {
return;
}
menu.findItem(R.id.action_switch_popup)
.setVisible(!((VideoPlayerImpl) player).popupPlayerSelected());
menu.findItem(R.id.action_switch_background)
.setVisible(!((VideoPlayerImpl) player).audioPlayerSelected());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@ import android.os.Binder;
import android.os.IBinder;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@ -33,7 +34,8 @@ import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import org.schabi.newpipe.R;
import org.schabi.newpipe.App;
import org.schabi.newpipe.databinding.PlayerBinding;
import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@ -46,9 +48,9 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
*/
public final class MainPlayer extends Service {
private static final String TAG = "MainPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG;
private static final boolean DEBUG = Player.DEBUG;
private VideoPlayerImpl playerImpl;
private Player player;
private WindowManager windowManager;
private final IBinder mBinder = new MainPlayer.LocalBinder();
@ -64,25 +66,23 @@ public final class MainPlayer extends Service {
//////////////////////////////////////////////////////////////////////////*/
static final String ACTION_CLOSE
= "org.schabi.newpipe.player.MainPlayer.CLOSE";
= App.PACKAGE_NAME + ".player.MainPlayer.CLOSE";
static final String ACTION_PLAY_PAUSE
= "org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE";
static final String ACTION_OPEN_CONTROLS
= "org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS";
= App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE";
static final String ACTION_REPEAT
= "org.schabi.newpipe.player.MainPlayer.REPEAT";
= App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
static final String ACTION_PLAY_NEXT
= "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT";
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_NEXT";
static final String ACTION_PLAY_PREVIOUS
= "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS";
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
static final String ACTION_FAST_REWIND
= "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND";
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_REWIND";
static final String ACTION_FAST_FORWARD
= "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD";
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_FORWARD";
static final String ACTION_SHUFFLE
= "org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE";
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_SHUFFLE";
public static final String ACTION_RECREATE_NOTIFICATION
= "org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
@ -101,13 +101,12 @@ public final class MainPlayer extends Service {
}
private void createView() {
final View layout = View.inflate(this, R.layout.player, null);
final PlayerBinding binding = PlayerBinding.inflate(LayoutInflater.from(this));
playerImpl = new VideoPlayerImpl(this);
playerImpl.setup(layout);
playerImpl.shouldUpdateOnProgress = true;
player = new Player(this);
player.setupFromView(binding);
NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this);
NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
}
@Override
@ -117,19 +116,19 @@ public final class MainPlayer extends Service {
+ "], flags = [" + flags + "], startId = [" + startId + "]");
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
&& playerImpl.playQueue == null) {
&& player.getPlayQueue() == null) {
// Player is not working, no need to process media button's action
return START_NOT_STICKY;
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|| intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) != null) {
NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this);
|| intent.getStringExtra(Player.PLAY_QUEUE_KEY) != null) {
NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
}
playerImpl.handleIntent(intent);
if (playerImpl.mediaSessionManager != null) {
playerImpl.mediaSessionManager.handleMediaButtonIntent(intent);
player.handleIntent(intent);
if (player.getMediaSessionManager() != null) {
player.getMediaSessionManager().handleMediaButtonIntent(intent);
}
return START_NOT_STICKY;
}
@ -139,20 +138,20 @@ public final class MainPlayer extends Service {
Log.d(TAG, "stop() called");
}
if (playerImpl.getPlayer() != null) {
playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady();
if (!player.exoPlayerIsNull()) {
player.saveWasPlaying();
// Releases wifi & cpu, disables keepScreenOn, etc.
if (!autoplayEnabled) {
playerImpl.onPause();
player.pause();
}
// We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth
playerImpl.getPlayer().stop(false);
playerImpl.setRecovery();
player.smoothStopPlayer();
player.setRecovery();
// Android TV will handle back button in case controls will be visible
// (one more additional unneeded click while the player is hidden)
playerImpl.hideControls(0, 0);
playerImpl.onQueueClosed();
player.hideControls(0, 0);
player.closeItemsList();
// Notification shows information about old stream but if a user selects
// a stream from backStack it's not actual anymore
// So we should hide the notification at all.
@ -166,7 +165,7 @@ public final class MainPlayer extends Service {
@Override
public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent);
if (!playerImpl.videoPlayerSelected()) {
if (!player.videoPlayerSelected()) {
return;
}
onDestroy();
@ -179,7 +178,23 @@ public final class MainPlayer extends Service {
if (DEBUG) {
Log.d(TAG, "destroy() called");
}
onClose();
if (player != null) {
// Exit from fullscreen when user closes the player via notification
if (player.isFullscreen()) {
player.toggleFullscreen();
}
removeViewFromParent();
player.saveStreamProgressState();
player.setRecovery();
player.stopActivityBinding();
player.removePopupFromView();
player.destroy();
}
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
stopSelf();
}
@Override
@ -192,32 +207,6 @@ public final class MainPlayer extends Service {
return mBinder;
}
/*//////////////////////////////////////////////////////////////////////////
// Actions
//////////////////////////////////////////////////////////////////////////*/
private void onClose() {
if (DEBUG) {
Log.d(TAG, "onClose() called");
}
if (playerImpl != null) {
// Exit from fullscreen when user closes the player via notification
if (playerImpl.isFullscreen()) {
playerImpl.toggleFullscreen();
}
removeViewFromParent();
playerImpl.setRecovery();
playerImpl.savePlaybackState();
playerImpl.stopActivityBinding();
playerImpl.removePopupFromView();
playerImpl.destroy();
}
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
stopSelf();
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@ -225,25 +214,25 @@ public final class MainPlayer extends Service {
boolean isLandscape() {
// DisplayMetrics from activity context knows about MultiWindow feature
// while DisplayMetrics from app context doesn't
final DisplayMetrics metrics = (playerImpl != null
&& playerImpl.getParentActivity() != null
? playerImpl.getParentActivity().getResources()
final DisplayMetrics metrics = (player != null
&& player.getParentActivity() != null
? player.getParentActivity().getResources()
: getResources()).getDisplayMetrics();
return metrics.heightPixels < metrics.widthPixels;
}
@Nullable
public View getView() {
if (playerImpl == null) {
if (player == null) {
return null;
}
return playerImpl.getRootView();
return player.getRootView();
}
public void removeViewFromParent() {
if (getView() != null && getView().getParent() != null) {
if (playerImpl.getParentActivity() != null) {
if (player.getParentActivity() != null) {
// This means view was added to fragment
final ViewGroup parent = (ViewGroup) getView().getParent();
parent.removeView(getView());
@ -261,8 +250,8 @@ public final class MainPlayer extends Service {
return MainPlayer.this;
}
public VideoPlayerImpl getPlayer() {
return MainPlayer.this.playerImpl;
public Player getPlayer() {
return MainPlayer.this.player;
}
}
}

View File

@ -43,7 +43,7 @@ import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
*/
public final class NotificationUtil {
private static final String TAG = NotificationUtil.class.getSimpleName();
private static final boolean DEBUG = BasePlayer.DEBUG;
private static final boolean DEBUG = Player.DEBUG;
private static final int NOTIFICATION_ID = 123789;
@Nullable private static NotificationUtil instance = null;
@ -76,7 +76,7 @@ public final class NotificationUtil {
* @param forceRecreate whether to force the recreation of the notification even if it already
* exists
*/
synchronized void createNotificationIfNeededAndUpdate(final VideoPlayerImpl player,
synchronized void createNotificationIfNeededAndUpdate(final Player player,
final boolean forceRecreate) {
if (forceRecreate || notificationBuilder == null) {
notificationBuilder = createNotification(player);
@ -85,14 +85,14 @@ public final class NotificationUtil {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
private synchronized NotificationCompat.Builder createNotification(
final VideoPlayerImpl player) {
private synchronized NotificationCompat.Builder createNotification(final Player player) {
if (DEBUG) {
Log.d(TAG, "createNotification()");
}
notificationManager = NotificationManagerCompat.from(player.context);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(player.context,
player.context.getString(R.string.notification_channel_id));
notificationManager = NotificationManagerCompat.from(player.getContext());
final NotificationCompat.Builder builder =
new NotificationCompat.Builder(player.getContext(),
player.getContext().getString(R.string.notification_channel_id));
initializeNotificationSlots(player);
@ -107,25 +107,25 @@ public final class NotificationUtil {
// build the compact slot indices array (need code to convert from Integer... because Java)
final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
player.context, player.sharedPreferences, nonNothingSlotCount);
player.getContext(), player.getPrefs(), nonNothingSlotCount);
final int[] compactSlots = new int[compactSlotList.size()];
for (int i = 0; i < compactSlotList.size(); i++) {
compactSlots[i] = compactSlotList.get(i);
}
builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(player.mediaSessionManager.getSessionToken())
.setMediaSession(player.getMediaSessionManager().getSessionToken())
.setShowActionsInCompactView(compactSlots))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
.setShowWhen(false)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setColor(ContextCompat.getColor(player.context, R.color.dark_background_color))
.setColorized(player.sharedPreferences.getBoolean(
player.context.getString(R.string.notification_colorize_key),
true))
.setDeleteIntent(PendingIntent.getBroadcast(player.context, NOTIFICATION_ID,
.setColor(ContextCompat.getColor(player.getContext(),
R.color.dark_background_color))
.setColorized(player.getPrefs().getBoolean(
player.getContext().getString(R.string.notification_colorize_key), true))
.setDeleteIntent(PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID,
new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT));
return builder;
@ -135,20 +135,20 @@ public final class NotificationUtil {
* Updates the notification builder and the button icons depending on the playback state.
* @param player the player currently open, to take data from
*/
private synchronized void updateNotification(final VideoPlayerImpl player) {
private synchronized void updateNotification(final Player player) {
if (DEBUG) {
Log.d(TAG, "updateNotification()");
}
// also update content intent, in case the user switched players
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.context,
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(),
NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT));
notificationBuilder.setContentTitle(player.getVideoTitle());
notificationBuilder.setContentText(player.getUploaderName());
notificationBuilder.setTicker(player.getVideoTitle());
updateActions(notificationBuilder, player);
final boolean showThumbnail = player.sharedPreferences.getBoolean(
player.context.getString(R.string.show_thumbnail_key), true);
final boolean showThumbnail = player.getPrefs().getBoolean(
player.getContext().getString(R.string.show_thumbnail_key), true);
if (showThumbnail) {
setLargeIcon(notificationBuilder, player);
}
@ -174,7 +174,7 @@ public final class NotificationUtil {
}
void createNotificationAndStartForeground(final VideoPlayerImpl player, final Service service) {
void createNotificationAndStartForeground(final Player player, final Service service) {
if (notificationBuilder == null) {
notificationBuilder = createNotification(player);
}
@ -203,17 +203,16 @@ public final class NotificationUtil {
// ACTIONS
/////////////////////////////////////////////////////
private void initializeNotificationSlots(final VideoPlayerImpl player) {
private void initializeNotificationSlots(final Player player) {
for (int i = 0; i < 5; ++i) {
notificationSlots[i] = player.sharedPreferences.getInt(
player.context.getString(NotificationConstants.SLOT_PREF_KEYS[i]),
notificationSlots[i] = player.getPrefs().getInt(
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
NotificationConstants.SLOT_DEFAULTS[i]);
}
}
@SuppressLint("RestrictedApi")
private void updateActions(final NotificationCompat.Builder builder,
final VideoPlayerImpl player) {
private void updateActions(final NotificationCompat.Builder builder, final Player player) {
builder.mActions.clear();
for (int i = 0; i < 5; ++i) {
addAction(builder, player, notificationSlots[i]);
@ -221,7 +220,7 @@ public final class NotificationUtil {
}
private void addAction(final NotificationCompat.Builder builder,
final VideoPlayerImpl player,
final Player player,
@NotificationConstants.Action final int slot) {
final NotificationCompat.Action action = getAction(player, slot);
if (action != null) {
@ -231,7 +230,7 @@ public final class NotificationUtil {
@Nullable
private NotificationCompat.Action getAction(
final VideoPlayerImpl player,
final Player player,
@NotificationConstants.Action final int selectedAction) {
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
switch (selectedAction) {
@ -252,7 +251,7 @@ public final class NotificationUtil {
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
case NotificationConstants.SMART_REWIND_PREVIOUS:
if (player.playQueue != null && player.playQueue.size() > 1) {
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
return getAction(player, R.drawable.exo_notification_previous,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
} else {
@ -261,7 +260,7 @@ public final class NotificationUtil {
}
case NotificationConstants.SMART_FORWARD_NEXT:
if (player.playQueue != null && player.playQueue.size() > 1) {
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
return getAction(player, R.drawable.exo_notification_next,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
} else {
@ -270,23 +269,23 @@ public final class NotificationUtil {
}
case NotificationConstants.PLAY_PAUSE_BUFFERING:
if (player.getCurrentState() == BasePlayer.STATE_PREFLIGHT
|| player.getCurrentState() == BasePlayer.STATE_BLOCKED
|| player.getCurrentState() == BasePlayer.STATE_BUFFERING) {
if (player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == Player.STATE_BLOCKED
|| player.getCurrentState() == Player.STATE_BUFFERING) {
// null intent -> show hourglass icon that does nothing when clicked
return new NotificationCompat.Action(R.drawable.ic_hourglass_top_white_24dp_png,
player.context.getString(R.string.notification_action_buffering),
player.getContext().getString(R.string.notification_action_buffering),
null);
}
case NotificationConstants.PLAY_PAUSE:
if (player.getCurrentState() == BasePlayer.STATE_COMPLETED) {
if (player.getCurrentState() == Player.STATE_COMPLETED) {
return getAction(player, R.drawable.ic_replay_white_24dp_png,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else if (player.isPlaying()
|| player.getCurrentState() == BasePlayer.STATE_PREFLIGHT
|| player.getCurrentState() == BasePlayer.STATE_BLOCKED
|| player.getCurrentState() == BasePlayer.STATE_BUFFERING) {
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == Player.STATE_BLOCKED
|| player.getCurrentState() == Player.STATE_BUFFERING) {
return getAction(player, R.drawable.exo_notification_pause,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else {
@ -307,7 +306,7 @@ public final class NotificationUtil {
}
case NotificationConstants.SHUFFLE:
if (player.playQueue != null && player.playQueue.isShuffled()) {
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
return getAction(player, R.drawable.exo_controls_shuffle_on,
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
} else {
@ -326,23 +325,23 @@ public final class NotificationUtil {
}
}
private NotificationCompat.Action getAction(final VideoPlayerImpl player,
private NotificationCompat.Action getAction(final Player player,
@DrawableRes final int drawable,
@StringRes final int title,
final String intentAction) {
return new NotificationCompat.Action(drawable, player.context.getString(title),
PendingIntent.getBroadcast(player.context, NOTIFICATION_ID,
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID,
new Intent(intentAction), FLAG_UPDATE_CURRENT));
}
private Intent getIntentForNotification(final VideoPlayerImpl player) {
private Intent getIntentForNotification(final Player player) {
if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
// Means we play in popup or audio only. Let's show the play queue
return NavigationHelper.getPlayQueueActivityIntent(player.context);
return NavigationHelper.getPlayQueueActivityIntent(player.getContext());
} else {
// We are playing in fragment. Don't open another activity just show fragment. That's it
final Intent intent = NavigationHelper.getPlayerIntent(
player.context, MainActivity.class, null, true);
player.getContext(), MainActivity.class, null, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
@ -355,10 +354,9 @@ public final class NotificationUtil {
// BITMAP
/////////////////////////////////////////////////////
private void setLargeIcon(final NotificationCompat.Builder builder,
final VideoPlayerImpl player) {
final boolean scaleImageToSquareAspectRatio = player.sharedPreferences.getBoolean(
player.context.getString(R.string.scale_to_square_image_in_notifications_key),
private void setLargeIcon(final NotificationCompat.Builder builder, final Player player) {
final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean(
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
false);
if (scaleImageToSquareAspectRatio) {
builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail()));

View File

@ -16,13 +16,11 @@ import android.widget.PopupMenu;
import android.widget.SeekBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
@ -48,20 +46,23 @@ import java.util.List;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static org.schabi.newpipe.util.ShareUtils.shareText;
public abstract class ServicePlayerActivity extends AppCompatActivity
public final class PlayQueueActivity extends AppCompatActivity
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, PlaybackParameterDialog.Callback {
private static final String TAG = PlayQueueActivity.class.getSimpleName();
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
protected BasePlayer player;
protected Player player;
private boolean serviceBound;
private ServiceConnection serviceConnection;
private boolean seeking;
private boolean redraw;
////////////////////////////////////////////////////////////////////////////
// Views
@ -73,24 +74,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private Menu menu;
////////////////////////////////////////////////////////////////////////////
// Abstracts
////////////////////////////////////////////////////////////////////////////
public abstract String getTag();
public abstract String getSupportActionTitle();
public abstract Intent getBindIntent();
public abstract void startPlayerListener();
public abstract void stopPlayerListener();
public abstract int getPlayerOptionMenuResource();
public abstract void setupMenu(Menu m);
////////////////////////////////////////////////////////////////////////////
// Activity Lifecycle
////////////////////////////////////////////////////////////////////////////
@ -107,35 +90,32 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
setSupportActionBar(queueControlBinding.toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(getSupportActionTitle());
getSupportActionBar().setTitle(R.string.title_activity_play_queue);
}
serviceConnection = getServiceConnection();
bind();
}
@Override
protected void onResume() {
super.onResume();
if (redraw) {
ActivityCompat.recreate(this);
redraw = false;
}
}
@Override
public boolean onCreateOptionsMenu(final Menu m) {
this.menu = m;
getMenuInflater().inflate(R.menu.menu_play_queue, m);
getMenuInflater().inflate(getPlayerOptionMenuResource(), m);
getMenuInflater().inflate(R.menu.menu_play_queue_bg, m);
onMaybeMuteChanged();
onPlaybackParameterChanged(player.getPlaybackParameters());
return true;
}
// Allow to setup visibility of menuItems
@Override
public boolean onPrepareOptionsMenu(final Menu m) {
setupMenu(m);
if (player != null) {
menu.findItem(R.id.action_switch_popup)
.setVisible(!player.popupPlayerSelected());
menu.findItem(R.id.action_switch_background)
.setVisible(!player.audioPlayerSelected());
}
return super.onPrepareOptionsMenu(m);
}
@ -167,14 +147,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
case R.id.action_switch_popup:
if (PermissionHelper.isPopupEnabled(this)) {
this.player.setRecovery();
NavigationHelper.playOnPopupPlayer(this, player.playQueue, true);
NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true);
} else {
PermissionHelper.showPopupEnablementToast(this);
}
return true;
case R.id.action_switch_background:
this.player.setRecovery();
NavigationHelper.playOnBackgroundPlayer(this, player.playQueue, true);
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
return true;
}
return super.onOptionsItemSelected(item);
@ -191,7 +171,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
////////////////////////////////////////////////////////////////////////////
private void bind() {
final boolean success = bindService(getBindIntent(), serviceConnection, BIND_AUTO_CREATE);
final Intent bindIntent = new Intent(this, MainPlayer.class);
final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
if (!success) {
unbindService(serviceConnection);
}
@ -202,7 +183,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
if (serviceBound) {
unbindService(serviceConnection);
serviceBound = false;
stopPlayerListener();
if (player != null) {
player.removeActivityListener(this);
}
if (player != null && player.getPlayQueueAdapter() != null) {
player.getPlayQueueAdapter().unsetSelectedListener();
@ -221,12 +204,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return new ServiceConnection() {
@Override
public void onServiceDisconnected(final ComponentName name) {
Log.d(getTag(), "Player service is disconnected");
Log.d(TAG, "Player service is disconnected");
}
@Override
public void onServiceConnected(final ComponentName name, final IBinder service) {
Log.d(getTag(), "Player service is connected");
Log.d(TAG, "Player service is connected");
if (service instanceof PlayerServiceBinder) {
player = ((PlayerServiceBinder) service).getPlayerInstance();
@ -235,12 +218,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
if (player == null || player.getPlayQueue() == null
|| player.getPlayQueueAdapter() == null || player.getPlayer() == null) {
|| player.getPlayQueueAdapter() == null || player.exoPlayerIsNull()) {
unbind();
finish();
} else {
buildComponents();
startPlayerListener();
if (player != null) {
player.setActivityListener(PlayQueueActivity.this);
}
}
}
};
@ -327,7 +312,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
final MenuItem share = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 3,
Menu.NONE, R.string.share);
share.setOnMenuItemClickListener(menuItem -> {
shareUrl(item.getTitle(), item.getUrl());
shareText(getApplicationContext(), item.getTitle(), item.getUrl());
return true;
});
@ -375,7 +360,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
@Override
public void selected(final PlayQueueItem item, final View view) {
if (player != null) {
player.onSelected(item);
player.selectQueueItem(item);
}
}
@ -436,15 +421,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
if (view.getId() == queueControlBinding.controlRepeat.getId()) {
player.onRepeatClicked();
} else if (view.getId() == queueControlBinding.controlBackward.getId()) {
player.onPlayPrevious();
player.playPrevious();
} else if (view.getId() == queueControlBinding.controlFastRewind.getId()) {
player.onFastRewind();
player.fastRewind();
} else if (view.getId() == queueControlBinding.controlPlayPause.getId()) {
player.onPlayPause();
player.playPause();
} else if (view.getId() == queueControlBinding.controlFastForward.getId()) {
player.onFastForward();
player.fastForward();
} else if (view.getId() == queueControlBinding.controlForward.getId()) {
player.onPlayNext();
player.playNext();
} else if (view.getId() == queueControlBinding.controlShuffle.getId()) {
player.onShuffleClicked();
} else if (view.getId() == queueControlBinding.metadata.getId()) {
@ -463,7 +448,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return;
}
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), getTag());
player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), TAG);
}
@Override
@ -517,22 +502,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(playlist);
PlaylistAppendDialog.onPlaylistFound(getApplicationContext(),
() -> d.show(getSupportFragmentManager(), getTag()),
() -> PlaylistCreationDialog.newInstance(d)
.show(getSupportFragmentManager(), getTag()
));
}
////////////////////////////////////////////////////////////////////////////
// Share
////////////////////////////////////////////////////////////////////////////
private void shareUrl(final String subject, final String url) {
final Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
intent.putExtra(Intent.EXTRA_TEXT, url);
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
() -> d.show(getSupportFragmentManager(), TAG),
() -> PlaylistCreationDialog.newInstance(d).show(getSupportFragmentManager(), TAG));
}
////////////////////////////////////////////////////////////////////////////
@ -616,15 +587,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private void onStateChanged(final int state) {
switch (state) {
case BasePlayer.STATE_PAUSED:
case Player.STATE_PAUSED:
queueControlBinding.controlPlayPause
.setImageResource(R.drawable.ic_play_arrow_white_24dp);
break;
case BasePlayer.STATE_PLAYING:
case Player.STATE_PLAYING:
queueControlBinding.controlPlayPause
.setImageResource(R.drawable.ic_pause_white_24dp);
break;
case BasePlayer.STATE_COMPLETED:
case Player.STATE_COMPLETED:
queueControlBinding.controlPlayPause
.setImageResource(R.drawable.ic_replay_white_24dp);
break;
@ -633,9 +604,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
switch (state) {
case BasePlayer.STATE_PAUSED:
case BasePlayer.STATE_PLAYING:
case BasePlayer.STATE_COMPLETED:
case Player.STATE_PAUSED:
case Player.STATE_PLAYING:
case Player.STATE_COMPLETED:
queueControlBinding.controlPlayPause.setClickable(true);
queueControlBinding.controlPlayPause.setVisibility(View.VISIBLE);
queueControlBinding.controlProgressBar.setVisibility(View.GONE);
@ -650,15 +621,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private void onPlayModeChanged(final int repeatMode, final boolean shuffled) {
switch (repeatMode) {
case Player.REPEAT_MODE_OFF:
case com.google.android.exoplayer2.Player.REPEAT_MODE_OFF:
queueControlBinding.controlRepeat
.setImageResource(R.drawable.exo_controls_repeat_off);
break;
case Player.REPEAT_MODE_ONE:
case com.google.android.exoplayer2.Player.REPEAT_MODE_ONE:
queueControlBinding.controlRepeat
.setImageResource(R.drawable.exo_controls_repeat_one);
break;
case Player.REPEAT_MODE_ALL:
case com.google.android.exoplayer2.Player.REPEAT_MODE_ALL:
queueControlBinding.controlRepeat
.setImageResource(R.drawable.exo_controls_repeat_all);
break;
@ -700,9 +671,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
// using rootView.getContext() because getApplicationContext() didn't work
final Context context = queueControlBinding.getRoot().getContext();
item.setIcon(ThemeHelper.resolveResourceIdFromAttr(context,
player.isMuted()
? R.attr.ic_volume_off
: R.attr.ic_volume_up));
player.isMuted() ? R.attr.ic_volume_off : R.attr.ic_volume_up));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -5,13 +5,13 @@ import android.os.Binder;
import androidx.annotation.NonNull;
class PlayerServiceBinder extends Binder {
private final BasePlayer basePlayer;
private final Player player;
PlayerServiceBinder(@NonNull final BasePlayer basePlayer) {
this.basePlayer = basePlayer;
PlayerServiceBinder(@NonNull final Player player) {
this.player = player;
}
BasePlayer getPlayerInstance() {
return basePlayer;
Player getPlayerInstance() {
return player;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -7,25 +7,25 @@ import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import org.schabi.newpipe.player.BasePlayer
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.player.MainPlayer
import org.schabi.newpipe.player.VideoPlayerImpl
import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.util.AnimationUtils
import org.schabi.newpipe.player.helper.PlayerHelper.savePopupPositionAndSizeToPrefs
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.max
import kotlin.math.min
/**
* Base gesture handling for [VideoPlayerImpl]
* Base gesture handling for [Player]
*
* This class contains the logic for the player gestures like View preparations
* and provides some abstract methods to make it easier separating the logic from the UI.
*/
abstract class BasePlayerGestureListener(
@JvmField
protected val playerImpl: VideoPlayerImpl,
protected val player: Player,
@JvmField
protected val service: MainPlayer
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
@ -78,7 +78,7 @@ abstract class BasePlayerGestureListener(
// ///////////////////////////////////////////////////////////////////
override fun onTouch(v: View, event: MotionEvent): Boolean {
return if (playerImpl.popupPlayerSelected()) {
return if (player.popupPlayerSelected()) {
onTouchInPopup(v, event)
} else {
onTouchInMain(v, event)
@ -86,14 +86,14 @@ abstract class BasePlayerGestureListener(
}
private fun onTouchInMain(v: View, event: MotionEvent): Boolean {
playerImpl.gestureDetector.onTouchEvent(event)
player.gestureDetector.onTouchEvent(event)
if (event.action == MotionEvent.ACTION_UP && isMovingInMain) {
isMovingInMain = false
onScrollEnd(MainPlayer.PlayerType.VIDEO, event)
}
return when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
v.parent.requestDisallowInterceptTouchEvent(playerImpl.isFullscreen)
v.parent.requestDisallowInterceptTouchEvent(player.isFullscreen)
true
}
MotionEvent.ACTION_UP -> {
@ -105,7 +105,7 @@ abstract class BasePlayerGestureListener(
}
private fun onTouchInPopup(v: View, event: MotionEvent): Boolean {
playerImpl.gestureDetector.onTouchEvent(event)
player.gestureDetector.onTouchEvent(event)
if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.")
@ -157,10 +157,10 @@ abstract class BasePlayerGestureListener(
initSecPointerY = (-1).toFloat()
onPopupResizingEnd()
playerImpl.changeState(playerImpl.currentState)
player.changeState(player.currentState)
}
if (!playerImpl.isPopupClosing) {
playerImpl.savePositionAndSize()
if (!player.isPopupClosing) {
savePopupPositionAndSizeToPrefs(player)
}
}
@ -190,19 +190,15 @@ abstract class BasePlayerGestureListener(
event.getY(0) - event.getY(1).toDouble()
)
val popupWidth = playerImpl.popupWidth.toDouble()
val popupWidth = player.popupLayoutParams!!.width.toDouble()
// change co-ordinates of popup so the center stays at the same position
val newWidth = popupWidth * currentPointerDistance / initPointerDistance
initPointerDistance = currentPointerDistance
playerImpl.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt()
player.popupLayoutParams!!.x += ((popupWidth - newWidth) / 2.0).toInt()
playerImpl.checkPopupPositionBounds()
playerImpl.updateScreenSize()
playerImpl.updatePopupSize(
min(playerImpl.screenWidth.toDouble(), newWidth).toInt(),
-1
)
player.checkPopupPositionBounds()
player.updateScreenSize()
player.changePopupSize(min(player.screenWidth.toDouble(), newWidth).toInt())
return true
}
}
@ -222,7 +218,7 @@ abstract class BasePlayerGestureListener(
return true
}
return if (playerImpl.popupPlayerSelected())
return if (player.popupPlayerSelected())
onDownInPopup(e)
else
true
@ -231,12 +227,10 @@ abstract class BasePlayerGestureListener(
private fun onDownInPopup(e: MotionEvent): Boolean {
// Fix popup position when the user touch it, it may have the wrong one
// because the soft input is visible (the draggable area is currently resized).
playerImpl.updateScreenSize()
playerImpl.checkPopupPositionBounds()
initialPopupX = playerImpl.popupLayoutParams.x
initialPopupY = playerImpl.popupLayoutParams.y
playerImpl.popupWidth = playerImpl.popupLayoutParams.width.toFloat()
playerImpl.popupHeight = playerImpl.popupLayoutParams.height.toFloat()
player.updateScreenSize()
player.checkPopupPositionBounds()
initialPopupX = player.popupLayoutParams!!.x
initialPopupY = player.popupLayoutParams!!.y
return super.onDown(e)
}
@ -255,15 +249,15 @@ abstract class BasePlayerGestureListener(
if (isDoubleTapping)
return true
if (playerImpl.popupPlayerSelected()) {
if (playerImpl.player == null)
if (player.popupPlayerSelected()) {
if (player.exoPlayerIsNull())
return false
onSingleTap(MainPlayer.PlayerType.POPUP)
return true
} else {
super.onSingleTapConfirmed(e)
if (playerImpl.currentState == BasePlayer.STATE_BLOCKED)
if (player.currentState == Player.STATE_BLOCKED)
return true
onSingleTap(MainPlayer.PlayerType.VIDEO)
@ -272,10 +266,10 @@ abstract class BasePlayerGestureListener(
}
override fun onLongPress(e: MotionEvent?) {
if (playerImpl.popupPlayerSelected()) {
playerImpl.updateScreenSize()
playerImpl.checkPopupPositionBounds()
playerImpl.updatePopupSize(playerImpl.screenWidth.toInt(), -1)
if (player.popupPlayerSelected()) {
player.updateScreenSize()
player.checkPopupPositionBounds()
player.changePopupSize(player.screenWidth.toInt())
}
}
@ -285,7 +279,7 @@ abstract class BasePlayerGestureListener(
distanceX: Float,
distanceY: Float
): Boolean {
return if (playerImpl.popupPlayerSelected()) {
return if (player.popupPlayerSelected()) {
onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY)
} else {
onScrollInMain(initialEvent, movingEvent, distanceX, distanceY)
@ -298,19 +292,18 @@ abstract class BasePlayerGestureListener(
velocityX: Float,
velocityY: Float
): Boolean {
return if (playerImpl.popupPlayerSelected()) {
return if (player.popupPlayerSelected()) {
val absVelocityX = abs(velocityX)
val absVelocityY = abs(velocityY)
if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) {
if (absVelocityX > tossFlingVelocity) {
playerImpl.popupLayoutParams.x = velocityX.toInt()
player.popupLayoutParams!!.x = velocityX.toInt()
}
if (absVelocityY > tossFlingVelocity) {
playerImpl.popupLayoutParams.y = velocityY.toInt()
player.popupLayoutParams!!.y = velocityY.toInt()
}
playerImpl.checkPopupPositionBounds()
playerImpl.windowManager
.updateViewLayout(playerImpl.rootView, playerImpl.popupLayoutParams)
player.checkPopupPositionBounds()
player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
return true
}
return false
@ -326,13 +319,13 @@ abstract class BasePlayerGestureListener(
distanceY: Float
): Boolean {
if (!playerImpl.isFullscreen) {
if (!player.isFullscreen) {
return false
}
val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service)
val isTouchingNavigationBar: Boolean =
initialEvent.y > (playerImpl.rootView.height - getNavigationBarHeight(service))
initialEvent.y > (player.rootView.height - getNavigationBarHeight(service))
if (isTouchingStatusBar || isTouchingNavigationBar) {
return false
}
@ -340,7 +333,7 @@ abstract class BasePlayerGestureListener(
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
if (
!isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
playerImpl.currentState == BasePlayer.STATE_COMPLETED
player.currentState == Player.STATE_COMPLETED
) {
return false
}
@ -371,7 +364,7 @@ abstract class BasePlayerGestureListener(
}
if (!isMovingInPopup) {
AnimationUtils.animateView(playerImpl.closeOverlayButton, true, 200)
player.closeOverlayButton.animate(true, 200)
}
isMovingInPopup = true
@ -381,20 +374,20 @@ abstract class BasePlayerGestureListener(
val diffY: Float = (movingEvent.rawY - initialEvent.rawY)
var posY: Float = (initialPopupY + diffY)
if (posX > playerImpl.screenWidth - playerImpl.popupWidth) {
posX = (playerImpl.screenWidth - playerImpl.popupWidth)
if (posX > player.screenWidth - player.popupLayoutParams!!.width) {
posX = (player.screenWidth - player.popupLayoutParams!!.width)
} else if (posX < 0) {
posX = 0f
}
if (posY > playerImpl.screenHeight - playerImpl.popupHeight) {
posY = (playerImpl.screenHeight - playerImpl.popupHeight)
if (posY > player.screenHeight - player.popupLayoutParams!!.height) {
posY = (player.screenHeight - player.popupLayoutParams!!.height)
} else if (posY < 0) {
posY = 0f
}
playerImpl.popupLayoutParams.x = posX.toInt()
playerImpl.popupLayoutParams.y = posY.toInt()
player.popupLayoutParams!!.x = posX.toInt()
player.popupLayoutParams!!.y = posY.toInt()
onScroll(
MainPlayer.PlayerType.POPUP,
@ -405,8 +398,7 @@ abstract class BasePlayerGestureListener(
distanceY
)
playerImpl.windowManager
.updateViewLayout(playerImpl.rootView, playerImpl.popupLayoutParams)
player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
return true
}
@ -474,16 +466,16 @@ abstract class BasePlayerGestureListener(
// ///////////////////////////////////////////////////////////////////
private fun getDisplayPortion(e: MotionEvent): DisplayPortion {
return if (playerImpl.playerType == MainPlayer.PlayerType.POPUP) {
return if (player.playerType == MainPlayer.PlayerType.POPUP) {
when {
e.x < playerImpl.popupWidth / 3.0 -> DisplayPortion.LEFT
e.x > playerImpl.popupWidth * 2.0 / 3.0 -> DisplayPortion.RIGHT
e.x < player.popupLayoutParams!!.width / 3.0 -> DisplayPortion.LEFT
e.x > player.popupLayoutParams!!.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
} else /* MainPlayer.PlayerType.VIDEO */ {
when {
e.x < playerImpl.rootView.width / 3.0 -> DisplayPortion.LEFT
e.x > playerImpl.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
e.x < player.rootView.width / 3.0 -> DisplayPortion.LEFT
e.x > player.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
}
@ -491,14 +483,14 @@ abstract class BasePlayerGestureListener(
// Currently needed for scrolling since there is no action more the middle portion
private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
return if (playerImpl.playerType == MainPlayer.PlayerType.POPUP) {
return if (player.playerType == MainPlayer.PlayerType.POPUP) {
when {
e.x < playerImpl.popupWidth / 2.0 -> DisplayPortion.LEFT_HALF
e.x < player.popupLayoutParams!!.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
} else /* MainPlayer.PlayerType.VIDEO */ {
when {
e.x < playerImpl.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF
e.x < player.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
}
@ -522,7 +514,7 @@ abstract class BasePlayerGestureListener(
companion object {
private const val TAG = "BasePlayerGestListener"
private val DEBUG = BasePlayer.DEBUG
private val DEBUG = Player.DEBUG
private const val DOUBLE_TAP_DELAY = 550L
private const val MOVEMENT_THRESHOLD = 40

View File

@ -6,9 +6,12 @@ import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import org.schabi.newpipe.R;
import java.util.Arrays;
@ -24,7 +27,7 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout>
private boolean skippingInterception = false;
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
R.id.detail_content_root_layout, R.id.relatedStreamsLayout,
R.id.playQueuePanel, R.id.viewpager, R.id.bottomControls,
R.id.itemsListPanel, R.id.view_pager, R.id.tab_layout, R.id.bottomControls,
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
@Override

View File

@ -11,17 +11,18 @@ import android.widget.ProgressBar;
import androidx.appcompat.content.res.AppCompatResources;
import org.jetbrains.annotations.NotNull;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.VideoPlayerImpl;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.helper.PlayerHelper;
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.ktx.AnimationType.ALPHA;
import static org.schabi.newpipe.ktx.AnimationType.SCALE_AND_ALPHA;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_DURATION;
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_HIDE_TIME;
import static org.schabi.newpipe.player.Player.STATE_PLAYING;
/**
* GestureListener for the player
@ -33,14 +34,14 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class PlayerGestureListener
extends BasePlayerGestureListener
implements View.OnTouchListener {
private static final String TAG = ".PlayerGestureListener";
private static final boolean DEBUG = BasePlayer.DEBUG;
private static final String TAG = PlayerGestureListener.class.getSimpleName();
private static final boolean DEBUG = MainActivity.DEBUG;
private final int maxVolume;
public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) {
super(playerImpl, service);
maxVolume = playerImpl.getAudioReactor().getMaxVolume();
public PlayerGestureListener(final Player player, final MainPlayer service) {
super(player, service);
maxVolume = player.getAudioReactor().getMaxVolume();
}
@Override
@ -48,46 +49,44 @@ public class PlayerGestureListener
@NotNull final DisplayPortion portion) {
if (DEBUG) {
Log.d(TAG, "onDoubleTap called with playerType = ["
+ playerImpl.getPlayerType() + "], portion = ["
+ portion + "]");
+ player.getPlayerType() + "], portion = [" + portion + "]");
}
if (playerImpl.isSomePopupMenuVisible()) {
playerImpl.hideControls(0, 0);
if (player.isSomePopupMenuVisible()) {
player.hideControls(0, 0);
}
if (portion == DisplayPortion.LEFT) {
playerImpl.onFastRewind();
player.fastRewind();
} else if (portion == DisplayPortion.MIDDLE) {
playerImpl.onPlayPause();
player.playPause();
} else if (portion == DisplayPortion.RIGHT) {
playerImpl.onFastForward();
player.fastForward();
}
}
@Override
public void onSingleTap(@NotNull final MainPlayer.PlayerType playerType) {
if (DEBUG) {
Log.d(TAG, "onSingleTap called with playerType = ["
+ playerImpl.getPlayerType() + "]");
Log.d(TAG, "onSingleTap called with playerType = [" + player.getPlayerType() + "]");
}
if (playerType == MainPlayer.PlayerType.POPUP) {
if (playerImpl.isControlsVisible()) {
playerImpl.hideControls(100, 100);
if (player.isControlsVisible()) {
player.hideControls(100, 100);
} else {
playerImpl.getPlayPauseButton().requestFocus();
playerImpl.showControlsThenHide();
player.getPlayPauseButton().requestFocus();
player.showControlsThenHide();
}
} else /* playerType == MainPlayer.PlayerType.VIDEO */ {
if (playerImpl.isControlsVisible()) {
playerImpl.hideControls(150, 0);
if (player.isControlsVisible()) {
player.hideControls(150, 0);
} else {
if (playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) {
playerImpl.showControls(0);
if (player.getCurrentState() == Player.STATE_COMPLETED) {
player.showControls(0);
} else {
playerImpl.showControlsThenHide();
player.showControlsThenHide();
}
}
}
@ -101,8 +100,7 @@ public class PlayerGestureListener
final float distanceX, final float distanceY) {
if (DEBUG) {
Log.d(TAG, "onScroll called with playerType = ["
+ playerImpl.getPlayerType() + "], portion = ["
+ portion + "]");
+ player.getPlayerType() + "], portion = [" + portion + "]");
}
if (playerType == MainPlayer.PlayerType.VIDEO) {
final boolean isBrightnessGestureEnabled =
@ -123,31 +121,31 @@ public class PlayerGestureListener
}
} else /* MainPlayer.PlayerType.POPUP */ {
final View closingOverlayView = playerImpl.getClosingOverlayView();
if (playerImpl.isInsideClosingRadius(movingEvent)) {
final View closingOverlayView = player.getClosingOverlayView();
if (player.isInsideClosingRadius(movingEvent)) {
if (closingOverlayView.getVisibility() == View.GONE) {
animateView(closingOverlayView, true, 250);
animate(closingOverlayView, true, 200);
}
} else {
if (closingOverlayView.getVisibility() == View.VISIBLE) {
animateView(closingOverlayView, false, 0);
animate(closingOverlayView, false, 200);
}
}
}
}
private void onScrollMainVolume(final float distanceX, final float distanceY) {
playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) playerImpl
.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength();
player.getVolumeProgressBar().incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) player
.getVolumeProgressBar().getProgress() / player.getMaxGestureLength();
final int currentVolume = (int) (maxVolume * currentProgressPercent);
playerImpl.getAudioReactor().setVolume(currentVolume);
player.getAudioReactor().setVolume(currentVolume);
if (DEBUG) {
Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
}
playerImpl.getVolumeImageView().setImageDrawable(
player.getVolumeImageView().setImageDrawable(
AppCompatResources.getDrawable(service, currentProgressPercent <= 0
? R.drawable.ic_volume_off_white_24dp
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_24dp
@ -155,23 +153,23 @@ public class PlayerGestureListener
: R.drawable.ic_volume_up_white_24dp)
);
if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200);
if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
animate(player.getVolumeRelativeLayout(), true, 200, SCALE_AND_ALPHA);
}
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE);
if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
player.getBrightnessRelativeLayout().setVisibility(View.GONE);
}
}
private void onScrollMainBrightness(final float distanceX, final float distanceY) {
final Activity parent = playerImpl.getParentActivity();
final Activity parent = player.getParentActivity();
if (parent == null) {
return;
}
final Window window = parent.getWindow();
final WindowManager.LayoutParams layoutParams = window.getAttributes();
final ProgressBar bar = playerImpl.getBrightnessProgressBar();
final ProgressBar bar = player.getBrightnessProgressBar();
final float oldBrightness = layoutParams.screenBrightness;
bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness))));
bar.incrementProgressBy((int) distanceY);
@ -188,7 +186,7 @@ public class PlayerGestureListener
+ "currentBrightness = " + currentProgressPercent);
}
playerImpl.getBrightnessImageView().setImageDrawable(
player.getBrightnessImageView().setImageDrawable(
AppCompatResources.getDrawable(service,
currentProgressPercent < 0.25
? R.drawable.ic_brightness_low_white_24dp
@ -197,11 +195,11 @@ public class PlayerGestureListener
: R.drawable.ic_brightness_high_white_24dp)
);
if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200);
if (player.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
animate(player.getBrightnessRelativeLayout(), true, 200, SCALE_AND_ALPHA);
}
if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE);
if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
player.getVolumeRelativeLayout().setVisibility(View.GONE);
}
}
@ -210,41 +208,35 @@ public class PlayerGestureListener
@NotNull final MotionEvent event) {
if (DEBUG) {
Log.d(TAG, "onScrollEnd called with playerType = ["
+ playerImpl.getPlayerType() + "]");
+ player.getPlayerType() + "]");
}
if (playerType == MainPlayer.PlayerType.VIDEO) {
if (DEBUG) {
Log.d(TAG, "onScrollEnd() called");
}
if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA,
false, 200, 200);
if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
animate(player.getVolumeRelativeLayout(), false, 200, SCALE_AND_ALPHA,
200);
}
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA,
false, 200, 200);
if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
animate(player.getBrightnessRelativeLayout(), false, 200, SCALE_AND_ALPHA,
200);
}
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
} else {
if (playerImpl == null) {
return;
}
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
if (playerImpl.isInsideClosingRadius(event)) {
playerImpl.closePopup();
} else {
animateView(playerImpl.getClosingOverlayView(), false, 0);
if (!playerImpl.isPopupClosing) {
animateView(playerImpl.getCloseOverlayButton(), false, 200);
}
if (player.isInsideClosingRadius(event)) {
player.closePopup();
} else if (!player.isPopupClosing()) {
animate(player.getCloseOverlayButton(), false, 200);
animate(player.getClosingOverlayView(), false, 200);
}
}
}
@ -254,12 +246,12 @@ public class PlayerGestureListener
if (DEBUG) {
Log.d(TAG, "onPopupResizingStart called");
}
playerImpl.showAndAnimateControl(-1, true);
playerImpl.getLoadingPanel().setVisibility(View.GONE);
player.showAndAnimateControl(-1, true);
player.getLoadingPanel().setVisibility(View.GONE);
playerImpl.hideControls(0, 0);
animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0);
animateView(playerImpl.getResizingIndicator(), true, 200, 0);
player.hideControls(0, 0);
animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0);
animate(player.getResizingIndicator(), true, 200, ALPHA, 0);
}
@Override
@ -267,7 +259,7 @@ public class PlayerGestureListener
if (DEBUG) {
Log.d(TAG, "onPopupResizingEnd called");
}
animateView(playerImpl.getResizingIndicator(), false, 100, 0);
animate(player.getResizingIndicator(), false, 100, ALPHA, 0);
}
}

View File

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

View File

@ -5,14 +5,14 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.Intent;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.audiofx.AudioEffect;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.media.AudioFocusRequestCompat;
import androidx.media.AudioManagerCompat;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.analytics.AnalyticsListener;
@ -21,20 +21,17 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
private static final String TAG = "AudioFocusReactor";
private static final boolean SHOULD_BUILD_FOCUS_REQUEST =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
private static final int DUCK_DURATION = 1500;
private static final float DUCK_AUDIO_TO = .2f;
private static final int FOCUS_GAIN_TYPE = AudioManager.AUDIOFOCUS_GAIN;
private static final int FOCUS_GAIN_TYPE = AudioManagerCompat.AUDIOFOCUS_GAIN;
private static final int STREAM_TYPE = AudioManager.STREAM_MUSIC;
private final SimpleExoPlayer player;
private final Context context;
private final AudioManager audioManager;
private final AudioFocusRequest request;
private final AudioFocusRequestCompat request;
public AudioReactor(@NonNull final Context context,
@NonNull final SimpleExoPlayer player) {
@ -43,15 +40,11 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
this.audioManager = ContextCompat.getSystemService(context, AudioManager.class);
player.addAnalyticsListener(this);
if (SHOULD_BUILD_FOCUS_REQUEST) {
request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE)
.setAcceptsDelayedFocusGain(true)
.setWillPauseWhenDucked(true)
.setOnAudioFocusChangeListener(this)
.build();
} else {
request = null;
}
request = new AudioFocusRequestCompat.Builder(FOCUS_GAIN_TYPE)
//.setAcceptsDelayedFocusGain(true)
.setWillPauseWhenDucked(true)
.setOnAudioFocusChangeListener(this)
.build();
}
public void dispose() {
@ -64,19 +57,11 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
//////////////////////////////////////////////////////////////////////////*/
public void requestAudioFocus() {
if (SHOULD_BUILD_FOCUS_REQUEST) {
audioManager.requestAudioFocus(request);
} else {
audioManager.requestAudioFocus(this, STREAM_TYPE, FOCUS_GAIN_TYPE);
}
AudioManagerCompat.requestAudioFocus(audioManager, request);
}
public void abandonAudioFocus() {
if (SHOULD_BUILD_FOCUS_REQUEST) {
audioManager.abandonAudioFocusRequest(request);
} else {
audioManager.abandonAudioFocus(this);
}
AudioManagerCompat.abandonAudioFocusRequest(audioManager, request);
}
public int getVolume() {
@ -88,7 +73,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
}
public int getMaxVolume() {
return audioManager.getStreamMaxVolume(STREAM_TYPE);
return AudioManagerCompat.getStreamMaxVolume(audioManager, STREAM_TYPE);
}
/*//////////////////////////////////////////////////////////////////////////

View File

@ -3,7 +3,6 @@ package org.schabi.newpipe.player.helper;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import androidx.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
@ -14,11 +13,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.SliderStrategy;
import static org.schabi.newpipe.player.BasePlayer.DEBUG;
import static org.schabi.newpipe.player.Player.DEBUG;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class PlaybackParameterDialog extends DialogFragment {

View File

@ -1,8 +1,15 @@
package org.schabi.newpipe.player.helper;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.PixelFormat;
import android.os.Build;
import android.provider.Settings;
import android.view.Gravity;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.CaptioningManager;
import androidx.annotation.IntDef;
@ -11,11 +18,14 @@ import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
import com.google.android.exoplayer2.util.MimeTypes;
import org.schabi.newpipe.R;
@ -27,6 +37,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
@ -41,13 +53,16 @@ import java.util.Formatter;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
@ -71,6 +86,15 @@ public final class PlayerHelper {
int AUTOPLAY_TYPE_NEVER = 2;
}
@Retention(SOURCE)
@IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND,
MINIMIZE_ON_EXIT_MODE_POPUP})
public @interface MinimizeMode {
int MINIMIZE_ON_EXIT_MODE_NONE = 0;
int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1;
int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
}
private PlayerHelper() { }
////////////////////////////////////////////////////////////////////////////
@ -121,14 +145,16 @@ public final class PlayerHelper {
@NonNull
public static String resizeTypeOf(@NonNull final Context context,
@AspectRatioFrameLayout.ResizeMode final int resizeMode) {
@ResizeMode final int resizeMode) {
switch (resizeMode) {
case RESIZE_MODE_FIT:
case AspectRatioFrameLayout.RESIZE_MODE_FIT:
return context.getResources().getString(R.string.resize_fit);
case RESIZE_MODE_FILL:
case AspectRatioFrameLayout.RESIZE_MODE_FILL:
return context.getResources().getString(R.string.resize_fill);
case RESIZE_MODE_ZOOM:
case AspectRatioFrameLayout.RESIZE_MODE_ZOOM:
return context.getResources().getString(R.string.resize_zoom);
case AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT:
case AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH:
default:
throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode);
}
@ -199,23 +225,23 @@ public final class PlayerHelper {
////////////////////////////////////////////////////////////////////////////
public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) {
return isResumeAfterAudioFocusGain(context, false);
return getPreferences(context)
.getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), false);
}
public static boolean isVolumeGestureEnabled(@NonNull final Context context) {
return isVolumeGestureEnabled(context, true);
return getPreferences(context)
.getBoolean(context.getString(R.string.volume_gesture_control_key), true);
}
public static boolean isBrightnessGestureEnabled(@NonNull final Context context) {
return isBrightnessGestureEnabled(context, true);
}
public static boolean isRememberingPopupDimensions(@NonNull final Context context) {
return isRememberingPopupDimensions(context, true);
return getPreferences(context)
.getBoolean(context.getString(R.string.brightness_gesture_control_key), true);
}
public static boolean isAutoQueueEnabled(@NonNull final Context context) {
return isAutoQueueEnabled(context, false);
return getPreferences(context)
.getBoolean(context.getString(R.string.auto_queue_key), false);
}
public static boolean isClearingQueueConfirmationRequired(@NonNull final Context context) {
@ -225,29 +251,27 @@ public final class PlayerHelper {
@MinimizeMode
public static int getMinimizeOnExitAction(@NonNull final Context context) {
final String defaultAction = context.getString(R.string.minimize_on_exit_none_key);
final String popupAction = context.getString(R.string.minimize_on_exit_popup_key);
final String backgroundAction = context.getString(R.string.minimize_on_exit_background_key);
final String action = getMinimizeOnExitAction(context, defaultAction);
if (action.equals(popupAction)) {
final String action = getPreferences(context)
.getString(context.getString(R.string.minimize_on_exit_key), "");
if (action.equals(context.getString(R.string.minimize_on_exit_popup_key))) {
return MINIMIZE_ON_EXIT_MODE_POPUP;
} else if (action.equals(backgroundAction)) {
return MINIMIZE_ON_EXIT_MODE_BACKGROUND;
} else {
} else if (action.equals(context.getString(R.string.minimize_on_exit_none_key))) {
return MINIMIZE_ON_EXIT_MODE_NONE;
} else {
return MINIMIZE_ON_EXIT_MODE_BACKGROUND; // default
}
}
@AutoplayType
public static int getAutoplayType(@NonNull final Context context) {
final String type = getAutoplayType(context, context.getString(R.string.autoplay_wifi_key));
final String type = getPreferences(context).getString(
context.getString(R.string.autoplay_key), "");
if (type.equals(context.getString(R.string.autoplay_always_key))) {
return AUTOPLAY_TYPE_ALWAYS;
} else if (type.equals(context.getString(R.string.autoplay_never_key))) {
return AUTOPLAY_TYPE_NEVER;
} else {
return AUTOPLAY_TYPE_WIFI;
return AUTOPLAY_TYPE_WIFI; // default
}
}
@ -350,14 +374,32 @@ public final class PlayerHelper {
return captioningManager.getFontScale();
}
/**
* @param context the Android context
* @return the screen brightness to use. A value less than 0 (the default) means to use the
* preferred screen brightness
*/
public static float getScreenBrightness(@NonNull final Context context) {
//a value of less than 0, the default, means to use the preferred screen brightness
return getScreenBrightness(context, -1);
final SharedPreferences sp = getPreferences(context);
final long timestamp =
sp.getLong(context.getString(R.string.screen_brightness_timestamp_key), 0);
// Hypothesis: 4h covers a viewing block, e.g. evening.
// External lightning conditions will change in the next
// viewing block so we fall back to the default brightness
if ((System.currentTimeMillis() - timestamp) > TimeUnit.HOURS.toMillis(4)) {
return -1;
} else {
return sp.getFloat(context.getString(R.string.screen_brightness_key), -1);
}
}
public static void setScreenBrightness(@NonNull final Context context,
final float setScreenBrightness) {
setScreenBrightness(context, setScreenBrightness, System.currentTimeMillis());
final float screenBrightness) {
getPreferences(context).edit()
.putFloat(context.getString(R.string.screen_brightness_key), screenBrightness)
.putLong(context.getString(R.string.screen_brightness_timestamp_key),
System.currentTimeMillis())
.apply();
}
public static boolean globalScreenOrientationLocked(final Context context) {
@ -376,88 +418,180 @@ public final class PlayerHelper {
return PreferenceManager.getDefaultSharedPreferences(context);
}
private static boolean isResumeAfterAudioFocusGain(@NonNull final Context context,
final boolean b) {
return getPreferences(context)
.getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), b);
}
private static boolean isVolumeGestureEnabled(@NonNull final Context context,
final boolean b) {
return getPreferences(context)
.getBoolean(context.getString(R.string.volume_gesture_control_key), b);
}
private static boolean isBrightnessGestureEnabled(@NonNull final Context context,
final boolean b) {
return getPreferences(context)
.getBoolean(context.getString(R.string.brightness_gesture_control_key), b);
}
private static boolean isRememberingPopupDimensions(@NonNull final Context context,
final boolean b) {
return getPreferences(context)
.getBoolean(context.getString(R.string.popup_remember_size_pos_key), b);
}
private static boolean isUsingInexactSeek(@NonNull final Context context) {
return getPreferences(context)
.getBoolean(context.getString(R.string.use_inexact_seek_key), false);
}
private static boolean isAutoQueueEnabled(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.auto_queue_key), b);
}
private static void setScreenBrightness(@NonNull final Context context,
final float screenBrightness, final long timestamp) {
final SharedPreferences.Editor editor = getPreferences(context).edit();
editor.putFloat(context.getString(R.string.screen_brightness_key), screenBrightness);
editor.putLong(context.getString(R.string.screen_brightness_timestamp_key), timestamp);
editor.apply();
}
private static float getScreenBrightness(@NonNull final Context context,
final float screenBrightness) {
final SharedPreferences sp = getPreferences(context);
final long timestamp = sp
.getLong(context.getString(R.string.screen_brightness_timestamp_key), 0);
// Hypothesis: 4h covers a viewing block, e.g. evening.
// External lightning conditions will change in the next
// viewing block so we fall back to the default brightness
if ((System.currentTimeMillis() - timestamp) > TimeUnit.HOURS.toMillis(4)) {
return screenBrightness;
} else {
return sp
.getFloat(context.getString(R.string.screen_brightness_key), screenBrightness);
}
}
private static String getMinimizeOnExitAction(@NonNull final Context context,
final String key) {
return getPreferences(context)
.getString(context.getString(R.string.minimize_on_exit_key), key);
}
private static String getAutoplayType(@NonNull final Context context,
final String key) {
return getPreferences(context).getString(context.getString(R.string.autoplay_key),
key);
}
private static SinglePlayQueue getAutoQueuedSinglePlayQueue(
final StreamInfoItem streamInfoItem) {
final SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem);
singlePlayQueue.getItem().setAutoQueued(true);
Objects.requireNonNull(singlePlayQueue.getItem()).setAutoQueued(true);
return singlePlayQueue;
}
@Retention(SOURCE)
@IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND,
MINIMIZE_ON_EXIT_MODE_POPUP})
public @interface MinimizeMode {
int MINIMIZE_ON_EXIT_MODE_NONE = 0;
int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1;
int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
////////////////////////////////////////////////////////////////////////////
// Utils used by player
////////////////////////////////////////////////////////////////////////////
public static MainPlayer.PlayerType retrievePlayerTypeFromIntent(final Intent intent) {
// If you want to open popup from the app just include Constants.POPUP_ONLY into an extra
return MainPlayer.PlayerType.values()[
intent.getIntExtra(PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal())];
}
public static boolean isPlaybackResumeEnabled(final Player player) {
return player.getPrefs().getBoolean(
player.getContext().getString(R.string.enable_watch_history_key), true)
&& player.getPrefs().getBoolean(
player.getContext().getString(R.string.enable_playback_resume_key), true);
}
@RepeatMode
public static int nextRepeatMode(@RepeatMode final int repeatMode) {
switch (repeatMode) {
case REPEAT_MODE_OFF:
return REPEAT_MODE_ONE;
case REPEAT_MODE_ONE:
return REPEAT_MODE_ALL;
case REPEAT_MODE_ALL: default:
return REPEAT_MODE_OFF;
}
}
@ResizeMode
public static int retrieveResizeModeFromPrefs(final Player player) {
return player.getPrefs().getInt(player.getContext().getString(R.string.last_resize_mode),
AspectRatioFrameLayout.RESIZE_MODE_FIT);
}
@SuppressLint("SwitchIntDef") // only fit, fill and zoom are supported by NewPipe
@ResizeMode
public static int nextResizeModeAndSaveToPrefs(final Player player,
@ResizeMode final int resizeMode) {
final int newResizeMode;
switch (resizeMode) {
case AspectRatioFrameLayout.RESIZE_MODE_FIT:
newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL;
break;
case AspectRatioFrameLayout.RESIZE_MODE_FILL:
newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
break;
case AspectRatioFrameLayout.RESIZE_MODE_ZOOM:
default:
newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
break;
}
player.getPrefs().edit().putInt(
player.getContext().getString(R.string.last_resize_mode), resizeMode).apply();
return newResizeMode;
}
public static PlaybackParameters retrievePlaybackParametersFromPrefs(final Player player) {
final float speed = player.getPrefs().getFloat(player.getContext().getString(
R.string.playback_speed_key), player.getPlaybackSpeed());
final float pitch = player.getPrefs().getFloat(player.getContext().getString(
R.string.playback_pitch_key), player.getPlaybackPitch());
final boolean skipSilence = player.getPrefs().getBoolean(player.getContext().getString(
R.string.playback_skip_silence_key), player.getPlaybackSkipSilence());
return new PlaybackParameters(speed, pitch, skipSilence);
}
public static void savePlaybackParametersToPrefs(final Player player,
final float speed,
final float pitch,
final boolean skipSilence) {
player.getPrefs().edit()
.putFloat(player.getContext().getString(R.string.playback_speed_key), speed)
.putFloat(player.getContext().getString(R.string.playback_pitch_key), pitch)
.putBoolean(player.getContext().getString(R.string.playback_skip_silence_key),
skipSilence)
.apply();
}
/**
* @param player {@code screenWidth} and {@code screenHeight} must have been initialized
* @return the popup starting layout params
*/
@SuppressLint("RtlHardcoded")
public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs(
final Player player) {
final boolean popupRememberSizeAndPos = player.getPrefs().getBoolean(
player.getContext().getString(R.string.popup_remember_size_pos_key), true);
final float defaultSize =
player.getContext().getResources().getDimension(R.dimen.popup_default_width);
final float popupWidth = popupRememberSizeAndPos
? player.getPrefs().getFloat(player.getContext().getString(
R.string.popup_saved_width_key), defaultSize)
: defaultSize;
final float popupHeight = getMinimumVideoHeight(popupWidth);
final WindowManager.LayoutParams popupLayoutParams = new WindowManager.LayoutParams(
(int) popupWidth, (int) popupHeight,
popupLayoutParamType(),
IDLE_WINDOW_FLAGS,
PixelFormat.TRANSLUCENT);
popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
final int centerX = (int) (player.getScreenWidth() / 2f - popupWidth / 2f);
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
popupLayoutParams.x = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString(
R.string.popup_saved_x_key), centerX) : centerX;
popupLayoutParams.y = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString(
R.string.popup_saved_y_key), centerY) : centerY;
return popupLayoutParams;
}
public static void savePopupPositionAndSizeToPrefs(final Player player) {
if (player.getPopupLayoutParams() != null) {
player.getPrefs().edit()
.putFloat(player.getContext().getString(R.string.popup_saved_width_key),
player.getPopupLayoutParams().width)
.putInt(player.getContext().getString(R.string.popup_saved_x_key),
player.getPopupLayoutParams().x)
.putInt(player.getContext().getString(R.string.popup_saved_y_key),
player.getPopupLayoutParams().y)
.apply();
}
}
public static float getMinimumVideoHeight(final float width) {
return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
}
@SuppressLint("RtlHardcoded")
public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() {
final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
popupLayoutParamType(),
flags,
PixelFormat.TRANSLUCENT);
closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
closeOverlayLayoutParams.softInputMode =
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
return closeOverlayLayoutParams;
}
public static int popupLayoutParamType() {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.O
? WindowManager.LayoutParams.TYPE_PHONE
: WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
}
public static int retrieveSeekDurationFromPreferences(final Player player) {
return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString(
player.getContext().getString(R.string.seek_duration_key),
player.getContext().getString(R.string.seek_duration_default_value))));
}
}

View File

@ -16,7 +16,7 @@ import org.schabi.newpipe.App;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.VideoPlayerImpl;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
import org.schabi.newpipe.player.playqueue.PlayQueue;
@ -33,7 +33,7 @@ public final class PlayerHolder {
private static ServiceConnection serviceConnection;
public static boolean bound;
private static MainPlayer playerService;
private static VideoPlayerImpl player;
private static Player player;
/**
* Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service,

View File

@ -3,11 +3,11 @@ package org.schabi.newpipe.player.mediasession;
import android.support.v4.media.MediaDescriptionCompat;
public interface MediaSessionCallback {
void onSkipToPrevious();
void playPrevious();
void onSkipToNext();
void playNext();
void onSkipToIndex(int index);
void playItemAtIndex(int index);
int getCurrentPlayingIndex();
@ -15,7 +15,7 @@ public interface MediaSessionCallback {
MediaDescriptionCompat getQueueMetadata(int index);
void onPlay();
void play();
void onPause();
void pause();
}

View File

@ -65,18 +65,18 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator
@Override
public void onSkipToPrevious(final Player player, final ControlDispatcher controlDispatcher) {
callback.onSkipToPrevious();
callback.playPrevious();
}
@Override
public void onSkipToQueueItem(final Player player, final ControlDispatcher controlDispatcher,
final long id) {
callback.onSkipToIndex((int) id);
callback.playItemAtIndex((int) id);
}
@Override
public void onSkipToNext(final Player player, final ControlDispatcher controlDispatcher) {
callback.onSkipToNext();
callback.playNext();
}
private void publishFloatingQueueWindow() {

View File

@ -14,9 +14,9 @@ public class PlayQueuePlaybackController extends DefaultControlDispatcher {
@Override
public boolean dispatchSetPlayWhenReady(final Player player, final boolean playWhenReady) {
if (playWhenReady) {
callback.onPlay();
callback.play();
} else {
callback.onPause();
callback.pause();
}
return true;
}

View File

@ -5,33 +5,33 @@ import android.os.Bundle;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
public class BasePlayerMediaSession implements MediaSessionCallback {
private final BasePlayer player;
public class PlayerMediaSession implements MediaSessionCallback {
private final Player player;
public BasePlayerMediaSession(final BasePlayer player) {
public PlayerMediaSession(final Player player) {
this.player = player;
}
@Override
public void onSkipToPrevious() {
player.onPlayPrevious();
public void playPrevious() {
player.playPrevious();
}
@Override
public void onSkipToNext() {
player.onPlayNext();
public void playNext() {
player.playNext();
}
@Override
public void onSkipToIndex(final int index) {
public void playItemAtIndex(final int index) {
if (player.getPlayQueue() == null) {
return;
}
player.onSelected(player.getPlayQueue().getItem(index));
player.selectQueueItem(player.getPlayQueue().getItem(index));
}
@Override
@ -52,11 +52,14 @@ public class BasePlayerMediaSession implements MediaSessionCallback {
@Override
public MediaDescriptionCompat getQueueMetadata(final int index) {
if (player.getPlayQueue() == null || player.getPlayQueue().getItem(index) == null) {
if (player.getPlayQueue() == null) {
return null;
}
final PlayQueueItem item = player.getPlayQueue().getItem(index);
if (item == null) {
return null;
}
final PlayQueueItem item = player.getPlayQueue().getItem(index);
final MediaDescriptionCompat.Builder descriptionBuilder
= new MediaDescriptionCompat.Builder()
.setMediaId(String.valueOf(index))
@ -83,12 +86,12 @@ public class BasePlayerMediaSession implements MediaSessionCallback {
}
@Override
public void onPlay() {
player.onPlay();
public void play() {
player.play();
}
@Override
public void onPause() {
player.onPause();
public void pause() {
player.pause();
}
}

View File

@ -21,8 +21,8 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import io.reactivex.rxjava3.core.BackpressureStrategy;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.BackpressureStrategy;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.subjects.BehaviorSubject;

View File

@ -14,7 +14,6 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
@ -207,8 +206,7 @@ public class ErrorActivity extends AppCompatActivity {
openPrivacyPolicyDialog(this, "EMAIL"));
activityErrorBinding.errorReportCopyButton.setOnClickListener(v -> {
ShareUtils.copyToClipboard(this, buildMarkdown());
Toast.makeText(this, R.string.msg_copied, Toast.LENGTH_SHORT).show();
ShareUtils.copyToClipboard(this, buildMarkdown());
});
activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
@ -246,11 +244,7 @@ public class ErrorActivity extends AppCompatActivity {
goToReturnActivity();
break;
case R.id.menu_item_share_error:
final Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, buildJson());
intent.setType("text/plain");
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson());
break;
}
return false;
@ -273,10 +267,10 @@ public class ErrorActivity extends AppCompatActivity {
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT)
.putExtra(Intent.EXTRA_TEXT, buildJson());
if (i.resolveActivity(getPackageManager()) != null) {
startActivity(i);
ShareUtils.openIntentInApp(context, i);
}
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL);
ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL, false);
}
})

View File

@ -2,12 +2,12 @@ package org.schabi.newpipe.settings;
import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.preference.PreferenceManager;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.util.ThemeHelper;

View File

@ -32,14 +32,9 @@ import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.ZipHelper;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.zip.ZipFile;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@ -49,13 +44,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
private ContentSettingsManager manager;
private File databasesDir;
private File newpipeDb;
private File newpipeDbJournal;
private File newpipeDbShm;
private File newpipeDbWal;
private File newpipeSettings;
private String thumbnailLoadToggleKey;
private String youtubeRestrictedModeEnabledKey;
@ -120,16 +108,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
final File homeDir = ContextCompat.getDataDir(requireContext());
databasesDir = new File(homeDir, "/databases");
newpipeDb = new File(homeDir, "/databases/newpipe.db");
newpipeDbJournal = new File(homeDir, "/databases/newpipe.db-journal");
newpipeDbShm = new File(homeDir, "/databases/newpipe.db-shm");
newpipeDbWal = new File(homeDir, "/databases/newpipe.db-wal");
newpipeSettings = new File(homeDir, "/databases/newpipe.settings");
newpipeSettings.delete();
manager = new ContentSettingsManager(homeDir);
manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir));
manager.deleteSettingsFile();
addPreferencesFromResource(R.xml.content_settings);
@ -224,33 +204,24 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
private void importDatabase(final String filePath) {
// check if file is supported
try (ZipFile zipFile = new ZipFile(filePath)) {
} catch (final IOException ioe) {
if (!ZipHelper.isValidZipFile(filePath)) {
Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
.show();
.show();
return;
}
try {
if (!databasesDir.exists() && !databasesDir.mkdir()) {
if (!manager.ensureDbDirectoryExists()) {
throw new Exception("Could not create databases dir");
}
final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(filePath,
newpipeDb.getPath(), "newpipe.db");
if (isDbFileExtracted) {
newpipeDbJournal.delete();
newpipeDbWal.delete();
newpipeDbShm.delete();
} else {
if (!manager.extractDb(filePath)) {
Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG)
.show();
.show();
}
//If settings file exist, ask if it should be imported.
if (ZipHelper.extractFileFromZip(filePath, newpipeSettings.getPath(),
"newpipe.settings")) {
if (manager.extractSettings(filePath)) {
final AlertDialog.Builder alert = new AlertDialog.Builder(getContext());
alert.setTitle(R.string.import_settings);
@ -261,7 +232,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
});
alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> {
dialog.dismiss();
loadSharedPreferences(newpipeSettings);
manager.loadSharedPreferences(PreferenceManager
.getDefaultSharedPreferences(requireContext()));
// restart app to properly load db
System.exit(0);
});
@ -275,34 +247,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
}
}
private void loadSharedPreferences(final File src) {
try (ObjectInputStream input = new ObjectInputStream(new FileInputStream(src))) {
final SharedPreferences.Editor prefEdit = PreferenceManager
.getDefaultSharedPreferences(requireContext()).edit();
prefEdit.clear();
final Map<String, ?> entries = (Map<String, ?>) input.readObject();
for (final Map.Entry<String, ?> entry : entries.entrySet()) {
final Object v = entry.getValue();
final String key = entry.getKey();
if (v instanceof Boolean) {
prefEdit.putBoolean(key, (Boolean) v);
} else if (v instanceof Float) {
prefEdit.putFloat(key, (Float) v);
} else if (v instanceof Integer) {
prefEdit.putInt(key, (Integer) v);
} else if (v instanceof Long) {
prefEdit.putLong(key, (Long) v);
} else if (v instanceof String) {
prefEdit.putString(key, (String) v);
}
}
prefEdit.commit();
} catch (final IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
/*//////////////////////////////////////////////////////////////////////////
// Error
//////////////////////////////////////////////////////////////////////////*/

View File

@ -3,22 +3,14 @@ package org.schabi.newpipe.settings
import android.content.SharedPreferences
import org.schabi.newpipe.util.ZipHelper
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.lang.Exception
import java.util.zip.ZipOutputStream
class ContentSettingsManager(
private val newpipeDb: File,
private val newpipeSettings: File
) {
constructor(homeDir: File) : this(
File(homeDir, "databases/newpipe.db"),
File(homeDir, "databases/newpipe.settings")
)
class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
/**
* Exports given [SharedPreferences] to the file in given outputPath.
@ -28,10 +20,10 @@ class ContentSettingsManager(
fun exportDatabase(preferences: SharedPreferences, outputPath: String) {
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputPath)))
.use { outZip ->
ZipHelper.addFileToZip(outZip, newpipeDb.path, "newpipe.db")
ZipHelper.addFileToZip(outZip, fileLocator.db.path, "newpipe.db")
try {
ObjectOutputStream(FileOutputStream(newpipeSettings)).use { output ->
ObjectOutputStream(FileOutputStream(fileLocator.settings)).use { output ->
output.writeObject(preferences.all)
output.flush()
}
@ -39,7 +31,72 @@ class ContentSettingsManager(
e.printStackTrace()
}
ZipHelper.addFileToZip(outZip, newpipeSettings.path, "newpipe.settings")
ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings")
}
}
fun deleteSettingsFile() {
fileLocator.settings.delete()
}
/**
* Tries to create database directory if it does not exist.
*
* @return Whether the directory exists afterwards.
*/
fun ensureDbDirectoryExists(): Boolean {
return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir()
}
fun extractDb(filePath: String): Boolean {
val success = ZipHelper.extractFileFromZip(filePath, fileLocator.db.path, "newpipe.db")
if (success) {
fileLocator.dbJournal.delete()
fileLocator.dbWal.delete()
fileLocator.dbShm.delete()
}
return success
}
fun extractSettings(filePath: String): Boolean {
return ZipHelper
.extractFileFromZip(filePath, fileLocator.settings.path, "newpipe.settings")
}
fun loadSharedPreferences(preferences: SharedPreferences) {
try {
val preferenceEditor = preferences.edit()
ObjectInputStream(FileInputStream(fileLocator.settings)).use { input ->
preferenceEditor.clear()
@Suppress("UNCHECKED_CAST")
val entries = input.readObject() as Map<String, *>
for ((key, value) in entries) {
when (value) {
is Boolean -> {
preferenceEditor.putBoolean(key, value)
}
is Float -> {
preferenceEditor.putFloat(key, value)
}
is Int -> {
preferenceEditor.putInt(key, value)
}
is Long -> {
preferenceEditor.putLong(key, value)
}
is String -> {
preferenceEditor.putString(key, value)
}
}
}
preferenceEditor.commit()
}
} catch (e: IOException) {
e.printStackTrace()
} catch (e: ClassNotFoundException) {
e.printStackTrace()
}
}
}

View File

@ -246,10 +246,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
// revoke permissions on the old save path (required for SAF only)
final Context context = getContext();
if (context == null) {
throw new NullPointerException("getContext()");
}
final Context context = requireContext();
forgetSAFTree(context, defaultPreferences.getString(key, ""));

View File

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

View File

@ -55,12 +55,14 @@ public final class NewPipeSettings {
isFirstRun = true;
}
PreferenceManager.setDefaultValues(context, R.xml.appearance_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.content_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.download_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.history_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.video_audio_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.download_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.appearance_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.history_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.content_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.notification_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.update_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true);
getVideoDownloadFolder(context);

View File

@ -30,8 +30,8 @@ import java.util.List;
import java.util.Vector;
import de.hdodenhof.circleimageview.CircleImageView;
import io.reactivex.rxjava3.core.Observer;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observer;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;

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