mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-09-29 23:40:49 +00:00
Compare commits
No commits in common. "dev" and "v0.26.1" have entirely different histories.
10
.github/CONTRIBUTING.md
vendored
10
.github/CONTRIBUTING.md
vendored
@ -42,6 +42,10 @@ You'll see *exactly* what is sent, be able to add **your comments**, and then se
|
|||||||
* Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions.
|
* Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions.
|
||||||
* NewPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out.
|
* NewPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out.
|
||||||
|
|
||||||
|
### Kotlin in NewPipe
|
||||||
|
* NewPipe will remain mostly Java for time being
|
||||||
|
* Contributions containing a simple conversion from Java to Kotlin should be avoided. Conversions to Kotlin should only be done if Kotlin actually brings improvements like bug fixes or better performance which are not, or only with much more effort, implementable in Java. The core team sees Java as an easier to learn and generally well adopted programming language.
|
||||||
|
|
||||||
### Creating a Pull Request (PR)
|
### Creating a Pull Request (PR)
|
||||||
|
|
||||||
* Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
|
* Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
|
||||||
@ -79,6 +83,6 @@ The [ktlint](https://github.com/pinterest/ktlint) plugin does the same job as ch
|
|||||||
|
|
||||||
## Communication
|
## Communication
|
||||||
|
|
||||||
* You can use a Matrix account to join the NewPipe channel at [#newpipe:matrix.newpipe-ev.de](https://matrix.to/#/#newpipe:matrix.newpipe-ev.de). Some convenient clients, available both for phone and desktop, are listed at that link.
|
* The #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) has the core team and other developers in it. [Click here for webchat](https://web.libera.chat/#newpipe)!
|
||||||
* Alternatively, the #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) can also be joined, as it is bridged to the Matrix room. [Click here for webchat](https://web.libera.chat/#newpipe)!
|
* You can also use a Matrix account to join the NewPipe channel at [#newpipe:libera.chat](https://matrix.to/#/#newpipe:libera.chat). Some convenient clients, available both for phone and desktop, are listed at that link.
|
||||||
* You can post your suggestions, changes, ideas etc. on either GitHub or Matrix (including via IRC).
|
* You can post your suggestions, changes, ideas etc. on either GitHub or IRC.
|
||||||
|
3
.github/DISCUSSION_TEMPLATE/questions.yml
vendored
3
.github/DISCUSSION_TEMPLATE/questions.yml
vendored
@ -1,3 +1,6 @@
|
|||||||
|
name: Question
|
||||||
|
description: Ask about anything NewPipe-related
|
||||||
|
labels: [question]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -3,9 +3,9 @@ contact_links:
|
|||||||
- name: ❓ Question
|
- name: ❓ Question
|
||||||
url: https://github.com/TeamNewPipe/NewPipe/discussions/new?category=questions
|
url: https://github.com/TeamNewPipe/NewPipe/discussions/new?category=questions
|
||||||
about: Ask about anything NewPipe-related
|
about: Ask about anything NewPipe-related
|
||||||
- name: 💬 Matrix
|
|
||||||
url: https://matrix.to/#/#newpipe:matrix.newpipe-ev.de
|
|
||||||
about: Chat with us via Matrix for quick Q/A
|
|
||||||
- name: 💬 IRC
|
- name: 💬 IRC
|
||||||
url: https://web.libera.chat/#newpipe
|
url: https://web.libera.chat/#newpipe
|
||||||
about: Chat with us via IRC for quick Q/A
|
about: Chat with us via IRC for quick Q/A
|
||||||
|
- name: 💬 Matrix
|
||||||
|
url: https://matrix.to/#/#newpipe:libera.chat
|
||||||
|
about: Chat with us via Matrix for quick Q/A
|
||||||
|
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@ -6,7 +6,6 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
- master
|
- master
|
||||||
- refactor
|
|
||||||
- release**
|
- release**
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'README.md'
|
- 'README.md'
|
||||||
@ -37,8 +36,8 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: gradle/wrapper-validation-action@v2
|
- uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
- name: create and checkout branch
|
- name: create and checkout branch
|
||||||
# push events already checked out the branch
|
# push events already checked out the branch
|
||||||
@ -48,7 +47,7 @@ jobs:
|
|||||||
run: git checkout -B "$BRANCH"
|
run: git checkout -B "$BRANCH"
|
||||||
|
|
||||||
- name: set up JDK 17
|
- name: set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
@ -58,13 +57,14 @@ jobs:
|
|||||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: app
|
name: app
|
||||||
path: app/build/outputs/apk/debug/*.apk
|
path: app/build/outputs/apk/debug/*.apk
|
||||||
|
|
||||||
test-android:
|
test-android:
|
||||||
runs-on: ubuntu-latest
|
# macos has hardware acceleration. See android-emulator-runner action
|
||||||
|
runs-on: macos-latest
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@ -80,16 +80,10 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Enable KVM
|
|
||||||
run: |
|
|
||||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
|
||||||
sudo udevadm control --reload-rules
|
|
||||||
sudo udevadm trigger --name-match=kvm
|
|
||||||
|
|
||||||
- name: set up JDK 17
|
- name: set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
@ -104,7 +98,7 @@ jobs:
|
|||||||
script: ./gradlew connectedCheck --stacktrace
|
script: ./gradlew connectedCheck --stacktrace
|
||||||
|
|
||||||
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
|
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: android-test-report-api${{ matrix.api-level }}
|
name: android-test-report-api${{ matrix.api-level }}
|
||||||
@ -117,19 +111,19 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
- name: Cache SonarCloud packages
|
- name: Cache SonarCloud packages
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/.sonar/cache
|
path: ~/.sonar/cache
|
||||||
key: ${{ runner.os }}-sonar
|
key: ${{ runner.os }}-sonar
|
||||||
|
6
.github/workflows/image-minimizer.yml
vendored
6
.github/workflows/image-minimizer.yml
vendored
@ -17,9 +17,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ jobs:
|
|||||||
run: npm i probe-image-size@7.2.3 --ignore-scripts
|
run: npm i probe-image-size@7.2.3 --ignore-scripts
|
||||||
|
|
||||||
- name: Minimize simple images
|
- name: Minimize simple images
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v6
|
||||||
timeout-minutes: 3
|
timeout-minutes: 3
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
|
2
.github/workflows/pr-labeler.yml
vendored
2
.github/workflows/pr-labeler.yml
vendored
@ -1,5 +1,5 @@
|
|||||||
name: "PR size labeler"
|
name: "PR size labeler"
|
||||||
on: [pull_request_target]
|
on: [pull_request]
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
|
||||||
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
|
||||||
<style type="text/css">
|
|
||||||
.st0{fill:#CD201F;}
|
|
||||||
.st1{fill:#FFFFFF;}
|
|
||||||
</style>
|
|
||||||
<g id="Alapkör">
|
|
||||||
<circle id="XMLID_23_" class="st0" cx="50" cy="50" r="50"/>
|
|
||||||
</g>
|
|
||||||
<g id="Elemek">
|
|
||||||
<path id="XMLID_19_" class="st1" d="M47,28.2c-9-5.3-15.3-9-15.3-9v61.7c0,0,30.4-18,52.3-30.9C72.1,43,57.7,34.5,47,28.2z"/>
|
|
||||||
</g>
|
|
||||||
<g id="Fedő">
|
|
||||||
<path id="XMLID_5_" class="st0" d="M48.4,40.1c-4.1-2.4-7-4.1-7-4.1V64c0,0,13.9-8.2,23.8-14C59.8,46.8,53.3,42.9,48.4,40.1z"/>
|
|
||||||
<rect id="XMLID_4_" x="41.4" y="55.6" class="st0" width="6.2" height="21"/>
|
|
||||||
</g>
|
|
||||||
<g id="Vonalak">
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 850 B |
13
README.md
13
README.md
@ -13,7 +13,7 @@
|
|||||||
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
|
||||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||||
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||||
<a href="https://matrix.to/#/#newpipe:matrix.newpipe-ev.de" alt="Matrix channel: #newpipe"><img src="https://img.shields.io/badge/Matrix%20chat-%23newpipe-blue"></a>
|
<a href="https://matrix.to/#/#newpipe:libera.chat" alt="Matrix channel: #newpipe"><img src="https://img.shields.io/badge/Matrix%20chat-%23newpipe-blue"></a>
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#supported-services">Supported Services</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#installation-and-updates">Installation and updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#supported-services">Supported Services</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#installation-and-updates">Installation and updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||||
@ -22,10 +22,9 @@
|
|||||||
|
|
||||||
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md)*
|
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md)*
|
||||||
|
|
||||||
> [!warning]
|
<b>WARNING: THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
|
||||||
> <b>THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
|
|
||||||
>
|
<b>PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
|
||||||
> <b>PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@ -104,10 +103,10 @@ You can install NewPipe using one of the following methods:
|
|||||||
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other (meaning that if you installed NewPipe using either method 1 or 2, you can also update NewPipe using the other), but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. When using method 5, each APK is signed with a different random key supplied by GitHub Actions, so you cannot even update it. You will have to backup and restore the app data each time you wish to use a new APK.
|
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other (meaning that if you installed NewPipe using either method 1 or 2, you can also update NewPipe using the other), but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. When using method 5, each APK is signed with a different random key supplied by GitHub Actions, so you cannot even update it. You will have to backup and restore the app data each time you wish to use a new APK.
|
||||||
|
|
||||||
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest update yet), we recommend following this procedure:
|
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest update yet), we recommend following this procedure:
|
||||||
1. Back up your data via Settings > Backup and Restore > Export Database so you keep your history, subscriptions, and playlists
|
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlists
|
||||||
2. Uninstall NewPipe
|
2. Uninstall NewPipe
|
||||||
3. Download the APK from the new source and install it
|
3. Download the APK from the new source and install it
|
||||||
4. Import the data from step 1 via Settings > Backup and Restore > Import Database
|
4. Import the data from step 1 via Settings > Content > Import Database
|
||||||
|
|
||||||
<b>Note: when you're importing a database into the official app, always make sure that it is the one you exported _from_ the official app. If you import a database exported from an APK other than the official app, it may break things. Such an action is unsupported, and you should only do so when you're absolutely certain you know what you're doing.</b>
|
<b>Note: when you're importing a database into the official app, always make sure that it is the one you exported _from_ the official app. If you import a database exported from an APK other than the official app, it may break things. Such an action is unsupported, and you should only do so when you're absolutely certain you know what you're doing.</b>
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 34
|
compileSdk 33
|
||||||
namespace 'org.schabi.newpipe'
|
namespace 'org.schabi.newpipe'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@ -20,8 +20,8 @@ android {
|
|||||||
resValue "string", "app_name", "NewPipe"
|
resValue "string", "app_name", "NewPipe"
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 999
|
versionCode 996
|
||||||
versionName "0.27.2"
|
versionName "0.26.1"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@ -98,9 +98,7 @@ android {
|
|||||||
resources {
|
resources {
|
||||||
// remove two files which belong to jsoup
|
// remove two files which belong to jsoup
|
||||||
// no idea how they ended up in the META-INF dir...
|
// no idea how they ended up in the META-INF dir...
|
||||||
excludes += ['META-INF/README.md', 'META-INF/CHANGES',
|
excludes += ['META-INF/README.md', 'META-INF/CHANGES']
|
||||||
// 'COPYRIGHT' belongs to RxJava...
|
|
||||||
'META-INF/COPYRIGHT']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,9 +106,9 @@ android {
|
|||||||
ext {
|
ext {
|
||||||
checkstyleVersion = '10.12.1'
|
checkstyleVersion = '10.12.1'
|
||||||
|
|
||||||
androidxLifecycleVersion = '2.6.2'
|
androidxLifecycleVersion = '2.5.1'
|
||||||
androidxRoomVersion = '2.6.1'
|
androidxRoomVersion = '2.5.2'
|
||||||
androidxWorkVersion = '2.8.1'
|
androidxWorkVersion = '2.7.1'
|
||||||
|
|
||||||
icepickVersion = '3.2.0'
|
icepickVersion = '3.2.0'
|
||||||
exoPlayerVersion = '2.18.7'
|
exoPlayerVersion = '2.18.7'
|
||||||
@ -120,6 +118,7 @@ ext {
|
|||||||
|
|
||||||
leakCanaryVersion = '2.12'
|
leakCanaryVersion = '2.12'
|
||||||
stethoVersion = '1.6.0'
|
stethoVersion = '1.6.0'
|
||||||
|
mockitoVersion = '4.0.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
@ -134,7 +133,7 @@ checkstyle {
|
|||||||
toolVersion = checkstyleVersion
|
toolVersion = checkstyleVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('runCheckstyle', Checkstyle) {
|
task runCheckstyle(type: Checkstyle) {
|
||||||
source 'src'
|
source 'src'
|
||||||
include '**/*.java'
|
include '**/*.java'
|
||||||
exclude '**/gen/**'
|
exclude '**/gen/**'
|
||||||
@ -155,7 +154,7 @@ tasks.register('runCheckstyle', Checkstyle) {
|
|||||||
def outputDir = "${project.buildDir}/reports/ktlint/"
|
def outputDir = "${project.buildDir}/reports/ktlint/"
|
||||||
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
||||||
|
|
||||||
tasks.register('runKtlint', JavaExec) {
|
task runKtlint(type: JavaExec) {
|
||||||
inputs.files(inputFiles)
|
inputs.files(inputFiles)
|
||||||
outputs.dir(outputDir)
|
outputs.dir(outputDir)
|
||||||
getMainClass().set("com.pinterest.ktlint.Main")
|
getMainClass().set("com.pinterest.ktlint.Main")
|
||||||
@ -164,7 +163,7 @@ tasks.register('runKtlint', JavaExec) {
|
|||||||
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('formatKtlint', JavaExec) {
|
task formatKtlint(type: JavaExec) {
|
||||||
inputs.files(inputFiles)
|
inputs.files(inputFiles)
|
||||||
outputs.dir(outputDir)
|
outputs.dir(outputDir)
|
||||||
getMainClass().set("com.pinterest.ktlint.Main")
|
getMainClass().set("com.pinterest.ktlint.Main")
|
||||||
@ -190,7 +189,7 @@ sonar {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
/** Desugaring **/
|
/** Desugaring **/
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.3'
|
||||||
|
|
||||||
/** NewPipe libraries **/
|
/** NewPipe libraries **/
|
||||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||||
@ -198,7 +197,7 @@ dependencies {
|
|||||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||||
// This works thanks to JitPack: https://jitpack.io/
|
// This works thanks to JitPack: https://jitpack.io/
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.2'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.23.1'
|
||||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
@ -209,28 +208,28 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
|
||||||
|
|
||||||
/** AndroidX **/
|
/** AndroidX **/
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.10.0'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
||||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||||
implementation 'androidx.media:media:1.7.0'
|
implementation 'androidx.media:media:1.6.0'
|
||||||
implementation 'androidx.preference:preference:1.2.1'
|
implementation 'androidx.preference:preference:1.2.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
||||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
||||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||||
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
||||||
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.9.0'
|
||||||
|
|
||||||
/** Third-party libraries **/
|
/** Third-party libraries **/
|
||||||
// Instance state boilerplate elimination
|
// Instance state boilerplate elimination
|
||||||
@ -238,10 +237,13 @@ dependencies {
|
|||||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation "org.jsoup:jsoup:1.17.2"
|
implementation "org.jsoup:jsoup:1.16.1"
|
||||||
|
|
||||||
// HTTP client
|
// HTTP client
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
implementation "com.squareup.okhttp3:okhttp:4.11.0"
|
||||||
|
// okhttp3:4.11.0 introduces a vulnerability from com.squareup.okio:okio@3.3.0,
|
||||||
|
// remove com.squareup.okio:okio when updating okhttp
|
||||||
|
implementation "com.squareup.okio:okio:3.4.0"
|
||||||
|
|
||||||
// Media player
|
// Media player
|
||||||
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
||||||
@ -270,19 +272,19 @@ dependencies {
|
|||||||
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
||||||
|
|
||||||
// Crash reporting
|
// Crash reporting
|
||||||
implementation "ch.acra:acra-core:5.11.3"
|
implementation "ch.acra:acra-core:5.10.1"
|
||||||
|
|
||||||
// Properly restarting
|
// Properly restarting
|
||||||
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
||||||
|
|
||||||
// Reactive extensions for Java VM
|
// Reactive extensions for Java VM
|
||||||
implementation "io.reactivex.rxjava3:rxjava:3.1.8"
|
implementation "io.reactivex.rxjava3:rxjava:3.1.6"
|
||||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
||||||
// RxJava binding APIs for Android UI widgets
|
// RxJava binding APIs for Android UI widgets
|
||||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||||
|
|
||||||
// Date and time formatting
|
// Date and time formatting
|
||||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final"
|
implementation "org.ocpsoft.prettytime:prettytime:5.0.6.Final"
|
||||||
|
|
||||||
/** Debugging **/
|
/** Debugging **/
|
||||||
// Memory leak detection
|
// Memory leak detection
|
||||||
@ -295,12 +297,13 @@ dependencies {
|
|||||||
|
|
||||||
/** Testing **/
|
/** Testing **/
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.mockito:mockito-core:5.6.0'
|
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
|
||||||
|
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
|
||||||
|
|
||||||
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
||||||
androidTestImplementation "androidx.test:runner:1.5.2"
|
androidTestImplementation "androidx.test:runner:1.5.2"
|
||||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||||
androidTestImplementation "org.assertj:assertj-core:3.24.2"
|
androidTestImplementation "org.assertj:assertj-core:3.23.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getGitWorkingBranch() {
|
static String getGitWorkingBranch() {
|
||||||
|
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
@ -7,7 +7,6 @@
|
|||||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||||
-keep class org.mozilla.javascript.** { *; }
|
-keep class org.mozilla.javascript.** { *; }
|
||||||
-keep class org.mozilla.classfile.ClassFileWriter
|
-keep class org.mozilla.classfile.ClassFileWriter
|
||||||
-dontwarn org.mozilla.javascript.JavaToJSONConverters
|
|
||||||
-dontwarn org.mozilla.javascript.tools.**
|
-dontwarn org.mozilla.javascript.tools.**
|
||||||
|
|
||||||
## Rules for ExoPlayer
|
## Rules for ExoPlayer
|
||||||
|
@ -1,737 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 8,
|
|
||||||
"identityHash": "012fc8e7ad3333f1597347f34e76a513",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "subscriptions",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "uid",
|
|
||||||
"columnName": "uid",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "serviceId",
|
|
||||||
"columnName": "service_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "url",
|
|
||||||
"columnName": "url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "avatarUrl",
|
|
||||||
"columnName": "avatar_url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subscriberCount",
|
|
||||||
"columnName": "subscriber_count",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "description",
|
|
||||||
"columnName": "description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "notificationMode",
|
|
||||||
"columnName": "notification_mode",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_subscriptions_service_id_url",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"service_id",
|
|
||||||
"url"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "search_history",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "creationDate",
|
|
||||||
"columnName": "creation_date",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "serviceId",
|
|
||||||
"columnName": "service_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "search",
|
|
||||||
"columnName": "search",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_search_history_search",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"search"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "streams",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "uid",
|
|
||||||
"columnName": "uid",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "serviceId",
|
|
||||||
"columnName": "service_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "url",
|
|
||||||
"columnName": "url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "streamType",
|
|
||||||
"columnName": "stream_type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "duration",
|
|
||||||
"columnName": "duration",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "uploader",
|
|
||||||
"columnName": "uploader",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "uploaderUrl",
|
|
||||||
"columnName": "uploader_url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "thumbnailUrl",
|
|
||||||
"columnName": "thumbnail_url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "viewCount",
|
|
||||||
"columnName": "view_count",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "textualUploadDate",
|
|
||||||
"columnName": "textual_upload_date",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "uploadDate",
|
|
||||||
"columnName": "upload_date",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isUploadDateApproximation",
|
|
||||||
"columnName": "is_upload_date_approximation",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_streams_service_id_url",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"service_id",
|
|
||||||
"url"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "stream_history",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "streamUid",
|
|
||||||
"columnName": "stream_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "accessDate",
|
|
||||||
"columnName": "access_date",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "repeatCount",
|
|
||||||
"columnName": "repeat_count",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"stream_id",
|
|
||||||
"access_date"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_stream_history_stream_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"stream_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "streams",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"stream_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "stream_state",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "streamUid",
|
|
||||||
"columnName": "stream_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "progressMillis",
|
|
||||||
"columnName": "progress_time",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"stream_id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "streams",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"stream_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "playlists",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "uid",
|
|
||||||
"columnName": "uid",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isThumbnailPermanent",
|
|
||||||
"columnName": "is_thumbnail_permanent",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "thumbnailStreamId",
|
|
||||||
"columnName": "thumbnail_stream_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_playlists_name",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "playlist_stream_join",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "playlistUid",
|
|
||||||
"columnName": "playlist_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "streamUid",
|
|
||||||
"columnName": "stream_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "index",
|
|
||||||
"columnName": "join_index",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"playlist_id",
|
|
||||||
"join_index"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_playlist_stream_join_playlist_id_join_index",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"playlist_id",
|
|
||||||
"join_index"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index_playlist_stream_join_stream_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"stream_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "playlists",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"playlist_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "streams",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"stream_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "remote_playlists",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "uid",
|
|
||||||
"columnName": "uid",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "serviceId",
|
|
||||||
"columnName": "service_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "url",
|
|
||||||
"columnName": "url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "thumbnailUrl",
|
|
||||||
"columnName": "thumbnail_url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "uploader",
|
|
||||||
"columnName": "uploader",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "streamCount",
|
|
||||||
"columnName": "stream_count",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_remote_playlists_name",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index_remote_playlists_service_id_url",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"service_id",
|
|
||||||
"url"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "feed",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "streamId",
|
|
||||||
"columnName": "stream_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subscriptionId",
|
|
||||||
"columnName": "subscription_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"stream_id",
|
|
||||||
"subscription_id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_feed_subscription_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"subscription_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "streams",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"stream_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "subscriptions",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"subscription_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "feed_group",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "uid",
|
|
||||||
"columnName": "uid",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "icon",
|
|
||||||
"columnName": "icon_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sortOrder",
|
|
||||||
"columnName": "sort_order",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_feed_group_sort_order",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"sort_order"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "feed_group_subscription_join",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "feedGroupId",
|
|
||||||
"columnName": "group_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subscriptionId",
|
|
||||||
"columnName": "subscription_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"group_id",
|
|
||||||
"subscription_id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_feed_group_subscription_join_subscription_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"subscription_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "feed_group",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"group_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "subscriptions",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"subscription_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "feed_last_updated",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "subscriptionId",
|
|
||||||
"columnName": "subscription_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastUpdated",
|
|
||||||
"columnName": "last_updated",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"subscription_id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "subscriptions",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"subscription_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '012fc8e7ad3333f1597347f34e76a513')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,730 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 9,
|
|
||||||
"identityHash": "7591e8039faa74d8c0517dc867af9d3e",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "subscriptions",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "uid",
|
|
||||||
"columnName": "uid",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "serviceId",
|
|
||||||
"columnName": "service_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "url",
|
|
||||||
"columnName": "url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "avatarUrl",
|
|
||||||
"columnName": "avatar_url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subscriberCount",
|
|
||||||
"columnName": "subscriber_count",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "description",
|
|
||||||
"columnName": "description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "notificationMode",
|
|
||||||
"columnName": "notification_mode",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_subscriptions_service_id_url",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"service_id",
|
|
||||||
"url"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "search_history",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "creationDate",
|
|
||||||
"columnName": "creation_date",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "serviceId",
|
|
||||||
"columnName": "service_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "search",
|
|
||||||
"columnName": "search",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_search_history_search",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"search"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "streams",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "uid",
|
|
||||||
"columnName": "uid",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "serviceId",
|
|
||||||
"columnName": "service_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "url",
|
|
||||||
"columnName": "url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "streamType",
|
|
||||||
"columnName": "stream_type",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "duration",
|
|
||||||
"columnName": "duration",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "uploader",
|
|
||||||
"columnName": "uploader",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "uploaderUrl",
|
|
||||||
"columnName": "uploader_url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "thumbnailUrl",
|
|
||||||
"columnName": "thumbnail_url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "viewCount",
|
|
||||||
"columnName": "view_count",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "textualUploadDate",
|
|
||||||
"columnName": "textual_upload_date",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "uploadDate",
|
|
||||||
"columnName": "upload_date",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isUploadDateApproximation",
|
|
||||||
"columnName": "is_upload_date_approximation",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_streams_service_id_url",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"service_id",
|
|
||||||
"url"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "stream_history",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "streamUid",
|
|
||||||
"columnName": "stream_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "accessDate",
|
|
||||||
"columnName": "access_date",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "repeatCount",
|
|
||||||
"columnName": "repeat_count",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"stream_id",
|
|
||||||
"access_date"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_stream_history_stream_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"stream_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "streams",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"stream_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "stream_state",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "streamUid",
|
|
||||||
"columnName": "stream_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "progressMillis",
|
|
||||||
"columnName": "progress_time",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"stream_id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "streams",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"stream_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "playlists",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL, `display_index` INTEGER NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "uid",
|
|
||||||
"columnName": "uid",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isThumbnailPermanent",
|
|
||||||
"columnName": "is_thumbnail_permanent",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "thumbnailStreamId",
|
|
||||||
"columnName": "thumbnail_stream_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "displayIndex",
|
|
||||||
"columnName": "display_index",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "playlist_stream_join",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "playlistUid",
|
|
||||||
"columnName": "playlist_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "streamUid",
|
|
||||||
"columnName": "stream_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "index",
|
|
||||||
"columnName": "join_index",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"playlist_id",
|
|
||||||
"join_index"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_playlist_stream_join_playlist_id_join_index",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"playlist_id",
|
|
||||||
"join_index"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index_playlist_stream_join_stream_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"stream_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "playlists",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"playlist_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "streams",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"stream_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "remote_playlists",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `display_index` INTEGER NOT NULL, `stream_count` INTEGER)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "uid",
|
|
||||||
"columnName": "uid",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "serviceId",
|
|
||||||
"columnName": "service_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "url",
|
|
||||||
"columnName": "url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "thumbnailUrl",
|
|
||||||
"columnName": "thumbnail_url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "uploader",
|
|
||||||
"columnName": "uploader",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "displayIndex",
|
|
||||||
"columnName": "display_index",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "streamCount",
|
|
||||||
"columnName": "stream_count",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_remote_playlists_service_id_url",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"service_id",
|
|
||||||
"url"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "feed",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "streamId",
|
|
||||||
"columnName": "stream_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subscriptionId",
|
|
||||||
"columnName": "subscription_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"stream_id",
|
|
||||||
"subscription_id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_feed_subscription_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"subscription_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "streams",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"stream_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "subscriptions",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"subscription_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "feed_group",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "uid",
|
|
||||||
"columnName": "uid",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "icon",
|
|
||||||
"columnName": "icon_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "sortOrder",
|
|
||||||
"columnName": "sort_order",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_feed_group_sort_order",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"sort_order"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "feed_group_subscription_join",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "feedGroupId",
|
|
||||||
"columnName": "group_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "subscriptionId",
|
|
||||||
"columnName": "subscription_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"group_id",
|
|
||||||
"subscription_id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_feed_group_subscription_join_subscription_id",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"subscription_id"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "feed_group",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"group_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "subscriptions",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"subscription_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "feed_last_updated",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "subscriptionId",
|
|
||||||
"columnName": "subscription_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastUpdated",
|
|
||||||
"columnName": "last_updated",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"subscription_id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "subscriptions",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "CASCADE",
|
|
||||||
"columns": [
|
|
||||||
"subscription_id"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7591e8039faa74d8c0517dc867af9d3e')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,14 +8,10 @@ import androidx.test.core.app.ApplicationProvider
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotEquals
|
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
|
|
||||||
import org.schabi.newpipe.extractor.ServiceList
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@ -24,17 +20,13 @@ class DatabaseMigrationTest {
|
|||||||
private const val DEFAULT_SERVICE_ID = 0
|
private const val DEFAULT_SERVICE_ID = 0
|
||||||
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
||||||
private const val DEFAULT_TITLE = "Test Title"
|
private const val DEFAULT_TITLE = "Test Title"
|
||||||
private const val DEFAULT_NAME = "Test Name"
|
|
||||||
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
||||||
private const val DEFAULT_DURATION = 480L
|
private const val DEFAULT_DURATION = 480L
|
||||||
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
||||||
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
||||||
|
|
||||||
private const val DEFAULT_SECOND_SERVICE_ID = 1
|
private const val DEFAULT_SECOND_SERVICE_ID = 0
|
||||||
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
||||||
|
|
||||||
private const val DEFAULT_THIRD_SERVICE_ID = 2
|
|
||||||
private const val DEFAULT_THIRD_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
@ -114,20 +106,6 @@ class DatabaseMigrationTest {
|
|||||||
Migrations.MIGRATION_6_7
|
Migrations.MIGRATION_6_7
|
||||||
)
|
)
|
||||||
|
|
||||||
testHelper.runMigrationsAndValidate(
|
|
||||||
AppDatabase.DATABASE_NAME,
|
|
||||||
Migrations.DB_VER_8,
|
|
||||||
true,
|
|
||||||
Migrations.MIGRATION_7_8
|
|
||||||
)
|
|
||||||
|
|
||||||
testHelper.runMigrationsAndValidate(
|
|
||||||
AppDatabase.DATABASE_NAME,
|
|
||||||
Migrations.DB_VER_9,
|
|
||||||
true,
|
|
||||||
Migrations.MIGRATION_8_9
|
|
||||||
)
|
|
||||||
|
|
||||||
val migratedDatabaseV3 = getMigratedDatabase()
|
val migratedDatabaseV3 = getMigratedDatabase()
|
||||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||||
|
|
||||||
@ -162,157 +140,6 @@ class DatabaseMigrationTest {
|
|||||||
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun migrateDatabaseFrom7to8() {
|
|
||||||
val databaseInV7 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_7)
|
|
||||||
|
|
||||||
val defaultSearch1 = " abc "
|
|
||||||
val defaultSearch2 = " abc"
|
|
||||||
|
|
||||||
val serviceId = DEFAULT_SERVICE_ID // YouTube
|
|
||||||
// Use id different to YouTube because two searches with the same query
|
|
||||||
// but different service are considered not equal.
|
|
||||||
val otherServiceId = ServiceList.SoundCloud.serviceId
|
|
||||||
|
|
||||||
databaseInV7.run {
|
|
||||||
insert(
|
|
||||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("service_id", serviceId)
|
|
||||||
put("search", defaultSearch1)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
insert(
|
|
||||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("service_id", serviceId)
|
|
||||||
put("search", defaultSearch2)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
insert(
|
|
||||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("service_id", otherServiceId)
|
|
||||||
put("search", defaultSearch1)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
insert(
|
|
||||||
"search_history", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("service_id", otherServiceId)
|
|
||||||
put("search", defaultSearch2)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
testHelper.runMigrationsAndValidate(
|
|
||||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_8,
|
|
||||||
true, Migrations.MIGRATION_7_8
|
|
||||||
)
|
|
||||||
|
|
||||||
testHelper.runMigrationsAndValidate(
|
|
||||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_9,
|
|
||||||
true, Migrations.MIGRATION_8_9
|
|
||||||
)
|
|
||||||
|
|
||||||
val migratedDatabaseV8 = getMigratedDatabase()
|
|
||||||
val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
|
|
||||||
|
|
||||||
assertEquals(2, listFromDB.size)
|
|
||||||
assertEquals("abc", listFromDB[0].search)
|
|
||||||
assertEquals("abc", listFromDB[1].search)
|
|
||||||
assertNotEquals(listFromDB[0].serviceId, listFromDB[1].serviceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun migrateDatabaseFrom8to9() {
|
|
||||||
val databaseInV8 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_8)
|
|
||||||
|
|
||||||
val localUid1: Long
|
|
||||||
val localUid2: Long
|
|
||||||
val remoteUid1: Long
|
|
||||||
val remoteUid2: Long
|
|
||||||
databaseInV8.run {
|
|
||||||
localUid1 = insert(
|
|
||||||
"playlists", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("name", DEFAULT_NAME + "1")
|
|
||||||
put("is_thumbnail_permanent", false)
|
|
||||||
put("thumbnail_stream_id", -1)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
localUid2 = insert(
|
|
||||||
"playlists", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("name", DEFAULT_NAME + "2")
|
|
||||||
put("is_thumbnail_permanent", false)
|
|
||||||
put("thumbnail_stream_id", -1)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
delete(
|
|
||||||
"playlists", "uid = ?",
|
|
||||||
Array(1) { localUid1 }
|
|
||||||
)
|
|
||||||
remoteUid1 = insert(
|
|
||||||
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("service_id", DEFAULT_SERVICE_ID)
|
|
||||||
put("url", DEFAULT_URL)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
remoteUid2 = insert(
|
|
||||||
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
|
||||||
put("url", DEFAULT_SECOND_URL)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
delete(
|
|
||||||
"remote_playlists", "uid = ?",
|
|
||||||
Array(1) { remoteUid2 }
|
|
||||||
)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
testHelper.runMigrationsAndValidate(
|
|
||||||
AppDatabase.DATABASE_NAME,
|
|
||||||
Migrations.DB_VER_9,
|
|
||||||
true,
|
|
||||||
Migrations.MIGRATION_8_9
|
|
||||||
)
|
|
||||||
|
|
||||||
val migratedDatabaseV9 = getMigratedDatabase()
|
|
||||||
var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
|
|
||||||
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
|
|
||||||
|
|
||||||
assertEquals(1, localListFromDB.size)
|
|
||||||
assertEquals(localUid2, localListFromDB[0].uid)
|
|
||||||
assertEquals(-1, localListFromDB[0].displayIndex)
|
|
||||||
assertEquals(1, remoteListFromDB.size)
|
|
||||||
assertEquals(remoteUid1, remoteListFromDB[0].uid)
|
|
||||||
assertEquals(-1, remoteListFromDB[0].displayIndex)
|
|
||||||
|
|
||||||
val localUid3 = migratedDatabaseV9.playlistDAO().insert(
|
|
||||||
PlaylistEntity(DEFAULT_NAME + "3", false, -1, -1)
|
|
||||||
)
|
|
||||||
val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert(
|
|
||||||
PlaylistRemoteEntity(
|
|
||||||
DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL,
|
|
||||||
DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
|
|
||||||
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
|
|
||||||
assertEquals(2, localListFromDB.size)
|
|
||||||
assertEquals(localUid3, localListFromDB[1].uid)
|
|
||||||
assertEquals(-1, localListFromDB[1].displayIndex)
|
|
||||||
assertEquals(2, remoteListFromDB.size)
|
|
||||||
assertEquals(remoteUid3, remoteListFromDB[1].uid)
|
|
||||||
assertEquals(-1, remoteListFromDB[1].displayIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMigratedDatabase(): AppDatabase {
|
private fun getMigratedDatabase(): AppDatabase {
|
||||||
val database: AppDatabase = Room.databaseBuilder(
|
val database: AppDatabase = Room.databaseBuilder(
|
||||||
ApplicationProvider.getApplicationContext(),
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
@ -85,13 +85,7 @@ class FeedDAOTest {
|
|||||||
|
|
||||||
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
|
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
|
||||||
assertNotNull(streams)
|
assertNotNull(streams)
|
||||||
assertEquals(
|
assertEquals(allowedStreams, streams!!.stream().map { it.stream }.toList().sortedBy { it.uid })
|
||||||
allowedStreams,
|
|
||||||
streams!!
|
|
||||||
.map { it.stream }
|
|
||||||
.sortedBy { it.uid }
|
|
||||||
.toList()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUnlinkDelete(time: String) {
|
private fun setupUnlinkDelete(time: String) {
|
||||||
|
@ -367,7 +367,6 @@
|
|||||||
<data android:host="tilvids.com" />
|
<data android:host="tilvids.com" />
|
||||||
<data android:host="video.lqdn.fr" />
|
<data android:host="video.lqdn.fr" />
|
||||||
<data android:host="video.ploud.fr" />
|
<data android:host="video.ploud.fr" />
|
||||||
<data android:host="subscribeto.me" />
|
|
||||||
|
|
||||||
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
||||||
<data android:pathPrefix="/w/" /> <!-- short video URLs -->
|
<data android:pathPrefix="/w/" /> <!-- short video URLs -->
|
||||||
|
@ -25,7 +25,6 @@ import android.view.ViewGroup;
|
|||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.os.BundleCompat;
|
|
||||||
import androidx.lifecycle.Lifecycle;
|
import androidx.lifecycle.Lifecycle;
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
|
||||||
@ -285,7 +284,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
|||||||
Bundle state = null;
|
Bundle state = null;
|
||||||
if (!mSavedState.isEmpty()) {
|
if (!mSavedState.isEmpty()) {
|
||||||
state = new Bundle();
|
state = new Bundle();
|
||||||
state.putParcelableArrayList("states", mSavedState);
|
state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0]));
|
||||||
}
|
}
|
||||||
for (int i = 0; i < mFragments.size(); i++) {
|
for (int i = 0; i < mFragments.size(); i++) {
|
||||||
final Fragment f = mFragments.get(i);
|
final Fragment f = mFragments.get(i);
|
||||||
@ -312,12 +311,13 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
|||||||
if (state != null) {
|
if (state != null) {
|
||||||
final Bundle bundle = (Bundle) state;
|
final Bundle bundle = (Bundle) state;
|
||||||
bundle.setClassLoader(loader);
|
bundle.setClassLoader(loader);
|
||||||
final var states = BundleCompat.getParcelableArrayList(bundle, "states",
|
final Parcelable[] fss = bundle.getParcelableArray("states");
|
||||||
Fragment.SavedState.class);
|
|
||||||
mSavedState.clear();
|
mSavedState.clear();
|
||||||
mFragments.clear();
|
mFragments.clear();
|
||||||
if (states != null) {
|
if (fss != null) {
|
||||||
mSavedState.addAll(states);
|
for (final Parcelable parcelable : fss) {
|
||||||
|
mSavedState.add((Fragment.SavedState) parcelable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final Iterable<String> keys = bundle.keySet();
|
final Iterable<String> keys = bundle.keySet();
|
||||||
for (final String key : keys) {
|
for (final String key : keys) {
|
||||||
|
@ -60,8 +60,6 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
|||||||
public class App extends Application {
|
public class App extends Application {
|
||||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||||
private static final String TAG = App.class.toString();
|
private static final String TAG = App.class.toString();
|
||||||
|
|
||||||
private boolean isFirstRun = false;
|
|
||||||
private static App app;
|
private static App app;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -87,13 +85,7 @@ public class App extends Application {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the last used preference version is set
|
// Initialize settings first because others inits can use its values
|
||||||
// to determine whether this is the first app run
|
|
||||||
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
.getInt(getString(R.string.last_used_preferences_version), -1);
|
|
||||||
isFirstRun = lastUsedPrefVersion == -1;
|
|
||||||
|
|
||||||
// Initialize settings first because other initializations can use its values
|
|
||||||
NewPipeSettings.initSettings(this);
|
NewPipeSettings.initSettings(this);
|
||||||
|
|
||||||
NewPipe.init(getDownloader(),
|
NewPipe.init(getDownloader(),
|
||||||
@ -263,7 +255,4 @@ public class App extends Application {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isFirstRun() {
|
|
||||||
return isFirstRun;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,6 @@ import android.widget.FrameLayout;
|
|||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
@ -52,7 +51,6 @@ import androidx.core.app.ActivityCompat;
|
|||||||
import androidx.core.view.GravityCompat;
|
import androidx.core.view.GravityCompat;
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentContainerView;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
@ -66,20 +64,17 @@ import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
|||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.MainFragment;
|
import org.schabi.newpipe.fragments.MainFragment;
|
||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||||
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
|
|
||||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.settings.UpdateSettingsFragment;
|
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.KioskTranslator;
|
import org.schabi.newpipe.util.KioskTranslator;
|
||||||
@ -87,7 +82,6 @@ import org.schabi.newpipe.util.Localization;
|
|||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PeertubeHelper;
|
import org.schabi.newpipe.util.PeertubeHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.ReleaseVersionUtil;
|
|
||||||
import org.schabi.newpipe.util.SerializedCache;
|
import org.schabi.newpipe.util.SerializedCache;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
@ -169,11 +163,6 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// if this is enabled by the user.
|
// if this is enabled by the user.
|
||||||
NotificationWorker.initialize(this);
|
NotificationWorker.initialize(this);
|
||||||
}
|
}
|
||||||
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
|
||||||
&& !App.getApp().isFirstRun()
|
|
||||||
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
|
||||||
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -183,8 +172,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
final App app = App.getApp();
|
final App app = App.getApp();
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||||
|
|
||||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
if (prefs.getBoolean(app.getString(R.string.update_app_key), true)) {
|
||||||
&& prefs.getBoolean(app.getString(R.string.update_check_consent_key), false)) {
|
|
||||||
// Start the worker which is checking all conditions
|
// Start the worker which is checking all conditions
|
||||||
// and eventually searching for a new version.
|
// and eventually searching for a new version.
|
||||||
NewVersionWorker.enqueueNewVersionCheckingWork(app, false);
|
NewVersionWorker.enqueueNewVersionCheckingWork(app, false);
|
||||||
@ -558,21 +546,14 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// interacts with a fragment inside fragment_holder so all back presses should be
|
// interacts with a fragment inside fragment_holder so all back presses should be
|
||||||
// handled by it
|
// handled by it
|
||||||
if (bottomSheetHiddenOrCollapsed()) {
|
if (bottomSheetHiddenOrCollapsed()) {
|
||||||
final FragmentManager fm = getSupportFragmentManager();
|
final Fragment fragment = getSupportFragmentManager()
|
||||||
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
.findFragmentById(R.id.fragment_holder);
|
||||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||||
// delegate the back press to it
|
// delegate the back press to it
|
||||||
if (fragment instanceof BackPressable) {
|
if (fragment instanceof BackPressable) {
|
||||||
if (((BackPressable) fragment).onBackPressed()) {
|
if (((BackPressable) fragment).onBackPressed()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (fragment instanceof CommentRepliesFragment) {
|
|
||||||
// expand DetailsFragment if CommentRepliesFragment was opened
|
|
||||||
// to show the top level comments again
|
|
||||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
|
||||||
// and no other CommentRepliesFragments are on top of the back stack
|
|
||||||
// to show the top level comments again.
|
|
||||||
openDetailFragmentFromCommentReplies(fm, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@ -648,17 +629,10 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
private void onHomeButtonPressed() {
|
private void onHomeButtonPressed() {
|
||||||
final FragmentManager fm = getSupportFragmentManager();
|
// If search fragment wasn't found in the backstack...
|
||||||
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
if (!NavigationHelper.tryGotoSearchFragment(getSupportFragmentManager())) {
|
||||||
|
// ...go to the main fragment
|
||||||
if (fragment instanceof CommentRepliesFragment) {
|
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
|
||||||
// and no other CommentRepliesFragments are on top of the back stack
|
|
||||||
// to show the top level comments again.
|
|
||||||
openDetailFragmentFromCommentReplies(fm, true);
|
|
||||||
} else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
|
||||||
// If search fragment wasn't found in the backstack go to the main fragment
|
|
||||||
NavigationHelper.gotoMainFragment(fm);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -854,68 +828,6 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openDetailFragmentFromCommentReplies(
|
|
||||||
@NonNull final FragmentManager fm,
|
|
||||||
final boolean popBackStack
|
|
||||||
) {
|
|
||||||
// obtain the name of the fragment under the replies fragment that's going to be popped
|
|
||||||
@Nullable final String fragmentUnderEntryName;
|
|
||||||
if (fm.getBackStackEntryCount() < 2) {
|
|
||||||
fragmentUnderEntryName = null;
|
|
||||||
} else {
|
|
||||||
fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
|
|
||||||
.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
// the root comment is the comment for which the user opened the replies page
|
|
||||||
@Nullable final CommentRepliesFragment repliesFragment =
|
|
||||||
(CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG);
|
|
||||||
@Nullable final CommentsInfoItem rootComment =
|
|
||||||
repliesFragment == null ? null : repliesFragment.getCommentsInfoItem();
|
|
||||||
|
|
||||||
// sometimes this function pops the backstack, other times it's handled by the system
|
|
||||||
if (popBackStack) {
|
|
||||||
fm.popBackStackImmediate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// only expand the bottom sheet back if there are no more nested comment replies fragments
|
|
||||||
// stacked under the one that is currently being popped
|
|
||||||
if (CommentRepliesFragment.TAG.equals(fragmentUnderEntryName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final BottomSheetBehavior<FragmentContainerView> behavior = BottomSheetBehavior
|
|
||||||
.from(mainBinding.fragmentPlayerHolder);
|
|
||||||
// do not return to the comment if the details fragment was closed
|
|
||||||
if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// scroll to the root comment once the bottom sheet expansion animation is finished
|
|
||||||
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
|
||||||
@Override
|
|
||||||
public void onStateChanged(@NonNull final View bottomSheet,
|
|
||||||
final int newState) {
|
|
||||||
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
|
||||||
final Fragment detailFragment = fm.findFragmentById(
|
|
||||||
R.id.fragment_player_holder);
|
|
||||||
if (detailFragment instanceof VideoDetailFragment && rootComment != null) {
|
|
||||||
// should always be the case
|
|
||||||
((VideoDetailFragment) detailFragment).scrollToComment(rootComment);
|
|
||||||
}
|
|
||||||
behavior.removeBottomSheetCallback(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
|
|
||||||
// not needed, listener is removed once the sheet is expanded
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean bottomSheetHiddenOrCollapsed() {
|
private boolean bottomSheetHiddenOrCollapsed() {
|
||||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
||||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
||||||
|
@ -7,8 +7,6 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
|
|||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8;
|
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
@ -29,7 +27,7 @@ public final class NewPipeDatabase {
|
|||||||
return Room
|
return Room
|
||||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
||||||
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
MIGRATION_5_6, MIGRATION_6_7)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,9 @@ import com.grack.nanojson.JsonParser
|
|||||||
import com.grack.nanojson.JsonParserException
|
import com.grack.nanojson.JsonParserException
|
||||||
import org.schabi.newpipe.extractor.downloader.Response
|
import org.schabi.newpipe.extractor.downloader.Response
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||||
import org.schabi.newpipe.util.ReleaseVersionUtil
|
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
|
||||||
|
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
|
||||||
|
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class NewVersionWorker(
|
class NewVersionWorker(
|
||||||
@ -82,7 +84,7 @@ class NewVersionWorker(
|
|||||||
@Throws(IOException::class, ReCaptchaException::class)
|
@Throws(IOException::class, ReCaptchaException::class)
|
||||||
private fun checkNewVersion() {
|
private fun checkNewVersion() {
|
||||||
// Check if the current apk is a github one or not.
|
// Check if the current apk is a github one or not.
|
||||||
if (!ReleaseVersionUtil.isReleaseApk) {
|
if (!isReleaseApk()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +93,7 @@ class NewVersionWorker(
|
|||||||
// Check if the last request has happened a certain time ago
|
// Check if the last request has happened a certain time ago
|
||||||
// to reduce the number of API requests.
|
// to reduce the number of API requests.
|
||||||
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
|
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
|
||||||
if (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) {
|
if (!isLastUpdateCheckExpired(expiry)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,7 +108,7 @@ class NewVersionWorker(
|
|||||||
try {
|
try {
|
||||||
// Store a timestamp which needs to be exceeded,
|
// Store a timestamp which needs to be exceeded,
|
||||||
// before a new request to the API is made.
|
// before a new request to the API is made.
|
||||||
val newExpiry = ReleaseVersionUtil.coerceUpdateCheckExpiry(response.getHeader("expires"))
|
val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires"))
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
|
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@ class AboutActivity : AppCompatActivity() {
|
|||||||
/**
|
/**
|
||||||
* List of all software components.
|
* List of all software components.
|
||||||
*/
|
*/
|
||||||
private val SOFTWARE_COMPONENTS = arrayListOf(
|
private val SOFTWARE_COMPONENTS = arrayOf(
|
||||||
SoftwareComponent(
|
SoftwareComponent(
|
||||||
"ACRA", "2013", "Kevin Gaudin",
|
"ACRA", "2013", "Kevin Gaudin",
|
||||||
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
|
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
|
||||||
|
@ -18,7 +18,6 @@ import org.schabi.newpipe.BuildConfig
|
|||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
||||||
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
||||||
import org.schabi.newpipe.ktx.parcelableArrayList
|
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
@ -26,15 +25,16 @@ import org.schabi.newpipe.util.external_communication.ShareUtils
|
|||||||
* Fragment containing the software licenses.
|
* Fragment containing the software licenses.
|
||||||
*/
|
*/
|
||||||
class LicenseFragment : Fragment() {
|
class LicenseFragment : Fragment() {
|
||||||
private lateinit var softwareComponents: List<SoftwareComponent>
|
private lateinit var softwareComponents: Array<SoftwareComponent>
|
||||||
private var activeSoftwareComponent: SoftwareComponent? = null
|
private var activeSoftwareComponent: SoftwareComponent? = null
|
||||||
private val compositeDisposable = CompositeDisposable()
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
softwareComponents = arguments?.parcelableArrayList<SoftwareComponent>(ARG_COMPONENTS)!!
|
softwareComponents = arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
|
||||||
.sortedBy { it.name } // Sort components by name
|
|
||||||
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
|
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
|
||||||
|
// Sort components by name
|
||||||
|
softwareComponents.sortBy { it.name }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@ -130,8 +130,7 @@ class LicenseFragment : Fragment() {
|
|||||||
StandardLicenses.GPL3,
|
StandardLicenses.GPL3,
|
||||||
BuildConfig.VERSION_NAME
|
BuildConfig.VERSION_NAME
|
||||||
)
|
)
|
||||||
|
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
|
||||||
fun newInstance(softwareComponents: ArrayList<SoftwareComponent>): LicenseFragment {
|
|
||||||
val fragment = LicenseFragment()
|
val fragment = LicenseFragment()
|
||||||
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
||||||
return fragment
|
return fragment
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package org.schabi.newpipe.database;
|
package org.schabi.newpipe.database;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.Migrations.DB_VER_9;
|
import static org.schabi.newpipe.database.Migrations.DB_VER_7;
|
||||||
|
|
||||||
import androidx.room.Database;
|
import androidx.room.Database;
|
||||||
import androidx.room.RoomDatabase;
|
import androidx.room.RoomDatabase;
|
||||||
@ -38,7 +38,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
|||||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||||
FeedLastUpdatedEntity.class
|
FeedLastUpdatedEntity.class
|
||||||
},
|
},
|
||||||
version = DB_VER_9
|
version = DB_VER_7
|
||||||
)
|
)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
public static final String DATABASE_NAME = "newpipe.db";
|
public static final String DATABASE_NAME = "newpipe.db";
|
||||||
|
@ -7,7 +7,7 @@ import java.time.Instant
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
class Converters {
|
object Converters {
|
||||||
/**
|
/**
|
||||||
* Convert a long value to a [OffsetDateTime].
|
* Convert a long value to a [OffsetDateTime].
|
||||||
*
|
*
|
||||||
@ -47,6 +47,6 @@ class Converters {
|
|||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
||||||
return FeedGroupIcon.entries.first { it.id == id }
|
return FeedGroupIcon.values().first { it.id == id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,8 +25,6 @@ public final class Migrations {
|
|||||||
public static final int DB_VER_5 = 5;
|
public static final int DB_VER_5 = 5;
|
||||||
public static final int DB_VER_6 = 6;
|
public static final int DB_VER_6 = 6;
|
||||||
public static final int DB_VER_7 = 7;
|
public static final int DB_VER_7 = 7;
|
||||||
public static final int DB_VER_8 = 8;
|
|
||||||
public static final int DB_VER_9 = 9;
|
|
||||||
|
|
||||||
private static final String TAG = Migrations.class.getName();
|
private static final String TAG = Migrations.class.getName();
|
||||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
@ -237,71 +235,6 @@ public final class Migrations {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final Migration MIGRATION_7_8 = new Migration(DB_VER_7, DB_VER_8) {
|
|
||||||
@Override
|
|
||||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
|
||||||
database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT "
|
|
||||||
+ "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)");
|
|
||||||
database.execSQL("UPDATE search_history SET search = trim(search)");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) {
|
|
||||||
@Override
|
|
||||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
|
||||||
try {
|
|
||||||
database.beginTransaction();
|
|
||||||
|
|
||||||
// Update playlists.
|
|
||||||
// Create a temp table to initialize display_index.
|
|
||||||
database.execSQL("CREATE TABLE `playlists_tmp` "
|
|
||||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
|
||||||
+ "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, "
|
|
||||||
+ "`thumbnail_stream_id` INTEGER NOT NULL, "
|
|
||||||
+ "`display_index` INTEGER NOT NULL)");
|
|
||||||
database.execSQL("INSERT INTO `playlists_tmp` "
|
|
||||||
+ "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
|
||||||
+ "`display_index`) "
|
|
||||||
+ "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
|
||||||
+ "-1 "
|
|
||||||
+ "FROM `playlists`");
|
|
||||||
|
|
||||||
// Replace the old table, note that this also removes the index on the name which
|
|
||||||
// we don't need anymore.
|
|
||||||
database.execSQL("DROP TABLE `playlists`");
|
|
||||||
database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`");
|
|
||||||
|
|
||||||
|
|
||||||
// Update remote_playlists.
|
|
||||||
// Create a temp table to initialize display_index.
|
|
||||||
database.execSQL("CREATE TABLE `remote_playlists_tmp` "
|
|
||||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
|
||||||
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
|
|
||||||
+ "`thumbnail_url` TEXT, `uploader` TEXT, "
|
|
||||||
+ "`display_index` INTEGER NOT NULL,"
|
|
||||||
+ "`stream_count` INTEGER)");
|
|
||||||
database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
|
|
||||||
+ "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, "
|
|
||||||
+ "`stream_count`)"
|
|
||||||
+ "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, "
|
|
||||||
+ "-1, `stream_count` FROM `remote_playlists`");
|
|
||||||
|
|
||||||
// Replace the old table, note that this also removes the index on the name which
|
|
||||||
// we don't need anymore.
|
|
||||||
database.execSQL("DROP TABLE `remote_playlists`");
|
|
||||||
database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`");
|
|
||||||
|
|
||||||
// Create index on the new table.
|
|
||||||
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
|
||||||
+ "ON `remote_playlists` (`service_id`, `url`)");
|
|
||||||
|
|
||||||
database.setTransactionSuccessful();
|
|
||||||
} finally {
|
|
||||||
database.endTransaction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private Migrations() {
|
private Migrations() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,17 +13,12 @@ public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry {
|
|||||||
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
|
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
|
||||||
public final long timesStreamIsContained;
|
public final long timesStreamIsContained;
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:ParameterNumber")
|
|
||||||
public PlaylistDuplicatesEntry(final long uid,
|
public PlaylistDuplicatesEntry(final long uid,
|
||||||
final String name,
|
final String name,
|
||||||
final String thumbnailUrl,
|
final String thumbnailUrl,
|
||||||
final boolean isThumbnailPermanent,
|
|
||||||
final long thumbnailStreamId,
|
|
||||||
final long displayIndex,
|
|
||||||
final long streamCount,
|
final long streamCount,
|
||||||
final long timesStreamIsContained) {
|
final long timesStreamIsContained) {
|
||||||
super(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex,
|
super(uid, name, thumbnailUrl, streamCount);
|
||||||
streamCount);
|
|
||||||
this.timesStreamIsContained = timesStreamIsContained;
|
this.timesStreamIsContained = timesStreamIsContained;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,22 @@
|
|||||||
package org.schabi.newpipe.database.playlist;
|
package org.schabi.newpipe.database.playlist;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public interface PlaylistLocalItem extends LocalItem {
|
public interface PlaylistLocalItem extends LocalItem {
|
||||||
String getOrderingName();
|
String getOrderingName();
|
||||||
|
|
||||||
long getDisplayIndex();
|
static List<PlaylistLocalItem> merge(
|
||||||
|
final List<PlaylistMetadataEntry> localPlaylists,
|
||||||
long getUid();
|
final List<PlaylistRemoteEntity> remotePlaylists) {
|
||||||
|
return Stream.concat(localPlaylists.stream(), remotePlaylists.stream())
|
||||||
void setDisplayIndex(long displayIndex);
|
.sorted(Comparator.comparing(PlaylistLocalItem::getOrderingName,
|
||||||
|
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,40 +2,27 @@ package org.schabi.newpipe.database.playlist;
|
|||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||||
|
|
||||||
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||||
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
||||||
|
|
||||||
@ColumnInfo(name = PLAYLIST_ID)
|
@ColumnInfo(name = PLAYLIST_ID)
|
||||||
private final long uid;
|
public final long uid;
|
||||||
@ColumnInfo(name = PLAYLIST_NAME)
|
@ColumnInfo(name = PLAYLIST_NAME)
|
||||||
public final String name;
|
public final String name;
|
||||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
|
|
||||||
private final boolean isThumbnailPermanent;
|
|
||||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
|
||||||
private final long thumbnailStreamId;
|
|
||||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||||
public final String thumbnailUrl;
|
public final String thumbnailUrl;
|
||||||
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
|
|
||||||
private long displayIndex;
|
|
||||||
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
||||||
public final long streamCount;
|
public final long streamCount;
|
||||||
|
|
||||||
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
|
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
|
||||||
final boolean isThumbnailPermanent, final long thumbnailStreamId,
|
final long streamCount) {
|
||||||
final long displayIndex, final long streamCount) {
|
|
||||||
this.uid = uid;
|
this.uid = uid;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
this.isThumbnailPermanent = isThumbnailPermanent;
|
|
||||||
this.thumbnailStreamId = thumbnailStreamId;
|
|
||||||
this.displayIndex = displayIndex;
|
|
||||||
this.streamCount = streamCount;
|
this.streamCount = streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,27 +35,4 @@ public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
|||||||
public String getOrderingName() {
|
public String getOrderingName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isThumbnailPermanent() {
|
|
||||||
return isThumbnailPermanent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getThumbnailStreamId() {
|
|
||||||
return thumbnailStreamId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getDisplayIndex() {
|
|
||||||
return displayIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getUid() {
|
|
||||||
return uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setDisplayIndex(final long displayIndex) {
|
|
||||||
this.displayIndex = displayIndex;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package org.schabi.newpipe.database.playlist.dao;
|
|||||||
|
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
import androidx.room.Transaction;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.database.BasicDAO;
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||||
@ -37,17 +36,4 @@ public interface PlaylistDAO extends BasicDAO<PlaylistEntity> {
|
|||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
|
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
|
||||||
Flowable<Long> getCount();
|
Flowable<Long> getCount();
|
||||||
|
|
||||||
@Transaction
|
|
||||||
default long upsertPlaylist(final PlaylistEntity playlist) {
|
|
||||||
final long playlistId = playlist.getUid();
|
|
||||||
|
|
||||||
if (playlistId == -1) {
|
|
||||||
// This situation is probably impossible.
|
|
||||||
return insert(playlist);
|
|
||||||
} else {
|
|
||||||
update(playlist);
|
|
||||||
return playlistId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ import java.util.List;
|
|||||||
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_DISPLAY_INDEX;
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
||||||
@ -32,18 +31,10 @@ public interface PlaylistRemoteDAO extends BasicDAO<PlaylistRemoteEntity> {
|
|||||||
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
|
||||||
+ REMOTE_PLAYLIST_ID + " = :playlistId")
|
|
||||||
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long playlistId);
|
|
||||||
|
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||||
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
|
|
||||||
+ " ORDER BY " + REMOTE_PLAYLIST_DISPLAY_INDEX)
|
|
||||||
Flowable<List<PlaylistRemoteEntity>> getPlaylists();
|
|
||||||
|
|
||||||
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
|
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
|
||||||
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
|
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
|
||||||
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
|
@ -18,12 +18,10 @@ import io.reactivex.rxjava3.core.Flowable;
|
|||||||
|
|
||||||
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
|
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
|
||||||
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
||||||
@ -93,9 +91,7 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||||||
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
|
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ","
|
||||||
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
|
||||||
+ PLAYLIST_DISPLAY_INDEX + ", "
|
|
||||||
|
|
||||||
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||||
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||||
@ -109,7 +105,7 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||||||
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||||
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
||||||
+ " GROUP BY " + PLAYLIST_ID
|
+ " GROUP BY " + PLAYLIST_ID
|
||||||
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
||||||
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
||||||
|
|
||||||
@RewriteQueriesToDropUnusedColumns
|
@RewriteQueriesToDropUnusedColumns
|
||||||
@ -130,9 +126,8 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||||||
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
|
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
|
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", "
|
||||||
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
+ PLAYLIST_NAME + ", "
|
||||||
+ PLAYLIST_DISPLAY_INDEX + ", "
|
|
||||||
|
|
||||||
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||||
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||||
@ -154,6 +149,6 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||||||
+ " AND :streamUrl = :streamUrl"
|
+ " AND :streamUrl = :streamUrl"
|
||||||
|
|
||||||
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
||||||
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
||||||
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,16 @@ package org.schabi.newpipe.database.playlist.model;
|
|||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.Ignore;
|
import androidx.room.Index;
|
||||||
import androidx.room.PrimaryKey;
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
|
||||||
|
|
||||||
@Entity(tableName = PLAYLIST_TABLE)
|
@Entity(tableName = PLAYLIST_TABLE,
|
||||||
|
indices = {@Index(value = {PLAYLIST_NAME})})
|
||||||
public class PlaylistEntity {
|
public class PlaylistEntity {
|
||||||
|
|
||||||
public static final String DEFAULT_THUMBNAIL = "drawable://"
|
public static final String DEFAULT_THUMBNAIL = "drawable://"
|
||||||
@ -21,7 +22,6 @@ public class PlaylistEntity {
|
|||||||
public static final String PLAYLIST_ID = "uid";
|
public static final String PLAYLIST_ID = "uid";
|
||||||
public static final String PLAYLIST_NAME = "name";
|
public static final String PLAYLIST_NAME = "name";
|
||||||
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||||
public static final String PLAYLIST_DISPLAY_INDEX = "display_index";
|
|
||||||
public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
|
public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
|
||||||
public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
|
public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
|
||||||
|
|
||||||
@ -38,24 +38,11 @@ public class PlaylistEntity {
|
|||||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||||
private long thumbnailStreamId;
|
private long thumbnailStreamId;
|
||||||
|
|
||||||
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
|
|
||||||
private long displayIndex;
|
|
||||||
|
|
||||||
public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
|
public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
|
||||||
final long thumbnailStreamId, final long displayIndex) {
|
final long thumbnailStreamId) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.isThumbnailPermanent = isThumbnailPermanent;
|
this.isThumbnailPermanent = isThumbnailPermanent;
|
||||||
this.thumbnailStreamId = thumbnailStreamId;
|
this.thumbnailStreamId = thumbnailStreamId;
|
||||||
this.displayIndex = displayIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
public PlaylistEntity(final PlaylistMetadataEntry item) {
|
|
||||||
this.uid = item.getUid();
|
|
||||||
this.name = item.name;
|
|
||||||
this.isThumbnailPermanent = item.isThumbnailPermanent();
|
|
||||||
this.thumbnailStreamId = item.getThumbnailStreamId();
|
|
||||||
this.displayIndex = item.getDisplayIndex();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getUid() {
|
public long getUid() {
|
||||||
@ -90,11 +77,4 @@ public class PlaylistEntity {
|
|||||||
this.isThumbnailPermanent = isThumbnailSet;
|
this.isThumbnailPermanent = isThumbnailSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getDisplayIndex() {
|
|
||||||
return displayIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDisplayIndex(final long displayIndex) {
|
|
||||||
this.displayIndex = displayIndex;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.RE
|
|||||||
|
|
||||||
@Entity(tableName = REMOTE_PLAYLIST_TABLE,
|
@Entity(tableName = REMOTE_PLAYLIST_TABLE,
|
||||||
indices = {
|
indices = {
|
||||||
|
@Index(value = {REMOTE_PLAYLIST_NAME}),
|
||||||
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
|
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
|
||||||
})
|
})
|
||||||
public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||||
@ -31,7 +32,6 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
public static final String REMOTE_PLAYLIST_URL = "url";
|
public static final String REMOTE_PLAYLIST_URL = "url";
|
||||||
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||||
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
||||||
public static final String REMOTE_PLAYLIST_DISPLAY_INDEX = "display_index";
|
|
||||||
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
||||||
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
@ -53,9 +53,6 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
|
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
|
||||||
private String uploader;
|
private String uploader;
|
||||||
|
|
||||||
@ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX)
|
|
||||||
private long displayIndex = -1; // Make sure the new item is on the top
|
|
||||||
|
|
||||||
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
||||||
private Long streamCount;
|
private Long streamCount;
|
||||||
|
|
||||||
@ -70,19 +67,6 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
this.streamCount = streamCount;
|
this.streamCount = streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Ignore
|
|
||||||
public PlaylistRemoteEntity(final int serviceId, final String name, final String url,
|
|
||||||
final String thumbnailUrl, final String uploader,
|
|
||||||
final long displayIndex, final Long streamCount) {
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
this.name = name;
|
|
||||||
this.url = url;
|
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
|
||||||
this.uploader = uploader;
|
|
||||||
this.displayIndex = displayIndex;
|
|
||||||
this.streamCount = streamCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
||||||
this(info.getServiceId(), info.getName(), info.getUrl(),
|
this(info.getServiceId(), info.getName(), info.getUrl(),
|
||||||
@ -109,7 +93,6 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getUid() {
|
public long getUid() {
|
||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
@ -158,16 +141,6 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
this.uploader = uploader;
|
this.uploader = uploader;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getDisplayIndex() {
|
|
||||||
return displayIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setDisplayIndex(final long displayIndex) {
|
|
||||||
this.displayIndex = displayIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getStreamCount() {
|
public Long getStreamCount() {
|
||||||
return streamCount;
|
return streamCount;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package org.schabi.newpipe.database.subscription;
|
package org.schabi.newpipe.database.subscription;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.Ignore;
|
import androidx.room.Ignore;
|
||||||
@ -96,12 +95,11 @@ public class SubscriptionEntity {
|
|||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public String getAvatarUrl() {
|
public String getAvatarUrl() {
|
||||||
return avatarUrl;
|
return avatarUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAvatarUrl(@Nullable final String avatarUrl) {
|
public void setAvatarUrl(final String avatarUrl) {
|
||||||
this.avatarUrl = avatarUrl;
|
this.avatarUrl = avatarUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.DialogInterface.OnDismissListener;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
@ -14,7 +16,6 @@ import android.net.Uri;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.provider.Settings;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -73,7 +74,6 @@ import org.schabi.newpipe.util.ThemeHelper;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -111,11 +111,14 @@ public class DownloadDialog extends DialogFragment
|
|||||||
@State
|
@State
|
||||||
int selectedSubtitleIndex = 0; // default to the first item
|
int selectedSubtitleIndex = 0; // default to the first item
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private OnDismissListener onDismissListener = null;
|
||||||
|
|
||||||
private StoredDirectoryHelper mainStorageAudio = null;
|
private StoredDirectoryHelper mainStorageAudio = null;
|
||||||
private StoredDirectoryHelper mainStorageVideo = null;
|
private StoredDirectoryHelper mainStorageVideo = null;
|
||||||
private DownloadManager downloadManager = null;
|
private DownloadManager downloadManager = null;
|
||||||
private ActionMenuItemView okButton = null;
|
private ActionMenuItemView okButton = null;
|
||||||
private Context context = null;
|
private Context context;
|
||||||
private boolean askForSavePath;
|
private boolean askForSavePath;
|
||||||
|
|
||||||
private AudioTrackAdapter audioTrackAdapter;
|
private AudioTrackAdapter audioTrackAdapter;
|
||||||
@ -143,6 +146,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
registerForActivityResult(
|
registerForActivityResult(
|
||||||
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
|
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Instance creation
|
// Instance creation
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -190,6 +194,13 @@ public class DownloadDialog extends DialogFragment
|
|||||||
this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param onDismissListener the listener to call in {@link #onDismiss(DialogInterface)}
|
||||||
|
*/
|
||||||
|
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
|
||||||
|
this.onDismissListener = onDismissListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Android lifecycle
|
// Android lifecycle
|
||||||
@ -209,8 +220,6 @@ public class DownloadDialog extends DialogFragment
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// context will remain null if dismiss() was called above, allowing to check whether the
|
|
||||||
// dialog is being dismissed in onViewCreated()
|
|
||||||
context = getContext();
|
context = getContext();
|
||||||
|
|
||||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||||
@ -295,9 +304,6 @@ public class DownloadDialog extends DialogFragment
|
|||||||
@Nullable final Bundle savedInstanceState) {
|
@Nullable final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
dialogBinding = DownloadDialogBinding.bind(view);
|
dialogBinding = DownloadDialogBinding.bind(view);
|
||||||
if (context == null) {
|
|
||||||
return; // the dialog is being dismissed, see the call to dismiss() in onCreate()
|
|
||||||
}
|
|
||||||
|
|
||||||
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
||||||
currentInfo.getName()));
|
currentInfo.getName()));
|
||||||
@ -357,6 +363,14 @@ public class DownloadDialog extends DialogFragment
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDismiss(@NonNull final DialogInterface dialog) {
|
||||||
|
super.onDismiss(dialog);
|
||||||
|
if (onDismissListener != null) {
|
||||||
|
onDismissListener.onDismiss(dialog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
@ -550,6 +564,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Listeners
|
// Listeners
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -768,7 +783,6 @@ public class DownloadDialog extends DialogFragment
|
|||||||
final StoredDirectoryHelper mainStorage;
|
final StoredDirectoryHelper mainStorage;
|
||||||
final MediaFormat format;
|
final MediaFormat format;
|
||||||
final String selectedMediaType;
|
final String selectedMediaType;
|
||||||
final long size;
|
|
||||||
|
|
||||||
// first, build the filename and get the output folder (if possible)
|
// first, build the filename and get the output folder (if possible)
|
||||||
// later, run a very very very large file checking logic
|
// later, run a very very very large file checking logic
|
||||||
@ -780,7 +794,6 @@ public class DownloadDialog extends DialogFragment
|
|||||||
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
||||||
mainStorage = mainStorageAudio;
|
mainStorage = mainStorageAudio;
|
||||||
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||||
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
|
|
||||||
if (format == MediaFormat.WEBMA_OPUS) {
|
if (format == MediaFormat.WEBMA_OPUS) {
|
||||||
mimeTmp = "audio/ogg";
|
mimeTmp = "audio/ogg";
|
||||||
filenameTmp += "opus";
|
filenameTmp += "opus";
|
||||||
@ -793,7 +806,6 @@ public class DownloadDialog extends DialogFragment
|
|||||||
selectedMediaType = getString(R.string.last_download_type_video_key);
|
selectedMediaType = getString(R.string.last_download_type_video_key);
|
||||||
mainStorage = mainStorageVideo;
|
mainStorage = mainStorageVideo;
|
||||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||||
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
|
|
||||||
if (format != null) {
|
if (format != null) {
|
||||||
mimeTmp = format.mimeType;
|
mimeTmp = format.mimeType;
|
||||||
filenameTmp += format.getSuffix();
|
filenameTmp += format.getSuffix();
|
||||||
@ -803,7 +815,6 @@ public class DownloadDialog extends DialogFragment
|
|||||||
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
||||||
mainStorage = mainStorageVideo; // subtitle & video files go together
|
mainStorage = mainStorageVideo; // subtitle & video files go together
|
||||||
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||||
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
|
|
||||||
if (format != null) {
|
if (format != null) {
|
||||||
mimeTmp = format.mimeType;
|
mimeTmp = format.mimeType;
|
||||||
}
|
}
|
||||||
@ -859,21 +870,6 @@ public class DownloadDialog extends DialogFragment
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for free storage space
|
|
||||||
final long freeSpace = mainStorage.getFreeStorageSpace();
|
|
||||||
if (freeSpace <= size) {
|
|
||||||
Toast.makeText(context, getString(R.
|
|
||||||
string.error_insufficient_storage), Toast.LENGTH_LONG).show();
|
|
||||||
// move the user to storage setting tab
|
|
||||||
final Intent storageSettingsIntent = new Intent(Settings.
|
|
||||||
ACTION_INTERNAL_STORAGE_SETTINGS);
|
|
||||||
if (storageSettingsIntent.resolveActivity(context.getPackageManager())
|
|
||||||
!= null) {
|
|
||||||
startActivity(storageSettingsIntent);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for existing file with the same name
|
// check for existing file with the same name
|
||||||
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
|
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
|
||||||
mimeTmp);
|
mimeTmp);
|
||||||
@ -1056,7 +1052,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
final char kind;
|
final char kind;
|
||||||
int threads = dialogBinding.threads.getProgress() + 1;
|
int threads = dialogBinding.threads.getProgress() + 1;
|
||||||
final String[] urls;
|
final String[] urls;
|
||||||
final List<MissionRecoveryInfo> recoveryInfo;
|
final MissionRecoveryInfo[] recoveryInfo;
|
||||||
String psName = null;
|
String psName = null;
|
||||||
String[] psArgs = null;
|
String[] psArgs = null;
|
||||||
long nearLength = 0;
|
long nearLength = 0;
|
||||||
@ -1121,7 +1117,9 @@ public class DownloadDialog extends DialogFragment
|
|||||||
urls = new String[] {
|
urls = new String[] {
|
||||||
selectedStream.getContent()
|
selectedStream.getContent()
|
||||||
};
|
};
|
||||||
recoveryInfo = List.of(new MissionRecoveryInfo(selectedStream));
|
recoveryInfo = new MissionRecoveryInfo[] {
|
||||||
|
new MissionRecoveryInfo(selectedStream)
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) {
|
if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) {
|
||||||
throw new IllegalArgumentException("Unsupported stream delivery format"
|
throw new IllegalArgumentException("Unsupported stream delivery format"
|
||||||
@ -1131,14 +1129,12 @@ public class DownloadDialog extends DialogFragment
|
|||||||
urls = new String[] {
|
urls = new String[] {
|
||||||
selectedStream.getContent(), secondaryStream.getContent()
|
selectedStream.getContent(), secondaryStream.getContent()
|
||||||
};
|
};
|
||||||
recoveryInfo = List.of(
|
recoveryInfo = new MissionRecoveryInfo[] {new MissionRecoveryInfo(selectedStream),
|
||||||
new MissionRecoveryInfo(selectedStream),
|
new MissionRecoveryInfo(secondaryStream)};
|
||||||
new MissionRecoveryInfo(secondaryStream)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
||||||
currentInfo.getUrl(), psName, psArgs, nearLength, new ArrayList<>(recoveryInfo));
|
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo);
|
||||||
|
|
||||||
Toast.makeText(context, getString(R.string.download_has_started),
|
Toast.makeText(context, getString(R.string.download_has_started),
|
||||||
Toast.LENGTH_SHORT).show();
|
Toast.LENGTH_SHORT).show();
|
||||||
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.error;
|
|||||||
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@ -12,14 +13,15 @@ import android.view.Menu;
|
|||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.content.IntentCompat;
|
|
||||||
|
|
||||||
import com.grack.nanojson.JsonWriter;
|
import com.grack.nanojson.JsonWriter;
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
import org.schabi.newpipe.BuildConfig;
|
||||||
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
@ -103,7 +105,7 @@ public class ErrorActivity extends AppCompatActivity {
|
|||||||
actionBar.setDisplayShowTitleEnabled(true);
|
actionBar.setDisplayShowTitleEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo.class);
|
errorInfo = intent.getParcelableExtra(ERROR_INFO);
|
||||||
|
|
||||||
// important add guru meditation
|
// important add guru meditation
|
||||||
addGuruMeditation();
|
addGuruMeditation();
|
||||||
@ -184,6 +186,25 @@ public class ErrorActivity extends AppCompatActivity {
|
|||||||
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
|
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the checked activity.
|
||||||
|
*
|
||||||
|
* @param returnActivity the activity to return to
|
||||||
|
* @return the casted return activity or null
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
static Class<? extends Activity> getReturnActivity(final Class<?> returnActivity) {
|
||||||
|
Class<? extends Activity> checkedReturnActivity = null;
|
||||||
|
if (returnActivity != null) {
|
||||||
|
if (Activity.class.isAssignableFrom(returnActivity)) {
|
||||||
|
checkedReturnActivity = returnActivity.asSubclass(Activity.class);
|
||||||
|
} else {
|
||||||
|
checkedReturnActivity = MainActivity.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return checkedReturnActivity;
|
||||||
|
}
|
||||||
|
|
||||||
private void buildInfo(final ErrorInfo info) {
|
private void buildInfo(final ErrorInfo info) {
|
||||||
String text = "";
|
String text = "";
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class ErrorUtil {
|
|||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun showSnackbar(context: Context, errorInfo: ErrorInfo) {
|
fun showSnackbar(context: Context, errorInfo: ErrorInfo) {
|
||||||
val rootView = (context as? Activity)?.findViewById<View>(android.R.id.content)
|
val rootView = if (context is Activity) context.findViewById<View>(R.id.content) else null
|
||||||
showSnackbar(context, rootView, errorInfo)
|
showSnackbar(context, rootView, errorInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ class ErrorUtil {
|
|||||||
fun showSnackbar(fragment: Fragment, errorInfo: ErrorInfo) {
|
fun showSnackbar(fragment: Fragment, errorInfo: ErrorInfo) {
|
||||||
var rootView = fragment.view
|
var rootView = fragment.view
|
||||||
if (rootView == null && fragment.activity != null) {
|
if (rootView == null && fragment.activity != null) {
|
||||||
rootView = fragment.requireActivity().findViewById(android.R.id.content)
|
rootView = fragment.requireActivity().findViewById(R.id.content)
|
||||||
}
|
}
|
||||||
showSnackbar(fragment.requireContext(), rootView, errorInfo)
|
showSnackbar(fragment.requireContext(), rootView, errorInfo)
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,8 @@ import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
|
|||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
|
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
|
||||||
*
|
*
|
||||||
@ -188,10 +190,11 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
|||||||
String abuseCookie = url.substring(abuseStart + 13, abuseEnd);
|
String abuseCookie = url.substring(abuseStart + 13, abuseEnd);
|
||||||
abuseCookie = Utils.decodeUrlUtf8(abuseCookie);
|
abuseCookie = Utils.decodeUrlUtf8(abuseCookie);
|
||||||
handleCookies(abuseCookie);
|
handleCookies(abuseCookie);
|
||||||
} catch (IllegalArgumentException | StringIndexOutOfBoundsException e) {
|
} catch (UnsupportedEncodingException | StringIndexOutOfBoundsException e) {
|
||||||
if (MainActivity.DEBUG) {
|
if (MainActivity.DEBUG) {
|
||||||
Log.e(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
|
e.printStackTrace();
|
||||||
+ abuseStart + " and ending at " + abuseEnd + " for url " + url, e);
|
Log.d(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
|
||||||
|
+ abuseStart + " and ending at " + abuseEnd + " for url " + url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ package org.schabi.newpipe.error;
|
|||||||
public enum UserAction {
|
public enum UserAction {
|
||||||
USER_REPORT("user report"),
|
USER_REPORT("user report"),
|
||||||
UI_ERROR("ui error"),
|
UI_ERROR("ui error"),
|
||||||
DATABASE_IMPORT_EXPORT("database import or export"),
|
|
||||||
SUBSCRIPTION_CHANGE("subscription change"),
|
SUBSCRIPTION_CHANGE("subscription change"),
|
||||||
SUBSCRIPTION_UPDATE("subscription update"),
|
SUBSCRIPTION_UPDATE("subscription update"),
|
||||||
SUBSCRIPTION_GET("get subscription"),
|
SUBSCRIPTION_GET("get subscription"),
|
||||||
@ -20,7 +19,6 @@ public enum UserAction {
|
|||||||
REQUESTED_PLAYLIST("requested playlist"),
|
REQUESTED_PLAYLIST("requested playlist"),
|
||||||
REQUESTED_KIOSK("requested kiosk"),
|
REQUESTED_KIOSK("requested kiosk"),
|
||||||
REQUESTED_COMMENTS("requested comments"),
|
REQUESTED_COMMENTS("requested comments"),
|
||||||
REQUESTED_COMMENT_REPLIES("requested comment replies"),
|
|
||||||
REQUESTED_FEED("requested feed"),
|
REQUESTED_FEED("requested feed"),
|
||||||
REQUESTED_BOOKMARK("bookmark"),
|
REQUESTED_BOOKMARK("bookmark"),
|
||||||
DELETE_FROM_HISTORY("delete from history"),
|
DELETE_FROM_HISTORY("delete from history"),
|
||||||
|
@ -220,7 +220,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
public void commitPlaylistTabs() {
|
public void commitPlaylistTabs() {
|
||||||
pagerAdapter.getLocalPlaylistFragments()
|
pagerAdapter.getLocalPlaylistFragments()
|
||||||
.stream()
|
.stream()
|
||||||
.forEach(LocalPlaylistFragment::saveImmediate);
|
.forEach(LocalPlaylistFragment::commitChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTabLayoutPosition() {
|
private void updateTabLayoutPosition() {
|
||||||
@ -245,10 +245,10 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
// change the background and icon color of the tab layout:
|
// change the background and icon color of the tab layout:
|
||||||
// service-colored at the top, app-background-colored at the bottom
|
// service-colored at the top, app-background-colored at the bottom
|
||||||
tabLayout.setBackgroundColor(ThemeHelper.resolveColorFromAttr(requireContext(),
|
tabLayout.setBackgroundColor(ThemeHelper.resolveColorFromAttr(requireContext(),
|
||||||
bottom ? android.R.attr.windowBackground : R.attr.colorPrimary));
|
bottom ? R.attr.colorSecondary : R.attr.colorPrimary));
|
||||||
|
|
||||||
@ColorInt final int iconColor = bottom
|
@ColorInt final int iconColor = bottom
|
||||||
? ThemeHelper.resolveColorFromAttr(requireContext(), android.R.attr.colorAccent)
|
? ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent)
|
||||||
: Color.WHITE;
|
: Color.WHITE;
|
||||||
tabLayout.setTabRippleColor(ColorStateList.valueOf(iconColor).withAlpha(32));
|
tabLayout.setTabRippleColor(ColorStateList.valueOf(iconColor).withAlpha(32));
|
||||||
tabLayout.setTabIconTint(ColorStateList.valueOf(iconColor));
|
tabLayout.setTabIconTint(ColorStateList.valueOf(iconColor));
|
||||||
@ -282,7 +282,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
|
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
|
||||||
* during runtime and changes are not committed immediately. However, in some cases,
|
* during runtime and changes are not committed immediately. However, in some cases,
|
||||||
* the changes need to be committed immediately by calling
|
* the changes need to be committed immediately by calling
|
||||||
* {@link LocalPlaylistFragment#saveImmediate()}.
|
* {@link LocalPlaylistFragment#commitChanges()}.
|
||||||
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
|
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
|
||||||
*/
|
*/
|
||||||
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
|
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
|
||||||
|
@ -64,7 +64,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the description to display.
|
* Get the description to display.
|
||||||
* @return description object, if available
|
* @return description object
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
protected abstract Description getDescription();
|
protected abstract Description getDescription();
|
||||||
@ -73,7 +73,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||||||
* Get the streaming service. Used for generating description links.
|
* Get the streaming service. Used for generating description links.
|
||||||
* @return streaming service
|
* @return streaming service
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@Nullable
|
||||||
protected abstract StreamingService getService();
|
protected abstract StreamingService getService();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,7 +93,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||||||
* Get the list of tags to display below the description.
|
* Get the list of tags to display below the description.
|
||||||
* @return tag list
|
* @return tag list
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@Nullable
|
||||||
public abstract List<String> getTags();
|
public abstract List<String> getTags();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -158,7 +158,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||||||
final LinearLayout layout,
|
final LinearLayout layout,
|
||||||
final boolean linkifyContent,
|
final boolean linkifyContent,
|
||||||
@StringRes final int type,
|
@StringRes final int type,
|
||||||
@NonNull final String content) {
|
@Nullable final String content) {
|
||||||
if (isBlank(content)) {
|
if (isBlank(content)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -221,12 +221,16 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||||||
urls.append(imageSizeToText(image.getWidth()));
|
urls.append(imageSizeToText(image.getWidth()));
|
||||||
} else {
|
} else {
|
||||||
switch (image.getEstimatedResolutionLevel()) {
|
switch (image.getEstimatedResolutionLevel()) {
|
||||||
case LOW -> urls.append(getString(R.string.image_quality_low));
|
case LOW:
|
||||||
case MEDIUM -> urls.append(getString(R.string.image_quality_medium));
|
urls.append(getString(R.string.image_quality_low));
|
||||||
case HIGH -> urls.append(getString(R.string.image_quality_high));
|
break;
|
||||||
default -> {
|
default: // unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
|
||||||
// unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
|
case MEDIUM:
|
||||||
}
|
urls.append(getString(R.string.image_quality_medium));
|
||||||
|
break;
|
||||||
|
case HIGH:
|
||||||
|
urls.append(getString(R.string.image_quality_high));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,7 +255,7 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||||||
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||||
final List<String> tags = getTags();
|
final List<String> tags = getTags();
|
||||||
|
|
||||||
if (!tags.isEmpty()) {
|
if (tags != null && !tags.isEmpty()) {
|
||||||
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
||||||
|
|
||||||
tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
|
tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
|
||||||
|
@ -7,7 +7,6 @@ import android.view.LayoutInflater;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
@ -24,43 +23,56 @@ import icepick.State;
|
|||||||
public class DescriptionFragment extends BaseDescriptionFragment {
|
public class DescriptionFragment extends BaseDescriptionFragment {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
StreamInfo streamInfo;
|
StreamInfo streamInfo = null;
|
||||||
|
|
||||||
|
public DescriptionFragment() {
|
||||||
|
}
|
||||||
|
|
||||||
public DescriptionFragment(final StreamInfo streamInfo) {
|
public DescriptionFragment(final StreamInfo streamInfo) {
|
||||||
this.streamInfo = streamInfo;
|
this.streamInfo = streamInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DescriptionFragment() {
|
|
||||||
// keep empty constructor for IcePick when resuming fragment from memory
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
protected Description getDescription() {
|
protected Description getDescription() {
|
||||||
|
if (streamInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return streamInfo.getDescription();
|
return streamInfo.getDescription();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
protected StreamingService getService() {
|
protected StreamingService getService() {
|
||||||
|
if (streamInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return streamInfo.getService();
|
return streamInfo.getService();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getServiceId() {
|
protected int getServiceId() {
|
||||||
|
if (streamInfo == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
return streamInfo.getServiceId();
|
return streamInfo.getServiceId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
protected String getStreamUrl() {
|
protected String getStreamUrl() {
|
||||||
|
if (streamInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return streamInfo.getUrl();
|
return streamInfo.getUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public List<String> getTags() {
|
public List<String> getTags() {
|
||||||
|
if (streamInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return streamInfo.getTags();
|
return streamInfo.getTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,8 +72,8 @@ import org.schabi.newpipe.error.ErrorUtil;
|
|||||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.Image;
|
import org.schabi.newpipe.extractor.Image;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
@ -106,17 +106,16 @@ import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
|||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.InfoCache;
|
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
@ -482,7 +481,7 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
// commit previous pending changes to database
|
// commit previous pending changes to database
|
||||||
if (fragment instanceof LocalPlaylistFragment) {
|
if (fragment instanceof LocalPlaylistFragment) {
|
||||||
((LocalPlaylistFragment) fragment).saveImmediate();
|
((LocalPlaylistFragment) fragment).commitChanges();
|
||||||
} else if (fragment instanceof MainFragment) {
|
} else if (fragment instanceof MainFragment) {
|
||||||
((MainFragment) fragment).commitPlaylistTabs();
|
((MainFragment) fragment).commitPlaylistTabs();
|
||||||
}
|
}
|
||||||
@ -1013,20 +1012,6 @@ public final class VideoDetailFragment
|
|||||||
updateTabLayoutVisibility();
|
updateTabLayoutVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void scrollToComment(final CommentsInfoItem comment) {
|
|
||||||
final int commentsTabPos = pageAdapter.getItemPositionByTitle(COMMENTS_TAB_TAG);
|
|
||||||
final Fragment fragment = pageAdapter.getItem(commentsTabPos);
|
|
||||||
if (!(fragment instanceof CommentsFragment)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// unexpand the app bar only if scrolling to the comment succeeded
|
|
||||||
if (((CommentsFragment) fragment).scrollToComment(comment)) {
|
|
||||||
binding.appBarLayout.setExpanded(false, false);
|
|
||||||
binding.viewPager.setCurrentItem(commentsTabPos, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Play Utils
|
// Play Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -1445,7 +1430,7 @@ public final class VideoDetailFragment
|
|||||||
super.showLoading();
|
super.showLoading();
|
||||||
|
|
||||||
//if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required
|
//if data is already cached, transition from VISIBLE -> INVISIBLE -> VISIBLE is not required
|
||||||
if (!ExtractorHelper.isCached(serviceId, url, InfoCache.Type.STREAM)) {
|
if (!ExtractorHelper.isCached(serviceId, url, InfoItem.InfoType.STREAM)) {
|
||||||
binding.detailContentRootHiding.setVisibility(View.INVISIBLE);
|
binding.detailContentRootHiding.setVisibility(View.INVISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,8 +231,6 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
|||||||
if (!result.getRelatedItems().isEmpty()) {
|
if (!result.getRelatedItems().isEmpty()) {
|
||||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||||
showListFooter(hasMoreItems());
|
showListFooter(hasMoreItems());
|
||||||
} else if (hasMoreItems()) {
|
|
||||||
loadMoreItems();
|
|
||||||
} else {
|
} else {
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
showEmptyState();
|
showEmptyState();
|
||||||
|
@ -2,12 +2,12 @@ package org.schabi.newpipe.fragments.list.channel;
|
|||||||
|
|
||||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT;
|
import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
@ -26,12 +26,14 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
|||||||
@State
|
@State
|
||||||
protected ChannelInfo channelInfo;
|
protected ChannelInfo channelInfo;
|
||||||
|
|
||||||
ChannelAboutFragment(@NonNull final ChannelInfo channelInfo) {
|
public static ChannelAboutFragment getInstance(final ChannelInfo channelInfo) {
|
||||||
this.channelInfo = channelInfo;
|
final ChannelAboutFragment fragment = new ChannelAboutFragment();
|
||||||
|
fragment.channelInfo = channelInfo;
|
||||||
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChannelAboutFragment() {
|
public ChannelAboutFragment() {
|
||||||
// keep empty constructor for IcePick when resuming fragment from memory
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -43,17 +45,26 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
|||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
protected Description getDescription() {
|
protected Description getDescription() {
|
||||||
|
if (channelInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT);
|
return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
protected StreamingService getService() {
|
protected StreamingService getService() {
|
||||||
|
if (channelInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return channelInfo.getService();
|
return channelInfo.getService();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getServiceId() {
|
protected int getServiceId() {
|
||||||
|
if (channelInfo == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
return channelInfo.getServiceId();
|
return channelInfo.getServiceId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,9 +74,12 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public List<String> getTags() {
|
public List<String> getTags() {
|
||||||
|
if (channelInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return channelInfo.getTags();
|
return channelInfo.getTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,11 +93,10 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Context context = getContext();
|
||||||
if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) {
|
if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) {
|
||||||
addMetadataItem(inflater, layout, false, R.string.metadata_subscribers,
|
addMetadataItem(inflater, layout, false, R.string.metadata_subscribers,
|
||||||
Localization.localizeNumber(
|
Localization.localizeNumber(context, channelInfo.getSubscriberCount()));
|
||||||
requireContext(),
|
|
||||||
channelInfo.getSubscriberCount()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addImagesMetadataItem(inflater, layout, R.string.metadata_avatars,
|
addImagesMetadataItem(inflater, layout, R.string.metadata_avatars,
|
||||||
|
@ -22,7 +22,6 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.graphics.ColorUtils;
|
import androidx.core.graphics.ColorUtils;
|
||||||
import androidx.core.view.MenuProvider;
|
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
@ -100,7 +99,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
private MenuItem menuRssButton;
|
private MenuItem menuRssButton;
|
||||||
private MenuItem menuNotifyButton;
|
private MenuItem menuNotifyButton;
|
||||||
private SubscriptionEntity channelSubscription;
|
private SubscriptionEntity channelSubscription;
|
||||||
private MenuProvider menuProvider;
|
|
||||||
|
|
||||||
public static ChannelFragment getInstance(final int serviceId, final String url,
|
public static ChannelFragment getInstance(final int serviceId, final String url,
|
||||||
final String name) {
|
final String name) {
|
||||||
@ -123,62 +121,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate(final Bundle savedInstanceState) {
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
menuProvider = new MenuProvider() {
|
setHasOptionsMenu(true);
|
||||||
@Override
|
|
||||||
public void onCreateMenu(@NonNull final Menu menu,
|
|
||||||
@NonNull final MenuInflater inflater) {
|
|
||||||
inflater.inflate(R.menu.menu_channel, menu);
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
|
||||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPrepareMenu(@NonNull final Menu menu) {
|
|
||||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
|
||||||
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
|
||||||
updateRssButton();
|
|
||||||
updateNotifyButton(channelSubscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.menu_item_notify:
|
|
||||||
final boolean value = !item.isChecked();
|
|
||||||
item.setEnabled(false);
|
|
||||||
setNotify(value);
|
|
||||||
break;
|
|
||||||
case R.id.action_settings:
|
|
||||||
NavigationHelper.openSettings(requireContext());
|
|
||||||
break;
|
|
||||||
case R.id.menu_item_rss:
|
|
||||||
if (currentInfo != null) {
|
|
||||||
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case R.id.menu_item_openInBrowser:
|
|
||||||
if (currentInfo != null) {
|
|
||||||
ShareUtils.openUrlInBrowser(requireContext(),
|
|
||||||
currentInfo.getOriginalUrl());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case R.id.menu_item_share:
|
|
||||||
if (currentInfo != null) {
|
|
||||||
ShareUtils.shareText(requireContext(), name,
|
|
||||||
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
activity.addMenuProvider(menuProvider);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -240,10 +183,67 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
}
|
}
|
||||||
disposables.clear();
|
disposables.clear();
|
||||||
binding = null;
|
binding = null;
|
||||||
activity.removeMenuProvider(menuProvider);
|
|
||||||
menuProvider = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Menu
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||||
|
@NonNull final MenuInflater inflater) {
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
inflater.inflate(R.menu.menu_channel, menu);
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||||
|
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrepareOptionsMenu(@NonNull final Menu menu) {
|
||||||
|
super.onPrepareOptionsMenu(menu);
|
||||||
|
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||||
|
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
||||||
|
updateNotifyButton(channelSubscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case R.id.menu_item_notify:
|
||||||
|
final boolean value = !item.isChecked();
|
||||||
|
item.setEnabled(false);
|
||||||
|
setNotify(value);
|
||||||
|
break;
|
||||||
|
case R.id.action_settings:
|
||||||
|
NavigationHelper.openSettings(requireContext());
|
||||||
|
break;
|
||||||
|
case R.id.menu_item_rss:
|
||||||
|
if (currentInfo != null) {
|
||||||
|
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case R.id.menu_item_openInBrowser:
|
||||||
|
if (currentInfo != null) {
|
||||||
|
ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case R.id.menu_item_share:
|
||||||
|
if (currentInfo != null) {
|
||||||
|
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
|
||||||
|
currentInfo.getAvatars());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Channel Subscription
|
// Channel Subscription
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -408,13 +408,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
animate(binding.channelSubscribeButton, true, 100, AnimationType.LIGHT_SCALE_AND_ALPHA);
|
animate(binding.channelSubscribeButton, true, 100, AnimationType.LIGHT_SCALE_AND_ALPHA);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateRssButton() {
|
|
||||||
if (menuRssButton == null || currentInfo == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
|
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
|
||||||
if (menuNotifyButton == null) {
|
if (menuNotifyButton == null) {
|
||||||
return;
|
return;
|
||||||
@ -481,7 +474,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
if (ChannelTabHelper.showChannelTab(
|
if (ChannelTabHelper.showChannelTab(
|
||||||
context, preferences, R.string.show_channel_tabs_about)) {
|
context, preferences, R.string.show_channel_tabs_about)) {
|
||||||
tabAdapter.addFragment(
|
tabAdapter.addFragment(
|
||||||
new ChannelAboutFragment(currentInfo),
|
ChannelAboutFragment.getInstance(currentInfo),
|
||||||
context.getString(R.string.channel_tab_about));
|
context.getString(R.string.channel_tab_about));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -617,7 +610,9 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
binding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
binding.subChannelAvatarView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRssButton();
|
if (menuRssButton != null) {
|
||||||
|
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
||||||
|
}
|
||||||
|
|
||||||
channelContentNotSupported = false;
|
channelContentNotSupported = false;
|
||||||
for (final Throwable throwable : result.getErrors()) {
|
for (final Throwable throwable : result.getErrors()) {
|
||||||
|
@ -1,170 +0,0 @@
|
|||||||
package org.schabi.newpipe.fragments.list.comments;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
|
||||||
import androidx.core.text.HtmlCompat;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.databinding.CommentRepliesHeaderBinding;
|
|
||||||
import org.schabi.newpipe.error.UserAction;
|
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
|
||||||
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
|
|
||||||
public final class CommentRepliesFragment
|
|
||||||
extends BaseListInfoFragment<CommentsInfoItem, CommentRepliesInfo> {
|
|
||||||
|
|
||||||
public static final String TAG = CommentRepliesFragment.class.getSimpleName();
|
|
||||||
|
|
||||||
@State
|
|
||||||
CommentsInfoItem commentsInfoItem; // the comment to show replies of
|
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Constructors and lifecycle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
// only called by the Android framework, after which readFrom is called and restores all data
|
|
||||||
public CommentRepliesFragment() {
|
|
||||||
super(UserAction.REQUESTED_COMMENT_REPLIES);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommentRepliesFragment(@NonNull final CommentsInfoItem commentsInfoItem) {
|
|
||||||
this();
|
|
||||||
this.commentsInfoItem = commentsInfoItem;
|
|
||||||
// setting "" as title since the title will be properly set right after
|
|
||||||
setInitialData(commentsInfoItem.getServiceId(), commentsInfoItem.getUrl(), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
|
||||||
@Nullable final ViewGroup container,
|
|
||||||
@Nullable final Bundle savedInstanceState) {
|
|
||||||
return inflater.inflate(R.layout.fragment_comments, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
disposables.clear();
|
|
||||||
super.onDestroyView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Supplier<View> getListHeaderSupplier() {
|
|
||||||
return () -> {
|
|
||||||
final CommentRepliesHeaderBinding binding = CommentRepliesHeaderBinding
|
|
||||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
|
||||||
final CommentsInfoItem item = commentsInfoItem;
|
|
||||||
|
|
||||||
// load the author avatar
|
|
||||||
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar);
|
|
||||||
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
|
|
||||||
? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
// setup author name and comment date
|
|
||||||
binding.authorName.setText(item.getUploaderName());
|
|
||||||
binding.uploadDate.setText(Localization.relativeTimeOrTextual(
|
|
||||||
getContext(), item.getUploadDate(), item.getTextualUploadDate()));
|
|
||||||
binding.authorTouchArea.setOnClickListener(
|
|
||||||
v -> NavigationHelper.openCommentAuthorIfPresent(requireActivity(), item));
|
|
||||||
|
|
||||||
// setup like count, hearted and pinned
|
|
||||||
binding.thumbsUpCount.setText(
|
|
||||||
Localization.likeCount(requireContext(), item.getLikeCount()));
|
|
||||||
// for heartImage goneMarginEnd was used, but there is no way to tell ConstraintLayout
|
|
||||||
// not to use a different margin only when both the next two views are gone
|
|
||||||
((ConstraintLayout.LayoutParams) binding.thumbsUpCount.getLayoutParams())
|
|
||||||
.setMarginEnd(DeviceUtils.dpToPx(
|
|
||||||
(item.isHeartedByUploader() || item.isPinned() ? 8 : 16),
|
|
||||||
requireContext()));
|
|
||||||
binding.heartImage.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
|
||||||
binding.pinnedImage.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
// setup comment content
|
|
||||||
TextLinkifier.fromDescription(binding.commentContent, item.getCommentText(),
|
|
||||||
HtmlCompat.FROM_HTML_MODE_LEGACY, getServiceById(item.getServiceId()),
|
|
||||||
item.getUrl(), disposables, null);
|
|
||||||
|
|
||||||
return binding.getRoot();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// State saving
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeTo(final Queue<Object> objectsToSave) {
|
|
||||||
super.writeTo(objectsToSave);
|
|
||||||
objectsToSave.add(commentsInfoItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
|
||||||
super.readFrom(savedObjects);
|
|
||||||
commentsInfoItem = (CommentsInfoItem) savedObjects.poll();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Data loading
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<CommentRepliesInfo> loadResult(final boolean forceLoad) {
|
|
||||||
return Single.fromCallable(() -> new CommentRepliesInfo(commentsInfoItem,
|
|
||||||
// the reply count string will be shown as the activity title
|
|
||||||
Localization.replyCount(requireContext(), commentsInfoItem.getReplyCount())));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
|
|
||||||
// commentsInfoItem.getUrl() should contain the url of the original
|
|
||||||
// ListInfo<CommentsInfoItem>, which should be the stream url
|
|
||||||
return ExtractorHelper.getMoreCommentItems(
|
|
||||||
serviceId, commentsInfoItem.getUrl(), currentNextPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Utils
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ItemViewMode getItemViewMode() {
|
|
||||||
return ItemViewMode.LIST;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the comment to which the replies are shown
|
|
||||||
*/
|
|
||||||
public CommentsInfoItem getCommentsInfoItem() {
|
|
||||||
return commentsInfoItem;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
package org.schabi.newpipe.fragments.list.comments;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
public final class CommentRepliesInfo extends ListInfo<CommentsInfoItem> {
|
|
||||||
/**
|
|
||||||
* This class is used to wrap the comment replies page into a ListInfo object.
|
|
||||||
*
|
|
||||||
* @param comment the comment from which to get replies
|
|
||||||
* @param name will be shown as the fragment title
|
|
||||||
*/
|
|
||||||
public CommentRepliesInfo(final CommentsInfoItem comment, final String name) {
|
|
||||||
super(comment.getServiceId(),
|
|
||||||
new ListLinkHandler("", "", "", Collections.emptyList(), null), name);
|
|
||||||
setNextPage(comment.getReplies());
|
|
||||||
setRelatedItems(Collections.emptyList()); // since it must be non-null
|
|
||||||
}
|
|
||||||
}
|
|
@ -110,14 +110,4 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, Com
|
|||||||
protected ItemViewMode getItemViewMode() {
|
protected ItemViewMode getItemViewMode() {
|
||||||
return ItemViewMode.LIST;
|
return ItemViewMode.LIST;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean scrollToComment(final CommentsInfoItem comment) {
|
|
||||||
final int position = infoListAdapter.getItemsList().indexOf(comment);
|
|
||||||
if (position < 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsList.scrollToPosition(position);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
package org.schabi.newpipe.fragments.list.playlist;
|
package org.schabi.newpipe.fragments.list.playlist;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||||
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@ -39,7 +37,6 @@ import org.schabi.newpipe.extractor.ListExtractor;
|
|||||||
import org.schabi.newpipe.extractor.ServiceList;
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
@ -51,10 +48,9 @@ import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
|||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.text.TextEllipsizer;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -89,9 +85,6 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
|
|
||||||
private MenuItem playlistBookmarkButton;
|
private MenuItem playlistBookmarkButton;
|
||||||
|
|
||||||
private long streamCount;
|
|
||||||
private long playlistOverallDurationSeconds;
|
|
||||||
|
|
||||||
public static PlaylistFragment getInstance(final int serviceId, final String url,
|
public static PlaylistFragment getInstance(final int serviceId, final String url,
|
||||||
final String name) {
|
final String name) {
|
||||||
final PlaylistFragment instance = new PlaylistFragment();
|
final PlaylistFragment instance = new PlaylistFragment();
|
||||||
@ -280,12 +273,6 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
animate(headerBinding.uploaderLayout, false, 200);
|
animate(headerBinding.uploaderLayout, false, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
|
||||||
super.handleNextItems(result);
|
|
||||||
setStreamCountAndOverallDuration(result.getItems(), !result.hasNextPage());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull final PlaylistInfo result) {
|
public void handleResult(@NonNull final PlaylistInfo result) {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
@ -331,32 +318,8 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
.into(headerBinding.uploaderAvatarView);
|
.into(headerBinding.uploaderAvatarView);
|
||||||
}
|
}
|
||||||
|
|
||||||
streamCount = result.getStreamCount();
|
headerBinding.playlistStreamCount.setText(Localization
|
||||||
setStreamCountAndOverallDuration(result.getRelatedItems(), !result.hasNextPage());
|
.localizeStreamCount(getContext(), result.getStreamCount()));
|
||||||
|
|
||||||
final Description description = result.getDescription();
|
|
||||||
if (description != null && description != Description.EMPTY_DESCRIPTION
|
|
||||||
&& !isBlank(description.getContent())) {
|
|
||||||
final TextEllipsizer ellipsizer = new TextEllipsizer(
|
|
||||||
headerBinding.playlistDescription, 5, getServiceById(result.getServiceId()));
|
|
||||||
ellipsizer.setStateChangeListener(isEllipsized ->
|
|
||||||
headerBinding.playlistDescriptionReadMore.setText(
|
|
||||||
Boolean.TRUE.equals(isEllipsized) ? R.string.show_more : R.string.show_less
|
|
||||||
));
|
|
||||||
ellipsizer.setOnContentChanged(canBeEllipsized -> {
|
|
||||||
headerBinding.playlistDescriptionReadMore.setVisibility(
|
|
||||||
Boolean.TRUE.equals(canBeEllipsized) ? View.VISIBLE : View.GONE);
|
|
||||||
if (Boolean.TRUE.equals(canBeEllipsized)) {
|
|
||||||
ellipsizer.ellipsize();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ellipsizer.setContent(description);
|
|
||||||
headerBinding.playlistDescriptionReadMore.setOnClickListener(v -> ellipsizer.toggle());
|
|
||||||
headerBinding.playlistDescription.setOnClickListener(v -> ellipsizer.toggle());
|
|
||||||
} else {
|
|
||||||
headerBinding.playlistDescription.setVisibility(View.GONE);
|
|
||||||
headerBinding.playlistDescriptionReadMore.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
|
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
|
||||||
@ -496,20 +459,4 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
playlistBookmarkButton.setIcon(drawable);
|
playlistBookmarkButton.setIcon(drawable);
|
||||||
playlistBookmarkButton.setTitle(titleRes);
|
playlistBookmarkButton.setTitle(titleRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setStreamCountAndOverallDuration(final List<StreamInfoItem> list,
|
|
||||||
final boolean isDurationComplete) {
|
|
||||||
if (activity != null && headerBinding != null) {
|
|
||||||
playlistOverallDurationSeconds += list.stream()
|
|
||||||
.mapToLong(x -> x.getDuration())
|
|
||||||
.sum();
|
|
||||||
headerBinding.playlistStreamCount.setText(
|
|
||||||
Localization.concatenateStrings(
|
|
||||||
Localization.localizeStreamCount(activity, streamCount),
|
|
||||||
Localization.getDurationString(playlistOverallDurationSeconds,
|
|
||||||
isDurationComplete, true))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package org.schabi.newpipe.fragments.list.search;
|
package org.schabi.newpipe.fragments.list.search;
|
||||||
|
|
||||||
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
@ -390,7 +389,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull final Bundle bundle) {
|
public void onSaveInstanceState(@NonNull final Bundle bundle) {
|
||||||
searchString = searchEditText != null
|
searchString = searchEditText != null
|
||||||
? getSearchEditString().trim()
|
? searchEditText.getText().toString()
|
||||||
: searchString;
|
: searchString;
|
||||||
super.onSaveInstanceState(bundle);
|
super.onSaveInstanceState(bundle);
|
||||||
}
|
}
|
||||||
@ -401,11 +400,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reloadContent() {
|
public void reloadContent() {
|
||||||
if (!TextUtils.isEmpty(searchString) || (searchEditText != null
|
if (!TextUtils.isEmpty(searchString)
|
||||||
&& !isSearchEditBlank())) {
|
|| (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
||||||
search(!TextUtils.isEmpty(searchString)
|
search(!TextUtils.isEmpty(searchString)
|
||||||
? searchString
|
? searchString
|
||||||
: getSearchEditString(), this.contentFilter, "");
|
: searchEditText.getText().toString(), this.contentFilter, "");
|
||||||
} else {
|
} else {
|
||||||
if (searchEditText != null) {
|
if (searchEditText != null) {
|
||||||
searchEditText.setText("");
|
searchEditText.setText("");
|
||||||
@ -499,8 +498,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
}
|
}
|
||||||
searchEditText.setText(searchString);
|
searchEditText.setText(searchString);
|
||||||
|
|
||||||
if (TextUtils.isEmpty(searchString)
|
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
|
||||||
|| isSearchEditBlank()) {
|
|
||||||
searchToolbarContainer.setTranslationX(100);
|
searchToolbarContainer.setTranslationX(100);
|
||||||
searchToolbarContainer.setAlpha(0.0f);
|
searchToolbarContainer.setAlpha(0.0f);
|
||||||
searchToolbarContainer.setVisibility(View.VISIBLE);
|
searchToolbarContainer.setVisibility(View.VISIBLE);
|
||||||
@ -524,7 +522,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||||
}
|
}
|
||||||
if (isSearchEditBlank()) {
|
if (TextUtils.isEmpty(searchEditText.getText())) {
|
||||||
NavigationHelper.gotoMainFragment(getFM());
|
NavigationHelper.gotoMainFragment(getFM());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -605,7 +603,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
s.removeSpan(span);
|
s.removeSpan(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String newText = getSearchEditString().trim();
|
final String newText = searchEditText.getText().toString();
|
||||||
suggestionPublisher.onNext(newText);
|
suggestionPublisher.onNext(newText);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -621,8 +619,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
} else if (event != null
|
} else if (event != null
|
||||||
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
||||||
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||||
searchEditText.setText(getSearchEditString().trim());
|
search(searchEditText.getText().toString(), new String[0], "");
|
||||||
search(getSearchEditString(), new String[0], "");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -697,7 +694,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
howManyDeleted -> suggestionPublisher
|
howManyDeleted -> suggestionPublisher
|
||||||
.onNext(getSearchEditString()),
|
.onNext(searchEditText.getText().toString()),
|
||||||
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
||||||
UserAction.DELETE_FROM_HISTORY,
|
UserAction.DELETE_FROM_HISTORY,
|
||||||
"Deleting item failed")));
|
"Deleting item failed")));
|
||||||
@ -808,13 +805,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void search(final String theSearchString,
|
||||||
* Perform a search.
|
|
||||||
* @param theSearchString the trimmed search string
|
|
||||||
* @param theContentFilter the content filter to use. FIXME: unused param
|
|
||||||
* @param theSortFilter FIXME: unused param
|
|
||||||
*/
|
|
||||||
private void search(@NonNull final String theSearchString,
|
|
||||||
final String[] theContentFilter,
|
final String[] theContentFilter,
|
||||||
final String theSortFilter) {
|
final String theSortFilter) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
@ -824,10 +815,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if theSearchString is a URL which can be opened by NewPipe directly
|
|
||||||
// and open it if possible.
|
|
||||||
try {
|
try {
|
||||||
final StreamingService streamingService = NewPipe.getServiceByUrl(theSearchString);
|
final StreamingService streamingService = NewPipe.getServiceByUrl(theSearchString);
|
||||||
|
if (streamingService != null) {
|
||||||
showLoading();
|
showLoading();
|
||||||
disposables.add(Observable
|
disposables.add(Observable
|
||||||
.fromCallable(() -> NavigationHelper.getIntentByLink(activity,
|
.fromCallable(() -> NavigationHelper.getIntentByLink(activity,
|
||||||
@ -839,11 +829,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
activity.startActivity(intent);
|
activity.startActivity(intent);
|
||||||
}, throwable -> showTextError(getString(R.string.unsupported_url))));
|
}, throwable -> showTextError(getString(R.string.unsupported_url))));
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
} catch (final Exception ignored) {
|
} catch (final Exception ignored) {
|
||||||
// Exception occurred, it's not a url
|
// Exception occurred, it's not a url
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare search
|
|
||||||
lastSearchedString = this.searchString;
|
lastSearchedString = this.searchString;
|
||||||
this.searchString = theSearchString;
|
this.searchString = theSearchString;
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
@ -852,17 +842,13 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
searchBinding.searchMetaInfoSeparator, disposables);
|
searchBinding.searchMetaInfoSeparator, disposables);
|
||||||
hideKeyboardSearch();
|
hideKeyboardSearch();
|
||||||
|
|
||||||
// store search query if search history is enabled
|
|
||||||
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
|
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
ignored -> {
|
ignored -> { },
|
||||||
},
|
|
||||||
throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED,
|
throwable -> showSnackBarError(new ErrorInfo(throwable, UserAction.SEARCHED,
|
||||||
theSearchString, serviceId))
|
theSearchString, serviceId))
|
||||||
));
|
));
|
||||||
|
|
||||||
// load search results
|
|
||||||
suggestionPublisher.onNext(theSearchString);
|
suggestionPublisher.onNext(theSearchString);
|
||||||
startLoading(false);
|
startLoading(false);
|
||||||
}
|
}
|
||||||
@ -952,14 +938,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
sortFilter = theSortFilter;
|
sortFilter = theSortFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getSearchEditString() {
|
|
||||||
return searchEditText.getText().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isSearchEditBlank() {
|
|
||||||
return isBlank(getSearchEditString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Suggestion Results
|
// Suggestion Results
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -1001,9 +979,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchSuggestion = result.getSearchSuggestion();
|
searchSuggestion = result.getSearchSuggestion();
|
||||||
if (searchSuggestion != null) {
|
|
||||||
searchSuggestion = searchSuggestion.trim();
|
|
||||||
}
|
|
||||||
isCorrectedSearch = result.isCorrectedSearch();
|
isCorrectedSearch = result.isCorrectedSearch();
|
||||||
|
|
||||||
// List<MetaInfo> cannot be bundled without creating some containers
|
// List<MetaInfo> cannot be bundled without creating some containers
|
||||||
@ -1105,7 +1080,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
howManyDeleted -> suggestionPublisher
|
howManyDeleted -> suggestionPublisher
|
||||||
.onNext(getSearchEditString()),
|
.onNext(searchEditText.getText().toString()),
|
||||||
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
throwable -> showSnackBarError(new ErrorInfo(throwable,
|
||||||
UserAction.DELETE_FROM_HISTORY, "Deleting item failed")));
|
UserAction.DELETE_FROM_HISTORY, "Deleting item failed")));
|
||||||
disposables.add(onDelete);
|
disposables.add(onDelete);
|
||||||
|
@ -21,17 +21,18 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
|
import org.schabi.newpipe.util.RelatedItemInfo;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
|
||||||
public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemsInfo>
|
public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, RelatedItemInfo>
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
private static final String INFO_KEY = "related_info_key";
|
private static final String INFO_KEY = "related_info_key";
|
||||||
|
|
||||||
private RelatedItemsInfo relatedItemsInfo;
|
private RelatedItemInfo relatedItemInfo;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
@ -68,7 +69,7 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<View> getListHeaderSupplier() {
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
if (relatedItemsInfo == null || relatedItemsInfo.getRelatedItems() == null) {
|
if (relatedItemInfo == null || relatedItemInfo.getRelatedItems() == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,8 +97,8 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<RelatedItemsInfo> loadResult(final boolean forceLoad) {
|
protected Single<RelatedItemInfo> loadResult(final boolean forceLoad) {
|
||||||
return Single.fromCallable(() -> relatedItemsInfo);
|
return Single.fromCallable(() -> relatedItemInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -109,7 +110,7 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull final RelatedItemsInfo result) {
|
public void handleResult(@NonNull final RelatedItemInfo result) {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
|
||||||
if (headerBinding != null) {
|
if (headerBinding != null) {
|
||||||
@ -136,23 +137,23 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
|
|||||||
|
|
||||||
private void setInitialData(final StreamInfo info) {
|
private void setInitialData(final StreamInfo info) {
|
||||||
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||||
if (this.relatedItemsInfo == null) {
|
if (this.relatedItemInfo == null) {
|
||||||
this.relatedItemsInfo = new RelatedItemsInfo(info);
|
this.relatedItemInfo = RelatedItemInfo.getInfo(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putSerializable(INFO_KEY, relatedItemsInfo);
|
outState.putSerializable(INFO_KEY, relatedItemInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
|
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
|
||||||
super.onRestoreInstanceState(savedState);
|
super.onRestoreInstanceState(savedState);
|
||||||
final Serializable serializable = savedState.getSerializable(INFO_KEY);
|
final Serializable serializable = savedState.getSerializable(INFO_KEY);
|
||||||
if (serializable instanceof RelatedItemsInfo) {
|
if (serializable instanceof RelatedItemInfo) {
|
||||||
this.relatedItemsInfo = (RelatedItemsInfo) serializable;
|
this.relatedItemInfo = (RelatedItemInfo) serializable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
package org.schabi.newpipe.fragments.list.videos;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
public final class RelatedItemsInfo extends ListInfo<InfoItem> {
|
|
||||||
/**
|
|
||||||
* This class is used to wrap the related items of a StreamInfo into a ListInfo object.
|
|
||||||
*
|
|
||||||
* @param info the stream info from which to get related items
|
|
||||||
*/
|
|
||||||
public RelatedItemsInfo(final StreamInfo info) {
|
|
||||||
super(info.getServiceId(), new ListLinkHandler(info.getOriginalUrl(), info.getUrl(),
|
|
||||||
info.getId(), Collections.emptyList(), null), info.getName());
|
|
||||||
setRelatedItems(new ArrayList<>(info.getRelatedItems()));
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,7 +13,8 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
||||||
|
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||||
@ -86,7 +87,8 @@ public class InfoItemBuilder {
|
|||||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
||||||
: new PlaylistInfoItemHolder(this, parent);
|
: new PlaylistInfoItemHolder(this, parent);
|
||||||
case COMMENT:
|
case COMMENT:
|
||||||
return new CommentInfoItemHolder(this, parent);
|
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent)
|
||||||
|
: new CommentsInfoItemHolder(this, parent);
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,8 @@ import org.schabi.newpipe.info_list.holder.ChannelCardInfoItemHolder;
|
|||||||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
||||||
|
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
||||||
@ -78,7 +79,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||||||
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
||||||
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
|
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
|
||||||
private static final int CARD_PLAYLIST_HOLDER_TYPE = 0x303;
|
private static final int CARD_PLAYLIST_HOLDER_TYPE = 0x303;
|
||||||
private static final int COMMENT_HOLDER_TYPE = 0x400;
|
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
|
||||||
|
private static final int COMMENT_HOLDER_TYPE = 0x401;
|
||||||
|
|
||||||
private final LayoutInflater layoutInflater;
|
private final LayoutInflater layoutInflater;
|
||||||
private final InfoItemBuilder infoItemBuilder;
|
private final InfoItemBuilder infoItemBuilder;
|
||||||
@ -269,7 +271,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||||||
return PLAYLIST_HOLDER_TYPE;
|
return PLAYLIST_HOLDER_TYPE;
|
||||||
}
|
}
|
||||||
case COMMENT:
|
case COMMENT:
|
||||||
return COMMENT_HOLDER_TYPE;
|
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
|
||||||
default:
|
default:
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@ -318,8 +320,10 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||||||
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
||||||
case CARD_PLAYLIST_HOLDER_TYPE:
|
case CARD_PLAYLIST_HOLDER_TYPE:
|
||||||
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
||||||
|
case MINI_COMMENT_HOLDER_TYPE:
|
||||||
|
return new CommentsMiniInfoItemHolder(infoItemBuilder, parent);
|
||||||
case COMMENT_HOLDER_TYPE:
|
case COMMENT_HOLDER_TYPE:
|
||||||
return new CommentInfoItemHolder(infoItemBuilder, parent);
|
return new CommentsInfoItemHolder(infoItemBuilder, parent);
|
||||||
default:
|
default:
|
||||||
return new FallbackViewHolder(new View(parent.getContext()));
|
return new FallbackViewHolder(new View(parent.getContext()));
|
||||||
}
|
}
|
||||||
|
@ -1,210 +0,0 @@
|
|||||||
package org.schabi.newpipe.info_list.holder;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
|
||||||
import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine;
|
|
||||||
|
|
||||||
import android.text.Spanned;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.text.style.ClickableSpan;
|
|
||||||
import android.text.style.URLSpan;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.text.TextEllipsizer;
|
|
||||||
|
|
||||||
public class CommentInfoItemHolder extends InfoItemHolder {
|
|
||||||
|
|
||||||
private static final int COMMENT_DEFAULT_LINES = 2;
|
|
||||||
private final int commentHorizontalPadding;
|
|
||||||
private final int commentVerticalPadding;
|
|
||||||
|
|
||||||
private final RelativeLayout itemRoot;
|
|
||||||
private final ImageView itemThumbnailView;
|
|
||||||
private final TextView itemContentView;
|
|
||||||
private final ImageView itemThumbsUpView;
|
|
||||||
private final TextView itemLikesCountView;
|
|
||||||
private final TextView itemTitleView;
|
|
||||||
private final ImageView itemHeartView;
|
|
||||||
private final ImageView itemPinnedView;
|
|
||||||
private final Button repliesButton;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final TextEllipsizer textEllipsizer;
|
|
||||||
|
|
||||||
public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
|
||||||
final ViewGroup parent) {
|
|
||||||
super(infoItemBuilder, R.layout.list_comment_item, parent);
|
|
||||||
|
|
||||||
itemRoot = itemView.findViewById(R.id.itemRoot);
|
|
||||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
|
||||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
|
||||||
itemThumbsUpView = itemView.findViewById(R.id.detail_thumbs_up_img_view);
|
|
||||||
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
|
||||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
|
||||||
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
|
|
||||||
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
|
|
||||||
repliesButton = itemView.findViewById(R.id.replies_button);
|
|
||||||
|
|
||||||
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
|
||||||
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
|
||||||
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
|
||||||
.getResources().getDimension(R.dimen.comments_vertical_padding);
|
|
||||||
|
|
||||||
textEllipsizer = new TextEllipsizer(itemContentView, COMMENT_DEFAULT_LINES, null);
|
|
||||||
textEllipsizer.setStateChangeListener(isEllipsized -> {
|
|
||||||
if (Boolean.TRUE.equals(isEllipsized)) {
|
|
||||||
denyLinkFocus();
|
|
||||||
} else {
|
|
||||||
determineMovementMethod();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateFromItem(final InfoItem infoItem,
|
|
||||||
final HistoryRecordManager historyRecordManager) {
|
|
||||||
if (!(infoItem instanceof CommentsInfoItem)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
|
||||||
|
|
||||||
|
|
||||||
// load the author avatar
|
|
||||||
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
|
|
||||||
if (ImageStrategy.shouldLoadImages()) {
|
|
||||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
|
||||||
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
|
||||||
commentVerticalPadding, commentVerticalPadding);
|
|
||||||
} else {
|
|
||||||
itemThumbnailView.setVisibility(View.GONE);
|
|
||||||
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
|
|
||||||
commentHorizontalPadding, commentVerticalPadding);
|
|
||||||
}
|
|
||||||
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
|
|
||||||
|
|
||||||
|
|
||||||
// setup the top row, with pinned icon, author name and comment date
|
|
||||||
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
|
||||||
itemTitleView.setText(Localization.concatenateStrings(item.getUploaderName(),
|
|
||||||
Localization.relativeTimeOrTextual(itemBuilder.getContext(), item.getUploadDate(),
|
|
||||||
item.getTextualUploadDate())));
|
|
||||||
|
|
||||||
|
|
||||||
// setup bottom row, with likes, heart and replies button
|
|
||||||
itemLikesCountView.setText(
|
|
||||||
Localization.likeCount(itemBuilder.getContext(), item.getLikeCount()));
|
|
||||||
|
|
||||||
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
final boolean hasReplies = item.getReplies() != null;
|
|
||||||
repliesButton.setOnClickListener(hasReplies ? v -> openCommentReplies(item) : null);
|
|
||||||
repliesButton.setVisibility(hasReplies ? View.VISIBLE : View.GONE);
|
|
||||||
repliesButton.setText(hasReplies
|
|
||||||
? Localization.replyCount(itemBuilder.getContext(), item.getReplyCount()) : "");
|
|
||||||
((RelativeLayout.LayoutParams) itemThumbsUpView.getLayoutParams()).topMargin =
|
|
||||||
hasReplies ? 0 : DeviceUtils.dpToPx(6, itemBuilder.getContext());
|
|
||||||
|
|
||||||
|
|
||||||
// setup comment content and click listeners to expand/ellipsize it
|
|
||||||
textEllipsizer.setStreamingService(getServiceById(item.getServiceId()));
|
|
||||||
textEllipsizer.setStreamUrl(item.getUrl());
|
|
||||||
textEllipsizer.setContent(item.getCommentText());
|
|
||||||
textEllipsizer.ellipsize();
|
|
||||||
|
|
||||||
//noinspection ClickableViewAccessibility
|
|
||||||
itemContentView.setOnTouchListener((v, event) -> {
|
|
||||||
final CharSequence text = itemContentView.getText();
|
|
||||||
if (text instanceof Spanned buffer) {
|
|
||||||
final int action = event.getAction();
|
|
||||||
|
|
||||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
|
|
||||||
final int offset = getOffsetForHorizontalLine(itemContentView, event);
|
|
||||||
final var links = buffer.getSpans(offset, offset, ClickableSpan.class);
|
|
||||||
|
|
||||||
if (links.length != 0) {
|
|
||||||
if (action == MotionEvent.ACTION_UP) {
|
|
||||||
links[0].onClick(itemContentView);
|
|
||||||
}
|
|
||||||
// we handle events that intersect links, so return true
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
|
||||||
textEllipsizer.toggle();
|
|
||||||
if (itemBuilder.getOnCommentsSelectedListener() != null) {
|
|
||||||
itemBuilder.getOnCommentsSelectedListener().selected(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
itemView.setOnLongClickListener(view -> {
|
|
||||||
if (DeviceUtils.isTv(itemBuilder.getContext())) {
|
|
||||||
openCommentAuthor(item);
|
|
||||||
} else {
|
|
||||||
final CharSequence text = itemContentView.getText();
|
|
||||||
if (text != null) {
|
|
||||||
ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openCommentAuthor(@NonNull final CommentsInfoItem item) {
|
|
||||||
NavigationHelper.openCommentAuthorIfPresent((FragmentActivity) itemBuilder.getContext(),
|
|
||||||
item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openCommentReplies(@NonNull final CommentsInfoItem item) {
|
|
||||||
NavigationHelper.openCommentRepliesFragment((FragmentActivity) itemBuilder.getContext(),
|
|
||||||
item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void allowLinkFocus() {
|
|
||||||
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void denyLinkFocus() {
|
|
||||||
itemContentView.setMovementMethod(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldFocusLinks() {
|
|
||||||
if (itemView.isInTouchMode()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final URLSpan[] urls = itemContentView.getUrls();
|
|
||||||
|
|
||||||
return urls != null && urls.length != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void determineMovementMethod() {
|
|
||||||
if (shouldFocusLinks()) {
|
|
||||||
allowLinkFocus();
|
|
||||||
} else {
|
|
||||||
denyLinkFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,63 @@
|
|||||||
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Created by Christian Schabesberger on 12.02.17.
|
||||||
|
*
|
||||||
|
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||||
|
* ChannelInfoItemHolder .java is part of NewPipe.
|
||||||
|
*
|
||||||
|
* NewPipe is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* NewPipe is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
||||||
|
public final TextView itemTitleView;
|
||||||
|
private final ImageView itemHeartView;
|
||||||
|
private final ImageView itemPinnedView;
|
||||||
|
|
||||||
|
public CommentsInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, R.layout.list_comments_item, parent);
|
||||||
|
|
||||||
|
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
||||||
|
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
|
||||||
|
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final InfoItem infoItem,
|
||||||
|
final HistoryRecordManager historyRecordManager) {
|
||||||
|
super.updateFromItem(infoItem, historyRecordManager);
|
||||||
|
|
||||||
|
if (!(infoItem instanceof CommentsInfoItem)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||||
|
|
||||||
|
itemTitleView.setText(item.getUploaderName());
|
||||||
|
|
||||||
|
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,280 @@
|
|||||||
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import static android.text.TextUtils.isEmpty;
|
||||||
|
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
|
import android.text.style.URLSpan;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.text.HtmlCompat;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
|
||||||
|
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
|
||||||
|
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
|
private static final String TAG = "CommentsMiniIIHolder";
|
||||||
|
private static final String ELLIPSIS = "…";
|
||||||
|
|
||||||
|
private static final int COMMENT_DEFAULT_LINES = 2;
|
||||||
|
private static final int COMMENT_EXPANDED_LINES = 1000;
|
||||||
|
|
||||||
|
private final int commentHorizontalPadding;
|
||||||
|
private final int commentVerticalPadding;
|
||||||
|
|
||||||
|
private final Paint paintAtContentSize;
|
||||||
|
private final float ellipsisWidthPx;
|
||||||
|
|
||||||
|
private final RelativeLayout itemRoot;
|
||||||
|
private final ImageView itemThumbnailView;
|
||||||
|
private final TextView itemContentView;
|
||||||
|
private final TextView itemLikesCountView;
|
||||||
|
private final TextView itemPublishedTime;
|
||||||
|
|
||||||
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
@Nullable private Description commentText;
|
||||||
|
@Nullable private StreamingService streamService;
|
||||||
|
@Nullable private String streamUrl;
|
||||||
|
|
||||||
|
CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
|
||||||
|
final ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, layoutId, parent);
|
||||||
|
|
||||||
|
itemRoot = itemView.findViewById(R.id.itemRoot);
|
||||||
|
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||||
|
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
||||||
|
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
|
||||||
|
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
||||||
|
|
||||||
|
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
||||||
|
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
||||||
|
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
||||||
|
.getResources().getDimension(R.dimen.comments_vertical_padding);
|
||||||
|
|
||||||
|
paintAtContentSize = new Paint();
|
||||||
|
paintAtContentSize.setTextSize(itemContentView.getTextSize());
|
||||||
|
ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
||||||
|
final ViewGroup parent) {
|
||||||
|
this(infoItemBuilder, R.layout.list_comments_mini_item, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final InfoItem infoItem,
|
||||||
|
final HistoryRecordManager historyRecordManager) {
|
||||||
|
if (!(infoItem instanceof CommentsInfoItem)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||||
|
|
||||||
|
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
|
||||||
|
if (ImageStrategy.shouldLoadImages()) {
|
||||||
|
itemThumbnailView.setVisibility(View.VISIBLE);
|
||||||
|
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
||||||
|
commentVerticalPadding, commentVerticalPadding);
|
||||||
|
} else {
|
||||||
|
itemThumbnailView.setVisibility(View.GONE);
|
||||||
|
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
|
||||||
|
commentHorizontalPadding, commentVerticalPadding);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
|
||||||
|
|
||||||
|
try {
|
||||||
|
streamService = NewPipe.getService(item.getServiceId());
|
||||||
|
} catch (final ExtractionException e) {
|
||||||
|
// should never happen
|
||||||
|
ErrorUtil.showUiErrorSnackbar(itemBuilder.getContext(), "Getting StreamingService", e);
|
||||||
|
Log.w(TAG, "Cannot obtain service from comment service id, defaulting to YouTube", e);
|
||||||
|
streamService = ServiceList.YouTube;
|
||||||
|
}
|
||||||
|
streamUrl = item.getUrl();
|
||||||
|
commentText = item.getCommentText();
|
||||||
|
ellipsize();
|
||||||
|
|
||||||
|
//noinspection ClickableViewAccessibility
|
||||||
|
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
|
||||||
|
|
||||||
|
if (item.getLikeCount() >= 0) {
|
||||||
|
itemLikesCountView.setText(
|
||||||
|
Localization.shortCount(
|
||||||
|
itemBuilder.getContext(),
|
||||||
|
item.getLikeCount()));
|
||||||
|
} else {
|
||||||
|
itemLikesCountView.setText("-");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.getUploadDate() != null) {
|
||||||
|
itemPublishedTime.setText(Localization.relativeTime(item.getUploadDate()
|
||||||
|
.offsetDateTime()));
|
||||||
|
} else {
|
||||||
|
itemPublishedTime.setText(item.getTextualUploadDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
itemView.setOnClickListener(view -> {
|
||||||
|
toggleEllipsize();
|
||||||
|
if (itemBuilder.getOnCommentsSelectedListener() != null) {
|
||||||
|
itemBuilder.getOnCommentsSelectedListener().selected(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
itemView.setOnLongClickListener(view -> {
|
||||||
|
if (DeviceUtils.isTv(itemBuilder.getContext())) {
|
||||||
|
openCommentAuthor(item);
|
||||||
|
} else {
|
||||||
|
final CharSequence text = itemContentView.getText();
|
||||||
|
if (text != null) {
|
||||||
|
ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openCommentAuthor(final CommentsInfoItem item) {
|
||||||
|
if (isEmpty(item.getUploaderUrl())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
|
||||||
|
try {
|
||||||
|
NavigationHelper.openChannelFragment(
|
||||||
|
activity.getSupportFragmentManager(),
|
||||||
|
item.getServiceId(),
|
||||||
|
item.getUploaderUrl(),
|
||||||
|
item.getUploaderName());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
ErrorUtil.showUiErrorSnackbar(activity, "Opening channel fragment", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void allowLinkFocus() {
|
||||||
|
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void denyLinkFocus() {
|
||||||
|
itemContentView.setMovementMethod(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldFocusLinks() {
|
||||||
|
if (itemView.isInTouchMode()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final URLSpan[] urls = itemContentView.getUrls();
|
||||||
|
|
||||||
|
return urls != null && urls.length != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void determineMovementMethod() {
|
||||||
|
if (shouldFocusLinks()) {
|
||||||
|
allowLinkFocus();
|
||||||
|
} else {
|
||||||
|
denyLinkFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ellipsize() {
|
||||||
|
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
|
||||||
|
linkifyCommentContentView(v -> {
|
||||||
|
boolean hasEllipsis = false;
|
||||||
|
|
||||||
|
final CharSequence charSeqText = itemContentView.getText();
|
||||||
|
if (charSeqText != null && itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
||||||
|
// Note that converting to String removes spans (i.e. links), but that's something
|
||||||
|
// we actually want since when the text is ellipsized we want all clicks on the
|
||||||
|
// comment to expand the comment, not to open links.
|
||||||
|
final String text = charSeqText.toString();
|
||||||
|
|
||||||
|
final Layout layout = itemContentView.getLayout();
|
||||||
|
final float lineWidth = layout.getLineWidth(COMMENT_DEFAULT_LINES - 1);
|
||||||
|
final float layoutWidth = layout.getWidth();
|
||||||
|
final int lineStart = layout.getLineStart(COMMENT_DEFAULT_LINES - 1);
|
||||||
|
final int lineEnd = layout.getLineEnd(COMMENT_DEFAULT_LINES - 1);
|
||||||
|
|
||||||
|
// remove characters up until there is enough space for the ellipsis
|
||||||
|
// (also summing 2 more pixels, just to be sure to avoid float rounding errors)
|
||||||
|
int end = lineEnd;
|
||||||
|
float removedCharactersWidth = 0.0f;
|
||||||
|
while (lineWidth - removedCharactersWidth + ellipsisWidthPx + 2.0f > layoutWidth
|
||||||
|
&& end >= lineStart) {
|
||||||
|
end -= 1;
|
||||||
|
// recalculate each time to account for ligatures or other similar things
|
||||||
|
removedCharactersWidth = paintAtContentSize.measureText(
|
||||||
|
text.substring(end, lineEnd));
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove trailing spaces and newlines
|
||||||
|
while (end > 0 && Character.isWhitespace(text.charAt(end - 1))) {
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String newVal = text.substring(0, end) + ELLIPSIS;
|
||||||
|
itemContentView.setText(newVal);
|
||||||
|
hasEllipsis = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemContentView.setMaxLines(COMMENT_DEFAULT_LINES);
|
||||||
|
if (hasEllipsis) {
|
||||||
|
denyLinkFocus();
|
||||||
|
} else {
|
||||||
|
determineMovementMethod();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleEllipsize() {
|
||||||
|
final CharSequence text = itemContentView.getText();
|
||||||
|
if (!isEmpty(text) && text.charAt(text.length() - 1) == ELLIPSIS.charAt(0)) {
|
||||||
|
expand();
|
||||||
|
} else if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
||||||
|
ellipsize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expand() {
|
||||||
|
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
|
||||||
|
linkifyCommentContentView(v -> determineMovementMethod());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void linkifyCommentContentView(@Nullable final Consumer<TextView> onCompletion) {
|
||||||
|
disposables.clear();
|
||||||
|
if (commentText != null) {
|
||||||
|
TextLinkifier.fromDescription(itemContentView, commentText,
|
||||||
|
HtmlCompat.FROM_HTML_MODE_LEGACY, streamService, streamUrl, disposables,
|
||||||
|
onCompletion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,10 @@ import org.schabi.newpipe.info_list.InfoItemBuilder;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 01.08.16.
|
* Created by Christian Schabesberger on 01.08.16.
|
||||||
* <p>
|
* <p>
|
||||||
@ -77,9 +81,7 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final String uploadDate = Localization.relativeTimeOrTextual(itemBuilder.getContext(),
|
final String uploadDate = getFormattedRelativeUploadDate(infoItem);
|
||||||
infoItem.getUploadDate(),
|
|
||||||
infoItem.getTextualUploadDate());
|
|
||||||
if (!TextUtils.isEmpty(uploadDate)) {
|
if (!TextUtils.isEmpty(uploadDate)) {
|
||||||
if (viewsAndDate.isEmpty()) {
|
if (viewsAndDate.isEmpty()) {
|
||||||
return uploadDate;
|
return uploadDate;
|
||||||
@ -90,4 +92,20 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
|||||||
|
|
||||||
return viewsAndDate;
|
return viewsAndDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getFormattedRelativeUploadDate(final StreamInfoItem infoItem) {
|
||||||
|
if (infoItem.getUploadDate() != null) {
|
||||||
|
String formattedRelativeTime = Localization
|
||||||
|
.relativeTime(infoItem.getUploadDate().offsetDateTime());
|
||||||
|
|
||||||
|
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext())
|
||||||
|
.getBoolean(itemBuilder.getContext()
|
||||||
|
.getString(R.string.show_original_time_ago_key), false)) {
|
||||||
|
formattedRelativeTime += " (" + infoItem.getTextualUploadDate() + ")";
|
||||||
|
}
|
||||||
|
return formattedRelativeTime;
|
||||||
|
} else {
|
||||||
|
return infoItem.getTextualUploadDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package org.schabi.newpipe.ktx
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.core.os.BundleCompat
|
|
||||||
|
|
||||||
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
|
|
||||||
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
|
|
||||||
}
|
|
@ -14,7 +14,6 @@ import org.schabi.newpipe.database.LocalItem;
|
|||||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
|
|
||||||
import org.schabi.newpipe.local.holder.LocalItemHolder;
|
import org.schabi.newpipe.local.holder.LocalItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalPlaylistCardItemHolder;
|
import org.schabi.newpipe.local.holder.LocalPlaylistCardItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
|
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
|
||||||
@ -25,7 +24,6 @@ import org.schabi.newpipe.local.holder.LocalPlaylistStreamItemHolder;
|
|||||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamCardItemHolder;
|
import org.schabi.newpipe.local.holder.LocalStatisticStreamCardItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder;
|
import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
|
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
|
|
||||||
import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder;
|
import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
|
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
|
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
|
||||||
@ -75,12 +73,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
|
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
|
||||||
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2001;
|
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2001;
|
||||||
private static final int LOCAL_PLAYLIST_CARD_HOLDER_TYPE = 0x2002;
|
private static final int LOCAL_PLAYLIST_CARD_HOLDER_TYPE = 0x2002;
|
||||||
private static final int LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE = 0x2003;
|
|
||||||
|
|
||||||
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x3000;
|
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x3000;
|
||||||
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x3001;
|
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x3001;
|
||||||
private static final int REMOTE_PLAYLIST_CARD_HOLDER_TYPE = 0x3002;
|
private static final int REMOTE_PLAYLIST_CARD_HOLDER_TYPE = 0x3002;
|
||||||
private static final int REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE = 0x3003;
|
|
||||||
|
|
||||||
private final LocalItemBuilder localItemBuilder;
|
private final LocalItemBuilder localItemBuilder;
|
||||||
private final ArrayList<LocalItem> localItems;
|
private final ArrayList<LocalItem> localItems;
|
||||||
@ -91,7 +87,6 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
private View header = null;
|
private View header = null;
|
||||||
private View footer = null;
|
private View footer = null;
|
||||||
private ItemViewMode itemViewMode = ItemViewMode.LIST;
|
private ItemViewMode itemViewMode = ItemViewMode.LIST;
|
||||||
private boolean useItemHandle = false;
|
|
||||||
|
|
||||||
public LocalItemListAdapter(final Context context) {
|
public LocalItemListAdapter(final Context context) {
|
||||||
recordManager = new HistoryRecordManager(context);
|
recordManager = new HistoryRecordManager(context);
|
||||||
@ -185,10 +180,6 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
this.itemViewMode = itemViewMode;
|
this.itemViewMode = itemViewMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUseItemHandle(final boolean useItemHandle) {
|
|
||||||
this.useItemHandle = useItemHandle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHeader(final View header) {
|
public void setHeader(final View header) {
|
||||||
final boolean changed = header != this.header;
|
final boolean changed = header != this.header;
|
||||||
this.header = header;
|
this.header = header;
|
||||||
@ -266,9 +257,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
final LocalItem item = localItems.get(position);
|
final LocalItem item = localItems.get(position);
|
||||||
switch (item.getLocalItemType()) {
|
switch (item.getLocalItemType()) {
|
||||||
case PLAYLIST_LOCAL_ITEM:
|
case PLAYLIST_LOCAL_ITEM:
|
||||||
if (useItemHandle) {
|
if (itemViewMode == ItemViewMode.CARD) {
|
||||||
return LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE;
|
|
||||||
} else if (itemViewMode == ItemViewMode.CARD) {
|
|
||||||
return LOCAL_PLAYLIST_CARD_HOLDER_TYPE;
|
return LOCAL_PLAYLIST_CARD_HOLDER_TYPE;
|
||||||
} else if (itemViewMode == ItemViewMode.GRID) {
|
} else if (itemViewMode == ItemViewMode.GRID) {
|
||||||
return LOCAL_PLAYLIST_GRID_HOLDER_TYPE;
|
return LOCAL_PLAYLIST_GRID_HOLDER_TYPE;
|
||||||
@ -276,9 +265,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
return LOCAL_PLAYLIST_HOLDER_TYPE;
|
return LOCAL_PLAYLIST_HOLDER_TYPE;
|
||||||
}
|
}
|
||||||
case PLAYLIST_REMOTE_ITEM:
|
case PLAYLIST_REMOTE_ITEM:
|
||||||
if (useItemHandle) {
|
if (itemViewMode == ItemViewMode.CARD) {
|
||||||
return REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE;
|
|
||||||
} else if (itemViewMode == ItemViewMode.CARD) {
|
|
||||||
return REMOTE_PLAYLIST_CARD_HOLDER_TYPE;
|
return REMOTE_PLAYLIST_CARD_HOLDER_TYPE;
|
||||||
} else if (itemViewMode == ItemViewMode.GRID) {
|
} else if (itemViewMode == ItemViewMode.GRID) {
|
||||||
return REMOTE_PLAYLIST_GRID_HOLDER_TYPE;
|
return REMOTE_PLAYLIST_GRID_HOLDER_TYPE;
|
||||||
@ -327,16 +314,12 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
return new LocalPlaylistGridItemHolder(localItemBuilder, parent);
|
return new LocalPlaylistGridItemHolder(localItemBuilder, parent);
|
||||||
case LOCAL_PLAYLIST_CARD_HOLDER_TYPE:
|
case LOCAL_PLAYLIST_CARD_HOLDER_TYPE:
|
||||||
return new LocalPlaylistCardItemHolder(localItemBuilder, parent);
|
return new LocalPlaylistCardItemHolder(localItemBuilder, parent);
|
||||||
case LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE:
|
|
||||||
return new LocalBookmarkPlaylistItemHolder(localItemBuilder, parent);
|
|
||||||
case REMOTE_PLAYLIST_HOLDER_TYPE:
|
case REMOTE_PLAYLIST_HOLDER_TYPE:
|
||||||
return new RemotePlaylistItemHolder(localItemBuilder, parent);
|
return new RemotePlaylistItemHolder(localItemBuilder, parent);
|
||||||
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE:
|
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE:
|
||||||
return new RemotePlaylistGridItemHolder(localItemBuilder, parent);
|
return new RemotePlaylistGridItemHolder(localItemBuilder, parent);
|
||||||
case REMOTE_PLAYLIST_CARD_HOLDER_TYPE:
|
case REMOTE_PLAYLIST_CARD_HOLDER_TYPE:
|
||||||
return new RemotePlaylistCardItemHolder(localItemBuilder, parent);
|
return new RemotePlaylistCardItemHolder(localItemBuilder, parent);
|
||||||
case REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE:
|
|
||||||
return new RemoteBookmarkPlaylistItemHolder(localItemBuilder, parent);
|
|
||||||
case STREAM_PLAYLIST_HOLDER_TYPE:
|
case STREAM_PLAYLIST_HOLDER_TYPE:
|
||||||
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
|
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
|
||||||
case STREAM_PLAYLIST_GRID_HOLDER_TYPE:
|
case STREAM_PLAYLIST_GRID_HOLDER_TYPE:
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
package org.schabi.newpipe.local.bookmark;
|
package org.schabi.newpipe.local.bookmark;
|
||||||
|
|
||||||
import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists;
|
|
||||||
|
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@ -16,8 +13,6 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
import org.reactivestreams.Subscription;
|
import org.reactivestreams.Subscription;
|
||||||
@ -32,45 +27,29 @@ import org.schabi.newpipe.databinding.DialogEditTextBinding;
|
|||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
|
|
||||||
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
|
|
||||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
|
||||||
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void>
|
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
|
||||||
implements DebounceSavable {
|
|
||||||
|
|
||||||
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
|
||||||
@State
|
@State
|
||||||
Parcelable itemsListState;
|
protected Parcelable itemsListState;
|
||||||
|
|
||||||
private Subscription databaseSubscription;
|
private Subscription databaseSubscription;
|
||||||
private CompositeDisposable disposables = new CompositeDisposable();
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
private LocalPlaylistManager localPlaylistManager;
|
private LocalPlaylistManager localPlaylistManager;
|
||||||
private RemotePlaylistManager remotePlaylistManager;
|
private RemotePlaylistManager remotePlaylistManager;
|
||||||
private ItemTouchHelper itemTouchHelper;
|
|
||||||
|
|
||||||
/* Have the bookmarked playlists been fully loaded from db */
|
|
||||||
private AtomicBoolean isLoadingComplete;
|
|
||||||
|
|
||||||
/* Gives enough time to avoid interrupting user sorting operations */
|
|
||||||
@Nullable
|
|
||||||
private DebounceSaver debounceSaver;
|
|
||||||
|
|
||||||
private List<Pair<Long, LocalItem.LocalItemType>> deletedItems;
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Fragment LifeCycle - Creation
|
// Fragment LifeCycle - Creation
|
||||||
@ -86,11 +65,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
localPlaylistManager = new LocalPlaylistManager(database);
|
localPlaylistManager = new LocalPlaylistManager(database);
|
||||||
remotePlaylistManager = new RemotePlaylistManager(database);
|
remotePlaylistManager = new RemotePlaylistManager(database);
|
||||||
disposables = new CompositeDisposable();
|
disposables = new CompositeDisposable();
|
||||||
|
|
||||||
isLoadingComplete = new AtomicBoolean();
|
|
||||||
debounceSaver = new DebounceSaver(3000, this);
|
|
||||||
|
|
||||||
deletedItems = new ArrayList<>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@ -117,20 +91,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
// Fragment LifeCycle - Views
|
// Fragment LifeCycle - Views
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
|
||||||
super.initViews(rootView, savedInstanceState);
|
|
||||||
|
|
||||||
itemListAdapter.setUseItemHandle(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
|
||||||
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
|
||||||
itemTouchHelper.attachToRecyclerView(itemsList);
|
|
||||||
|
|
||||||
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
|
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(final LocalItem selectedItem) {
|
public void selected(final LocalItem selectedItem) {
|
||||||
@ -138,7 +102,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
|
|
||||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||||
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.getUid(),
|
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid,
|
||||||
entry.name);
|
entry.name);
|
||||||
|
|
||||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||||
@ -159,14 +123,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
|
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void drag(final LocalItem selectedItem,
|
|
||||||
final RecyclerView.ViewHolder viewHolder) {
|
|
||||||
if (itemTouchHelper != null) {
|
|
||||||
itemTouchHelper.startDrag(viewHolder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,13 +134,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
public void startLoading(final boolean forceLoad) {
|
public void startLoading(final boolean forceLoad) {
|
||||||
super.startLoading(forceLoad);
|
super.startLoading(forceLoad);
|
||||||
|
|
||||||
if (debounceSaver != null) {
|
Flowable.combineLatest(localPlaylistManager.getPlaylists(),
|
||||||
disposables.add(debounceSaver.getDebouncedSaver());
|
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
|
||||||
debounceSaver.setNoChangesToSave();
|
|
||||||
}
|
|
||||||
isLoadingComplete.set(false);
|
|
||||||
|
|
||||||
getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
|
|
||||||
.onBackpressureLatest()
|
.onBackpressureLatest()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(getPlaylistsSubscriber());
|
.subscribe(getPlaylistsSubscriber());
|
||||||
@ -198,9 +149,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||||
|
|
||||||
// Save on exit
|
|
||||||
saveImmediate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -215,27 +163,19 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
}
|
}
|
||||||
|
|
||||||
databaseSubscription = null;
|
databaseSubscription = null;
|
||||||
itemTouchHelper = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if (debounceSaver != null) {
|
|
||||||
debounceSaver.getDebouncedSaveSignal().onComplete();
|
|
||||||
}
|
|
||||||
if (disposables != null) {
|
if (disposables != null) {
|
||||||
disposables.dispose();
|
disposables.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
debounceSaver = null;
|
|
||||||
disposables = null;
|
disposables = null;
|
||||||
localPlaylistManager = null;
|
localPlaylistManager = null;
|
||||||
remotePlaylistManager = null;
|
remotePlaylistManager = null;
|
||||||
itemsListState = null;
|
itemsListState = null;
|
||||||
|
|
||||||
isLoadingComplete = null;
|
|
||||||
deletedItems = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
@ -243,12 +183,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
|
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
|
||||||
return new Subscriber<>() {
|
return new Subscriber<List<PlaylistLocalItem>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(final Subscription s) {
|
public void onSubscribe(final Subscription s) {
|
||||||
showLoading();
|
showLoading();
|
||||||
isLoadingComplete.set(false);
|
|
||||||
|
|
||||||
if (databaseSubscription != null) {
|
if (databaseSubscription != null) {
|
||||||
databaseSubscription.cancel();
|
databaseSubscription.cancel();
|
||||||
}
|
}
|
||||||
@ -258,10 +196,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNext(final List<PlaylistLocalItem> subscriptions) {
|
public void onNext(final List<PlaylistLocalItem> subscriptions) {
|
||||||
if (debounceSaver == null || !debounceSaver.getIsModified()) {
|
|
||||||
handleResult(subscriptions);
|
handleResult(subscriptions);
|
||||||
isLoadingComplete.set(true);
|
|
||||||
}
|
|
||||||
if (databaseSubscription != null) {
|
if (databaseSubscription != null) {
|
||||||
databaseSubscription.request(1);
|
databaseSubscription.request(1);
|
||||||
}
|
}
|
||||||
@ -274,8 +209,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onComplete() {
|
public void onComplete() { }
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,183 +244,12 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Playlist Metadata Manipulation
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
private void changeLocalPlaylistName(final long id, final String name) {
|
|
||||||
if (localPlaylistManager == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Updating playlist id=[" + id + "] "
|
|
||||||
+ "with new name=[" + name + "] items");
|
|
||||||
}
|
|
||||||
|
|
||||||
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
|
|
||||||
new ErrorInfo(throwable,
|
|
||||||
UserAction.REQUESTED_BOOKMARK,
|
|
||||||
"Changing playlist name")));
|
|
||||||
disposables.add(disposable);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteItem(final PlaylistLocalItem item) {
|
|
||||||
if (itemListAdapter == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
itemListAdapter.removeItem(item);
|
|
||||||
|
|
||||||
if (item instanceof PlaylistMetadataEntry) {
|
|
||||||
deletedItems.add(new Pair<>(item.getUid(),
|
|
||||||
LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM));
|
|
||||||
} else if (item instanceof PlaylistRemoteEntity) {
|
|
||||||
deletedItems.add(new Pair<>(item.getUid(),
|
|
||||||
LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debounceSaver != null) {
|
|
||||||
debounceSaver.setHasChangesToSave();
|
|
||||||
saveImmediate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void saveImmediate() {
|
|
||||||
if (itemListAdapter == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// List must be loaded and modified in order to save
|
|
||||||
if (isLoadingComplete == null || debounceSaver == null
|
|
||||||
|| !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<LocalItem> items = itemListAdapter.getItemsList();
|
|
||||||
final List<PlaylistMetadataEntry> localItemsUpdate = new ArrayList<>();
|
|
||||||
final List<Long> localItemsDeleteUid = new ArrayList<>();
|
|
||||||
final List<PlaylistRemoteEntity> remoteItemsUpdate = new ArrayList<>();
|
|
||||||
final List<Long> remoteItemsDeleteUid = new ArrayList<>();
|
|
||||||
|
|
||||||
// Calculate display index
|
|
||||||
for (int i = 0; i < items.size(); i++) {
|
|
||||||
final LocalItem item = items.get(i);
|
|
||||||
|
|
||||||
if (item instanceof PlaylistMetadataEntry
|
|
||||||
&& ((PlaylistMetadataEntry) item).getDisplayIndex() != i) {
|
|
||||||
((PlaylistMetadataEntry) item).setDisplayIndex(i);
|
|
||||||
localItemsUpdate.add((PlaylistMetadataEntry) item);
|
|
||||||
} else if (item instanceof PlaylistRemoteEntity
|
|
||||||
&& ((PlaylistRemoteEntity) item).getDisplayIndex() != i) {
|
|
||||||
((PlaylistRemoteEntity) item).setDisplayIndex(i);
|
|
||||||
remoteItemsUpdate.add((PlaylistRemoteEntity) item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find deleted items
|
|
||||||
for (final Pair<Long, LocalItem.LocalItemType> item : deletedItems) {
|
|
||||||
if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)) {
|
|
||||||
localItemsDeleteUid.add(item.first);
|
|
||||||
} else if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)) {
|
|
||||||
remoteItemsDeleteUid.add(item.first);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deletedItems.clear();
|
|
||||||
|
|
||||||
// 1. Update local playlists
|
|
||||||
// 2. Update remote playlists
|
|
||||||
// 3. Set NoChangesToSave
|
|
||||||
disposables.add(localPlaylistManager.updatePlaylists(localItemsUpdate, localItemsDeleteUid)
|
|
||||||
.mergeWith(remotePlaylistManager.updatePlaylists(
|
|
||||||
remoteItemsUpdate, remoteItemsDeleteUid))
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(() -> {
|
|
||||||
if (debounceSaver != null) {
|
|
||||||
debounceSaver.setNoChangesToSave();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
throwable -> showError(new ErrorInfo(throwable,
|
|
||||||
UserAction.REQUESTED_BOOKMARK, "Saving playlist"))
|
|
||||||
));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
|
||||||
// if adding grid layout, also include ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT
|
|
||||||
// with an `if (shouldUseGridLayout()) ...`
|
|
||||||
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
|
|
||||||
ItemTouchHelper.ACTION_STATE_IDLE) {
|
|
||||||
@Override
|
|
||||||
public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
|
|
||||||
final int viewSize,
|
|
||||||
final int viewSizeOutOfBounds,
|
|
||||||
final int totalSize,
|
|
||||||
final long msSinceStartScroll) {
|
|
||||||
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView,
|
|
||||||
viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll);
|
|
||||||
final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
|
|
||||||
Math.abs(standardSpeed));
|
|
||||||
return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onMove(@NonNull final RecyclerView recyclerView,
|
|
||||||
@NonNull final RecyclerView.ViewHolder source,
|
|
||||||
@NonNull final RecyclerView.ViewHolder target) {
|
|
||||||
|
|
||||||
// Allow swap LocalBookmarkPlaylistItemHolder and RemoteBookmarkPlaylistItemHolder.
|
|
||||||
if (itemListAdapter == null
|
|
||||||
|| source.getItemViewType() != target.getItemViewType()
|
|
||||||
&& !(
|
|
||||||
(
|
|
||||||
(source instanceof LocalBookmarkPlaylistItemHolder)
|
|
||||||
|| (source instanceof RemoteBookmarkPlaylistItemHolder)
|
|
||||||
)
|
|
||||||
&& (
|
|
||||||
(target instanceof LocalBookmarkPlaylistItemHolder)
|
|
||||||
|| (target instanceof RemoteBookmarkPlaylistItemHolder)
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int sourceIndex = source.getBindingAdapterPosition();
|
|
||||||
final int targetIndex = target.getBindingAdapterPosition();
|
|
||||||
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
|
||||||
if (isSwapped && debounceSaver != null) {
|
|
||||||
debounceSaver.setHasChangesToSave();
|
|
||||||
}
|
|
||||||
return isSwapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLongPressDragEnabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isItemViewSwipeEnabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
|
|
||||||
final int swipeDir) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Utils
|
// Utils
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
|
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
|
||||||
showDeleteDialog(item.getName(), item);
|
showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
||||||
@ -494,7 +257,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
final String delete = getString(R.string.delete);
|
final String delete = getString(R.string.delete);
|
||||||
final String unsetThumbnail = getString(R.string.unset_playlist_thumbnail);
|
final String unsetThumbnail = getString(R.string.unset_playlist_thumbnail);
|
||||||
final boolean isThumbnailPermanent = localPlaylistManager
|
final boolean isThumbnailPermanent = localPlaylistManager
|
||||||
.getIsPlaylistThumbnailPermanent(selectedItem.getUid());
|
.getIsPlaylistThumbnailPermanent(selectedItem.uid);
|
||||||
|
|
||||||
final ArrayList<String> items = new ArrayList<>();
|
final ArrayList<String> items = new ArrayList<>();
|
||||||
items.add(rename);
|
items.add(rename);
|
||||||
@ -507,12 +270,13 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
if (items.get(index).equals(rename)) {
|
if (items.get(index).equals(rename)) {
|
||||||
showRenameDialog(selectedItem);
|
showRenameDialog(selectedItem);
|
||||||
} else if (items.get(index).equals(delete)) {
|
} else if (items.get(index).equals(delete)) {
|
||||||
showDeleteDialog(selectedItem.name, selectedItem);
|
showDeleteDialog(selectedItem.name,
|
||||||
|
localPlaylistManager.deletePlaylist(selectedItem.uid));
|
||||||
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
|
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
|
||||||
final long thumbnailStreamId = localPlaylistManager
|
final long thumbnailStreamId = localPlaylistManager
|
||||||
.getAutomaticPlaylistThumbnailStreamId(selectedItem.getUid());
|
.getAutomaticPlaylistThumbnailStreamId(selectedItem.uid);
|
||||||
localPlaylistManager
|
localPlaylistManager
|
||||||
.changePlaylistThumbnail(selectedItem.getUid(), thumbnailStreamId, false)
|
.changePlaylistThumbnail(selectedItem.uid, thumbnailStreamId, false)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe();
|
.subscribe();
|
||||||
}
|
}
|
||||||
@ -534,13 +298,13 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
.setView(dialogBinding.getRoot())
|
.setView(dialogBinding.getRoot())
|
||||||
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
|
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
|
||||||
changeLocalPlaylistName(
|
changeLocalPlaylistName(
|
||||||
selectedItem.getUid(),
|
selectedItem.uid,
|
||||||
dialogBinding.dialogEditText.getText().toString()))
|
dialogBinding.dialogEditText.getText().toString()))
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showDeleteDialog(final String name, final PlaylistLocalItem item) {
|
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) {
|
||||||
if (activity == null || disposables == null) {
|
if (activity == null || disposables == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -549,8 +313,35 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
.setTitle(name)
|
.setTitle(name)
|
||||||
.setMessage(R.string.delete_playlist_prompt)
|
.setMessage(R.string.delete_playlist_prompt)
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.setPositiveButton(R.string.delete, (dialog, i) -> deleteItem(item))
|
.setPositiveButton(R.string.delete, (dialog, i) ->
|
||||||
|
disposables.add(deleteReactor
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(ignored -> { /*Do nothing on success*/ }, throwable ->
|
||||||
|
showError(new ErrorInfo(throwable,
|
||||||
|
UserAction.REQUESTED_BOOKMARK,
|
||||||
|
"Deleting playlist")))))
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void changeLocalPlaylistName(final long id, final String name) {
|
||||||
|
if (localPlaylistManager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Updating playlist id=[" + id + "] "
|
||||||
|
+ "with new name=[" + name + "] items");
|
||||||
|
}
|
||||||
|
|
||||||
|
localPlaylistManager.renamePlaylist(id, name);
|
||||||
|
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
|
||||||
|
new ErrorInfo(throwable,
|
||||||
|
UserAction.REQUESTED_BOOKMARK,
|
||||||
|
"Changing playlist name")));
|
||||||
|
disposables.add(disposable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,95 +0,0 @@
|
|||||||
package org.schabi.newpipe.local.bookmark;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
|
||||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
|
||||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes care of remote and local playlists at once, hence "merged".
|
|
||||||
*/
|
|
||||||
public final class MergedPlaylistManager {
|
|
||||||
|
|
||||||
private MergedPlaylistManager() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Flowable<List<PlaylistLocalItem>> getMergedOrderedPlaylists(
|
|
||||||
final LocalPlaylistManager localPlaylistManager,
|
|
||||||
final RemotePlaylistManager remotePlaylistManager) {
|
|
||||||
return Flowable.combineLatest(
|
|
||||||
localPlaylistManager.getPlaylists(),
|
|
||||||
remotePlaylistManager.getPlaylists(),
|
|
||||||
MergedPlaylistManager::merge
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge localPlaylists and remotePlaylists by the display index.
|
|
||||||
* If two items have the same display index, sort them in {@code CASE_INSENSITIVE_ORDER}.
|
|
||||||
*
|
|
||||||
* @param localPlaylists local playlists, already sorted by display index
|
|
||||||
* @param remotePlaylists remote playlists, already sorted by display index
|
|
||||||
* @return merged playlists
|
|
||||||
*/
|
|
||||||
public static List<PlaylistLocalItem> merge(
|
|
||||||
final List<PlaylistMetadataEntry> localPlaylists,
|
|
||||||
final List<PlaylistRemoteEntity> remotePlaylists) {
|
|
||||||
|
|
||||||
// This algorithm is similar to the merge operation in merge sort.
|
|
||||||
final List<PlaylistLocalItem> result = new ArrayList<>(
|
|
||||||
localPlaylists.size() + remotePlaylists.size());
|
|
||||||
final List<PlaylistLocalItem> itemsWithSameIndex = new ArrayList<>();
|
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
int j = 0;
|
|
||||||
while (i < localPlaylists.size()) {
|
|
||||||
while (j < remotePlaylists.size()) {
|
|
||||||
if (remotePlaylists.get(j).getDisplayIndex()
|
|
||||||
<= localPlaylists.get(i).getDisplayIndex()) {
|
|
||||||
addItem(result, remotePlaylists.get(j), itemsWithSameIndex);
|
|
||||||
j++;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addItem(result, localPlaylists.get(i), itemsWithSameIndex);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
while (j < remotePlaylists.size()) {
|
|
||||||
addItem(result, remotePlaylists.get(j), itemsWithSameIndex);
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
addItemsWithSameIndex(result, itemsWithSameIndex);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addItem(final List<PlaylistLocalItem> result,
|
|
||||||
final PlaylistLocalItem item,
|
|
||||||
final List<PlaylistLocalItem> itemsWithSameIndex) {
|
|
||||||
if (!itemsWithSameIndex.isEmpty()
|
|
||||||
&& itemsWithSameIndex.get(0).getDisplayIndex() != item.getDisplayIndex()) {
|
|
||||||
// The new item has a different display index, add previous items with same
|
|
||||||
// index to the result.
|
|
||||||
addItemsWithSameIndex(result, itemsWithSameIndex);
|
|
||||||
itemsWithSameIndex.clear();
|
|
||||||
}
|
|
||||||
itemsWithSameIndex.add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addItemsWithSameIndex(final List<PlaylistLocalItem> result,
|
|
||||||
final List<PlaylistLocalItem> itemsWithSameIndex) {
|
|
||||||
Collections.sort(itemsWithSameIndex,
|
|
||||||
Comparator.comparing(PlaylistLocalItem::getOrderingName,
|
|
||||||
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
|
|
||||||
result.addAll(itemsWithSameIndex);
|
|
||||||
}
|
|
||||||
}
|
|
@ -155,14 +155,14 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
|||||||
|
|
||||||
final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
|
final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
|
||||||
|
|
||||||
playlistDisposables.add(manager.appendToPlaylist(playlist.getUid(), streams)
|
playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(ignored -> {
|
.subscribe(ignored -> {
|
||||||
successToast.show();
|
successToast.show();
|
||||||
|
|
||||||
if (playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
|
if (playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
|
||||||
playlistDisposables.add(manager
|
playlistDisposables.add(manager
|
||||||
.changePlaylistThumbnail(playlist.getUid(), streams.get(0).getUid(),
|
.changePlaylistThumbnail(playlist.uid, streams.get(0).getUid(),
|
||||||
false)
|
false)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(ignore -> successToast.show()));
|
.subscribe(ignore -> successToast.show()));
|
||||||
|
@ -549,7 +549,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
|
|
||||||
var typeface = Typeface.DEFAULT
|
var typeface = Typeface.DEFAULT
|
||||||
var backgroundSupplier = { ctx: Context ->
|
var backgroundSupplier = { ctx: Context ->
|
||||||
resolveDrawable(ctx, android.R.attr.selectableItemBackground)
|
resolveDrawable(ctx, R.attr.selectableItemBackground)
|
||||||
}
|
}
|
||||||
if (doCheck) {
|
if (doCheck) {
|
||||||
// If the uploadDate is null or true we should highlight the item
|
// If the uploadDate is null or true we should highlight the item
|
||||||
@ -562,7 +562,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
LayerDrawable(
|
LayerDrawable(
|
||||||
arrayOf(
|
arrayOf(
|
||||||
resolveDrawable(ctx, R.attr.dashed_border),
|
resolveDrawable(ctx, R.attr.dashed_border),
|
||||||
resolveDrawable(ctx, android.R.attr.selectableItemBackground)
|
resolveDrawable(ctx, R.attr.selectableItemBackground)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@ class NotificationWorker(
|
|||||||
.enqueueUniquePeriodicWork(
|
.enqueueUniquePeriodicWork(
|
||||||
WORK_TAG,
|
WORK_TAG,
|
||||||
if (force) {
|
if (force) {
|
||||||
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
ExistingPeriodicWorkPolicy.REPLACE
|
||||||
} else {
|
} else {
|
||||||
ExistingPeriodicWorkPolicy.KEEP
|
ExistingPeriodicWorkPolicy.KEEP
|
||||||
},
|
},
|
||||||
|
@ -26,7 +26,7 @@ object FeedEventManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed class Event {
|
sealed class Event {
|
||||||
data object IdleEvent : Event()
|
object IdleEvent : Event()
|
||||||
data class ProgressEvent(val currentProgress: Int = -1, val maxProgress: Int = -1, @StringRes val progressMessage: Int = 0) : Event() {
|
data class ProgressEvent(val currentProgress: Int = -1, val maxProgress: Int = -1, @StringRes val progressMessage: Int = 0) : Event() {
|
||||||
constructor(@StringRes progressMessage: Int) : this(-1, -1, progressMessage)
|
constructor(@StringRes progressMessage: Int) : this(-1, -1, progressMessage)
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ data class FeedUpdateInfo(
|
|||||||
@NotificationMode
|
@NotificationMode
|
||||||
val notificationMode: Int,
|
val notificationMode: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val avatarUrl: String?,
|
val avatarUrl: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val serviceId: Int,
|
val serviceId: Int,
|
||||||
// description and subscriberCount are null if the constructor info is from the fast feed method
|
// description and subscriberCount are null if the constructor info is from the fast feed method
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
package org.schabi.newpipe.local.holder;
|
|
||||||
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
|
||||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
|
|
||||||
public class LocalBookmarkPlaylistItemHolder extends LocalPlaylistItemHolder {
|
|
||||||
private final View itemHandleView;
|
|
||||||
|
|
||||||
public LocalBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
|
|
||||||
final ViewGroup parent) {
|
|
||||||
this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId,
|
|
||||||
final ViewGroup parent) {
|
|
||||||
super(infoItemBuilder, layoutId, parent);
|
|
||||||
itemHandleView = itemView.findViewById(R.id.itemHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateFromItem(final LocalItem localItem,
|
|
||||||
final HistoryRecordManager historyRecordManager,
|
|
||||||
final DateTimeFormatter dateTimeFormatter) {
|
|
||||||
if (!(localItem instanceof PlaylistMetadataEntry)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
|
|
||||||
|
|
||||||
itemHandleView.setOnTouchListener(getOnTouchListener(item));
|
|
||||||
|
|
||||||
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private View.OnTouchListener getOnTouchListener(final PlaylistMetadataEntry item) {
|
|
||||||
return (view, motionEvent) -> {
|
|
||||||
view.performClick();
|
|
||||||
if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null
|
|
||||||
&& motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
|
||||||
itemBuilder.getOnItemSelectedListener().drag(item,
|
|
||||||
LocalBookmarkPlaylistItemHolder.this);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
package org.schabi.newpipe.local.holder;
|
|
||||||
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
|
|
||||||
public class RemoteBookmarkPlaylistItemHolder extends RemotePlaylistItemHolder {
|
|
||||||
private final View itemHandleView;
|
|
||||||
|
|
||||||
public RemoteBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
|
|
||||||
final ViewGroup parent) {
|
|
||||||
this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoteBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId,
|
|
||||||
final ViewGroup parent) {
|
|
||||||
super(infoItemBuilder, layoutId, parent);
|
|
||||||
itemHandleView = itemView.findViewById(R.id.itemHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateFromItem(final LocalItem localItem,
|
|
||||||
final HistoryRecordManager historyRecordManager,
|
|
||||||
final DateTimeFormatter dateTimeFormatter) {
|
|
||||||
if (!(localItem instanceof PlaylistRemoteEntity)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
|
|
||||||
|
|
||||||
itemHandleView.setOnTouchListener(getOnTouchListener(item));
|
|
||||||
|
|
||||||
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private View.OnTouchListener getOnTouchListener(final PlaylistRemoteEntity item) {
|
|
||||||
return (view, motionEvent) -> {
|
|
||||||
view.performClick();
|
|
||||||
if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null
|
|
||||||
&& motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
|
||||||
itemBuilder.getOnItemSelectedListener().drag(item,
|
|
||||||
RemoteBookmarkPlaylistItemHolder.this);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,7 +14,6 @@ import org.schabi.newpipe.util.ServiceHelper;
|
|||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||||
|
|
||||||
public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
|
public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
|
||||||
final ViewGroup parent) {
|
final ViewGroup parent) {
|
||||||
super(infoItemBuilder, parent);
|
super(infoItemBuilder, parent);
|
||||||
|
@ -49,8 +49,6 @@ import org.schabi.newpipe.local.BaseLocalListFragment;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
|
||||||
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
@ -60,6 +58,7 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -69,10 +68,12 @@ import io.reactivex.rxjava3.core.Single;
|
|||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||||
|
|
||||||
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
|
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
|
||||||
implements PlaylistControlViewHolder, DebounceSavable {
|
implements PlaylistControlViewHolder {
|
||||||
|
/** Save the list 10 seconds after the last change occurred. */
|
||||||
|
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
|
||||||
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
||||||
@State
|
@State
|
||||||
protected Long playlistId;
|
protected Long playlistId;
|
||||||
@ -89,12 +90,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
private LocalPlaylistManager playlistManager;
|
private LocalPlaylistManager playlistManager;
|
||||||
private Subscription databaseSubscription;
|
private Subscription databaseSubscription;
|
||||||
|
|
||||||
|
private PublishSubject<Long> debouncedSaveSignal;
|
||||||
private CompositeDisposable disposables;
|
private CompositeDisposable disposables;
|
||||||
|
|
||||||
/** Whether the playlist has been fully loaded from db. */
|
/** Whether the playlist has been fully loaded from db. */
|
||||||
private AtomicBoolean isLoadingComplete;
|
private AtomicBoolean isLoadingComplete;
|
||||||
/** Used to debounce saving playlist edits to disk. */
|
/** Whether the playlist has been modified (e.g. items reordered or deleted) */
|
||||||
private DebounceSaver debounceSaver;
|
private AtomicBoolean isModified;
|
||||||
/** Flag to prevent simultaneous rewrites of the playlist. */
|
/** Flag to prevent simultaneous rewrites of the playlist. */
|
||||||
private boolean isRewritingPlaylist = false;
|
private boolean isRewritingPlaylist = false;
|
||||||
|
|
||||||
@ -119,11 +121,12 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
public void onCreate(final Bundle savedInstanceState) {
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
|
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
|
||||||
|
debouncedSaveSignal = PublishSubject.create();
|
||||||
|
|
||||||
disposables = new CompositeDisposable();
|
disposables = new CompositeDisposable();
|
||||||
|
|
||||||
isLoadingComplete = new AtomicBoolean();
|
isLoadingComplete = new AtomicBoolean();
|
||||||
debounceSaver = new DebounceSaver(this);
|
isModified = new AtomicBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -163,6 +166,17 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
return headerBinding;
|
return headerBinding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Commit changes immediately if the playlist has been modified.</p>
|
||||||
|
* Delete operations and other modifications will be committed to ensure that the database
|
||||||
|
* is up to date, e.g. when the user adds the just deleted stream from another fragment.
|
||||||
|
*/
|
||||||
|
public void commitChanges() {
|
||||||
|
if (isModified != null && isModified.get()) {
|
||||||
|
saveImmediate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
@ -229,13 +243,10 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
if (disposables != null) {
|
if (disposables != null) {
|
||||||
disposables.clear();
|
disposables.clear();
|
||||||
}
|
}
|
||||||
|
disposables.add(getDebouncedSaver());
|
||||||
if (debounceSaver != null) {
|
|
||||||
disposables.add(debounceSaver.getDebouncedSaver());
|
|
||||||
debounceSaver.setNoChangesToSave();
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoadingComplete.set(false);
|
isLoadingComplete.set(false);
|
||||||
|
isModified.set(false);
|
||||||
|
|
||||||
playlistManager.getPlaylistStreams(playlistId)
|
playlistManager.getPlaylistStreams(playlistId)
|
||||||
.onBackpressureLatest()
|
.onBackpressureLatest()
|
||||||
@ -293,8 +304,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if (debounceSaver != null) {
|
if (debouncedSaveSignal != null) {
|
||||||
debounceSaver.getDebouncedSaveSignal().onComplete();
|
debouncedSaveSignal.onComplete();
|
||||||
}
|
}
|
||||||
if (disposables != null) {
|
if (disposables != null) {
|
||||||
disposables.dispose();
|
disposables.dispose();
|
||||||
@ -303,11 +314,12 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
tabsPagerAdapter.getLocalPlaylistFragments().remove(this);
|
tabsPagerAdapter.getLocalPlaylistFragments().remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
debounceSaver = null;
|
debouncedSaveSignal = null;
|
||||||
playlistManager = null;
|
playlistManager = null;
|
||||||
disposables = null;
|
disposables = null;
|
||||||
|
|
||||||
isLoadingComplete = null;
|
isLoadingComplete = null;
|
||||||
|
isModified = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
@ -331,7 +343,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
@Override
|
@Override
|
||||||
public void onNext(final List<PlaylistStreamEntry> streams) {
|
public void onNext(final List<PlaylistStreamEntry> streams) {
|
||||||
// Skip handling the result after it has been modified
|
// Skip handling the result after it has been modified
|
||||||
if (debounceSaver == null || !debounceSaver.getIsModified()) {
|
if (isModified == null || !isModified.get()) {
|
||||||
handleResult(streams);
|
handleResult(streams);
|
||||||
isLoadingComplete.set(true);
|
isLoadingComplete.set(true);
|
||||||
}
|
}
|
||||||
@ -483,14 +495,14 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
|
|
||||||
itemListAdapter.clearStreamItemList();
|
itemListAdapter.clearStreamItemList();
|
||||||
itemListAdapter.addItems(itemsToKeep);
|
itemListAdapter.addItems(itemsToKeep);
|
||||||
debounceSaver.setHasChangesToSave();
|
saveChanges();
|
||||||
|
|
||||||
if (thumbnailVideoRemoved) {
|
if (thumbnailVideoRemoved) {
|
||||||
updateThumbnailUrl();
|
updateThumbnailUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
final long videoCount = itemListAdapter.getItemsList().size();
|
final long videoCount = itemListAdapter.getItemsList().size();
|
||||||
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
setVideoCount(videoCount);
|
||||||
if (videoCount == 0) {
|
if (videoCount == 0) {
|
||||||
showEmptyState();
|
showEmptyState();
|
||||||
}
|
}
|
||||||
@ -520,7 +532,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
||||||
itemsListState = null;
|
itemsListState = null;
|
||||||
}
|
}
|
||||||
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
setVideoCount(itemListAdapter.getItemsList().size());
|
||||||
|
|
||||||
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
|
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
|
||||||
|
|
||||||
@ -653,8 +665,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
.subscribe(itemsToKeep -> {
|
.subscribe(itemsToKeep -> {
|
||||||
itemListAdapter.clearStreamItemList();
|
itemListAdapter.clearStreamItemList();
|
||||||
itemListAdapter.addItems(itemsToKeep);
|
itemListAdapter.addItems(itemsToKeep);
|
||||||
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
setVideoCount(itemListAdapter.getItemsList().size());
|
||||||
debounceSaver.setHasChangesToSave();
|
saveChanges();
|
||||||
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
isRewritingPlaylist = false;
|
isRewritingPlaylist = false;
|
||||||
@ -672,24 +684,42 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
updateThumbnailUrl();
|
updateThumbnailUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
setVideoCount(itemListAdapter.getItemsList().size());
|
||||||
debounceSaver.setHasChangesToSave();
|
saveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void saveChanges() {
|
||||||
* <p>Commit changes immediately if the playlist has been modified.</p>
|
if (isModified == null || debouncedSaveSignal == null) {
|
||||||
* Delete operations and other modifications will be committed to ensure that the database
|
return;
|
||||||
* is up to date, e.g. when the user adds the just deleted stream from another fragment.
|
}
|
||||||
*/
|
|
||||||
@Override
|
isModified.set(true);
|
||||||
public void saveImmediate() {
|
debouncedSaveSignal.onNext(System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Disposable getDebouncedSaver() {
|
||||||
|
if (debouncedSaveSignal == null) {
|
||||||
|
return Disposable.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return debouncedSaveSignal
|
||||||
|
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(ignored -> saveImmediate(), throwable ->
|
||||||
|
showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE,
|
||||||
|
"Debounced saver")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveImmediate() {
|
||||||
if (playlistManager == null || itemListAdapter == null) {
|
if (playlistManager == null || itemListAdapter == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// List must be loaded and modified in order to save
|
// List must be loaded and modified in order to save
|
||||||
if (isLoadingComplete == null || debounceSaver == null
|
if (isLoadingComplete == null || isModified == null
|
||||||
|| !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
|
|| !isLoadingComplete.get() || !isModified.get()) {
|
||||||
|
Log.w(TAG, "Attempting to save playlist when local playlist "
|
||||||
|
+ "is not loaded or not modified: playlist id=[" + playlistId + "]");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -710,8 +740,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() -> {
|
() -> {
|
||||||
if (debounceSaver != null) {
|
if (isModified != null) {
|
||||||
debounceSaver.setNoChangesToSave();
|
isModified.set(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
throwable -> showError(new ErrorInfo(throwable,
|
throwable -> showError(new ErrorInfo(throwable,
|
||||||
@ -754,7 +784,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
final int targetIndex = target.getBindingAdapterPosition();
|
final int targetIndex = target.getBindingAdapterPosition();
|
||||||
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
||||||
if (isSwapped) {
|
if (isSwapped) {
|
||||||
debounceSaver.setHasChangesToSave();
|
saveChanges();
|
||||||
}
|
}
|
||||||
return isSwapped;
|
return isSwapped;
|
||||||
}
|
}
|
||||||
@ -825,21 +855,10 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
this.name = !TextUtils.isEmpty(title) ? title : "";
|
this.name = !TextUtils.isEmpty(title) ? title : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setStreamCountAndOverallDuration(final ArrayList<LocalItem> itemsList) {
|
private void setVideoCount(final long count) {
|
||||||
if (activity != null && headerBinding != null) {
|
if (activity != null && headerBinding != null) {
|
||||||
final long streamCount = itemsList.size();
|
headerBinding.playlistStreamCount.setText(Localization
|
||||||
final long playlistOverallDurationSeconds = itemsList.stream()
|
.localizeStreamCount(activity, count));
|
||||||
.filter(PlaylistStreamEntry.class::isInstance)
|
|
||||||
.map(PlaylistStreamEntry.class::cast)
|
|
||||||
.map(PlaylistStreamEntry::getStreamEntity)
|
|
||||||
.mapToLong(StreamEntity::getDuration)
|
|
||||||
.sum();
|
|
||||||
headerBinding.playlistStreamCount.setText(
|
|
||||||
Localization.concatenateStrings(
|
|
||||||
Localization.localizeStreamCount(activity, streamCount),
|
|
||||||
Localization.getDurationString(playlistOverallDurationSeconds,
|
|
||||||
true, true))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import java.util.List;
|
|||||||
import io.reactivex.rxjava3.core.Completable;
|
import io.reactivex.rxjava3.core.Completable;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
import io.reactivex.rxjava3.core.Maybe;
|
import io.reactivex.rxjava3.core.Maybe;
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class LocalPlaylistManager {
|
public class LocalPlaylistManager {
|
||||||
@ -42,13 +43,10 @@ public class LocalPlaylistManager {
|
|||||||
return Maybe.empty();
|
return Maybe.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to the database directly.
|
|
||||||
// Make sure the new playlist is always on the top of bookmark.
|
|
||||||
// The index will be reassigned to non-negative number in BookmarkFragment.
|
|
||||||
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
||||||
final List<Long> streamIds = streamTable.upsertAll(streams);
|
final List<Long> streamIds = streamTable.upsertAll(streams);
|
||||||
final PlaylistEntity newPlaylist = new PlaylistEntity(name, false,
|
final PlaylistEntity newPlaylist = new PlaylistEntity(name, false,
|
||||||
streamIds.get(0), -1);
|
streamIds.get(0));
|
||||||
|
|
||||||
return insertJoinEntities(playlistTable.insert(newPlaylist),
|
return insertJoinEntities(playlistTable.insert(newPlaylist),
|
||||||
streamIds, 0);
|
streamIds, 0);
|
||||||
@ -91,20 +89,8 @@ public class LocalPlaylistManager {
|
|||||||
})).subscribeOn(Schedulers.io());
|
})).subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Completable updatePlaylists(final List<PlaylistMetadataEntry> updateItems,
|
public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
|
||||||
final List<Long> deletedItems) {
|
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
|
||||||
final List<PlaylistEntity> items = new ArrayList<>(updateItems.size());
|
|
||||||
for (final PlaylistMetadataEntry item : updateItems) {
|
|
||||||
items.add(new PlaylistEntity(item));
|
|
||||||
}
|
|
||||||
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
|
|
||||||
for (final Long uid : deletedItems) {
|
|
||||||
playlistTable.deletePlaylist(uid);
|
|
||||||
}
|
|
||||||
for (final PlaylistEntity item : items) {
|
|
||||||
playlistTable.upsertPlaylist(item);
|
|
||||||
}
|
|
||||||
})).subscribeOn(Schedulers.io());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<PlaylistStreamEntry>> getDistinctPlaylistStreams(final long playlistId) {
|
public Flowable<List<PlaylistStreamEntry>> getDistinctPlaylistStreams(final long playlistId) {
|
||||||
@ -124,14 +110,15 @@ public class LocalPlaylistManager {
|
|||||||
.subscribeOn(Schedulers.io());
|
.subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
|
|
||||||
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
|
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
|
||||||
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
|
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Single<Integer> deletePlaylist(final long playlistId) {
|
||||||
|
return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId))
|
||||||
|
.subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
public Maybe<Integer> renamePlaylist(final long playlistId, final String name) {
|
public Maybe<Integer> renamePlaylist(final long playlistId, final String name) {
|
||||||
return modifyPlaylist(playlistId, name, THUMBNAIL_ID_LEAVE_UNCHANGED, false);
|
return modifyPlaylist(playlistId, name, THUMBNAIL_ID_LEAVE_UNCHANGED, false);
|
||||||
}
|
}
|
||||||
|
@ -7,23 +7,20 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Completable;
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class RemotePlaylistManager {
|
public class RemotePlaylistManager {
|
||||||
|
|
||||||
private final AppDatabase database;
|
|
||||||
private final PlaylistRemoteDAO playlistRemoteTable;
|
private final PlaylistRemoteDAO playlistRemoteTable;
|
||||||
|
|
||||||
public RemotePlaylistManager(final AppDatabase db) {
|
public RemotePlaylistManager(final AppDatabase db) {
|
||||||
database = db;
|
|
||||||
playlistRemoteTable = db.playlistRemoteDAO();
|
playlistRemoteTable = db.playlistRemoteDAO();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<PlaylistRemoteEntity>> getPlaylists() {
|
public Flowable<List<PlaylistRemoteEntity>> getPlaylists() {
|
||||||
return playlistRemoteTable.getPlaylists().subscribeOn(Schedulers.io());
|
return playlistRemoteTable.getAll().subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) {
|
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) {
|
||||||
@ -36,18 +33,6 @@ public class RemotePlaylistManager {
|
|||||||
.subscribeOn(Schedulers.io());
|
.subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Completable updatePlaylists(final List<PlaylistRemoteEntity> updateItems,
|
|
||||||
final List<Long> deletedItems) {
|
|
||||||
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
|
|
||||||
for (final Long uid: deletedItems) {
|
|
||||||
playlistRemoteTable.deletePlaylist(uid);
|
|
||||||
}
|
|
||||||
for (final PlaylistRemoteEntity item: updateItems) {
|
|
||||||
playlistRemoteTable.upsert(item);
|
|
||||||
}
|
|
||||||
})).subscribeOn(Schedulers.io());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {
|
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {
|
||||||
return Single.fromCallable(() -> {
|
return Single.fromCallable(() -> {
|
||||||
final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo);
|
final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo);
|
||||||
|
@ -100,9 +100,7 @@ class SubscriptionManager(context: Context) {
|
|||||||
val subscriptionEntity = subscriptionTable.getSubscription(info.uid)
|
val subscriptionEntity = subscriptionTable.getSubscription(info.uid)
|
||||||
|
|
||||||
subscriptionEntity.name = info.name
|
subscriptionEntity.name = info.name
|
||||||
|
subscriptionEntity.avatarUrl = info.avatarUrl
|
||||||
// some services do not provide an avatar URL
|
|
||||||
info.avatarUrl?.let { subscriptionEntity.avatarUrl = it }
|
|
||||||
|
|
||||||
// these two fields are null if the feed info was fetched using the fast feed method
|
// these two fields are null if the feed info was fetched using the fast feed method
|
||||||
info.description?.let { subscriptionEntity.description = it }
|
info.description?.let { subscriptionEntity.description = it }
|
||||||
|
@ -55,10 +55,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||||||
private var groupSortOrder: Long = -1
|
private var groupSortOrder: Long = -1
|
||||||
|
|
||||||
sealed class ScreenState : Serializable {
|
sealed class ScreenState : Serializable {
|
||||||
data object InitialScreen : ScreenState()
|
object InitialScreen : ScreenState()
|
||||||
data object IconPickerScreen : ScreenState()
|
object IconPickerScreen : ScreenState()
|
||||||
data object SubscriptionsPickerScreen : ScreenState()
|
object SubscriptionsPickerScreen : ScreenState()
|
||||||
data object DeleteScreen : ScreenState()
|
object DeleteScreen : ScreenState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@State @JvmField var selectedIcon: FeedGroupIcon? = null
|
@State @JvmField var selectedIcon: FeedGroupIcon? = null
|
||||||
@ -370,7 +370,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||||||
|
|
||||||
private fun setupIconPicker() {
|
private fun setupIconPicker() {
|
||||||
val groupAdapter = GroupieAdapter()
|
val groupAdapter = GroupieAdapter()
|
||||||
groupAdapter.addAll(FeedGroupIcon.entries.map { PickerIconItem(it) })
|
groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(it) })
|
||||||
|
|
||||||
feedGroupCreateBinding.iconSelector.apply {
|
feedGroupCreateBinding.iconSelector.apply {
|
||||||
layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false)
|
layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false)
|
||||||
|
@ -110,8 +110,8 @@ class FeedGroupDialogViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed class DialogEvent {
|
sealed class DialogEvent {
|
||||||
data object ProcessingEvent : DialogEvent()
|
object ProcessingEvent : DialogEvent()
|
||||||
data object SuccessEvent : DialogEvent()
|
object SuccessEvent : DialogEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Filter(val query: String, val showOnlyUngrouped: Boolean)
|
data class Filter(val query: String, val showOnlyUngrouped: Boolean)
|
||||||
|
@ -25,7 +25,6 @@ import android.content.Intent;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.core.content.IntentCompat;
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
@ -66,7 +65,7 @@ public class SubscriptionsExportService extends BaseImportExportService {
|
|||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Uri path = IntentCompat.getParcelableExtra(intent, KEY_FILE_PATH, Uri.class);
|
final Uri path = intent.getParcelableExtra(KEY_FILE_PATH);
|
||||||
if (path == null) {
|
if (path == null) {
|
||||||
stopAndReportError(new IllegalStateException(
|
stopAndReportError(new IllegalStateException(
|
||||||
"Exporting to a file, but the path is null"),
|
"Exporting to a file, but the path is null"),
|
||||||
|
@ -30,7 +30,6 @@ import android.util.Pair;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.IntentCompat;
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
@ -109,7 +108,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
|||||||
if (currentMode == CHANNEL_URL_MODE) {
|
if (currentMode == CHANNEL_URL_MODE) {
|
||||||
channelUrl = intent.getStringExtra(KEY_VALUE);
|
channelUrl = intent.getStringExtra(KEY_VALUE);
|
||||||
} else {
|
} else {
|
||||||
final Uri uri = IntentCompat.getParcelableExtra(intent, KEY_VALUE, Uri.class);
|
final Uri uri = intent.getParcelableExtra(KEY_VALUE);
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
stopAndReportError(new IllegalStateException(
|
stopAndReportError(new IllegalStateException(
|
||||||
"Importing from input stream, but file path is null"),
|
"Importing from input stream, but file path is null"),
|
||||||
|
@ -569,16 +569,16 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
private void onPlayModeChanged(final int repeatMode, final boolean shuffled) {
|
private void onPlayModeChanged(final int repeatMode, final boolean shuffled) {
|
||||||
switch (repeatMode) {
|
switch (repeatMode) {
|
||||||
case com.google.android.exoplayer2.Player.REPEAT_MODE_OFF:
|
case com.google.android.exoplayer2.Player.REPEAT_MODE_OFF:
|
||||||
queueControlBinding.controlRepeat.setImageResource(
|
queueControlBinding.controlRepeat
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_off);
|
.setImageResource(R.drawable.exo_controls_repeat_off);
|
||||||
break;
|
break;
|
||||||
case com.google.android.exoplayer2.Player.REPEAT_MODE_ONE:
|
case com.google.android.exoplayer2.Player.REPEAT_MODE_ONE:
|
||||||
queueControlBinding.controlRepeat.setImageResource(
|
queueControlBinding.controlRepeat
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_one);
|
.setImageResource(R.drawable.exo_controls_repeat_one);
|
||||||
break;
|
break;
|
||||||
case com.google.android.exoplayer2.Player.REPEAT_MODE_ALL:
|
case com.google.android.exoplayer2.Player.REPEAT_MODE_ALL:
|
||||||
queueControlBinding.controlRepeat.setImageResource(
|
queueControlBinding.controlRepeat
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_all);
|
.setImageResource(R.drawable.exo_controls_repeat_all);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,12 +160,13 @@ class MainPlayerGestureListener(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
initialEvent: MotionEvent?,
|
initialEvent: MotionEvent,
|
||||||
movingEvent: MotionEvent,
|
movingEvent: MotionEvent,
|
||||||
distanceX: Float,
|
distanceX: Float,
|
||||||
distanceY: Float
|
distanceY: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (initialEvent == null || !playerUi.isFullscreen) {
|
|
||||||
|
if (!playerUi.isFullscreen) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ class PopupPlayerGestureListener(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onFling(
|
override fun onFling(
|
||||||
e1: MotionEvent?,
|
e1: MotionEvent,
|
||||||
e2: MotionEvent,
|
e2: MotionEvent,
|
||||||
velocityX: Float,
|
velocityX: Float,
|
||||||
velocityY: Float
|
velocityY: Float
|
||||||
@ -218,14 +218,11 @@ class PopupPlayerGestureListener(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
initialEvent: MotionEvent?,
|
initialEvent: MotionEvent,
|
||||||
movingEvent: MotionEvent,
|
movingEvent: MotionEvent,
|
||||||
distanceX: Float,
|
distanceX: Float,
|
||||||
distanceY: Float
|
distanceY: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (initialEvent == null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isResizing) {
|
if (isResizing) {
|
||||||
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
|
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
|
||||||
|
@ -342,14 +342,14 @@ public class PlaybackParameterDialog extends DialogFragment {
|
|||||||
final Map<Boolean, TextView> pitchCtrlModeComponentMapping =
|
final Map<Boolean, TextView> pitchCtrlModeComponentMapping =
|
||||||
getPitchControlModeComponentMappings();
|
getPitchControlModeComponentMappings();
|
||||||
pitchCtrlModeComponentMapping.forEach((v, textView) -> textView.setBackground(
|
pitchCtrlModeComponentMapping.forEach((v, textView) -> textView.setBackground(
|
||||||
resolveDrawable(requireContext(), android.R.attr.selectableItemBackground)));
|
resolveDrawable(requireContext(), R.attr.selectableItemBackground)));
|
||||||
|
|
||||||
// Mark the selected textview
|
// Mark the selected textview
|
||||||
final TextView textView = pitchCtrlModeComponentMapping.get(semitones);
|
final TextView textView = pitchCtrlModeComponentMapping.get(semitones);
|
||||||
if (textView != null) {
|
if (textView != null) {
|
||||||
textView.setBackground(new LayerDrawable(new Drawable[]{
|
textView.setBackground(new LayerDrawable(new Drawable[]{
|
||||||
resolveDrawable(requireContext(), R.attr.dashed_border),
|
resolveDrawable(requireContext(), R.attr.dashed_border),
|
||||||
resolveDrawable(requireContext(), android.R.attr.selectableItemBackground)
|
resolveDrawable(requireContext(), R.attr.selectableItemBackground)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,14 +415,14 @@ public class PlaybackParameterDialog extends DialogFragment {
|
|||||||
// Bring all textviews into a normal state
|
// Bring all textviews into a normal state
|
||||||
final Map<Double, TextView> stepSiteComponentMapping = getStepSizeComponentMappings();
|
final Map<Double, TextView> stepSiteComponentMapping = getStepSizeComponentMappings();
|
||||||
stepSiteComponentMapping.forEach((v, textView) -> textView.setBackground(
|
stepSiteComponentMapping.forEach((v, textView) -> textView.setBackground(
|
||||||
resolveDrawable(requireContext(), android.R.attr.selectableItemBackground)));
|
resolveDrawable(requireContext(), R.attr.selectableItemBackground)));
|
||||||
|
|
||||||
// Mark the selected textview
|
// Mark the selected textview
|
||||||
final TextView textView = stepSiteComponentMapping.get(newStepSize);
|
final TextView textView = stepSiteComponentMapping.get(newStepSize);
|
||||||
if (textView != null) {
|
if (textView != null) {
|
||||||
textView.setBackground(new LayerDrawable(new Drawable[]{
|
textView.setBackground(new LayerDrawable(new Drawable[]{
|
||||||
resolveDrawable(requireContext(), R.attr.dashed_border),
|
resolveDrawable(requireContext(), R.attr.dashed_border),
|
||||||
resolveDrawable(requireContext(), android.R.attr.selectableItemBackground)
|
resolveDrawable(requireContext(), R.attr.selectableItemBackground)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
package org.schabi.newpipe.player.mediasession;
|
package org.schabi.newpipe.player.mediasession;
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.os.Build;
|
|
||||||
import android.support.v4.media.MediaMetadataCompat;
|
import android.support.v4.media.MediaMetadataCompat;
|
||||||
import android.support.v4.media.session.MediaSessionCompat;
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@ -16,40 +14,26 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.media.session.MediaButtonReceiver;
|
import androidx.media.session.MediaButtonReceiver;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.ForwardingPlayer;
|
import com.google.android.exoplayer2.ForwardingPlayer;
|
||||||
import com.google.android.exoplayer2.Player.RepeatMode;
|
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.player.notification.NotificationActionData;
|
|
||||||
import org.schabi.newpipe.player.notification.NotificationConstants;
|
|
||||||
import org.schabi.newpipe.player.ui.PlayerUi;
|
import org.schabi.newpipe.player.ui.PlayerUi;
|
||||||
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
public class MediaSessionPlayerUi extends PlayerUi
|
public class MediaSessionPlayerUi extends PlayerUi
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
private static final String TAG = "MediaSessUi";
|
private static final String TAG = "MediaSessUi";
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private MediaSessionCompat mediaSession;
|
private MediaSessionCompat mediaSession;
|
||||||
@Nullable
|
|
||||||
private MediaSessionConnector sessionConnector;
|
private MediaSessionConnector sessionConnector;
|
||||||
|
|
||||||
private final String ignoreHardwareMediaButtonsKey;
|
private final String ignoreHardwareMediaButtonsKey;
|
||||||
private boolean shouldIgnoreHardwareMediaButtons = false;
|
private boolean shouldIgnoreHardwareMediaButtons = false;
|
||||||
|
|
||||||
// used to check whether any notification action changed, before sending costly updates
|
|
||||||
private List<NotificationActionData> prevNotificationActions = List.of();
|
|
||||||
|
|
||||||
|
|
||||||
public MediaSessionPlayerUi(@NonNull final Player player) {
|
public MediaSessionPlayerUi(@NonNull final Player player) {
|
||||||
super(player);
|
super(player);
|
||||||
ignoreHardwareMediaButtonsKey =
|
ignoreHardwareMediaButtonsKey =
|
||||||
@ -79,10 +63,6 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||||||
|
|
||||||
sessionConnector.setMetadataDeduplicationEnabled(true);
|
sessionConnector.setMetadataDeduplicationEnabled(true);
|
||||||
sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata());
|
sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata());
|
||||||
|
|
||||||
// force updating media session actions by resetting the previous ones
|
|
||||||
prevNotificationActions = List.of();
|
|
||||||
updateMediaSessionActions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -100,7 +80,6 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||||||
mediaSession.release();
|
mediaSession.release();
|
||||||
mediaSession = null;
|
mediaSession = null;
|
||||||
}
|
}
|
||||||
prevNotificationActions = List.of();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -184,114 +163,4 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||||||
|
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void updateMediaSessionActions() {
|
|
||||||
// On Android 13+ (or Android T or API 33+) the actions in the player notification can't be
|
|
||||||
// controlled directly anymore, but are instead derived from custom media session actions.
|
|
||||||
// However the system allows customizing only two of these actions, since the other three
|
|
||||||
// are fixed to play-pause-buffering, previous, next.
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
// Although setting media session actions on older android versions doesn't seem to
|
|
||||||
// cause any trouble, it also doesn't seem to do anything, so we don't do anything to
|
|
||||||
// save battery. Check out NotificationUtil.updateActions() to see what happens on
|
|
||||||
// older android versions.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sessionConnector == null) {
|
|
||||||
// sessionConnector will be null after destroyPlayer is called
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// only use the fourth and fifth actions (the settings page also shows only the last 2 on
|
|
||||||
// Android 13+)
|
|
||||||
final List<NotificationActionData> newNotificationActions = IntStream.of(3, 4)
|
|
||||||
.map(i -> player.getPrefs().getInt(
|
|
||||||
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
|
||||||
NotificationConstants.SLOT_DEFAULTS[i]))
|
|
||||||
.mapToObj(action -> NotificationActionData
|
|
||||||
.fromNotificationActionEnum(player, action))
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
// avoid costly notification actions update, if nothing changed from last time
|
|
||||||
if (!newNotificationActions.equals(prevNotificationActions)) {
|
|
||||||
prevNotificationActions = newNotificationActions;
|
|
||||||
sessionConnector.setCustomActionProviders(
|
|
||||||
newNotificationActions.stream()
|
|
||||||
.map(data -> new SessionConnectorActionProvider(data, context))
|
|
||||||
.toArray(SessionConnectorActionProvider[]::new));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBlocked() {
|
|
||||||
super.onBlocked();
|
|
||||||
updateMediaSessionActions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlaying() {
|
|
||||||
super.onPlaying();
|
|
||||||
updateMediaSessionActions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBuffering() {
|
|
||||||
super.onBuffering();
|
|
||||||
updateMediaSessionActions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPaused() {
|
|
||||||
super.onPaused();
|
|
||||||
updateMediaSessionActions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPausedSeek() {
|
|
||||||
super.onPausedSeek();
|
|
||||||
updateMediaSessionActions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCompleted() {
|
|
||||||
super.onCompleted();
|
|
||||||
updateMediaSessionActions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
|
|
||||||
super.onRepeatModeChanged(repeatMode);
|
|
||||||
updateMediaSessionActions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
|
|
||||||
super.onShuffleModeEnabledChanged(shuffleModeEnabled);
|
|
||||||
updateMediaSessionActions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBroadcastReceived(final Intent intent) {
|
|
||||||
super.onBroadcastReceived(intent);
|
|
||||||
if (ACTION_RECREATE_NOTIFICATION.equals(intent.getAction())) {
|
|
||||||
// the notification actions changed
|
|
||||||
updateMediaSessionActions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMetadataChanged(@NonNull final StreamInfo info) {
|
|
||||||
super.onMetadataChanged(info);
|
|
||||||
updateMediaSessionActions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayQueueEdited() {
|
|
||||||
super.onPlayQueueEdited();
|
|
||||||
updateMediaSessionActions();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
package org.schabi.newpipe.player.mediasession;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.v4.media.session.PlaybackStateCompat;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.player.notification.NotificationActionData;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
public class SessionConnectorActionProvider implements MediaSessionConnector.CustomActionProvider {
|
|
||||||
|
|
||||||
private final NotificationActionData data;
|
|
||||||
@NonNull
|
|
||||||
private final WeakReference<Context> context;
|
|
||||||
|
|
||||||
public SessionConnectorActionProvider(final NotificationActionData notificationActionData,
|
|
||||||
@NonNull final Context context) {
|
|
||||||
this.data = notificationActionData;
|
|
||||||
this.context = new WeakReference<>(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCustomAction(@NonNull final Player player,
|
|
||||||
@NonNull final String action,
|
|
||||||
@Nullable final Bundle extras) {
|
|
||||||
final Context actualContext = context.get();
|
|
||||||
if (actualContext != null) {
|
|
||||||
actualContext.sendBroadcast(new Intent(action));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public PlaybackStateCompat.CustomAction getCustomAction(@NonNull final Player player) {
|
|
||||||
return new PlaybackStateCompat.CustomAction.Builder(
|
|
||||||
data.action(), data.name(), data.icon()
|
|
||||||
).build();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,205 +0,0 @@
|
|||||||
package org.schabi.newpipe.player.notification;
|
|
||||||
|
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.player.Player;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public final class NotificationActionData {
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final String action;
|
|
||||||
@NonNull
|
|
||||||
private final String name;
|
|
||||||
@DrawableRes
|
|
||||||
private final int icon;
|
|
||||||
|
|
||||||
|
|
||||||
public NotificationActionData(@NonNull final String action, @NonNull final String name,
|
|
||||||
@DrawableRes final int icon) {
|
|
||||||
this.action = action;
|
|
||||||
this.name = name;
|
|
||||||
this.icon = icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public String action() {
|
|
||||||
return action;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public String name() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@DrawableRes
|
|
||||||
public int icon() {
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("PrivateResource") // we currently use Exoplayer's internal strings and icons
|
|
||||||
@Nullable
|
|
||||||
public static NotificationActionData fromNotificationActionEnum(
|
|
||||||
@NonNull final Player player,
|
|
||||||
@NotificationConstants.Action final int selectedAction
|
|
||||||
) {
|
|
||||||
|
|
||||||
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
|
|
||||||
final Context ctx = player.getContext();
|
|
||||||
|
|
||||||
switch (selectedAction) {
|
|
||||||
case NotificationConstants.PREVIOUS:
|
|
||||||
return new NotificationActionData(ACTION_PLAY_PREVIOUS,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_previous_description), baseActionIcon);
|
|
||||||
|
|
||||||
case NotificationConstants.NEXT:
|
|
||||||
return new NotificationActionData(ACTION_PLAY_NEXT,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_next_description), baseActionIcon);
|
|
||||||
|
|
||||||
case NotificationConstants.REWIND:
|
|
||||||
return new NotificationActionData(ACTION_FAST_REWIND,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_rewind_description), baseActionIcon);
|
|
||||||
|
|
||||||
case NotificationConstants.FORWARD:
|
|
||||||
return new NotificationActionData(ACTION_FAST_FORWARD,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_fastforward_description), baseActionIcon);
|
|
||||||
|
|
||||||
case NotificationConstants.SMART_REWIND_PREVIOUS:
|
|
||||||
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
|
||||||
return new NotificationActionData(ACTION_PLAY_PREVIOUS,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_previous_description),
|
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_notification_previous);
|
|
||||||
} else {
|
|
||||||
return new NotificationActionData(ACTION_FAST_REWIND,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_rewind_description),
|
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_controls_rewind);
|
|
||||||
}
|
|
||||||
|
|
||||||
case NotificationConstants.SMART_FORWARD_NEXT:
|
|
||||||
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
|
||||||
return new NotificationActionData(ACTION_PLAY_NEXT,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_next_description),
|
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_notification_next);
|
|
||||||
} else {
|
|
||||||
return new NotificationActionData(ACTION_FAST_FORWARD,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_fastforward_description),
|
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_controls_fastforward);
|
|
||||||
}
|
|
||||||
|
|
||||||
case NotificationConstants.PLAY_PAUSE_BUFFERING:
|
|
||||||
if (player.getCurrentState() == Player.STATE_PREFLIGHT
|
|
||||||
|| player.getCurrentState() == Player.STATE_BLOCKED
|
|
||||||
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
|
||||||
return new NotificationActionData(ACTION_PLAY_PAUSE,
|
|
||||||
ctx.getString(R.string.notification_action_buffering),
|
|
||||||
R.drawable.ic_hourglass_top);
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallthrough
|
|
||||||
case NotificationConstants.PLAY_PAUSE:
|
|
||||||
if (player.getCurrentState() == Player.STATE_COMPLETED) {
|
|
||||||
return new NotificationActionData(ACTION_PLAY_PAUSE,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_pause_description),
|
|
||||||
R.drawable.ic_replay);
|
|
||||||
} else if (player.isPlaying()
|
|
||||||
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|
|
||||||
|| player.getCurrentState() == Player.STATE_BLOCKED
|
|
||||||
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
|
||||||
return new NotificationActionData(ACTION_PLAY_PAUSE,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_pause_description),
|
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_notification_pause);
|
|
||||||
} else {
|
|
||||||
return new NotificationActionData(ACTION_PLAY_PAUSE,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_play_description),
|
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_notification_play);
|
|
||||||
}
|
|
||||||
|
|
||||||
case NotificationConstants.REPEAT:
|
|
||||||
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
|
|
||||||
return new NotificationActionData(ACTION_REPEAT,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_repeat_all_description),
|
|
||||||
com.google.android.exoplayer2.ext.mediasession.R.drawable
|
|
||||||
.exo_media_action_repeat_all);
|
|
||||||
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
|
|
||||||
return new NotificationActionData(ACTION_REPEAT,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_repeat_one_description),
|
|
||||||
com.google.android.exoplayer2.ext.mediasession.R.drawable
|
|
||||||
.exo_media_action_repeat_one);
|
|
||||||
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
|
|
||||||
return new NotificationActionData(ACTION_REPEAT,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_repeat_off_description),
|
|
||||||
com.google.android.exoplayer2.ext.mediasession.R.drawable
|
|
||||||
.exo_media_action_repeat_off);
|
|
||||||
}
|
|
||||||
|
|
||||||
case NotificationConstants.SHUFFLE:
|
|
||||||
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
|
|
||||||
return new NotificationActionData(ACTION_SHUFFLE,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_shuffle_on_description),
|
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_controls_shuffle_on);
|
|
||||||
} else {
|
|
||||||
return new NotificationActionData(ACTION_SHUFFLE,
|
|
||||||
ctx.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_shuffle_off_description),
|
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_controls_shuffle_off);
|
|
||||||
}
|
|
||||||
|
|
||||||
case NotificationConstants.CLOSE:
|
|
||||||
return new NotificationActionData(ACTION_CLOSE, ctx.getString(R.string.close),
|
|
||||||
R.drawable.ic_close);
|
|
||||||
|
|
||||||
case NotificationConstants.NOTHING:
|
|
||||||
default:
|
|
||||||
// do nothing
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(@Nullable final Object obj) {
|
|
||||||
return (obj instanceof NotificationActionData other)
|
|
||||||
&& this.action.equals(other.action)
|
|
||||||
&& this.name.equals(other.name)
|
|
||||||
&& this.icon == other.icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(action, name, icon);
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,7 +13,7 @@ import org.schabi.newpipe.util.Localization;
|
|||||||
|
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.util.Collection;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.SortedSet;
|
import java.util.SortedSet;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
@ -65,29 +65,23 @@ public final class NotificationConstants {
|
|||||||
public static final int CLOSE = 11;
|
public static final int CLOSE = 11;
|
||||||
|
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD,
|
@IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT,
|
||||||
SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT,
|
PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT, SHUFFLE, CLOSE})
|
||||||
SHUFFLE, CLOSE})
|
|
||||||
public @interface Action { }
|
public @interface Action { }
|
||||||
|
|
||||||
@Action
|
|
||||||
public static final int[] ALL_ACTIONS = {NOTHING, PREVIOUS, NEXT, REWIND, FORWARD,
|
|
||||||
SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT,
|
|
||||||
SHUFFLE, CLOSE};
|
|
||||||
|
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
public static final int[] ACTION_ICONS = {
|
public static final int[] ACTION_ICONS = {
|
||||||
0,
|
0,
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_icon_previous,
|
R.drawable.exo_icon_previous,
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_icon_next,
|
R.drawable.exo_icon_next,
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_icon_rewind,
|
R.drawable.exo_icon_rewind,
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_icon_fastforward,
|
R.drawable.exo_icon_fastforward,
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_icon_previous,
|
R.drawable.exo_icon_previous,
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_icon_next,
|
R.drawable.exo_icon_next,
|
||||||
R.drawable.ic_pause,
|
R.drawable.ic_pause,
|
||||||
R.drawable.ic_hourglass_top,
|
R.drawable.ic_hourglass_top,
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_icon_repeat_all,
|
R.drawable.exo_icon_repeat_all,
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_icon_shuffle_on,
|
R.drawable.exo_icon_shuffle_on,
|
||||||
R.drawable.ic_close,
|
R.drawable.ic_close,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,6 +95,16 @@ public final class NotificationConstants {
|
|||||||
CLOSE,
|
CLOSE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@Action
|
||||||
|
public static final int[][] SLOT_ALLOWED_ACTIONS = {
|
||||||
|
new int[] {PREVIOUS, REWIND, SMART_REWIND_PREVIOUS},
|
||||||
|
new int[] {REWIND, PLAY_PAUSE, PLAY_PAUSE_BUFFERING},
|
||||||
|
new int[] {NEXT, FORWARD, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING},
|
||||||
|
new int[] {NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS,
|
||||||
|
SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE},
|
||||||
|
new int[] {NOTHING, NEXT, FORWARD, SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE},
|
||||||
|
};
|
||||||
|
|
||||||
public static final int[] SLOT_PREF_KEYS = {
|
public static final int[] SLOT_PREF_KEYS = {
|
||||||
R.string.notification_slot_0_key,
|
R.string.notification_slot_0_key,
|
||||||
R.string.notification_slot_1_key,
|
R.string.notification_slot_1_key,
|
||||||
@ -122,41 +126,29 @@ public final class NotificationConstants {
|
|||||||
public static String getActionName(@NonNull final Context context, @Action final int action) {
|
public static String getActionName(@NonNull final Context context, @Action final int action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case PREVIOUS:
|
case PREVIOUS:
|
||||||
return context.getString(com.google.android.exoplayer2.ui.R.string
|
return context.getString(R.string.exo_controls_previous_description);
|
||||||
.exo_controls_previous_description);
|
|
||||||
case NEXT:
|
case NEXT:
|
||||||
return context.getString(com.google.android.exoplayer2.ui.R.string
|
return context.getString(R.string.exo_controls_next_description);
|
||||||
.exo_controls_next_description);
|
|
||||||
case REWIND:
|
case REWIND:
|
||||||
return context.getString(com.google.android.exoplayer2.ui.R.string
|
return context.getString(R.string.exo_controls_rewind_description);
|
||||||
.exo_controls_rewind_description);
|
|
||||||
case FORWARD:
|
case FORWARD:
|
||||||
return context.getString(com.google.android.exoplayer2.ui.R.string
|
return context.getString(R.string.exo_controls_fastforward_description);
|
||||||
.exo_controls_fastforward_description);
|
|
||||||
case SMART_REWIND_PREVIOUS:
|
case SMART_REWIND_PREVIOUS:
|
||||||
return Localization.concatenateStrings(
|
return Localization.concatenateStrings(
|
||||||
context.getString(com.google.android.exoplayer2.ui.R.string
|
context.getString(R.string.exo_controls_rewind_description),
|
||||||
.exo_controls_rewind_description),
|
context.getString(R.string.exo_controls_previous_description));
|
||||||
context.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_previous_description));
|
|
||||||
case SMART_FORWARD_NEXT:
|
case SMART_FORWARD_NEXT:
|
||||||
return Localization.concatenateStrings(
|
return Localization.concatenateStrings(
|
||||||
context.getString(com.google.android.exoplayer2.ui.R.string
|
context.getString(R.string.exo_controls_fastforward_description),
|
||||||
.exo_controls_fastforward_description),
|
context.getString(R.string.exo_controls_next_description));
|
||||||
context.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_next_description));
|
|
||||||
case PLAY_PAUSE:
|
case PLAY_PAUSE:
|
||||||
return Localization.concatenateStrings(
|
return Localization.concatenateStrings(
|
||||||
context.getString(com.google.android.exoplayer2.ui.R.string
|
context.getString(R.string.exo_controls_play_description),
|
||||||
.exo_controls_play_description),
|
context.getString(R.string.exo_controls_pause_description));
|
||||||
context.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_pause_description));
|
|
||||||
case PLAY_PAUSE_BUFFERING:
|
case PLAY_PAUSE_BUFFERING:
|
||||||
return Localization.concatenateStrings(
|
return Localization.concatenateStrings(
|
||||||
context.getString(com.google.android.exoplayer2.ui.R.string
|
context.getString(R.string.exo_controls_play_description),
|
||||||
.exo_controls_play_description),
|
context.getString(R.string.exo_controls_pause_description),
|
||||||
context.getString(com.google.android.exoplayer2.ui.R.string
|
|
||||||
.exo_controls_pause_description),
|
|
||||||
context.getString(R.string.notification_action_buffering));
|
context.getString(R.string.notification_action_buffering));
|
||||||
case REPEAT:
|
case REPEAT:
|
||||||
return context.getString(R.string.notification_action_repeat);
|
return context.getString(R.string.notification_action_repeat);
|
||||||
@ -173,11 +165,14 @@ public final class NotificationConstants {
|
|||||||
/**
|
/**
|
||||||
* @param context the context to use
|
* @param context the context to use
|
||||||
* @param sharedPreferences the shared preferences to query values from
|
* @param sharedPreferences the shared preferences to query values from
|
||||||
|
* @param slotCount remove indices >= than this value (set to {@code 5} to do nothing, or make
|
||||||
|
* it lower if there are slots with empty actions)
|
||||||
* @return a sorted list of the indices of the slots to use as compact slots
|
* @return a sorted list of the indices of the slots to use as compact slots
|
||||||
*/
|
*/
|
||||||
public static Collection<Integer> getCompactSlotsFromPreferences(
|
public static List<Integer> getCompactSlotsFromPreferences(
|
||||||
@NonNull final Context context,
|
@NonNull final Context context,
|
||||||
final SharedPreferences sharedPreferences) {
|
final SharedPreferences sharedPreferences,
|
||||||
|
final int slotCount) {
|
||||||
final SortedSet<Integer> compactSlots = new TreeSet<>();
|
final SortedSet<Integer> compactSlots = new TreeSet<>();
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
final int compactSlot = sharedPreferences.getInt(
|
final int compactSlot = sharedPreferences.getInt(
|
||||||
@ -185,14 +180,14 @@ public final class NotificationConstants {
|
|||||||
|
|
||||||
if (compactSlot == Integer.MAX_VALUE) {
|
if (compactSlot == Integer.MAX_VALUE) {
|
||||||
// settings not yet populated, return default values
|
// settings not yet populated, return default values
|
||||||
return SLOT_COMPACT_DEFAULTS;
|
return new ArrayList<>(SLOT_COMPACT_DEFAULTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (compactSlot >= 0) {
|
// a negative value (-1) is set when the user does not want a particular compact slot
|
||||||
// compact slot is < 0 if there are less than 3 checked checkboxes
|
if (compactSlot >= 0 && compactSlot < slotCount) {
|
||||||
compactSlots.add(compactSlot);
|
compactSlots.add(compactSlot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return compactSlots;
|
return new ArrayList<>(compactSlots);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
package org.schabi.newpipe.player.notification;
|
package org.schabi.newpipe.player.notification;
|
||||||
|
|
||||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
|
||||||
import static androidx.media.app.NotificationCompat.MediaStyle;
|
|
||||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ServiceInfo;
|
import android.content.pm.ServiceInfo;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
import androidx.core.app.NotificationManagerCompat;
|
||||||
import androidx.core.app.PendingIntentCompat;
|
import androidx.core.app.PendingIntentCompat;
|
||||||
@ -26,12 +23,23 @@ import org.schabi.newpipe.player.Player;
|
|||||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
import static androidx.media.app.NotificationCompat.MediaStyle;
|
||||||
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||||
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a utility class for player notifications.
|
* This is a utility class for player notifications.
|
||||||
*/
|
*/
|
||||||
@ -92,21 +100,29 @@ public final class NotificationUtil {
|
|||||||
final NotificationCompat.Builder builder =
|
final NotificationCompat.Builder builder =
|
||||||
new NotificationCompat.Builder(player.getContext(),
|
new NotificationCompat.Builder(player.getContext(),
|
||||||
player.getContext().getString(R.string.notification_channel_id));
|
player.getContext().getString(R.string.notification_channel_id));
|
||||||
final MediaStyle mediaStyle = new MediaStyle();
|
|
||||||
|
|
||||||
// setup media style (compact notification slots and media session)
|
initializeNotificationSlots();
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
// notification actions are ignored on Android 13+, and are replaced by code in
|
// count the number of real slots, to make sure compact slots indices are not out of bound
|
||||||
// MediaSessionPlayerUi
|
int nonNothingSlotCount = 5;
|
||||||
final int[] compactSlots = initializeNotificationSlots();
|
if (notificationSlots[3] == NotificationConstants.NOTHING) {
|
||||||
mediaStyle.setShowActionsInCompactView(compactSlots);
|
--nonNothingSlotCount;
|
||||||
}
|
}
|
||||||
|
if (notificationSlots[4] == NotificationConstants.NOTHING) {
|
||||||
|
--nonNothingSlotCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the compact slot indices array (need code to convert from Integer... because Java)
|
||||||
|
final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
|
||||||
|
player.getContext(), player.getPrefs(), nonNothingSlotCount);
|
||||||
|
final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray();
|
||||||
|
|
||||||
|
final MediaStyle mediaStyle = new MediaStyle().setShowActionsInCompactView(compactSlots);
|
||||||
player.UIs()
|
player.UIs()
|
||||||
.get(MediaSessionPlayerUi.class)
|
.get(MediaSessionPlayerUi.class)
|
||||||
.flatMap(MediaSessionPlayerUi::getSessionToken)
|
.flatMap(MediaSessionPlayerUi::getSessionToken)
|
||||||
.ifPresent(mediaStyle::setMediaSession);
|
.ifPresent(mediaStyle::setMediaSession);
|
||||||
|
|
||||||
// setup notification builder
|
|
||||||
builder.setStyle(mediaStyle)
|
builder.setStyle(mediaStyle)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
@ -141,12 +157,8 @@ public final class NotificationUtil {
|
|||||||
notificationBuilder.setContentText(player.getUploaderName());
|
notificationBuilder.setContentText(player.getUploaderName());
|
||||||
notificationBuilder.setTicker(player.getVideoTitle());
|
notificationBuilder.setTicker(player.getVideoTitle());
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
// notification actions are ignored on Android 13+, and are replaced by code in
|
|
||||||
// MediaSessionPlayerUi
|
|
||||||
updateActions(notificationBuilder);
|
updateActions(notificationBuilder);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@ -197,35 +209,12 @@ public final class NotificationUtil {
|
|||||||
// ACTIONS
|
// ACTIONS
|
||||||
/////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////
|
||||||
|
|
||||||
/**
|
private void initializeNotificationSlots() {
|
||||||
* The compact slots array from settings contains indices from 0 to 4, each referring to one of
|
|
||||||
* the five actions configurable by the user. However, if the user sets an action to "Nothing",
|
|
||||||
* then all of the actions coming after will have a "settings index" different than the index
|
|
||||||
* of the corresponding action when sent to the system.
|
|
||||||
*
|
|
||||||
* @return the indices of compact slots referred to the list of non-nothing actions that will be
|
|
||||||
* sent to the system
|
|
||||||
*/
|
|
||||||
private int[] initializeNotificationSlots() {
|
|
||||||
final Collection<Integer> settingsCompactSlots = NotificationConstants
|
|
||||||
.getCompactSlotsFromPreferences(player.getContext(), player.getPrefs());
|
|
||||||
final List<Integer> adjustedCompactSlots = new ArrayList<>();
|
|
||||||
|
|
||||||
int nonNothingIndex = 0;
|
|
||||||
for (int i = 0; i < 5; ++i) {
|
for (int i = 0; i < 5; ++i) {
|
||||||
notificationSlots[i] = player.getPrefs().getInt(
|
notificationSlots[i] = player.getPrefs().getInt(
|
||||||
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||||
NotificationConstants.SLOT_DEFAULTS[i]);
|
NotificationConstants.SLOT_DEFAULTS[i]);
|
||||||
|
|
||||||
if (notificationSlots[i] != NotificationConstants.NOTHING) {
|
|
||||||
if (settingsCompactSlots.contains(i)) {
|
|
||||||
adjustedCompactSlots.add(nonNothingIndex);
|
|
||||||
}
|
}
|
||||||
nonNothingIndex += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return adjustedCompactSlots.stream().mapToInt(Integer::intValue).toArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@ -238,15 +227,115 @@ public final class NotificationUtil {
|
|||||||
|
|
||||||
private void addAction(final NotificationCompat.Builder builder,
|
private void addAction(final NotificationCompat.Builder builder,
|
||||||
@NotificationConstants.Action final int slot) {
|
@NotificationConstants.Action final int slot) {
|
||||||
@Nullable final NotificationActionData data =
|
final NotificationCompat.Action action = getAction(slot);
|
||||||
NotificationActionData.fromNotificationActionEnum(player, slot);
|
if (action != null) {
|
||||||
if (data == null) {
|
builder.addAction(action);
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final PendingIntent intent = PendingIntentCompat.getBroadcast(player.getContext(),
|
@Nullable
|
||||||
NOTIFICATION_ID, new Intent(data.action()), FLAG_UPDATE_CURRENT, false);
|
private NotificationCompat.Action getAction(
|
||||||
builder.addAction(new NotificationCompat.Action(data.icon(), data.name(), intent));
|
@NotificationConstants.Action final int selectedAction) {
|
||||||
|
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
|
||||||
|
switch (selectedAction) {
|
||||||
|
case NotificationConstants.PREVIOUS:
|
||||||
|
return getAction(baseActionIcon,
|
||||||
|
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
||||||
|
|
||||||
|
case NotificationConstants.NEXT:
|
||||||
|
return getAction(baseActionIcon,
|
||||||
|
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
||||||
|
|
||||||
|
case NotificationConstants.REWIND:
|
||||||
|
return getAction(baseActionIcon,
|
||||||
|
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
||||||
|
|
||||||
|
case NotificationConstants.FORWARD:
|
||||||
|
return getAction(baseActionIcon,
|
||||||
|
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
||||||
|
|
||||||
|
case NotificationConstants.SMART_REWIND_PREVIOUS:
|
||||||
|
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
||||||
|
return getAction(R.drawable.exo_notification_previous,
|
||||||
|
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
||||||
|
} else {
|
||||||
|
return getAction(R.drawable.exo_controls_rewind,
|
||||||
|
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
||||||
|
}
|
||||||
|
|
||||||
|
case NotificationConstants.SMART_FORWARD_NEXT:
|
||||||
|
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
||||||
|
return getAction(R.drawable.exo_notification_next,
|
||||||
|
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
||||||
|
} else {
|
||||||
|
return getAction(R.drawable.exo_controls_fastforward,
|
||||||
|
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
||||||
|
}
|
||||||
|
|
||||||
|
case NotificationConstants.PLAY_PAUSE_BUFFERING:
|
||||||
|
if (player.getCurrentState() == Player.STATE_PREFLIGHT
|
||||||
|
|| player.getCurrentState() == Player.STATE_BLOCKED
|
||||||
|
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
||||||
|
// null intent -> show hourglass icon that does nothing when clicked
|
||||||
|
return new NotificationCompat.Action(R.drawable.ic_hourglass_top,
|
||||||
|
player.getContext().getString(R.string.notification_action_buffering),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallthrough
|
||||||
|
case NotificationConstants.PLAY_PAUSE:
|
||||||
|
if (player.getCurrentState() == Player.STATE_COMPLETED) {
|
||||||
|
return getAction(R.drawable.ic_replay,
|
||||||
|
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
||||||
|
} else if (player.isPlaying()
|
||||||
|
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|
||||||
|
|| player.getCurrentState() == Player.STATE_BLOCKED
|
||||||
|
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
||||||
|
return getAction(R.drawable.exo_notification_pause,
|
||||||
|
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
||||||
|
} else {
|
||||||
|
return getAction(R.drawable.exo_notification_play,
|
||||||
|
R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
case NotificationConstants.REPEAT:
|
||||||
|
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
|
||||||
|
return getAction(R.drawable.exo_media_action_repeat_all,
|
||||||
|
R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
|
||||||
|
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
|
||||||
|
return getAction(R.drawable.exo_media_action_repeat_one,
|
||||||
|
R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
|
||||||
|
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
|
||||||
|
return getAction(R.drawable.exo_media_action_repeat_off,
|
||||||
|
R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
case NotificationConstants.SHUFFLE:
|
||||||
|
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
|
||||||
|
return getAction(R.drawable.exo_controls_shuffle_on,
|
||||||
|
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
|
||||||
|
} else {
|
||||||
|
return getAction(R.drawable.exo_controls_shuffle_off,
|
||||||
|
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
case NotificationConstants.CLOSE:
|
||||||
|
return getAction(R.drawable.ic_close,
|
||||||
|
R.string.close, ACTION_CLOSE);
|
||||||
|
|
||||||
|
case NotificationConstants.NOTHING:
|
||||||
|
default:
|
||||||
|
// do nothing
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private NotificationCompat.Action getAction(@DrawableRes final int drawable,
|
||||||
|
@StringRes final int title,
|
||||||
|
final String intentAction) {
|
||||||
|
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
|
||||||
|
PendingIntentCompat.getBroadcast(player.getContext(), NOTIFICATION_ID,
|
||||||
|
new Intent(intentAction), FLAG_UPDATE_CURRENT, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Intent getIntentForNotification() {
|
private Intent getIntentForNotification() {
|
||||||
@ -275,7 +364,7 @@ public final class NotificationUtil {
|
|||||||
final Bitmap thumbnail = player.getThumbnail();
|
final Bitmap thumbnail = player.getThumbnail();
|
||||||
if (thumbnail == null || !showThumbnail) {
|
if (thumbnail == null || !showThumbnail) {
|
||||||
// since the builder is reused, make sure the thumbnail is unset if there is not one
|
// since the builder is reused, make sure the thumbnail is unset if there is not one
|
||||||
builder.setLargeIcon((Bitmap) null);
|
builder.setLargeIcon(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -952,14 +952,11 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
|||||||
super.onRepeatModeChanged(repeatMode);
|
super.onRepeatModeChanged(repeatMode);
|
||||||
|
|
||||||
if (repeatMode == REPEAT_MODE_ALL) {
|
if (repeatMode == REPEAT_MODE_ALL) {
|
||||||
binding.repeatButton.setImageResource(
|
binding.repeatButton.setImageResource(R.drawable.exo_controls_repeat_all);
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_all);
|
|
||||||
} else if (repeatMode == REPEAT_MODE_ONE) {
|
} else if (repeatMode == REPEAT_MODE_ONE) {
|
||||||
binding.repeatButton.setImageResource(
|
binding.repeatButton.setImageResource(R.drawable.exo_controls_repeat_one);
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_one);
|
|
||||||
} else /* repeatMode == REPEAT_MODE_OFF */ {
|
} else /* repeatMode == REPEAT_MODE_OFF */ {
|
||||||
binding.repeatButton.setImageResource(
|
binding.repeatButton.setImageResource(R.drawable.exo_controls_repeat_off);
|
||||||
com.google.android.exoplayer2.ui.R.drawable.exo_controls_repeat_off);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,301 +0,0 @@
|
|||||||
package org.schabi.newpipe.settings;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResult;
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.preference.Preference;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import com.grack.nanojson.JsonParserException;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
|
||||||
import org.schabi.newpipe.error.UserAction;
|
|
||||||
import org.schabi.newpipe.settings.export.BackupFileLocator;
|
|
||||||
import org.schabi.newpipe.settings.export.ImportExportManager;
|
|
||||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.ZipHelper;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
|
||||||
|
|
||||||
private static final String ZIP_MIME_TYPE = "application/zip";
|
|
||||||
|
|
||||||
private final SimpleDateFormat exportDateFormat =
|
|
||||||
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
|
|
||||||
private ImportExportManager manager;
|
|
||||||
private String importExportDataPathKey;
|
|
||||||
private final ActivityResultLauncher<Intent> requestImportPathLauncher =
|
|
||||||
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
|
|
||||||
this::requestImportPathResult);
|
|
||||||
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
|
|
||||||
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
|
|
||||||
this::requestExportPathResult);
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
|
|
||||||
@Nullable final String rootKey) {
|
|
||||||
final File homeDir = ContextCompat.getDataDir(requireContext());
|
|
||||||
Objects.requireNonNull(homeDir);
|
|
||||||
manager = new ImportExportManager(new BackupFileLocator(homeDir));
|
|
||||||
|
|
||||||
importExportDataPathKey = getString(R.string.import_export_data_path);
|
|
||||||
|
|
||||||
|
|
||||||
addPreferencesFromResourceRegistry();
|
|
||||||
|
|
||||||
final Preference importDataPreference = requirePreference(R.string.import_data);
|
|
||||||
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
|
|
||||||
NoFileManagerSafeGuard.launchSafe(
|
|
||||||
requestImportPathLauncher,
|
|
||||||
StoredFileHelper.getPicker(requireContext(),
|
|
||||||
ZIP_MIME_TYPE, getImportExportDataUri()),
|
|
||||||
TAG,
|
|
||||||
getContext()
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
final Preference exportDataPreference = requirePreference(R.string.export_data);
|
|
||||||
exportDataPreference.setOnPreferenceClickListener((final Preference p) -> {
|
|
||||||
NoFileManagerSafeGuard.launchSafe(
|
|
||||||
requestExportPathLauncher,
|
|
||||||
StoredFileHelper.getNewPicker(requireContext(),
|
|
||||||
"NewPipeData-" + exportDateFormat.format(new Date()) + ".zip",
|
|
||||||
ZIP_MIME_TYPE, getImportExportDataUri()),
|
|
||||||
TAG,
|
|
||||||
getContext()
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
final Preference resetSettings = findPreference(getString(R.string.reset_settings));
|
|
||||||
// Resets all settings by deleting shared preference and restarting the app
|
|
||||||
// A dialogue will pop up to confirm if user intends to reset all settings
|
|
||||||
assert resetSettings != null;
|
|
||||||
resetSettings.setOnPreferenceClickListener(preference -> {
|
|
||||||
// Show Alert Dialogue
|
|
||||||
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
|
||||||
builder.setMessage(R.string.reset_all_settings);
|
|
||||||
builder.setCancelable(true);
|
|
||||||
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
|
|
||||||
// Deletes all shared preferences xml files.
|
|
||||||
final SharedPreferences sharedPreferences =
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(requireContext());
|
|
||||||
sharedPreferences.edit().clear().apply();
|
|
||||||
// Restarts the app
|
|
||||||
if (getActivity() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
NavigationHelper.restartApp(getActivity());
|
|
||||||
});
|
|
||||||
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> {
|
|
||||||
});
|
|
||||||
final AlertDialog alertDialog = builder.create();
|
|
||||||
alertDialog.show();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void requestExportPathResult(final ActivityResult result) {
|
|
||||||
assureCorrectAppLanguage(requireContext());
|
|
||||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
|
||||||
// will be saved only on success
|
|
||||||
final Uri lastExportDataUri = result.getData().getData();
|
|
||||||
|
|
||||||
final StoredFileHelper file = new StoredFileHelper(
|
|
||||||
requireContext(), result.getData().getData(), ZIP_MIME_TYPE);
|
|
||||||
|
|
||||||
exportDatabase(file, lastExportDataUri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void requestImportPathResult(final ActivityResult result) {
|
|
||||||
assureCorrectAppLanguage(requireContext());
|
|
||||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
|
||||||
// will be saved only on success
|
|
||||||
final Uri lastImportDataUri = result.getData().getData();
|
|
||||||
|
|
||||||
final StoredFileHelper file = new StoredFileHelper(
|
|
||||||
requireContext(), result.getData().getData(), ZIP_MIME_TYPE);
|
|
||||||
|
|
||||||
new androidx.appcompat.app.AlertDialog.Builder(requireActivity())
|
|
||||||
.setMessage(R.string.override_current_data)
|
|
||||||
.setPositiveButton(R.string.ok, (d, id) ->
|
|
||||||
importDatabase(file, lastImportDataUri))
|
|
||||||
.setNegativeButton(R.string.cancel, (d, id) ->
|
|
||||||
d.cancel())
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) {
|
|
||||||
try {
|
|
||||||
//checkpoint before export
|
|
||||||
NewPipeDatabase.checkpoint();
|
|
||||||
|
|
||||||
final SharedPreferences preferences = PreferenceManager
|
|
||||||
.getDefaultSharedPreferences(requireContext());
|
|
||||||
manager.exportDatabase(preferences, file);
|
|
||||||
|
|
||||||
saveLastImportExportDataUri(exportDataUri); // save export path only on success
|
|
||||||
Toast.makeText(requireContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT)
|
|
||||||
.show();
|
|
||||||
} catch (final Exception e) {
|
|
||||||
showErrorSnackbar(e, "Exporting database and settings");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void importDatabase(final StoredFileHelper file, final Uri importDataUri) {
|
|
||||||
// check if file is supported
|
|
||||||
if (!ZipHelper.isValidZipFile(file)) {
|
|
||||||
Toast.makeText(requireContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
|
|
||||||
.show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!manager.ensureDbDirectoryExists()) {
|
|
||||||
throw new IOException("Could not create databases dir");
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace the current database
|
|
||||||
if (!manager.extractDb(file)) {
|
|
||||||
Toast.makeText(requireContext(), R.string.could_not_import_all_files,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// if settings file exist, ask if it should be imported.
|
|
||||||
final boolean hasJsonPrefs = manager.exportHasJsonPrefs(file);
|
|
||||||
if (hasJsonPrefs || manager.exportHasSerializedPrefs(file)) {
|
|
||||||
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle(R.string.import_settings)
|
|
||||||
.setMessage(hasJsonPrefs ? null : requireContext()
|
|
||||||
.getString(R.string.import_settings_vulnerable_format))
|
|
||||||
.setOnDismissListener(dialog -> finishImport(importDataUri))
|
|
||||||
.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
finishImport(importDataUri);
|
|
||||||
})
|
|
||||||
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
final Context context = requireContext();
|
|
||||||
final SharedPreferences prefs = PreferenceManager
|
|
||||||
.getDefaultSharedPreferences(context);
|
|
||||||
try {
|
|
||||||
if (hasJsonPrefs) {
|
|
||||||
manager.loadJsonPrefs(file, prefs);
|
|
||||||
} else {
|
|
||||||
manager.loadSerializedPrefs(file, prefs);
|
|
||||||
}
|
|
||||||
} catch (IOException | ClassNotFoundException | JsonParserException e) {
|
|
||||||
createErrorNotification(e, "Importing preferences");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cleanImport(context, prefs);
|
|
||||||
finishImport(importDataUri);
|
|
||||||
})
|
|
||||||
.show();
|
|
||||||
} else {
|
|
||||||
finishImport(importDataUri);
|
|
||||||
}
|
|
||||||
} catch (final Exception e) {
|
|
||||||
showErrorSnackbar(e, "Importing database and settings");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove settings that are not supposed to be imported on different devices
|
|
||||||
* and reset them to default values.
|
|
||||||
* @param context the context used for the import
|
|
||||||
* @param prefs the preferences used while running the import
|
|
||||||
*/
|
|
||||||
private void cleanImport(@NonNull final Context context,
|
|
||||||
@NonNull final SharedPreferences prefs) {
|
|
||||||
// Check if media tunnelling needs to be disabled automatically,
|
|
||||||
// if it was disabled automatically in the imported preferences.
|
|
||||||
final String tunnelingKey = context.getString(R.string.disable_media_tunneling_key);
|
|
||||||
final String automaticTunnelingKey =
|
|
||||||
context.getString(R.string.disabled_media_tunneling_automatically_key);
|
|
||||||
// R.string.disable_media_tunneling_key should always be true
|
|
||||||
// if R.string.disabled_media_tunneling_automatically_key equals 1,
|
|
||||||
// but we double check here just to be sure and to avoid regressions
|
|
||||||
// caused by possible later modification of the media tunneling functionality.
|
|
||||||
// R.string.disabled_media_tunneling_automatically_key == 0:
|
|
||||||
// automatic value overridden by user in settings
|
|
||||||
// R.string.disabled_media_tunneling_automatically_key == -1: not set
|
|
||||||
final boolean wasMediaTunnelingDisabledAutomatically =
|
|
||||||
prefs.getInt(automaticTunnelingKey, -1) == 1
|
|
||||||
&& prefs.getBoolean(tunnelingKey, false);
|
|
||||||
if (wasMediaTunnelingDisabledAutomatically) {
|
|
||||||
prefs.edit()
|
|
||||||
.putInt(automaticTunnelingKey, -1)
|
|
||||||
.putBoolean(tunnelingKey, false)
|
|
||||||
.apply();
|
|
||||||
NewPipeSettings.setMediaTunneling(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save import path and restart app.
|
|
||||||
*
|
|
||||||
* @param importDataUri The import path to save
|
|
||||||
*/
|
|
||||||
private void finishImport(final Uri importDataUri) {
|
|
||||||
// save import path only on success
|
|
||||||
saveLastImportExportDataUri(importDataUri);
|
|
||||||
// restart app to properly load db
|
|
||||||
NavigationHelper.restartApp(requireActivity());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Uri getImportExportDataUri() {
|
|
||||||
final String path = defaultPreferences.getString(importExportDataPathKey, null);
|
|
||||||
return isBlank(path) ? null : Uri.parse(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveLastImportExportDataUri(final Uri importExportDataUri) {
|
|
||||||
final SharedPreferences.Editor editor = defaultPreferences.edit()
|
|
||||||
.putString(importExportDataPathKey, importExportDataUri.toString());
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showErrorSnackbar(final Throwable e, final String request) {
|
|
||||||
ErrorUtil.showSnackbar(this, new ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createErrorNotification(final Throwable e, final String request) {
|
|
||||||
ErrorUtil.createNotification(
|
|
||||||
requireContext(),
|
|
||||||
new ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +1,106 @@
|
|||||||
package org.schabi.newpipe.settings;
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||||
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResult;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.DownloaderImpl;
|
import org.schabi.newpipe.DownloaderImpl;
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||||
import org.schabi.newpipe.extractor.localization.Localization;
|
import org.schabi.newpipe.extractor.localization.Localization;
|
||||||
|
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||||
|
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.ZipHelper;
|
||||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class ContentSettingsFragment extends BasePreferenceFragment {
|
public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||||
|
private static final String ZIP_MIME_TYPE = "application/zip";
|
||||||
|
|
||||||
|
private final SimpleDateFormat exportDateFormat =
|
||||||
|
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
|
||||||
|
|
||||||
|
private ContentSettingsManager manager;
|
||||||
|
|
||||||
|
private String importExportDataPathKey;
|
||||||
private String youtubeRestrictedModeEnabledKey;
|
private String youtubeRestrictedModeEnabledKey;
|
||||||
|
|
||||||
private Localization initialSelectedLocalization;
|
private Localization initialSelectedLocalization;
|
||||||
private ContentCountry initialSelectedContentCountry;
|
private ContentCountry initialSelectedContentCountry;
|
||||||
private String initialLanguage;
|
private String initialLanguage;
|
||||||
|
private final ActivityResultLauncher<Intent> requestImportPathLauncher =
|
||||||
|
registerForActivityResult(new StartActivityForResult(), this::requestImportPathResult);
|
||||||
|
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
|
||||||
|
registerForActivityResult(new StartActivityForResult(), this::requestExportPathResult);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||||
|
final File homeDir = ContextCompat.getDataDir(requireContext());
|
||||||
|
Objects.requireNonNull(homeDir);
|
||||||
|
manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir));
|
||||||
|
manager.deleteSettingsFile();
|
||||||
|
|
||||||
|
importExportDataPathKey = getString(R.string.import_export_data_path);
|
||||||
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
||||||
|
|
||||||
addPreferencesFromResourceRegistry();
|
addPreferencesFromResourceRegistry();
|
||||||
|
|
||||||
|
final Preference importDataPreference = requirePreference(R.string.import_data);
|
||||||
|
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
|
||||||
|
NoFileManagerSafeGuard.launchSafe(
|
||||||
|
requestImportPathLauncher,
|
||||||
|
StoredFileHelper.getPicker(requireContext(),
|
||||||
|
ZIP_MIME_TYPE, getImportExportDataUri()),
|
||||||
|
TAG,
|
||||||
|
getContext()
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final Preference exportDataPreference = requirePreference(R.string.export_data);
|
||||||
|
exportDataPreference.setOnPreferenceClickListener((final Preference p) -> {
|
||||||
|
NoFileManagerSafeGuard.launchSafe(
|
||||||
|
requestExportPathLauncher,
|
||||||
|
StoredFileHelper.getNewPicker(requireContext(),
|
||||||
|
"NewPipeData-" + exportDateFormat.format(new Date()) + ".zip",
|
||||||
|
ZIP_MIME_TYPE, getImportExportDataUri()),
|
||||||
|
TAG,
|
||||||
|
getContext()
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
initialSelectedLocalization = org.schabi.newpipe.util.Localization
|
initialSelectedLocalization = org.schabi.newpipe.util.Localization
|
||||||
.getPreferredLocalization(requireContext());
|
.getPreferredLocalization(requireContext());
|
||||||
initialSelectedContentCountry = org.schabi.newpipe.util.Localization
|
initialSelectedContentCountry = org.schabi.newpipe.util.Localization
|
||||||
@ -88,4 +158,151 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
|||||||
NewPipe.setupLocalization(selectedLocalization, selectedContentCountry);
|
NewPipe.setupLocalization(selectedLocalization, selectedContentCountry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void requestExportPathResult(final ActivityResult result) {
|
||||||
|
assureCorrectAppLanguage(getContext());
|
||||||
|
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||||
|
// will be saved only on success
|
||||||
|
final Uri lastExportDataUri = result.getData().getData();
|
||||||
|
|
||||||
|
final StoredFileHelper file =
|
||||||
|
new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE);
|
||||||
|
|
||||||
|
exportDatabase(file, lastExportDataUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestImportPathResult(final ActivityResult result) {
|
||||||
|
assureCorrectAppLanguage(getContext());
|
||||||
|
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||||
|
// will be saved only on success
|
||||||
|
final Uri lastImportDataUri = result.getData().getData();
|
||||||
|
|
||||||
|
final StoredFileHelper file =
|
||||||
|
new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE);
|
||||||
|
|
||||||
|
new AlertDialog.Builder(requireActivity())
|
||||||
|
.setMessage(R.string.override_current_data)
|
||||||
|
.setPositiveButton(R.string.ok, (d, id) ->
|
||||||
|
importDatabase(file, lastImportDataUri))
|
||||||
|
.setNegativeButton(R.string.cancel, (d, id) ->
|
||||||
|
d.cancel())
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) {
|
||||||
|
try {
|
||||||
|
//checkpoint before export
|
||||||
|
NewPipeDatabase.checkpoint();
|
||||||
|
|
||||||
|
final SharedPreferences preferences = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(requireContext());
|
||||||
|
manager.exportDatabase(preferences, file);
|
||||||
|
|
||||||
|
saveLastImportExportDataUri(exportDataUri); // save export path only on success
|
||||||
|
Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show();
|
||||||
|
} catch (final Exception e) {
|
||||||
|
ErrorUtil.showUiErrorSnackbar(this, "Exporting database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importDatabase(final StoredFileHelper file, final Uri importDataUri) {
|
||||||
|
// check if file is supported
|
||||||
|
if (!ZipHelper.isValidZipFile(file)) {
|
||||||
|
Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
|
||||||
|
.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!manager.ensureDbDirectoryExists()) {
|
||||||
|
throw new IOException("Could not create databases dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manager.extractDb(file)) {
|
||||||
|
Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if settings file exist, ask if it should be imported.
|
||||||
|
if (manager.extractSettings(file)) {
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(R.string.import_settings)
|
||||||
|
.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
finishImport(importDataUri);
|
||||||
|
})
|
||||||
|
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
final Context context = requireContext();
|
||||||
|
final SharedPreferences prefs = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(context);
|
||||||
|
manager.loadSharedPreferences(prefs);
|
||||||
|
cleanImport(context, prefs);
|
||||||
|
finishImport(importDataUri);
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
} else {
|
||||||
|
finishImport(importDataUri);
|
||||||
|
}
|
||||||
|
} catch (final Exception e) {
|
||||||
|
ErrorUtil.showUiErrorSnackbar(this, "Importing database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove settings that are not supposed to be imported on different devices
|
||||||
|
* and reset them to default values.
|
||||||
|
* @param context the context used for the import
|
||||||
|
* @param prefs the preferences used while running the import
|
||||||
|
*/
|
||||||
|
private void cleanImport(@NonNull final Context context,
|
||||||
|
@NonNull final SharedPreferences prefs) {
|
||||||
|
// Check if media tunnelling needs to be disabled automatically,
|
||||||
|
// if it was disabled automatically in the imported preferences.
|
||||||
|
final String tunnelingKey = context.getString(R.string.disable_media_tunneling_key);
|
||||||
|
final String automaticTunnelingKey =
|
||||||
|
context.getString(R.string.disabled_media_tunneling_automatically_key);
|
||||||
|
// R.string.disable_media_tunneling_key should always be true
|
||||||
|
// if R.string.disabled_media_tunneling_automatically_key equals 1,
|
||||||
|
// but we double check here just to be sure and to avoid regressions
|
||||||
|
// caused by possible later modification of the media tunneling functionality.
|
||||||
|
// R.string.disabled_media_tunneling_automatically_key == 0:
|
||||||
|
// automatic value overridden by user in settings
|
||||||
|
// R.string.disabled_media_tunneling_automatically_key == -1: not set
|
||||||
|
final boolean wasMediaTunnelingDisabledAutomatically =
|
||||||
|
prefs.getInt(automaticTunnelingKey, -1) == 1
|
||||||
|
&& prefs.getBoolean(tunnelingKey, false);
|
||||||
|
if (wasMediaTunnelingDisabledAutomatically) {
|
||||||
|
prefs.edit()
|
||||||
|
.putInt(automaticTunnelingKey, -1)
|
||||||
|
.putBoolean(tunnelingKey, false)
|
||||||
|
.apply();
|
||||||
|
NewPipeSettings.setMediaTunneling(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save import path and restart system.
|
||||||
|
*
|
||||||
|
* @param importDataUri The import path to save
|
||||||
|
*/
|
||||||
|
private void finishImport(final Uri importDataUri) {
|
||||||
|
// save import path only on success
|
||||||
|
saveLastImportExportDataUri(importDataUri);
|
||||||
|
// restart app to properly load db
|
||||||
|
NavigationHelper.restartApp(requireActivity());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Uri getImportExportDataUri() {
|
||||||
|
final String path = defaultPreferences.getString(importExportDataPathKey, null);
|
||||||
|
return isBlank(path) ? null : Uri.parse(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveLastImportExportDataUri(final Uri importExportDataUri) {
|
||||||
|
final SharedPreferences.Editor editor = defaultPreferences.edit()
|
||||||
|
.putString(importExportDataPathKey, importExportDataUri.toString());
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,120 @@
|
|||||||
|
package org.schabi.newpipe.settings
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
|
import org.schabi.newpipe.MainActivity.DEBUG
|
||||||
|
import org.schabi.newpipe.streams.io.SharpOutputStream
|
||||||
|
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||||
|
import org.schabi.newpipe.util.ZipHelper
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.ObjectInputStream
|
||||||
|
import java.io.ObjectOutputStream
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
|
class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
|
||||||
|
companion object {
|
||||||
|
const val TAG = "ContentSetManager"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports given [SharedPreferences] to the file in given outputPath.
|
||||||
|
* It also creates the file.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) {
|
||||||
|
file.create()
|
||||||
|
ZipOutputStream(SharpOutputStream(file.stream).buffered())
|
||||||
|
.use { outZip ->
|
||||||
|
ZipHelper.addFileToZip(outZip, fileLocator.db.path, "newpipe.db")
|
||||||
|
|
||||||
|
try {
|
||||||
|
ObjectOutputStream(fileLocator.settings.outputStream()).use { output ->
|
||||||
|
output.writeObject(preferences.all)
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.e(TAG, "Unable to exportDatabase", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteSettingsFile() {
|
||||||
|
fileLocator.settings.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to create database directory if it does not exist.
|
||||||
|
*
|
||||||
|
* @return Whether the directory exists afterwards.
|
||||||
|
*/
|
||||||
|
fun ensureDbDirectoryExists(): Boolean {
|
||||||
|
return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractDb(file: StoredFileHelper): Boolean {
|
||||||
|
val success = ZipHelper.extractFileFromZip(file, fileLocator.db.path, "newpipe.db")
|
||||||
|
if (success) {
|
||||||
|
fileLocator.dbJournal.delete()
|
||||||
|
fileLocator.dbWal.delete()
|
||||||
|
fileLocator.dbShm.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractSettings(file: StoredFileHelper): Boolean {
|
||||||
|
return ZipHelper.extractFileFromZip(file, fileLocator.settings.path, "newpipe.settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all shared preferences from the app and load the preferences supplied to the manager.
|
||||||
|
*/
|
||||||
|
fun loadSharedPreferences(preferences: SharedPreferences) {
|
||||||
|
try {
|
||||||
|
val preferenceEditor = preferences.edit()
|
||||||
|
|
||||||
|
ObjectInputStream(fileLocator.settings.inputStream()).use { input ->
|
||||||
|
preferenceEditor.clear()
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val entries = input.readObject() as Map<String, *>
|
||||||
|
for ((key, value) in entries) {
|
||||||
|
when (value) {
|
||||||
|
is Boolean -> {
|
||||||
|
preferenceEditor.putBoolean(key, value)
|
||||||
|
}
|
||||||
|
is Float -> {
|
||||||
|
preferenceEditor.putFloat(key, value)
|
||||||
|
}
|
||||||
|
is Int -> {
|
||||||
|
preferenceEditor.putInt(key, value)
|
||||||
|
}
|
||||||
|
is Long -> {
|
||||||
|
preferenceEditor.putLong(key, value)
|
||||||
|
}
|
||||||
|
is String -> {
|
||||||
|
preferenceEditor.putString(key, value)
|
||||||
|
}
|
||||||
|
is Set<*> -> {
|
||||||
|
// There are currently only Sets with type String possible
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
preferenceEditor.putStringSet(key, value as Set<String>?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preferenceEditor.commit()
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.e(TAG, "Unable to loadSharedPreferences", e)
|
||||||
|
}
|
||||||
|
} catch (e: ClassNotFoundException) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.e(TAG, "Unable to loadSharedPreferences", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -30,6 +30,7 @@ import org.schabi.newpipe.util.FilePickerActivityHelper;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
||||||
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||||
@ -124,7 +125,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
rawUri = decodeUrlUtf8(rawUri);
|
rawUri = decodeUrlUtf8(rawUri);
|
||||||
} catch (final IllegalArgumentException e) {
|
} catch (final UnsupportedEncodingException e) {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ public class MainSettingsFragment extends BasePreferenceFragment {
|
|||||||
setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called
|
setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called
|
||||||
|
|
||||||
// Check if the app is updatable
|
// Check if the app is updatable
|
||||||
if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
if (!ReleaseVersionUtil.isReleaseApk()) {
|
||||||
getPreferenceScreen().removePreference(
|
getPreferenceScreen().removePreference(
|
||||||
findPreference(getString(R.string.update_pref_screen_key)));
|
findPreference(getString(R.string.update_pref_screen_key)));
|
||||||
|
|
||||||
|
@ -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") }
|
||||||
|
}
|
@ -11,7 +11,6 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.App;
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
|
||||||
@ -45,8 +44,14 @@ public final class NewPipeSettings {
|
|||||||
private NewPipeSettings() { }
|
private NewPipeSettings() { }
|
||||||
|
|
||||||
public static void initSettings(final Context context) {
|
public static void initSettings(final Context context) {
|
||||||
|
// check if the last used preference version is set
|
||||||
|
// to determine whether this is the first app run
|
||||||
|
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getInt(context.getString(R.string.last_used_preferences_version), -1);
|
||||||
|
final boolean isFirstRun = lastUsedPrefVersion == -1;
|
||||||
|
|
||||||
// first run migrations, then setDefaultValues, since the latter requires the correct types
|
// first run migrations, then setDefaultValues, since the latter requires the correct types
|
||||||
SettingMigrations.runMigrationsIfNeeded(context);
|
SettingMigrations.runMigrationsIfNeeded(context, isFirstRun);
|
||||||
|
|
||||||
// readAgain is true so that if new settings are added their default value is set
|
// readAgain is true so that if new settings are added their default value is set
|
||||||
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
|
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
|
||||||
@ -58,12 +63,11 @@ public final class NewPipeSettings {
|
|||||||
PreferenceManager.setDefaultValues(context, R.xml.player_notification_settings, true);
|
PreferenceManager.setDefaultValues(context, R.xml.player_notification_settings, true);
|
||||||
PreferenceManager.setDefaultValues(context, R.xml.update_settings, true);
|
PreferenceManager.setDefaultValues(context, R.xml.update_settings, true);
|
||||||
PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true);
|
PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true);
|
||||||
PreferenceManager.setDefaultValues(context, R.xml.backup_restore_settings, true);
|
|
||||||
|
|
||||||
saveDefaultVideoDownloadDirectory(context);
|
saveDefaultVideoDownloadDirectory(context);
|
||||||
saveDefaultAudioDownloadDirectory(context);
|
saveDefaultAudioDownloadDirectory(context);
|
||||||
|
|
||||||
disableMediaTunnelingIfNecessary(context);
|
disableMediaTunnelingIfNecessary(context, isFirstRun);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void saveDefaultVideoDownloadDirectory(final Context context) {
|
static void saveDefaultVideoDownloadDirectory(final Context context) {
|
||||||
@ -141,7 +145,8 @@ public final class NewPipeSettings {
|
|||||||
R.string.show_remote_search_suggestions_key);
|
R.string.show_remote_search_suggestions_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void disableMediaTunnelingIfNecessary(@NonNull final Context context) {
|
private static void disableMediaTunnelingIfNecessary(@NonNull final Context context,
|
||||||
|
final boolean isFirstRun) {
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
final String disabledTunnelingKey = context.getString(R.string.disable_media_tunneling_key);
|
final String disabledTunnelingKey = context.getString(R.string.disable_media_tunneling_key);
|
||||||
final String disabledTunnelingAutomaticallyKey =
|
final String disabledTunnelingAutomaticallyKey =
|
||||||
@ -156,7 +161,7 @@ public final class NewPipeSettings {
|
|||||||
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
|
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
|
||||||
&& !prefs.getBoolean(disabledTunnelingKey, false);
|
&& !prefs.getBoolean(disabledTunnelingKey, false);
|
||||||
|
|
||||||
if (App.getApp().isFirstRun()
|
if (Boolean.TRUE.equals(isFirstRun)
|
||||||
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
|
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
|
||||||
setMediaTunneling(context);
|
setMediaTunneling(context);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
package org.schabi.newpipe.settings;
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -33,6 +31,7 @@ import java.util.List;
|
|||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
public class SelectPlaylistFragment extends DialogFragment {
|
public class SelectPlaylistFragment extends DialogFragment {
|
||||||
@ -91,7 +90,8 @@ public class SelectPlaylistFragment extends DialogFragment {
|
|||||||
final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database);
|
final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database);
|
||||||
final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database);
|
final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database);
|
||||||
|
|
||||||
disposable = getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
|
disposable = Flowable.combineLatest(localPlaylistManager.getPlaylists(),
|
||||||
|
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(this::displayPlaylists, this::onError);
|
.subscribe(this::displayPlaylists, this::onError);
|
||||||
}
|
}
|
||||||
@ -118,7 +118,7 @@ public class SelectPlaylistFragment extends DialogFragment {
|
|||||||
|
|
||||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||||
onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.name);
|
onSelectedListener.onLocalPlaylistSelected(entry.uid, entry.name);
|
||||||
|
|
||||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||||
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
||||||
|
@ -7,7 +7,6 @@ import android.util.Log;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.App;
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
@ -164,14 +163,15 @@ public final class SettingMigrations {
|
|||||||
private static final int VERSION = 6;
|
private static final int VERSION = 6;
|
||||||
|
|
||||||
|
|
||||||
public static void runMigrationsIfNeeded(@NonNull final Context context) {
|
public static void runMigrationsIfNeeded(@NonNull final Context context,
|
||||||
|
final boolean isFirstRun) {
|
||||||
// setup migrations and check if there is something to do
|
// setup migrations and check if there is something to do
|
||||||
sp = PreferenceManager.getDefaultSharedPreferences(context);
|
sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
final String lastPrefVersionKey = context.getString(R.string.last_used_preferences_version);
|
final String lastPrefVersionKey = context.getString(R.string.last_used_preferences_version);
|
||||||
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
|
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
|
||||||
|
|
||||||
// no migration to run, already up to date
|
// no migration to run, already up to date
|
||||||
if (App.getApp().isFirstRun()) {
|
if (isFirstRun) {
|
||||||
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
|
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
|
||||||
return;
|
return;
|
||||||
} else if (lastPrefVersion == VERSION) {
|
} else if (lastPrefVersion == VERSION) {
|
||||||
|
@ -266,7 +266,7 @@ public class SettingsActivity extends AppCompatActivity implements
|
|||||||
*/
|
*/
|
||||||
private void ensureSearchRepresentsApplicationState() {
|
private void ensureSearchRepresentsApplicationState() {
|
||||||
// Check if the update settings are available
|
// Check if the update settings are available
|
||||||
if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
if (!ReleaseVersionUtil.isReleaseApk()) {
|
||||||
SettingsResourceRegistry.getInstance()
|
SettingsResourceRegistry.getInstance()
|
||||||
.getEntryByPreferencesResId(R.xml.update_settings)
|
.getEntryByPreferencesResId(R.xml.update_settings)
|
||||||
.setSearchable(false);
|
.setSearchable(false);
|
||||||
|
@ -41,7 +41,6 @@ public final class SettingsResourceRegistry {
|
|||||||
add(UpdateSettingsFragment.class, R.xml.update_settings);
|
add(UpdateSettingsFragment.class, R.xml.update_settings);
|
||||||
add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings);
|
add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings);
|
||||||
add(ExoPlayerSettingsFragment.class, R.xml.exoplayer_settings);
|
add(ExoPlayerSettingsFragment.class, R.xml.exoplayer_settings);
|
||||||
add(BackupRestoreSettingsFragment.class, R.xml.backup_restore_settings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private SettingRegistryEntry add(
|
private SettingRegistryEntry add(
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user