Merge branch 'master' into sponsorblock
# Conflicts: # app/build.gradle # app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java # app/src/main/java/org/schabi/newpipe/DownloaderImpl.java # app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java # app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java # app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java # app/src/main/res/values-so/strings.xml # app/src/main/res/values/strings.xml # fastlane/metadata/android/de/full_description.txt
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -58,7 +58,7 @@ Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Please fill this out when you do not provide a log generate by NewPipe -->
|
<!-- Please fill this section if you did not provide a log generated by NewPipe -->
|
||||||
|
|
||||||
### Device info
|
### Device info
|
||||||
|
|
||||||
|
|
36
.github/workflows/ci.yml
vendored
|
@ -1,6 +1,7 @@
|
||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
|
@ -36,20 +37,11 @@ jobs:
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
java-version: 8
|
java-version: 8
|
||||||
distribution: "adopt"
|
distribution: "temurin"
|
||||||
|
cache: 'gradle'
|
||||||
- name: Cache Gradle dependencies
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.gradle/caches
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
|
||||||
restore-keys: ${{ runner.os }}-gradle
|
|
||||||
|
|
||||||
- name: Check if kotlin files are formatted correctly
|
|
||||||
run: ./gradlew runKtlint
|
|
||||||
|
|
||||||
- name: Build debug APK and run jvm tests
|
- name: Build debug APK and run jvm tests
|
||||||
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace
|
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
|
@ -71,14 +63,8 @@ jobs:
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
java-version: 8
|
java-version: 8
|
||||||
distribution: "adopt"
|
distribution: "temurin"
|
||||||
|
cache: 'gradle'
|
||||||
- name: Cache Gradle dependencies
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.gradle/caches
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
|
||||||
restore-keys: ${{ runner.os }}-gradle
|
|
||||||
|
|
||||||
- name: Run android tests
|
- name: Run android tests
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
uses: reactivecircus/android-emulator-runner@v2
|
||||||
|
@ -99,7 +85,8 @@ jobs:
|
||||||
# uses: actions/setup-java@v2
|
# uses: actions/setup-java@v2
|
||||||
# with:
|
# with:
|
||||||
# java-version: 11 # Sonar requires JDK 11
|
# java-version: 11 # Sonar requires JDK 11
|
||||||
# distribution: "adopt"
|
# distribution: "temurin"
|
||||||
|
# cache: 'gradle'
|
||||||
|
|
||||||
# - name: Cache SonarCloud packages
|
# - name: Cache SonarCloud packages
|
||||||
# uses: actions/cache@v2
|
# uses: actions/cache@v2
|
||||||
|
@ -108,13 +95,6 @@ jobs:
|
||||||
# key: ${{ runner.os }}-sonar
|
# key: ${{ runner.os }}-sonar
|
||||||
# restore-keys: ${{ runner.os }}-sonar
|
# restore-keys: ${{ runner.os }}-sonar
|
||||||
|
|
||||||
# - name: Cache Gradle packages
|
|
||||||
# uses: actions/cache@v2
|
|
||||||
# with:
|
|
||||||
# path: ~/.gradle/caches
|
|
||||||
# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
|
||||||
# restore-keys: ${{ runner.os }}-gradle
|
|
||||||
|
|
||||||
# - name: Build and analyze
|
# - name: Build and analyze
|
||||||
# env:
|
# env:
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||||
|
|
20
.github/workflows/no-response.yml
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
name: No Response
|
||||||
|
|
||||||
|
# Both `issue_comment` and `scheduled` event types are required for this Action
|
||||||
|
# to work properly.
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
schedule:
|
||||||
|
# Run daily at midnight.
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
noResponse:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: lee-dohm/no-response@v0.5.0
|
||||||
|
with:
|
||||||
|
token: ${{ github.token }}
|
||||||
|
daysUntilClose: 14
|
||||||
|
responseRequiredLabel: waiting-for-author
|
|
@ -4,7 +4,7 @@ plugins {
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-parcelize'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'checkstyle'
|
apply plugin: 'checkstyle'
|
||||||
|
|
||||||
|
@ -14,11 +14,11 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.polymorphicshade.newpipe"
|
applicationId "org.polymorphicshade.newpipe"
|
||||||
resValue "string", "app_name", "NewPipe Sponsorblock"
|
resValue "string", "app_name", "NewPipe SponsorBlock"
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 975
|
versionCode 976
|
||||||
versionName "0.21.9_r2"
|
versionName "0.21.10"
|
||||||
|
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
|
@ -84,11 +84,6 @@ android {
|
||||||
jvmTarget = JavaVersion.VERSION_1_8
|
jvmTarget = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required and used only by groupie
|
|
||||||
androidExtensions {
|
|
||||||
experimental = true
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||||
}
|
}
|
||||||
|
@ -165,7 +160,10 @@ task formatKtlint(type: JavaExec) {
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint
|
if (!System.properties.containsKey('skipFormatKtlint')) {
|
||||||
|
preDebugBuild.dependsOn formatKtlint
|
||||||
|
}
|
||||||
|
preDebugBuild.dependsOn runCheckstyle, runKtlint
|
||||||
}
|
}
|
||||||
|
|
||||||
sonarqube {
|
sonarqube {
|
||||||
|
@ -186,7 +184,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.21.9'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.10'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||||
|
@ -243,7 +241,8 @@ dependencies {
|
||||||
// Circular ImageView
|
// Circular ImageView
|
||||||
implementation "de.hdodenhof:circleimageview:3.1.0"
|
implementation "de.hdodenhof:circleimageview:3.1.0"
|
||||||
// Image loading
|
// Image loading
|
||||||
implementation "com.nostra13.universalimageloader:universal-image-loader:1.9.5"
|
//noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
|
||||||
|
implementation "com.squareup.picasso:picasso:2.8"
|
||||||
|
|
||||||
// Markdown library for Android
|
// Markdown library for Android
|
||||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||||
|
@ -255,6 +254,9 @@ dependencies {
|
||||||
// Crash reporting
|
// Crash reporting
|
||||||
implementation "ch.acra:acra-core:5.7.0"
|
implementation "ch.acra:acra-core:5.7.0"
|
||||||
|
|
||||||
|
// Properly restarting
|
||||||
|
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
||||||
|
|
||||||
// Reactive extensions for Java VM
|
// Reactive extensions for Java VM
|
||||||
implementation "io.reactivex.rxjava3:rxjava:3.0.7"
|
implementation "io.reactivex.rxjava3:rxjava:3.0.7"
|
||||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||||
|
|
713
app/schemas/org.schabi.newpipe.database.AppDatabase/4.json
Normal file
|
@ -0,0 +1,713 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 4,
|
||||||
|
"identityHash": "d8070091972a7011bce18aed62f80b90",
|
||||||
|
"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)",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"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}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd8070091972a7011bce18aed62f80b90')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,134 +0,0 @@
|
||||||
package org.schabi.newpipe.database
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.sqlite.SQLiteDatabase
|
|
||||||
import androidx.room.Room
|
|
||||||
import androidx.room.testing.MigrationTestHelper
|
|
||||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
|
||||||
import androidx.test.core.app.ApplicationProvider
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class AppDatabaseTest {
|
|
||||||
companion object {
|
|
||||||
private const val DEFAULT_SERVICE_ID = 0
|
|
||||||
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
|
||||||
private const val DEFAULT_TITLE = "Test Title"
|
|
||||||
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
|
||||||
private const val DEFAULT_DURATION = 480L
|
|
||||||
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
|
||||||
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
|
||||||
|
|
||||||
private const val DEFAULT_SECOND_SERVICE_ID = 0
|
|
||||||
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
val testHelper = MigrationTestHelper(
|
|
||||||
InstrumentationRegistry.getInstrumentation(),
|
|
||||||
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun migrateDatabaseFrom2to3() {
|
|
||||||
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
|
|
||||||
|
|
||||||
databaseInV2.run {
|
|
||||||
insert(
|
|
||||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
// put("uid", null)
|
|
||||||
put("service_id", DEFAULT_SERVICE_ID)
|
|
||||||
put("url", DEFAULT_URL)
|
|
||||||
put("title", DEFAULT_TITLE)
|
|
||||||
put("stream_type", DEFAULT_TYPE.name)
|
|
||||||
put("duration", DEFAULT_DURATION)
|
|
||||||
put("uploader", DEFAULT_UPLOADER_NAME)
|
|
||||||
put("thumbnail_url", DEFAULT_THUMBNAIL)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
insert(
|
|
||||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
// put("uid", null)
|
|
||||||
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
|
||||||
put("url", DEFAULT_SECOND_URL)
|
|
||||||
// put("title", null)
|
|
||||||
// put("stream_type", null)
|
|
||||||
// put("duration", null)
|
|
||||||
// put("uploader", null)
|
|
||||||
// put("thumbnail_url", null)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
insert(
|
|
||||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
|
||||||
ContentValues().apply {
|
|
||||||
// put("uid", null)
|
|
||||||
put("service_id", DEFAULT_SERVICE_ID)
|
|
||||||
// put("url", null)
|
|
||||||
// put("title", null)
|
|
||||||
// put("stream_type", null)
|
|
||||||
// put("duration", null)
|
|
||||||
// put("uploader", null)
|
|
||||||
// put("thumbnail_url", null)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
testHelper.runMigrationsAndValidate(
|
|
||||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
|
|
||||||
true, Migrations.MIGRATION_2_3
|
|
||||||
)
|
|
||||||
|
|
||||||
val migratedDatabaseV3 = getMigratedDatabase()
|
|
||||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
|
||||||
|
|
||||||
// Only expect 2, the one with the null url will be ignored
|
|
||||||
assertEquals(2, listFromDB.size)
|
|
||||||
|
|
||||||
val streamFromMigratedDatabase = listFromDB[0]
|
|
||||||
assertEquals(DEFAULT_SERVICE_ID, streamFromMigratedDatabase.serviceId)
|
|
||||||
assertEquals(DEFAULT_URL, streamFromMigratedDatabase.url)
|
|
||||||
assertEquals(DEFAULT_TITLE, streamFromMigratedDatabase.title)
|
|
||||||
assertEquals(DEFAULT_TYPE, streamFromMigratedDatabase.streamType)
|
|
||||||
assertEquals(DEFAULT_DURATION, streamFromMigratedDatabase.duration)
|
|
||||||
assertEquals(DEFAULT_UPLOADER_NAME, streamFromMigratedDatabase.uploader)
|
|
||||||
assertEquals(DEFAULT_THUMBNAIL, streamFromMigratedDatabase.thumbnailUrl)
|
|
||||||
assertNull(streamFromMigratedDatabase.viewCount)
|
|
||||||
assertNull(streamFromMigratedDatabase.textualUploadDate)
|
|
||||||
assertNull(streamFromMigratedDatabase.uploadDate)
|
|
||||||
assertNull(streamFromMigratedDatabase.isUploadDateApproximation)
|
|
||||||
|
|
||||||
val secondStreamFromMigratedDatabase = listFromDB[1]
|
|
||||||
assertEquals(DEFAULT_SECOND_SERVICE_ID, secondStreamFromMigratedDatabase.serviceId)
|
|
||||||
assertEquals(DEFAULT_SECOND_URL, secondStreamFromMigratedDatabase.url)
|
|
||||||
assertEquals("", secondStreamFromMigratedDatabase.title)
|
|
||||||
// Should fallback to VIDEO_STREAM
|
|
||||||
assertEquals(StreamType.VIDEO_STREAM, secondStreamFromMigratedDatabase.streamType)
|
|
||||||
assertEquals(0, secondStreamFromMigratedDatabase.duration)
|
|
||||||
assertEquals("", secondStreamFromMigratedDatabase.uploader)
|
|
||||||
assertEquals("", secondStreamFromMigratedDatabase.thumbnailUrl)
|
|
||||||
assertNull(secondStreamFromMigratedDatabase.viewCount)
|
|
||||||
assertNull(secondStreamFromMigratedDatabase.textualUploadDate)
|
|
||||||
assertNull(secondStreamFromMigratedDatabase.uploadDate)
|
|
||||||
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMigratedDatabase(): AppDatabase {
|
|
||||||
val database: AppDatabase = Room.databaseBuilder(
|
|
||||||
ApplicationProvider.getApplicationContext(),
|
|
||||||
AppDatabase::class.java, AppDatabase.DATABASE_NAME
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
testHelper.closeWhenFinished(database)
|
|
||||||
return database
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -45,7 +45,8 @@ class LocalPlaylistManagerTest {
|
||||||
fun createPlaylist() {
|
fun createPlaylist() {
|
||||||
val stream = StreamEntity(
|
val stream = StreamEntity(
|
||||||
serviceId = 1, url = "https://newpipe.net/", title = "title",
|
serviceId = 1, url = "https://newpipe.net/", title = "title",
|
||||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader"
|
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||||
|
uploaderUrl = "https://newpipe.net/"
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = manager.createPlaylist("name", listOf(stream))
|
val result = manager.createPlaylist("name", listOf(stream))
|
||||||
|
@ -69,12 +70,14 @@ class LocalPlaylistManagerTest {
|
||||||
fun createPlaylist_nonExistentStreamsAreUpserted() {
|
fun createPlaylist_nonExistentStreamsAreUpserted() {
|
||||||
val stream = StreamEntity(
|
val stream = StreamEntity(
|
||||||
serviceId = 1, url = "https://newpipe.net/", title = "title",
|
serviceId = 1, url = "https://newpipe.net/", title = "title",
|
||||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader"
|
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||||
|
uploaderUrl = "https://newpipe.net/"
|
||||||
)
|
)
|
||||||
database.streamDAO().insert(stream)
|
database.streamDAO().insert(stream)
|
||||||
val upserted = StreamEntity(
|
val upserted = StreamEntity(
|
||||||
serviceId = 1, url = "https://newpipe.net/2", title = "title2",
|
serviceId = 1, url = "https://newpipe.net/2", title = "title2",
|
||||||
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader"
|
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
|
||||||
|
uploaderUrl = "https://newpipe.net/"
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = manager.createPlaylist("name", listOf(stream, upserted))
|
val result = manager.createPlaylist("name", listOf(stream, upserted))
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.os.Bundle;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
|
|
||||||
import leakcanary.LeakCanary;
|
import leakcanary.LeakCanary;
|
||||||
|
|
||||||
|
@ -15,10 +16,13 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||||
|
|
||||||
final Preference showMemoryLeaksPreference
|
final Preference showMemoryLeaksPreference
|
||||||
= findPreference(getString(R.string.show_memory_leaks_key));
|
= findPreference(getString(R.string.show_memory_leaks_key));
|
||||||
|
final Preference showImageIndicatorsPreference
|
||||||
|
= findPreference(getString(R.string.show_image_indicators_key));
|
||||||
final Preference crashTheAppPreference
|
final Preference crashTheAppPreference
|
||||||
= findPreference(getString(R.string.crash_the_app_key));
|
= findPreference(getString(R.string.crash_the_app_key));
|
||||||
|
|
||||||
assert showMemoryLeaksPreference != null;
|
assert showMemoryLeaksPreference != null;
|
||||||
|
assert showImageIndicatorsPreference != null;
|
||||||
assert crashTheAppPreference != null;
|
assert crashTheAppPreference != null;
|
||||||
|
|
||||||
showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
|
showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
|
||||||
|
@ -26,6 +30,11 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
crashTheAppPreference.setOnPreferenceClickListener(preference -> {
|
crashTheAppPreference.setOnPreferenceClickListener(preference -> {
|
||||||
throw new RuntimeException();
|
throw new RuntimeException();
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,9 +11,7 @@ import androidx.core.app.NotificationManagerCompat;
|
||||||
import androidx.multidex.MultiDexApplication;
|
import androidx.multidex.MultiDexApplication;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
|
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
|
||||||
|
|
||||||
import org.acra.ACRA;
|
import org.acra.ACRA;
|
||||||
import org.acra.config.ACRAConfigurationException;
|
import org.acra.config.ACRAConfigurationException;
|
||||||
|
@ -28,6 +26,7 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
|
|
||||||
|
@ -65,9 +64,9 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class App extends MultiDexApplication {
|
public class App extends MultiDexApplication {
|
||||||
protected static final String TAG = App.class.toString();
|
|
||||||
private static App app;
|
|
||||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||||
|
private static final String TAG = App.class.toString();
|
||||||
|
private static App app;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private Disposable disposable = null;
|
private Disposable disposable = null;
|
||||||
|
@ -89,6 +88,12 @@ public class App extends MultiDexApplication {
|
||||||
|
|
||||||
app = this;
|
app = this;
|
||||||
|
|
||||||
|
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||||
|
Log.i(TAG, "This is a phoenix process! "
|
||||||
|
+ "Aborting initialization of App[onCreate]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize settings first because others inits can use its values
|
// Initialize settings first because others inits can use its values
|
||||||
NewPipeSettings.initSettings(this);
|
NewPipeSettings.initSettings(this);
|
||||||
|
|
||||||
|
@ -103,7 +108,12 @@ public class App extends MultiDexApplication {
|
||||||
ServiceHelper.initServices(this);
|
ServiceHelper.initServices(this);
|
||||||
|
|
||||||
// Initialize image loader
|
// Initialize image loader
|
||||||
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50));
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
PicassoHelper.init(this);
|
||||||
|
PicassoHelper.setShouldLoadImages(
|
||||||
|
prefs.getBoolean(getString(R.string.download_thumbnail_key), true));
|
||||||
|
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
||||||
|
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
||||||
|
|
||||||
configureRxJavaErrorHandler();
|
configureRxJavaErrorHandler();
|
||||||
|
|
||||||
|
@ -117,6 +127,7 @@ public class App extends MultiDexApplication {
|
||||||
disposable.dispose();
|
disposable.dispose();
|
||||||
}
|
}
|
||||||
super.onTerminate();
|
super.onTerminate();
|
||||||
|
PicassoHelper.terminate();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Downloader getDownloader() {
|
protected Downloader getDownloader() {
|
||||||
|
@ -201,15 +212,6 @@ public class App extends MultiDexApplication {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb,
|
|
||||||
final int diskCacheSizeMb) {
|
|
||||||
return new ImageLoaderConfiguration.Builder(this)
|
|
||||||
.memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024))
|
|
||||||
.diskCacheSize(diskCacheSizeMb * 1024 * 1024)
|
|
||||||
.imageDownloader(new ImageDownloader(getApplicationContext()))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
|
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
|
||||||
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||||
|
|
|
@ -10,16 +10,13 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
|
||||||
|
|
||||||
import icepick.Icepick;
|
import icepick.Icepick;
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import leakcanary.AppWatcher;
|
import leakcanary.AppWatcher;
|
||||||
|
|
||||||
public abstract class BaseFragment extends Fragment {
|
public abstract class BaseFragment extends Fragment {
|
||||||
public static final ImageLoader IMAGE_LOADER = ImageLoader.getInstance();
|
|
||||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||||
protected final boolean DEBUG = MainActivity.DEBUG;
|
protected static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
protected AppCompatActivity activity;
|
protected AppCompatActivity activity;
|
||||||
//These values are used for controlling fragments when they are part of the frontpage
|
//These values are used for controlling fragments when they are part of the frontpage
|
||||||
@State
|
@State
|
||||||
|
|
|
@ -17,7 +17,6 @@ import org.schabi.newpipe.util.InfoCache;
|
||||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.security.KeyManagementException;
|
import java.security.KeyManagementException;
|
||||||
import java.security.KeyStore;
|
import java.security.KeyStore;
|
||||||
import java.security.KeyStoreException;
|
import java.security.KeyStoreException;
|
||||||
|
@ -200,47 +199,6 @@ public final class DownloaderImpl extends Downloader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public InputStream stream(final String siteUrl) throws IOException {
|
|
||||||
try {
|
|
||||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
|
||||||
.method("GET", null).url(siteUrl)
|
|
||||||
.addHeader("User-Agent", USER_AGENT);
|
|
||||||
|
|
||||||
final String cookies = getCookies(siteUrl);
|
|
||||||
if (!cookies.isEmpty()) {
|
|
||||||
requestBuilder.addHeader("Cookie", cookies);
|
|
||||||
}
|
|
||||||
|
|
||||||
final okhttp3.Request request = requestBuilder.build();
|
|
||||||
|
|
||||||
OkHttpClient tmpClient = client;
|
|
||||||
final okhttp3.Response response;
|
|
||||||
|
|
||||||
if (customTimeout != null) {
|
|
||||||
tmpClient = new OkHttpClient.Builder()
|
|
||||||
.readTimeout(customTimeout, TimeUnit.SECONDS)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
response = tmpClient.newCall(request).execute();
|
|
||||||
|
|
||||||
final ResponseBody body = response.body();
|
|
||||||
|
|
||||||
if (response.code() == 429) {
|
|
||||||
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (body == null) {
|
|
||||||
response.close();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return body.byteStream();
|
|
||||||
} catch (final ReCaptchaException e) {
|
|
||||||
throw new IOException(e.getMessage(), e.getCause());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response execute(@NonNull final Request request)
|
public Response execute(@NonNull final Request request)
|
||||||
throws IOException, ReCaptchaException {
|
throws IOException, ReCaptchaException {
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public class ImageDownloader extends BaseImageDownloader {
|
|
||||||
private final Resources resources;
|
|
||||||
private final SharedPreferences preferences;
|
|
||||||
private final String downloadThumbnailKey;
|
|
||||||
|
|
||||||
public ImageDownloader(final Context context) {
|
|
||||||
super(context);
|
|
||||||
this.resources = context.getResources();
|
|
||||||
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDownloadingThumbnail() {
|
|
||||||
return preferences.getBoolean(downloadThumbnailKey, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ResourceType")
|
|
||||||
@Override
|
|
||||||
public InputStream getStream(final String imageUri, final Object extra) throws IOException {
|
|
||||||
if (isDownloadingThumbnail()) {
|
|
||||||
return super.getStream(imageUri, extra);
|
|
||||||
} else {
|
|
||||||
return resources.openRawResource(R.drawable.dummy_thumbnail_dark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected InputStream getStreamFromNetwork(final String imageUri, final Object extra)
|
|
||||||
throws IOException {
|
|
||||||
final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader();
|
|
||||||
return downloader.stream(imageUri);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,6 +11,7 @@ import org.schabi.newpipe.database.AppDatabase;
|
||||||
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
|
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
|
||||||
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
|
||||||
|
|
||||||
public final class NewPipeDatabase {
|
public final class NewPipeDatabase {
|
||||||
private static volatile AppDatabase databaseInstance;
|
private static volatile AppDatabase databaseInstance;
|
||||||
|
@ -22,7 +23,7 @@ public final class NewPipeDatabase {
|
||||||
private static AppDatabase getDatabase(final Context context) {
|
private static AppDatabase getDatabase(final Context context) {
|
||||||
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)
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
70
app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.ContextThemeWrapper;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.PopupMenu;
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||||
|
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public final class QueueItemMenuUtil {
|
||||||
|
public static void openPopupMenu(final PlayQueue playQueue,
|
||||||
|
final PlayQueueItem item,
|
||||||
|
final View view,
|
||||||
|
final boolean hideDetails,
|
||||||
|
final FragmentManager fragmentManager,
|
||||||
|
final Context context) {
|
||||||
|
final ContextThemeWrapper themeWrapper =
|
||||||
|
new ContextThemeWrapper(context, R.style.DarkPopupMenu);
|
||||||
|
|
||||||
|
final PopupMenu popupMenu = new PopupMenu(themeWrapper, view);
|
||||||
|
popupMenu.inflate(R.menu.menu_play_queue_item);
|
||||||
|
|
||||||
|
if (hideDetails) {
|
||||||
|
popupMenu.getMenu().findItem(R.id.menu_item_details).setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
popupMenu.setOnMenuItemClickListener(menuItem -> {
|
||||||
|
switch (menuItem.getItemId()) {
|
||||||
|
case R.id.menu_item_remove:
|
||||||
|
final int index = playQueue.indexOf(item);
|
||||||
|
playQueue.remove(index);
|
||||||
|
return true;
|
||||||
|
case R.id.menu_item_details:
|
||||||
|
// playQueue is null since we don't want any queue change
|
||||||
|
NavigationHelper.openVideoDetail(context, item.getServiceId(),
|
||||||
|
item.getUrl(), item.getTitle(), null,
|
||||||
|
false);
|
||||||
|
return true;
|
||||||
|
case R.id.menu_item_append_playlist:
|
||||||
|
final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(
|
||||||
|
Collections.singletonList(item)
|
||||||
|
);
|
||||||
|
PlaylistAppendDialog.onPlaylistFound(context,
|
||||||
|
() -> d.show(fragmentManager, "QueueItemMenuUtil@append_playlist"),
|
||||||
|
() -> PlaylistCreationDialog.newInstance(d)
|
||||||
|
.show(fragmentManager, "QueueItemMenuUtil@append_playlist"));
|
||||||
|
return true;
|
||||||
|
case R.id.menu_item_share:
|
||||||
|
shareText(context, item.getTitle(), item.getUrl(),
|
||||||
|
item.getThumbnailUrl());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
popupMenu.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private QueueItemMenuUtil() { }
|
||||||
|
}
|
|
@ -162,10 +162,18 @@ class AboutActivity : AppCompatActivity() {
|
||||||
"OkHttp", "2019", "Square, Inc.",
|
"OkHttp", "2019", "Square, Inc.",
|
||||||
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
|
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
|
||||||
),
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Picasso", "2013", "Square, Inc.",
|
||||||
|
"https://square.github.io/picasso/", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
SoftwareComponent(
|
SoftwareComponent(
|
||||||
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
||||||
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2
|
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2
|
||||||
),
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"ProcessPhoenix", "2015", "Jake Wharton",
|
||||||
|
"https://github.com/JakeWharton/ProcessPhoenix", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
SoftwareComponent(
|
SoftwareComponent(
|
||||||
"RxAndroid", "2015", "The RxAndroid authors",
|
"RxAndroid", "2015", "The RxAndroid authors",
|
||||||
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2
|
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2
|
||||||
|
@ -177,11 +185,6 @@ class AboutActivity : AppCompatActivity() {
|
||||||
SoftwareComponent(
|
SoftwareComponent(
|
||||||
"RxJava", "2016 - 2020", "RxJava Contributors",
|
"RxJava", "2016 - 2020", "RxJava Contributors",
|
||||||
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
|
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
|
|
||||||
"https://github.com/nostra13/Android-Universal-Image-Loader",
|
|
||||||
StandardLicenses.APACHE2
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
private const val POS_ABOUT = 0
|
private const val POS_ABOUT = 0
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package org.schabi.newpipe.about
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -108,7 +108,7 @@ object LicenseFragmentHelper {
|
||||||
alert.setView(webView)
|
alert.setView(webView)
|
||||||
Localization.assureCorrectAppLanguage(context)
|
Localization.assureCorrectAppLanguage(context)
|
||||||
alert.setNegativeButton(
|
alert.setNegativeButton(
|
||||||
context.getString(R.string.finish)
|
context.getString(R.string.ok)
|
||||||
) { dialog, _ -> dialog.dismiss() }
|
) { dialog, _ -> dialog.dismiss() }
|
||||||
alert.show()
|
alert.show()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package org.schabi.newpipe.about
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class SoftwareComponent
|
class SoftwareComponent
|
||||||
|
|
|
@ -27,7 +27,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
|
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.Migrations.DB_VER_3;
|
import static org.schabi.newpipe.database.Migrations.DB_VER_4;
|
||||||
|
|
||||||
@TypeConverters({Converters.class})
|
@TypeConverters({Converters.class})
|
||||||
@Database(
|
@Database(
|
||||||
|
@ -38,7 +38,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_3;
|
||||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||||
FeedLastUpdatedEntity.class
|
FeedLastUpdatedEntity.class
|
||||||
},
|
},
|
||||||
version = DB_VER_3
|
version = DB_VER_4
|
||||||
)
|
)
|
||||||
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";
|
||||||
|
|
|
@ -9,9 +9,19 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
|
|
||||||
public final class Migrations {
|
public final class Migrations {
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Test new migrations manually by importing a database from daily usage //
|
||||||
|
// and checking if the migration works (Use the Database Inspector //
|
||||||
|
// https://developer.android.com/studio/inspect/database). //
|
||||||
|
// If you add a migration point it out in the pull request, so that //
|
||||||
|
// others remember to test it themselves. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public static final int DB_VER_1 = 1;
|
public static final int DB_VER_1 = 1;
|
||||||
public static final int DB_VER_2 = 2;
|
public static final int DB_VER_2 = 2;
|
||||||
public static final int DB_VER_3 = 3;
|
public static final int DB_VER_3 = 3;
|
||||||
|
public static final int DB_VER_4 = 4;
|
||||||
|
|
||||||
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;
|
||||||
|
@ -160,5 +170,14 @@ public final class Migrations {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_3_4 = new Migration(DB_VER_3, DB_VER_4) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL(
|
||||||
|
"ALTER TABLE streams ADD COLUMN uploader_url TEXT"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private Migrations() { }
|
private Migrations() { }
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ data class PlaylistStreamEntry(
|
||||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||||
item.duration = streamEntity.duration
|
item.duration = streamEntity.duration
|
||||||
item.uploaderName = streamEntity.uploader
|
item.uploaderName = streamEntity.uploader
|
||||||
|
item.uploaderUrl = streamEntity.uploaderUrl
|
||||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
item.thumbnailUrl = streamEntity.thumbnailUrl
|
||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
|
@ -29,6 +29,7 @@ class StreamStatisticsEntry(
|
||||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||||
item.duration = streamEntity.duration
|
item.duration = streamEntity.duration
|
||||||
item.uploaderName = streamEntity.uploader
|
item.uploaderName = streamEntity.uploader
|
||||||
|
item.uploaderUrl = streamEntity.uploaderUrl
|
||||||
item.thumbnailUrl = streamEntity.thumbnailUrl
|
item.thumbnailUrl = streamEntity.thumbnailUrl
|
||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
|
@ -6,6 +6,7 @@ import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
|
import io.reactivex.rxjava3.core.Completable
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import org.schabi.newpipe.database.BasicDAO
|
import org.schabi.newpipe.database.BasicDAO
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
@ -29,6 +30,9 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||||
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
|
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
|
||||||
abstract fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>>
|
abstract fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>>
|
||||||
|
|
||||||
|
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
|
||||||
|
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
internal abstract fun silentInsertInternal(stream: StreamEntity): Long
|
internal abstract fun silentInsertInternal(stream: StreamEntity): Long
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,9 @@ data class StreamEntity(
|
||||||
@ColumnInfo(name = STREAM_UPLOADER)
|
@ColumnInfo(name = STREAM_UPLOADER)
|
||||||
var uploader: String,
|
var uploader: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_UPLOADER_URL)
|
||||||
|
var uploaderUrl: String? = null,
|
||||||
|
|
||||||
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
|
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
|
||||||
var thumbnailUrl: String? = null,
|
var thumbnailUrl: String? = null,
|
||||||
|
|
||||||
|
@ -64,7 +67,7 @@ data class StreamEntity(
|
||||||
constructor(item: StreamInfoItem) : this(
|
constructor(item: StreamInfoItem) : this(
|
||||||
serviceId = item.serviceId, url = item.url, title = item.name,
|
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||||
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
||||||
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
||||||
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
||||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||||
)
|
)
|
||||||
|
@ -73,7 +76,7 @@ data class StreamEntity(
|
||||||
constructor(info: StreamInfo) : this(
|
constructor(info: StreamInfo) : this(
|
||||||
serviceId = info.serviceId, url = info.url, title = info.name,
|
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||||
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||||
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
uploaderUrl = info.uploaderUrl, thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||||
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
||||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||||
)
|
)
|
||||||
|
@ -82,13 +85,14 @@ data class StreamEntity(
|
||||||
constructor(item: PlayQueueItem) : this(
|
constructor(item: PlayQueueItem) : this(
|
||||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||||
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||||
thumbnailUrl = item.thumbnailUrl
|
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toStreamInfoItem(): StreamInfoItem {
|
fun toStreamInfoItem(): StreamInfoItem {
|
||||||
val item = StreamInfoItem(serviceId, url, title, streamType)
|
val item = StreamInfoItem(serviceId, url, title, streamType)
|
||||||
item.duration = duration
|
item.duration = duration
|
||||||
item.uploaderName = uploader
|
item.uploaderName = uploader
|
||||||
|
item.uploaderUrl = uploaderUrl
|
||||||
item.thumbnailUrl = thumbnailUrl
|
item.thumbnailUrl = thumbnailUrl
|
||||||
|
|
||||||
if (viewCount != null) item.viewCount = viewCount as Long
|
if (viewCount != null) item.viewCount = viewCount as Long
|
||||||
|
@ -109,6 +113,7 @@ data class StreamEntity(
|
||||||
const val STREAM_TYPE = "stream_type"
|
const val STREAM_TYPE = "stream_type"
|
||||||
const val STREAM_DURATION = "duration"
|
const val STREAM_DURATION = "duration"
|
||||||
const val STREAM_UPLOADER = "uploader"
|
const val STREAM_UPLOADER = "uploader"
|
||||||
|
const val STREAM_UPLOADER_URL = "uploader_url"
|
||||||
const val STREAM_THUMBNAIL_URL = "thumbnail_url"
|
const val STREAM_THUMBNAIL_URL = "thumbnail_url"
|
||||||
|
|
||||||
const val STREAM_VIEWS = "view_count"
|
const val STREAM_VIEWS = "view_count"
|
||||||
|
|
|
@ -688,7 +688,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
new AlertDialog.Builder(context)
|
new AlertDialog.Builder(context)
|
||||||
.setTitle(R.string.general_error)
|
.setTitle(R.string.general_error)
|
||||||
.setMessage(msg)
|
.setMessage(msg)
|
||||||
.setNegativeButton(getString(R.string.finish), null)
|
.setNegativeButton(getString(R.string.ok), null)
|
||||||
.create()
|
.create()
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
@ -871,7 +871,7 @@ public class DownloadDialog extends DialogFragment
|
||||||
final AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
|
final AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
|
||||||
.setTitle(R.string.download_dialog_title)
|
.setTitle(R.string.download_dialog_title)
|
||||||
.setMessage(msgBody)
|
.setMessage(msgBody)
|
||||||
.setNegativeButton(android.R.string.cancel, null);
|
.setNegativeButton(R.string.cancel, null);
|
||||||
final StoredFileHelper finalStorage = storage;
|
final StoredFileHelper finalStorage = storage;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ package org.schabi.newpipe.error
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.Info
|
import org.schabi.newpipe.extractor.Info
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
|
|
@ -6,6 +6,8 @@ import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.Nullable
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.jakewharton.rxbinding4.view.clicks
|
import com.jakewharton.rxbinding4.view.clicks
|
||||||
|
@ -37,22 +39,39 @@ class ErrorPanelHelper(
|
||||||
onRetry: Runnable
|
onRetry: Runnable
|
||||||
) {
|
) {
|
||||||
private val context: Context = rootView.context!!
|
private val context: Context = rootView.context!!
|
||||||
|
|
||||||
private val errorPanelRoot: View = rootView.findViewById(R.id.error_panel)
|
private val errorPanelRoot: View = rootView.findViewById(R.id.error_panel)
|
||||||
private val errorTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_view)
|
|
||||||
private val errorServiceInfoTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_info_view)
|
// the only element that is visible by default
|
||||||
private val errorServiceExplenationTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_explenation_view)
|
private val errorTextView: TextView =
|
||||||
private val errorButtonAction: Button = errorPanelRoot.findViewById(R.id.error_button_action)
|
errorPanelRoot.findViewById(R.id.error_message_view)
|
||||||
private val errorButtonRetry: Button = errorPanelRoot.findViewById(R.id.error_button_retry)
|
private val errorServiceInfoTextView: TextView =
|
||||||
|
errorPanelRoot.findViewById(R.id.error_message_service_info_view)
|
||||||
|
private val errorServiceExplanationTextView: TextView =
|
||||||
|
errorPanelRoot.findViewById(R.id.error_message_service_explanation_view)
|
||||||
|
private val errorActionButton: Button =
|
||||||
|
errorPanelRoot.findViewById(R.id.error_action_button)
|
||||||
|
private val errorRetryButton: Button =
|
||||||
|
errorPanelRoot.findViewById(R.id.error_retry_button)
|
||||||
|
|
||||||
private var errorDisposable: Disposable? = null
|
private var errorDisposable: Disposable? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
errorDisposable = errorButtonRetry.clicks()
|
errorDisposable = errorRetryButton.clicks()
|
||||||
.debounce(300, TimeUnit.MILLISECONDS)
|
.debounce(300, TimeUnit.MILLISECONDS)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { onRetry.run() }
|
.subscribe { onRetry.run() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ensureDefaultVisibility() {
|
||||||
|
errorTextView.isVisible = true
|
||||||
|
|
||||||
|
errorServiceInfoTextView.isVisible = false
|
||||||
|
errorServiceExplanationTextView.isVisible = false
|
||||||
|
errorActionButton.isVisible = false
|
||||||
|
errorRetryButton.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
fun showError(errorInfo: ErrorInfo) {
|
fun showError(errorInfo: ErrorInfo) {
|
||||||
|
|
||||||
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
|
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
|
||||||
|
@ -62,10 +81,14 @@ class ErrorPanelHelper(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
errorButtonAction.isVisible = true
|
ensureDefaultVisibility()
|
||||||
|
|
||||||
if (errorInfo.throwable is ReCaptchaException) {
|
if (errorInfo.throwable is ReCaptchaException) {
|
||||||
errorButtonAction.setText(R.string.recaptcha_solve)
|
errorTextView.setText(R.string.recaptcha_request_toast)
|
||||||
errorButtonAction.setOnClickListener {
|
|
||||||
|
showAndSetErrorButtonAction(
|
||||||
|
R.string.recaptcha_solve
|
||||||
|
) {
|
||||||
// Starting ReCaptcha Challenge Activity
|
// Starting ReCaptcha Challenge Activity
|
||||||
val intent = Intent(context, ReCaptchaActivity::class.java)
|
val intent = Intent(context, ReCaptchaActivity::class.java)
|
||||||
intent.putExtra(
|
intent.putExtra(
|
||||||
|
@ -73,78 +96,70 @@ class ErrorPanelHelper(
|
||||||
(errorInfo.throwable as ReCaptchaException).url
|
(errorInfo.throwable as ReCaptchaException).url
|
||||||
)
|
)
|
||||||
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
|
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
|
||||||
errorButtonAction.setOnClickListener(null)
|
errorActionButton.setOnClickListener(null)
|
||||||
}
|
}
|
||||||
errorTextView.setText(R.string.recaptcha_request_toast)
|
|
||||||
// additional info is only provided by AccountTerminatedException
|
errorRetryButton.isVisible = true
|
||||||
errorServiceInfoTextView.isVisible = false
|
|
||||||
errorServiceExplenationTextView.isVisible = false
|
|
||||||
errorButtonRetry.isVisible = true
|
|
||||||
} else if (errorInfo.throwable is AccountTerminatedException) {
|
} else if (errorInfo.throwable is AccountTerminatedException) {
|
||||||
errorButtonRetry.isVisible = false
|
|
||||||
errorButtonAction.isVisible = false
|
|
||||||
errorTextView.setText(R.string.account_terminated)
|
errorTextView.setText(R.string.account_terminated)
|
||||||
|
|
||||||
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
|
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
|
||||||
errorServiceInfoTextView.setText(
|
errorServiceInfoTextView.text = context.resources.getString(
|
||||||
context.resources.getString(
|
R.string.service_provides_reason,
|
||||||
R.string.service_provides_reason,
|
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
|
||||||
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
errorServiceExplenationTextView.setText(
|
|
||||||
(errorInfo.throwable as AccountTerminatedException).message
|
|
||||||
)
|
)
|
||||||
errorServiceInfoTextView.isVisible = true
|
errorServiceInfoTextView.isVisible = true
|
||||||
errorServiceExplenationTextView.isVisible = true
|
|
||||||
} else {
|
errorServiceExplanationTextView.text =
|
||||||
errorServiceInfoTextView.isVisible = false
|
(errorInfo.throwable as AccountTerminatedException).message
|
||||||
errorServiceExplenationTextView.isVisible = false
|
errorServiceExplanationTextView.isVisible = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errorButtonAction.setText(R.string.error_snackbar_action)
|
showAndSetErrorButtonAction(
|
||||||
errorButtonAction.setOnClickListener {
|
R.string.error_snackbar_action
|
||||||
|
) {
|
||||||
ErrorActivity.reportError(context, errorInfo)
|
ErrorActivity.reportError(context, errorInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// additional info is only provided by AccountTerminatedException
|
errorTextView.setText(getExceptionDescription(errorInfo.throwable))
|
||||||
errorServiceInfoTextView.isVisible = false
|
|
||||||
errorServiceExplenationTextView.isVisible = false
|
|
||||||
|
|
||||||
// hide retry button by default, then show only if not unavailable/unsupported content
|
if (errorInfo.throwable !is ContentNotAvailableException &&
|
||||||
errorButtonRetry.isVisible = false
|
errorInfo.throwable !is ContentNotSupportedException
|
||||||
errorTextView.setText(
|
) {
|
||||||
when (errorInfo.throwable) {
|
// show retry button only for content which is not unavailable or unsupported
|
||||||
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
|
errorRetryButton.isVisible = true
|
||||||
is GeographicRestrictionException -> R.string.georestricted_content
|
}
|
||||||
is PaidContentException -> R.string.paid_content
|
|
||||||
is PrivateContentException -> R.string.private_content
|
|
||||||
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
|
|
||||||
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
|
|
||||||
is ContentNotAvailableException -> R.string.content_not_available
|
|
||||||
is ContentNotSupportedException -> R.string.content_not_supported
|
|
||||||
else -> {
|
|
||||||
// show retry button only for content which is not unavailable or unsupported
|
|
||||||
errorButtonRetry.isVisible = true
|
|
||||||
if (errorInfo.throwable != null && errorInfo.throwable!!.isNetworkRelated) {
|
|
||||||
R.string.network_error
|
|
||||||
} else {
|
|
||||||
R.string.error_snackbar_message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
errorPanelRoot.animate(true, 300)
|
|
||||||
|
setRootVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the errorButtonAction, sets a text into it and sets the click listener.
|
||||||
|
*/
|
||||||
|
private fun showAndSetErrorButtonAction(
|
||||||
|
@StringRes resid: Int,
|
||||||
|
@Nullable listener: View.OnClickListener
|
||||||
|
) {
|
||||||
|
errorActionButton.isVisible = true
|
||||||
|
errorActionButton.setText(resid)
|
||||||
|
errorActionButton.setOnClickListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showTextError(errorString: String) {
|
fun showTextError(errorString: String) {
|
||||||
errorButtonAction.isVisible = false
|
ensureDefaultVisibility()
|
||||||
errorButtonRetry.isVisible = false
|
|
||||||
errorTextView.text = errorString
|
errorTextView.text = errorString
|
||||||
|
|
||||||
|
setRootVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setRootVisible() {
|
||||||
|
errorPanelRoot.animate(true, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hide() {
|
fun hide() {
|
||||||
errorButtonAction.setOnClickListener(null)
|
errorActionButton.setOnClickListener(null)
|
||||||
errorPanelRoot.animate(false, 150)
|
errorPanelRoot.animate(false, 150)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,13 +168,35 @@ class ErrorPanelHelper(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dispose() {
|
fun dispose() {
|
||||||
errorButtonAction.setOnClickListener(null)
|
errorActionButton.setOnClickListener(null)
|
||||||
errorButtonRetry.setOnClickListener(null)
|
errorRetryButton.setOnClickListener(null)
|
||||||
errorDisposable?.dispose()
|
errorDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG: String = ErrorPanelHelper::class.simpleName!!
|
val TAG: String = ErrorPanelHelper::class.simpleName!!
|
||||||
val DEBUG: Boolean = MainActivity.DEBUG
|
val DEBUG: Boolean = MainActivity.DEBUG
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
public fun getExceptionDescription(throwable: Throwable?): Int {
|
||||||
|
return when (throwable) {
|
||||||
|
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
|
||||||
|
is GeographicRestrictionException -> R.string.georestricted_content
|
||||||
|
is PaidContentException -> R.string.paid_content
|
||||||
|
is PrivateContentException -> R.string.private_content
|
||||||
|
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
|
||||||
|
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
|
||||||
|
is ContentNotAvailableException -> R.string.content_not_available
|
||||||
|
is ContentNotSupportedException -> R.string.content_not_supported
|
||||||
|
else -> {
|
||||||
|
// show retry button only for content which is not unavailable or unsupported
|
||||||
|
if (throwable != null && throwable.isNetworkRelated) {
|
||||||
|
R.string.network_error
|
||||||
|
} else {
|
||||||
|
R.string.error_snackbar_message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.schabi.newpipe.error;
|
package org.schabi.newpipe.error;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
@ -66,6 +67,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||||
private ActivityRecaptchaBinding recaptchaBinding;
|
private ActivityRecaptchaBinding recaptchaBinding;
|
||||||
private String foundCookies = "";
|
private String foundCookies = "";
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
ThemeHelper.setTheme(this);
|
ThemeHelper.setTheme(this);
|
||||||
|
|
|
@ -48,9 +48,7 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.material.appbar.AppBarLayout;
|
import com.google.android.material.appbar.AppBarLayout;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
import com.google.android.material.tabs.TabLayout;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
import com.squareup.picasso.Callback;
|
||||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
|
||||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.App;
|
import org.schabi.newpipe.App;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
@ -90,15 +88,15 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
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.ImageDisplayConstants;
|
|
||||||
import org.schabi.newpipe.util.VideoSegment;
|
import org.schabi.newpipe.util.VideoSegment;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
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.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.SponsorBlockUtils;
|
import org.schabi.newpipe.util.SponsorBlockUtils;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -154,6 +152,8 @@ public final class VideoDetailFragment
|
||||||
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
|
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
|
||||||
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
||||||
|
|
||||||
|
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
|
||||||
|
|
||||||
// tabs
|
// tabs
|
||||||
private boolean showComments;
|
private boolean showComments;
|
||||||
private boolean showRelatedItems;
|
private boolean showRelatedItems;
|
||||||
|
@ -206,7 +206,7 @@ public final class VideoDetailFragment
|
||||||
@Nullable
|
@Nullable
|
||||||
private MainPlayer playerService;
|
private MainPlayer playerService;
|
||||||
private Player player;
|
private Player player;
|
||||||
private PlayerHolder playerHolder = PlayerHolder.getInstance();
|
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Service management
|
// Service management
|
||||||
|
@ -225,7 +225,7 @@ public final class VideoDetailFragment
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLandscape()) {
|
if (DeviceUtils.isLandscape(requireContext())) {
|
||||||
// If the video is playing but orientation changed
|
// If the video is playing but orientation changed
|
||||||
// let's make the video in fullscreen again
|
// let's make the video in fullscreen again
|
||||||
checkLandscape();
|
checkLandscape();
|
||||||
|
@ -246,7 +246,7 @@ public final class VideoDetailFragment
|
||||||
&& isAutoplayEnabled()
|
&& isAutoplayEnabled()
|
||||||
&& player.getParentActivity() == null)) {
|
&& player.getParentActivity() == null)) {
|
||||||
autoPlayEnabled = true; // forcefully start playing
|
autoPlayEnabled = true; // forcefully start playing
|
||||||
openVideoPlayer();
|
openVideoPlayerAutoFullscreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -431,7 +431,7 @@ public final class VideoDetailFragment
|
||||||
showRelatedItems = sharedPreferences.getBoolean(key, true);
|
showRelatedItems = sharedPreferences.getBoolean(key, true);
|
||||||
tabSettingsChanged = true;
|
tabSettingsChanged = true;
|
||||||
} else if (key.equals(getString(R.string.show_description_key))) {
|
} else if (key.equals(getString(R.string.show_description_key))) {
|
||||||
showComments = sharedPreferences.getBoolean(key, true);
|
showDescription = sharedPreferences.getBoolean(key, true);
|
||||||
tabSettingsChanged = true;
|
tabSettingsChanged = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -507,7 +507,7 @@ public final class VideoDetailFragment
|
||||||
break;
|
break;
|
||||||
case R.id.detail_thumbnail_root_layout:
|
case R.id.detail_thumbnail_root_layout:
|
||||||
autoPlayEnabled = true; // forcefully start playing
|
autoPlayEnabled = true; // forcefully start playing
|
||||||
openVideoPlayer();
|
openVideoPlayerAutoFullscreen();
|
||||||
break;
|
break;
|
||||||
case R.id.detail_title_root_layout:
|
case R.id.detail_title_root_layout:
|
||||||
toggleTitleAndSecondaryControls();
|
toggleTitleAndSecondaryControls();
|
||||||
|
@ -524,7 +524,7 @@ public final class VideoDetailFragment
|
||||||
showSystemUi();
|
showSystemUi();
|
||||||
} else {
|
} else {
|
||||||
autoPlayEnabled = true; // forcefully start playing
|
autoPlayEnabled = true; // forcefully start playing
|
||||||
openVideoPlayer();
|
openVideoPlayer(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setOverlayPlayPauseImage(isPlayerAvailable() && player.isPlaying());
|
setOverlayPlayPauseImage(isPlayerAvailable() && player.isPlaying());
|
||||||
|
@ -694,33 +694,24 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initThumbnailViews(@NonNull final StreamInfo info) {
|
private void initThumbnailViews(@NonNull final StreamInfo info) {
|
||||||
binding.detailThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
PicassoHelper.loadThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
|
.into(binding.detailThumbnailImageView, new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess() {
|
||||||
|
// nothing to do, the image was loaded correctly into the thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
if (!isEmpty(info.getThumbnailUrl())) {
|
@Override
|
||||||
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
|
public void onError(final Exception e) {
|
||||||
@Override
|
showSnackBarError(new ErrorInfo(e, UserAction.LOAD_IMAGE,
|
||||||
public void onLoadingFailed(final String imageUri, final View view,
|
info.getThumbnailUrl(), info));
|
||||||
final FailReason failReason) {
|
}
|
||||||
showSnackBarError(new ErrorInfo(failReason.getCause(), UserAction.LOAD_IMAGE,
|
});
|
||||||
imageUri, info));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
IMAGE_LOADER.displayImage(info.getThumbnailUrl(), binding.detailThumbnailImageView,
|
PicassoHelper.loadAvatar(info.getSubChannelAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
|
.into(binding.detailSubChannelThumbnailView);
|
||||||
}
|
PicassoHelper.loadAvatar(info.getUploaderAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
|
.into(binding.detailUploaderThumbnailView);
|
||||||
if (!isEmpty(info.getSubChannelAvatarUrl())) {
|
|
||||||
IMAGE_LOADER.displayImage(info.getSubChannelAvatarUrl(),
|
|
||||||
binding.detailSubChannelThumbnailView,
|
|
||||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isEmpty(info.getUploaderAvatarUrl())) {
|
|
||||||
IMAGE_LOADER.displayImage(info.getUploaderAvatarUrl(),
|
|
||||||
binding.detailUploaderThumbnailView,
|
|
||||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -759,27 +750,26 @@ public final class VideoDetailFragment
|
||||||
&& player.getPlayQueue() != null
|
&& player.getPlayQueue() != null
|
||||||
&& player.videoPlayerSelected()
|
&& player.videoPlayerSelected()
|
||||||
&& player.getPlayQueue().previous()) {
|
&& player.getPlayQueue().previous()) {
|
||||||
return true;
|
return true; // no code here, as previous() was used in the if
|
||||||
}
|
}
|
||||||
|
|
||||||
// That means that we are on the start of the stack,
|
// That means that we are on the start of the stack,
|
||||||
// return false to let the MainActivity handle the onBack
|
|
||||||
if (stack.size() <= 1) {
|
if (stack.size() <= 1) {
|
||||||
restoreDefaultOrientation();
|
restoreDefaultOrientation();
|
||||||
|
return false; // let MainActivity handle the onBack (e.g. to minimize the mini player)
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove top
|
// Remove top
|
||||||
stack.pop();
|
stack.pop();
|
||||||
// Get stack item from the new top
|
// Get stack item from the new top
|
||||||
assert stack.peek() != null;
|
setupFromHistoryItem(Objects.requireNonNull(stack.peek()));
|
||||||
setupFromHistoryItem(stack.peek());
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupFromHistoryItem(final StackItem item) {
|
private void setupFromHistoryItem(final StackItem item) {
|
||||||
setAutoPlay(false);
|
setAutoPlay(false);
|
||||||
hideMainPlayer();
|
hideMainPlayerOnLoadingNewStream();
|
||||||
|
|
||||||
setInitialData(item.getServiceId(), item.getUrl(),
|
setInitialData(item.getServiceId(), item.getUrl(),
|
||||||
item.getTitle() == null ? "" : item.getTitle(), item.getPlayQueue());
|
item.getTitle() == null ? "" : item.getTitle(), item.getPlayQueue());
|
||||||
|
@ -899,7 +889,7 @@ public final class VideoDetailFragment
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(result -> {
|
.subscribe(result -> {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
hideMainPlayer();
|
hideMainPlayerOnLoadingNewStream();
|
||||||
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
|
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
|
||||||
getString(R.string.show_age_restricted_content), false)) {
|
getString(R.string.show_age_restricted_content), false)) {
|
||||||
hideAgeRestrictedContent();
|
hideAgeRestrictedContent();
|
||||||
|
@ -914,8 +904,9 @@ public final class VideoDetailFragment
|
||||||
stack.push(new StackItem(serviceId, url, title, playQueue));
|
stack.push(new StackItem(serviceId, url, title, playQueue));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAutoplayEnabled()) {
|
if (isAutoplayEnabled()) {
|
||||||
openVideoPlayer();
|
openVideoPlayerAutoFullscreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
|
||||||
|
@ -1120,7 +1111,29 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void openVideoPlayer() {
|
/**
|
||||||
|
* Opens the video player, in fullscreen if needed. In order to open fullscreen, the activity
|
||||||
|
* is toggled to landscape orientation (which will then cause fullscreen mode).
|
||||||
|
*
|
||||||
|
* @param directlyFullscreenIfApplicable whether to open fullscreen if we are not already
|
||||||
|
* in landscape and screen orientation is locked
|
||||||
|
*/
|
||||||
|
public void openVideoPlayer(final boolean directlyFullscreenIfApplicable) {
|
||||||
|
if (directlyFullscreenIfApplicable
|
||||||
|
&& !DeviceUtils.isLandscape(requireContext())
|
||||||
|
&& PlayerHelper.globalScreenOrientationLocked(requireContext())) {
|
||||||
|
// Make sure the bottom sheet turns out expanded. When this code kicks in the bottom
|
||||||
|
// sheet could not have fully expanded yet, and thus be in the STATE_SETTLING state.
|
||||||
|
// When the activity is rotated, and its state is saved and then restored, the bottom
|
||||||
|
// sheet would forget what it was doing, since even if STATE_SETTLING is restored, it
|
||||||
|
// doesn't tell which state it was settling to, and thus the bottom sheet settles to
|
||||||
|
// STATE_COLLAPSED. This can be solved by manually setting the state that will be
|
||||||
|
// restored (i.e. bottomSheetState) to STATE_EXPANDED.
|
||||||
|
bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||||
|
// toggle landscape in order to open directly in fullscreen
|
||||||
|
onScreenRotationButtonClicked();
|
||||||
|
}
|
||||||
|
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
||||||
showExternalPlaybackDialog();
|
showExternalPlaybackDialog();
|
||||||
|
@ -1129,6 +1142,18 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the option to start directly fullscreen is enabled, calls
|
||||||
|
* {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable = true}, so that
|
||||||
|
* if the user is not already in landscape and he has screen orientation locked the activity
|
||||||
|
* rotates and fullscreen starts. Otherwise, if the option to start directly fullscreen is
|
||||||
|
* disabled, calls {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable
|
||||||
|
* = false}, hence preventing it from going directly fullscreen.
|
||||||
|
*/
|
||||||
|
public void openVideoPlayerAutoFullscreen() {
|
||||||
|
openVideoPlayer(PlayerHelper.isStartMainPlayerFullscreenEnabled(requireContext()));
|
||||||
|
}
|
||||||
|
|
||||||
private void openNormalBackgroundPlayer(final boolean append) {
|
private void openNormalBackgroundPlayer(final boolean append) {
|
||||||
// See UI changes while remote playQueue changes
|
// See UI changes while remote playQueue changes
|
||||||
if (!isPlayerAvailable()) {
|
if (!isPlayerAvailable()) {
|
||||||
|
@ -1162,12 +1187,19 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
addVideoPlayerView();
|
addVideoPlayerView();
|
||||||
|
|
||||||
final Intent playerIntent = NavigationHelper
|
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
|
||||||
.getPlayerIntent(requireContext(), MainPlayer.class, queue, true, autoPlayEnabled);
|
MainPlayer.class, queue, true, autoPlayEnabled);
|
||||||
ContextCompat.startForegroundService(activity, playerIntent);
|
ContextCompat.startForegroundService(activity, playerIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hideMainPlayer() {
|
/**
|
||||||
|
* When the video detail fragment is already showing details for a video and the user opens a
|
||||||
|
* new one, the video detail fragment changes all of its old data to the new stream, so if there
|
||||||
|
* is a video player currently open it should be hidden. This method does exactly that. If
|
||||||
|
* autoplay is enabled, the underlying player is not stopped completely, since it is going to
|
||||||
|
* be reused in a few milliseconds and the flickering would be annoying.
|
||||||
|
*/
|
||||||
|
private void hideMainPlayerOnLoadingNewStream() {
|
||||||
if (!isPlayerServiceAvailable()
|
if (!isPlayerServiceAvailable()
|
||||||
|| playerService.getView() == null
|
|| playerService.getView() == null
|
||||||
|| !player.videoPlayerSelected()) {
|
|| !player.videoPlayerSelected()) {
|
||||||
|
@ -1175,8 +1207,12 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
removeVideoPlayerView();
|
removeVideoPlayerView();
|
||||||
playerService.stop(isAutoplayEnabled());
|
if (isAutoplayEnabled()) {
|
||||||
playerService.getView().setVisibility(View.GONE);
|
playerService.stopForImmediateReusing();
|
||||||
|
playerService.getView().setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
playerHolder.stopService();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue setupPlayQueueForIntent(final boolean append) {
|
private PlayQueue setupPlayQueueForIntent(final boolean append) {
|
||||||
|
@ -1269,7 +1305,7 @@ public final class VideoDetailFragment
|
||||||
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||||
|
|
||||||
if (getView() != null) {
|
if (getView() != null) {
|
||||||
final int height = (isInMultiWindow()
|
final int height = (DeviceUtils.isInMultiWindow(activity)
|
||||||
? requireView()
|
? requireView()
|
||||||
: activity.getWindow().getDecorView()).getHeight();
|
: activity.getWindow().getDecorView()).getHeight();
|
||||||
setHeightThumbnail(height, metrics);
|
setHeightThumbnail(height, metrics);
|
||||||
|
@ -1292,7 +1328,7 @@ public final class VideoDetailFragment
|
||||||
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
|
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
|
||||||
|
|
||||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||||
final int height = (isInMultiWindow()
|
final int height = (DeviceUtils.isInMultiWindow(activity)
|
||||||
? requireView()
|
? requireView()
|
||||||
: activity.getWindow().getDecorView()).getHeight();
|
: activity.getWindow().getDecorView()).getHeight();
|
||||||
// Height is zero when the view is not yet displayed like after orientation change
|
// Height is zero when the view is not yet displayed like after orientation change
|
||||||
|
@ -1403,17 +1439,15 @@ public final class VideoDetailFragment
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void restoreDefaultOrientation() {
|
private void restoreDefaultOrientation() {
|
||||||
if (!isPlayerAvailable() || !player.videoPlayerSelected() || activity == null) {
|
if (isPlayerAvailable() && player.videoPlayerSelected()) {
|
||||||
return;
|
toggleFullscreenIfInFullscreenMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleFullscreenIfInFullscreenMode();
|
|
||||||
|
|
||||||
// This will show systemUI and pause the player.
|
// This will show systemUI and pause the player.
|
||||||
// User can tap on Play button and video will be in fullscreen mode again
|
// User can tap on Play button and video will be in fullscreen mode again
|
||||||
// Note for tablet: trying to avoid orientation changes since it's not easy
|
// Note for tablet: trying to avoid orientation changes since it's not easy
|
||||||
// to physically rotate the tablet every time
|
// to physically rotate the tablet every time
|
||||||
if (!DeviceUtils.isTablet(activity)) {
|
if (activity != null && !DeviceUtils.isTablet(activity)) {
|
||||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1454,8 +1488,7 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IMAGE_LOADER.cancelDisplayTask(binding.detailThumbnailImageView);
|
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
|
||||||
IMAGE_LOADER.cancelDisplayTask(binding.detailSubChannelThumbnailView);
|
|
||||||
binding.detailThumbnailImageView.setImageBitmap(null);
|
binding.detailThumbnailImageView.setImageBitmap(null);
|
||||||
binding.detailSubChannelThumbnailView.setImageBitmap(null);
|
binding.detailSubChannelThumbnailView.setImageBitmap(null);
|
||||||
}
|
}
|
||||||
|
@ -1845,7 +1878,7 @@ public final class VideoDetailFragment
|
||||||
|| error.type == ExoPlaybackException.TYPE_UNEXPECTED) {
|
|| error.type == ExoPlaybackException.TYPE_UNEXPECTED) {
|
||||||
// Properly exit from fullscreen
|
// Properly exit from fullscreen
|
||||||
toggleFullscreenIfInFullscreenMode();
|
toggleFullscreenIfInFullscreenMode();
|
||||||
hideMainPlayer();
|
hideMainPlayerOnLoadingNewStream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1901,13 +1934,14 @@ public final class VideoDetailFragment
|
||||||
// from landscape to portrait every time.
|
// from landscape to portrait every time.
|
||||||
// Just turn on fullscreen mode in landscape orientation
|
// Just turn on fullscreen mode in landscape orientation
|
||||||
// or portrait & unlocked global orientation
|
// or portrait & unlocked global orientation
|
||||||
|
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
||||||
if (DeviceUtils.isTablet(activity)
|
if (DeviceUtils.isTablet(activity)
|
||||||
&& (!globalScreenOrientationLocked(activity) || isLandscape())) {
|
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
||||||
player.toggleFullscreen();
|
player.toggleFullscreen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int newOrientation = isLandscape()
|
final int newOrientation = isLandscape
|
||||||
? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
|
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
|
||||||
|
|
||||||
|
@ -1979,15 +2013,17 @@ public final class VideoDetailFragment
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||||
|
|
||||||
// In multiWindow mode status bar is not transparent for devices with cutout
|
// In multiWindow mode status bar is not transparent for devices with cutout
|
||||||
// if I include this flag. So without it is better in this case
|
// if I include this flag. So without it is better in this case
|
||||||
if (!isInMultiWindow()) {
|
final boolean isInMultiWindow = DeviceUtils.isInMultiWindow(activity);
|
||||||
|
if (!isInMultiWindow) {
|
||||||
visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
|
visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||||
}
|
}
|
||||||
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||||
&& (isInMultiWindow() || (isPlayerAvailable() && player.isFullscreen()))) {
|
&& (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) {
|
||||||
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
||||||
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
||||||
}
|
}
|
||||||
|
@ -2059,15 +2095,6 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isLandscape() {
|
|
||||||
return getResources().getDisplayMetrics().heightPixels < getResources()
|
|
||||||
.getDisplayMetrics().widthPixels;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isInMultiWindow() {
|
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Means that the player fragment was swiped away via BottomSheetLayout
|
* Means that the player fragment was swiped away via BottomSheetLayout
|
||||||
* and is empty but ready for any new actions. See cleanUp()
|
* and is empty but ready for any new actions. See cleanUp()
|
||||||
|
@ -2107,8 +2134,8 @@ public final class VideoDetailFragment
|
||||||
private void showClearingQueueConfirmation(final Runnable onAllow) {
|
private void showClearingQueueConfirmation(final Runnable onAllow) {
|
||||||
new AlertDialog.Builder(activity)
|
new AlertDialog.Builder(activity)
|
||||||
.setTitle(R.string.clear_queue_confirmation_description)
|
.setTitle(R.string.clear_queue_confirmation_description)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||||
onAllow.run();
|
onAllow.run();
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
}).show();
|
}).show();
|
||||||
|
@ -2123,7 +2150,7 @@ public final class VideoDetailFragment
|
||||||
resolutions[i] = sortedVideoStreams.get(i).getResolution();
|
resolutions[i] = sortedVideoStreams.get(i).getResolution();
|
||||||
}
|
}
|
||||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
|
final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
|
.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
|
||||||
ShareUtils.openUrlInBrowser(requireActivity(), url)
|
ShareUtils.openUrlInBrowser(requireActivity(), url)
|
||||||
);
|
);
|
||||||
|
@ -2250,7 +2277,7 @@ public final class VideoDetailFragment
|
||||||
setOverlayElementsClickable(false);
|
setOverlayElementsClickable(false);
|
||||||
hideSystemUiIfNeeded();
|
hideSystemUiIfNeeded();
|
||||||
// Conditions when the player should be expanded to fullscreen
|
// Conditions when the player should be expanded to fullscreen
|
||||||
if (isLandscape()
|
if (DeviceUtils.isLandscape(requireContext())
|
||||||
&& isPlayerAvailable()
|
&& isPlayerAvailable()
|
||||||
&& player.isPlaying()
|
&& player.isPlaying()
|
||||||
&& !player.isFullscreen()
|
&& !player.isFullscreen()
|
||||||
|
@ -2305,10 +2332,8 @@ public final class VideoDetailFragment
|
||||||
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
||||||
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
||||||
binding.overlayThumbnail.setImageResource(R.drawable.dummy_thumbnail_dark);
|
binding.overlayThumbnail.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||||
if (!isEmpty(thumbnailUrl)) {
|
PicassoHelper.loadThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
IMAGE_LOADER.displayImage(thumbnailUrl, binding.overlayThumbnail,
|
.into(binding.overlayThumbnail);
|
||||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
|
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
|
||||||
|
|
|
@ -40,10 +40,10 @@ import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
|
||||||
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.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -66,7 +66,10 @@ import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||||
|
|
||||||
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||||
implements View.OnClickListener {
|
implements View.OnClickListener {
|
||||||
|
|
||||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||||
|
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
||||||
|
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
private Disposable subscribeButtonMonitor;
|
private Disposable subscribeButtonMonitor;
|
||||||
|
|
||||||
|
@ -421,10 +424,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||||
@Override
|
@Override
|
||||||
public void showLoading() {
|
public void showLoading() {
|
||||||
super.showLoading();
|
super.showLoading();
|
||||||
|
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
||||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelBannerImage);
|
|
||||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelAvatarView);
|
|
||||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.subChannelAvatarView);
|
|
||||||
animate(headerBinding.channelSubscribeButton, false, 100);
|
animate(headerBinding.channelSubscribeButton, false, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -433,13 +433,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
|
||||||
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
headerBinding.getRoot().setVisibility(View.VISIBLE);
|
||||||
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerBinding.channelBannerImage,
|
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||||
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
|
.into(headerBinding.channelBannerImage);
|
||||||
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerBinding.channelAvatarView,
|
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
.into(headerBinding.channelAvatarView);
|
||||||
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(),
|
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
|
||||||
headerBinding.subChannelAvatarView,
|
.into(headerBinding.subChannelAvatarView);
|
||||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
|
||||||
|
|
||||||
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
|
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
|
||||||
if (result.getSubscriberCount() >= 0) {
|
if (result.getSubscriberCount() >= 0) {
|
||||||
|
|
|
@ -41,7 +41,7 @@ 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.player.playqueue.PlaylistPlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
@ -64,12 +64,16 @@ import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||||
|
|
||||||
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
|
|
||||||
|
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
||||||
|
|
||||||
private CompositeDisposable disposables;
|
private CompositeDisposable disposables;
|
||||||
private Subscription bookmarkReactor;
|
private Subscription bookmarkReactor;
|
||||||
private AtomicBoolean isBookmarkButtonReady;
|
private AtomicBoolean isBookmarkButtonReady;
|
||||||
|
|
||||||
private RemotePlaylistManager remotePlaylistManager;
|
private RemotePlaylistManager remotePlaylistManager;
|
||||||
private PlaylistRemoteEntity playlistEntity;
|
private PlaylistRemoteEntity playlistEntity;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -274,7 +278,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
animate(headerBinding.getRoot(), false, 200);
|
animate(headerBinding.getRoot(), false, 200);
|
||||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||||
|
|
||||||
IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView);
|
PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
|
||||||
animate(headerBinding.uploaderLayout, false, 200);
|
animate(headerBinding.uploaderLayout, false, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,8 +321,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
R.drawable.ic_radio)
|
R.drawable.ic_radio)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
IMAGE_LOADER.displayImage(avatarUrl, headerBinding.uploaderAvatarView,
|
PicassoHelper.loadAvatar(avatarUrl).tag(PICASSO_PLAYLIST_TAG)
|
||||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
.into(headerBinding.uploaderAvatarView);
|
||||||
}
|
}
|
||||||
|
|
||||||
headerBinding.playlistStreamCount.setText(Localization
|
headerBinding.playlistStreamCount.setText(Localization
|
||||||
|
|
|
@ -57,6 +57,7 @@ import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||||
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;
|
||||||
|
@ -65,16 +66,19 @@ import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
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.Observable;
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
@ -143,7 +147,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
@Nullable private Map<Integer, String> menuItemToFilterName = null;
|
@Nullable private Map<Integer, String> menuItemToFilterName = null;
|
||||||
private StreamingService service;
|
private StreamingService service;
|
||||||
private Page nextPage;
|
private Page nextPage;
|
||||||
private boolean isSuggestionsEnabled = true;
|
private boolean showLocalSuggestions = true;
|
||||||
|
private boolean showRemoteSuggestions = true;
|
||||||
|
|
||||||
private Disposable searchDisposable;
|
private Disposable searchDisposable;
|
||||||
private Disposable suggestionDisposable;
|
private Disposable suggestionDisposable;
|
||||||
|
@ -194,26 +199,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
public void onAttach(@NonNull final Context context) {
|
public void onAttach(@NonNull final Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
|
|
||||||
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||||
|
showLocalSuggestions = NewPipeSettings.showLocalSearchSuggestions(activity, prefs);
|
||||||
|
showRemoteSuggestions = NewPipeSettings.showRemoteSearchSuggestions(activity, prefs);
|
||||||
|
|
||||||
suggestionListAdapter = new SuggestionListAdapter(activity);
|
suggestionListAdapter = new SuggestionListAdapter(activity);
|
||||||
final SharedPreferences preferences
|
|
||||||
= PreferenceManager.getDefaultSharedPreferences(activity);
|
|
||||||
final boolean isSearchHistoryEnabled = preferences
|
|
||||||
.getBoolean(getString(R.string.enable_search_history_key), true);
|
|
||||||
suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled);
|
|
||||||
|
|
||||||
historyRecordManager = new HistoryRecordManager(context);
|
historyRecordManager = new HistoryRecordManager(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
final SharedPreferences preferences
|
|
||||||
= PreferenceManager.getDefaultSharedPreferences(activity);
|
|
||||||
isSuggestionsEnabled = preferences
|
|
||||||
.getBoolean(getString(R.string.show_search_suggestions_key), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||||
@Nullable final Bundle savedInstanceState) {
|
@Nullable final Bundle savedInstanceState) {
|
||||||
|
@ -222,6 +215,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||||
|
searchBinding = FragmentSearchBinding.bind(rootView);
|
||||||
super.onViewCreated(rootView, savedInstanceState);
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
showSearchOnStart();
|
showSearchOnStart();
|
||||||
initSearchListeners();
|
initSearchListeners();
|
||||||
|
@ -348,7 +342,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
@Override
|
@Override
|
||||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
searchBinding = FragmentSearchBinding.bind(rootView);
|
|
||||||
|
|
||||||
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
||||||
new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
||||||
|
@ -554,7 +547,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 (isSuggestionsEnabled && !isErrorPanelVisible()) {
|
if ((showLocalSuggestions || showRemoteSuggestions) && !isErrorPanelVisible()) {
|
||||||
showSuggestionsPanel();
|
showSuggestionsPanel();
|
||||||
}
|
}
|
||||||
if (DeviceUtils.isTv(getContext())) {
|
if (DeviceUtils.isTv(getContext())) {
|
||||||
|
@ -567,7 +560,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
Log.d(TAG, "onFocusChange() called with: "
|
Log.d(TAG, "onFocusChange() called with: "
|
||||||
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||||
}
|
}
|
||||||
if (isSuggestionsEnabled && hasFocus && !isErrorPanelVisible()) {
|
if ((showLocalSuggestions || showRemoteSuggestions)
|
||||||
|
&& hasFocus && !isErrorPanelVisible()) {
|
||||||
showSuggestionsPanel();
|
showSuggestionsPanel();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -743,6 +737,34 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Observable<List<SuggestionItem>> getLocalSuggestionsObservable(
|
||||||
|
final String query, final int similarQueryLimit) {
|
||||||
|
return historyRecordManager
|
||||||
|
.getRelatedSearches(query, similarQueryLimit, 25)
|
||||||
|
.toObservable()
|
||||||
|
.map(searchHistoryEntries -> {
|
||||||
|
final Set<SuggestionItem> result = new HashSet<>(); // remove duplicates
|
||||||
|
for (final SearchHistoryEntry entry : searchHistoryEntries) {
|
||||||
|
result.add(new SuggestionItem(true, entry.getSearch()));
|
||||||
|
}
|
||||||
|
return new ArrayList<>(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Observable<List<SuggestionItem>> getRemoteSuggestionsObservable(final String query) {
|
||||||
|
return ExtractorHelper
|
||||||
|
.suggestionsFor(serviceId, query)
|
||||||
|
.toObservable()
|
||||||
|
.map(strings -> {
|
||||||
|
final List<SuggestionItem> result = new ArrayList<>();
|
||||||
|
for (final String entry : strings) {
|
||||||
|
result.add(new SuggestionItem(false, entry));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void initSuggestionObserver() {
|
private void initSuggestionObserver() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "initSuggestionObserver() called");
|
Log.d(TAG, "initSuggestionObserver() called");
|
||||||
|
@ -753,73 +775,53 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
|
|
||||||
suggestionDisposable = suggestionPublisher
|
suggestionDisposable = suggestionPublisher
|
||||||
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
||||||
.startWithItem(searchString != null
|
.startWithItem(searchString == null ? "" : searchString)
|
||||||
? searchString
|
|
||||||
: "")
|
|
||||||
.filter(ss -> isSuggestionsEnabled)
|
|
||||||
.switchMap(query -> {
|
.switchMap(query -> {
|
||||||
final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager
|
// Only show remote suggestions if they are enabled in settings and
|
||||||
.getRelatedSearches(query, 3, 25);
|
// the query length is at least THRESHOLD_NETWORK_SUGGESTION
|
||||||
final Observable<List<SuggestionItem>> local = flowable.toObservable()
|
final boolean shallShowRemoteSuggestionsNow = showRemoteSuggestions
|
||||||
.map(searchHistoryEntries -> {
|
&& query.length() >= THRESHOLD_NETWORK_SUGGESTION;
|
||||||
final List<SuggestionItem> result = new ArrayList<>();
|
|
||||||
for (final SearchHistoryEntry entry : searchHistoryEntries) {
|
|
||||||
result.add(new SuggestionItem(true, entry.getSearch()));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) {
|
if (showLocalSuggestions && shallShowRemoteSuggestionsNow) {
|
||||||
// Only pass through if the query length
|
return Observable.zip(
|
||||||
// is equal or greater than THRESHOLD_NETWORK_SUGGESTION
|
getLocalSuggestionsObservable(query, 3),
|
||||||
return local.materialize();
|
getRemoteSuggestionsObservable(query),
|
||||||
|
(local, remote) -> {
|
||||||
|
remote.removeIf(remoteItem -> local.stream().anyMatch(
|
||||||
|
localItem -> localItem.equals(remoteItem)));
|
||||||
|
local.addAll(remote);
|
||||||
|
return local;
|
||||||
|
})
|
||||||
|
.materialize();
|
||||||
|
} else if (showLocalSuggestions) {
|
||||||
|
return getLocalSuggestionsObservable(query, 25)
|
||||||
|
.materialize();
|
||||||
|
} else if (shallShowRemoteSuggestionsNow) {
|
||||||
|
return getRemoteSuggestionsObservable(query)
|
||||||
|
.materialize();
|
||||||
|
} else {
|
||||||
|
return Single.fromCallable(Collections::<SuggestionItem>emptyList)
|
||||||
|
.toObservable()
|
||||||
|
.materialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Observable<List<SuggestionItem>> network = ExtractorHelper
|
|
||||||
.suggestionsFor(serviceId, query)
|
|
||||||
.onErrorReturn(throwable -> {
|
|
||||||
if (!ExceptionUtils.isNetworkRelated(throwable)) {
|
|
||||||
showSnackBarError(new ErrorInfo(throwable,
|
|
||||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
|
||||||
}
|
|
||||||
return new ArrayList<>();
|
|
||||||
})
|
|
||||||
.toObservable()
|
|
||||||
.map(strings -> {
|
|
||||||
final List<SuggestionItem> result = new ArrayList<>();
|
|
||||||
for (final String entry : strings) {
|
|
||||||
result.add(new SuggestionItem(false, entry));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Observable.zip(local, network, (localResult, networkResult) -> {
|
|
||||||
final List<SuggestionItem> result = new ArrayList<>();
|
|
||||||
if (localResult.size() > 0) {
|
|
||||||
result.addAll(localResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove duplicates
|
|
||||||
networkResult.removeIf(networkItem ->
|
|
||||||
localResult.stream().anyMatch(localItem ->
|
|
||||||
localItem.query.equals(networkItem.query)));
|
|
||||||
|
|
||||||
if (networkResult.size() > 0) {
|
|
||||||
result.addAll(networkResult);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}).materialize();
|
|
||||||
})
|
})
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(listNotification -> {
|
.subscribe(
|
||||||
if (listNotification.isOnNext()) {
|
listNotification -> {
|
||||||
handleSuggestions(listNotification.getValue());
|
if (listNotification.isOnNext()) {
|
||||||
} else if (listNotification.isOnError()) {
|
if (listNotification.getValue() != null) {
|
||||||
showError(new ErrorInfo(listNotification.getError(),
|
handleSuggestions(listNotification.getValue());
|
||||||
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
}
|
||||||
}
|
} else if (listNotification.isOnError()
|
||||||
});
|
&& listNotification.getError() != null
|
||||||
|
&& !ExceptionUtils.isInterruptedCaused(
|
||||||
|
listNotification.getError())) {
|
||||||
|
showSnackBarError(new ErrorInfo(listNotification.getError(),
|
||||||
|
UserAction.GET_SUGGESTIONS, searchString, serviceId));
|
||||||
|
}
|
||||||
|
}, throwable -> showSnackBarError(new ErrorInfo(
|
||||||
|
throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package org.schabi.newpipe.fragments.list.search;
|
package org.schabi.newpipe.fragments.list.search;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
public class SuggestionItem {
|
public class SuggestionItem {
|
||||||
final boolean fromHistory;
|
final boolean fromHistory;
|
||||||
public final String query;
|
public final String query;
|
||||||
|
@ -9,6 +11,20 @@ public class SuggestionItem {
|
||||||
this.query = query;
|
this.query = query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object o) {
|
||||||
|
if (o instanceof SuggestionItem) {
|
||||||
|
return query.equals(((SuggestionItem) o).query);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return query.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "[" + fromHistory + "→" + query + "]";
|
return "[" + fromHistory + "→" + query + "]";
|
||||||
|
|
|
@ -19,7 +19,6 @@ public class SuggestionListAdapter
|
||||||
private final ArrayList<SuggestionItem> items = new ArrayList<>();
|
private final ArrayList<SuggestionItem> items = new ArrayList<>();
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private OnSuggestionItemSelected listener;
|
private OnSuggestionItemSelected listener;
|
||||||
private boolean showSuggestionHistory = true;
|
|
||||||
|
|
||||||
public SuggestionListAdapter(final Context context) {
|
public SuggestionListAdapter(final Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
@ -27,16 +26,7 @@ public class SuggestionListAdapter
|
||||||
|
|
||||||
public void setItems(final List<SuggestionItem> items) {
|
public void setItems(final List<SuggestionItem> items) {
|
||||||
this.items.clear();
|
this.items.clear();
|
||||||
if (showSuggestionHistory) {
|
this.items.addAll(items);
|
||||||
this.items.addAll(items);
|
|
||||||
} else {
|
|
||||||
// remove history items if history is disabled
|
|
||||||
for (final SuggestionItem item : items) {
|
|
||||||
if (!item.fromHistory) {
|
|
||||||
this.items.add(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,10 +34,6 @@ public class SuggestionListAdapter
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setShowSuggestionHistory(final boolean v) {
|
|
||||||
showSuggestionHistory = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
|
public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
|
||||||
return new SuggestionItemHolder(LayoutInflater.from(context)
|
return new SuggestionItemHolder(LayoutInflater.from(context)
|
||||||
|
|
|
@ -6,8 +6,6 @@ import android.view.ViewGroup;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
|
@ -51,7 +49,6 @@ import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
public class InfoItemBuilder {
|
public class InfoItemBuilder {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
|
||||||
|
|
||||||
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
|
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
|
||||||
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
|
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
|
||||||
|
@ -101,10 +98,6 @@ public class InfoItemBuilder {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImageLoader getImageLoader() {
|
|
||||||
return imageLoader;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() {
|
public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() {
|
||||||
return onStreamSelectedListener;
|
return onStreamSelectedListener;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,12 @@ package org.schabi.newpipe.info_list
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader
|
|
||||||
import com.xwray.groupie.GroupieViewHolder
|
import com.xwray.groupie.GroupieViewHolder
|
||||||
import com.xwray.groupie.Item
|
import com.xwray.groupie.Item
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.stream.StreamSegment
|
import org.schabi.newpipe.extractor.stream.StreamSegment
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper
|
||||||
|
|
||||||
class StreamSegmentItem(
|
class StreamSegmentItem(
|
||||||
private val item: StreamSegment,
|
private val item: StreamSegment,
|
||||||
|
@ -24,10 +23,8 @@ class StreamSegmentItem(
|
||||||
|
|
||||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||||
item.previewUrl?.let {
|
item.previewUrl?.let {
|
||||||
ImageLoader.getInstance().displayImage(
|
PicassoHelper.loadThumbnail(it)
|
||||||
it, viewHolder.root.findViewById<ImageView>(R.id.previewImage),
|
.into(viewHolder.root.findViewById<ImageView>(R.id.previewImage))
|
||||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
|
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
|
||||||
if (item.channelName == null) {
|
if (item.channelName == null) {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import de.hdodenhof.circleimageview.CircleImageView;
|
import de.hdodenhof.circleimageview.CircleImageView;
|
||||||
|
@ -43,10 +43,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||||
itemTitleView.setText(item.getName());
|
itemTitleView.setText(item.getName());
|
||||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||||
|
|
||||||
itemBuilder.getImageLoader()
|
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||||
.displayImage(item.getThumbnailUrl(),
|
|
||||||
itemThumbnailView,
|
|
||||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
package org.schabi.newpipe.info_list.holder;
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.text.style.URLSpan;
|
import android.text.style.URLSpan;
|
||||||
import android.text.util.Linkify;
|
import android.text.util.Linkify;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.ErrorActivity;
|
import org.schabi.newpipe.error.ErrorActivity;
|
||||||
|
@ -21,30 +20,29 @@ 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.CommentTextOnTouchListener;
|
import org.schabi.newpipe.util.CommentTextOnTouchListener;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
|
||||||
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.external_communication.TimestampExtractor;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import de.hdodenhof.circleimageview.CircleImageView;
|
import de.hdodenhof.circleimageview.CircleImageView;
|
||||||
|
|
||||||
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
|
private static final String TAG = "CommentsMiniIIHolder";
|
||||||
|
|
||||||
private static final int COMMENT_DEFAULT_LINES = 2;
|
private static final int COMMENT_DEFAULT_LINES = 2;
|
||||||
private static final int COMMENT_EXPANDED_LINES = 1000;
|
private static final int COMMENT_EXPANDED_LINES = 1000;
|
||||||
private static final Pattern PATTERN = Pattern.compile("(\\d+:)?(\\d+)?:(\\d+)");
|
|
||||||
private final String downloadThumbnailKey;
|
|
||||||
private final int commentHorizontalPadding;
|
private final int commentHorizontalPadding;
|
||||||
private final int commentVerticalPadding;
|
private final int commentVerticalPadding;
|
||||||
|
|
||||||
private SharedPreferences preferences = null;
|
|
||||||
private final RelativeLayout itemRoot;
|
private final RelativeLayout itemRoot;
|
||||||
public final CircleImageView itemThumbnailView;
|
public final CircleImageView itemThumbnailView;
|
||||||
private final TextView itemContentView;
|
private final TextView itemContentView;
|
||||||
private final TextView itemLikesCountView;
|
private final TextView itemLikesCountView;
|
||||||
private final TextView itemDislikesCountView;
|
|
||||||
private final TextView itemPublishedTime;
|
private final TextView itemPublishedTime;
|
||||||
|
|
||||||
private String commentText;
|
private String commentText;
|
||||||
|
@ -53,20 +51,21 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() {
|
private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() {
|
||||||
@Override
|
@Override
|
||||||
public String transformUrl(final Matcher match, final String url) {
|
public String transformUrl(final Matcher match, final String url) {
|
||||||
int timestamp = 0;
|
try {
|
||||||
final String hours = match.group(1);
|
final TimestampExtractor.TimestampMatchDTO timestampMatchDTO =
|
||||||
final String minutes = match.group(2);
|
TimestampExtractor.getTimestampFromMatcher(match, commentText);
|
||||||
final String seconds = match.group(3);
|
|
||||||
if (hours != null) {
|
if (timestampMatchDTO == null) {
|
||||||
timestamp += (Integer.parseInt(hours.replace(":", "")) * 3600);
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return streamUrl + url.replace(
|
||||||
|
match.group(0),
|
||||||
|
"#timestamp=" + timestampMatchDTO.seconds());
|
||||||
|
} catch (final Exception ex) {
|
||||||
|
Log.e(TAG, "Unable to process url='" + url + "' as timestampLink", ex);
|
||||||
|
return url;
|
||||||
}
|
}
|
||||||
if (minutes != null) {
|
|
||||||
timestamp += (Integer.parseInt(minutes.replace(":", "")) * 60);
|
|
||||||
}
|
|
||||||
if (seconds != null) {
|
|
||||||
timestamp += (Integer.parseInt(seconds));
|
|
||||||
}
|
|
||||||
return streamUrl + url.replace(match.group(0), "#timestamp=" + timestamp);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -77,13 +76,9 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
itemRoot = itemView.findViewById(R.id.itemRoot);
|
itemRoot = itemView.findViewById(R.id.itemRoot);
|
||||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||||
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
||||||
itemDislikesCountView = itemView.findViewById(R.id.detail_thumbs_down_count_view);
|
|
||||||
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
|
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
|
||||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
||||||
|
|
||||||
downloadThumbnailKey = infoItemBuilder.getContext().
|
|
||||||
getString(R.string.download_thumbnail_key);
|
|
||||||
|
|
||||||
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
||||||
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
||||||
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
||||||
|
@ -103,14 +98,8 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
}
|
}
|
||||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||||
|
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext());
|
PicassoHelper.loadAvatar(item.getUploaderAvatarUrl()).into(itemThumbnailView);
|
||||||
|
if (PicassoHelper.getShouldLoadImages()) {
|
||||||
itemBuilder.getImageLoader()
|
|
||||||
.displayImage(item.getUploaderAvatarUrl(),
|
|
||||||
itemThumbnailView,
|
|
||||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
|
||||||
|
|
||||||
if (preferences.getBoolean(downloadThumbnailKey, true)) {
|
|
||||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
itemThumbnailView.setVisibility(View.VISIBLE);
|
||||||
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
||||||
commentVerticalPadding, commentVerticalPadding);
|
commentVerticalPadding, commentVerticalPadding);
|
||||||
|
@ -254,7 +243,14 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void linkify() {
|
private void linkify() {
|
||||||
Linkify.addLinks(itemContentView, Linkify.WEB_URLS);
|
Linkify.addLinks(
|
||||||
Linkify.addLinks(itemContentView, PATTERN, null, null, timestampLink);
|
itemContentView,
|
||||||
|
Linkify.WEB_URLS);
|
||||||
|
Linkify.addLinks(
|
||||||
|
itemContentView,
|
||||||
|
TimestampExtractor.TIMESTAMPS_PATTERN,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
timestampLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||||
|
@ -46,9 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||||
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
||||||
itemUploaderView.setText(item.getUploaderName());
|
itemUploaderView.setText(item.getUploaderName());
|
||||||
|
|
||||||
itemBuilder.getImageLoader()
|
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||||
.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
|
||||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
|
@ -83,10 +83,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
itemBuilder.getImageLoader()
|
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||||
.displayImage(item.getThumbnailUrl(),
|
|
||||||
itemThumbnailView,
|
|
||||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
package org.schabi.newpipe.local;
|
package org.schabi.newpipe.local;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
@ -31,7 +27,6 @@ import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
public class LocalItemBuilder {
|
public class LocalItemBuilder {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
|
||||||
|
|
||||||
private OnClickGesture<LocalItem> onSelectedListener;
|
private OnClickGesture<LocalItem> onSelectedListener;
|
||||||
|
|
||||||
|
@ -43,11 +38,6 @@ public class LocalItemBuilder {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void displayImage(final String url, final ImageView view,
|
|
||||||
final DisplayImageOptions options) {
|
|
||||||
imageLoader.displayImage(url, view, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public OnClickGesture<LocalItem> getOnItemSelectedListener() {
|
public OnClickGesture<LocalItem> getOnItemSelectedListener() {
|
||||||
return onSelectedListener;
|
return onSelectedListener;
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,7 +206,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod)
|
putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setPositiveButton(resources.getString(R.string.finish), null)
|
.setPositiveButton(resources.getString(R.string.ok), null)
|
||||||
.create()
|
.create()
|
||||||
.show()
|
.show()
|
||||||
return true
|
return true
|
||||||
|
@ -362,6 +362,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
StreamDialogEntry.mark_as_watched
|
StreamDialogEntry.mark_as_watched
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
entries.add(StreamDialogEntry.show_channel_details)
|
||||||
|
|
||||||
StreamDialogEntry.setEnabledEntries(entries)
|
StreamDialogEntry.setEnabledEntries(entries)
|
||||||
InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which ->
|
InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which ->
|
||||||
|
|
|
@ -5,7 +5,6 @@ import android.text.TextUtils
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader
|
|
||||||
import com.xwray.groupie.viewbinding.BindableItem
|
import com.xwray.groupie.viewbinding.BindableItem
|
||||||
import org.schabi.newpipe.MainActivity
|
import org.schabi.newpipe.MainActivity
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
|
@ -16,8 +15,8 @@ import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
data class StreamItem(
|
data class StreamItem(
|
||||||
|
@ -93,10 +92,7 @@ data class StreamItem(
|
||||||
viewBinding.itemProgressView.visibility = View.GONE
|
viewBinding.itemProgressView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageLoader.getInstance().displayImage(
|
PicassoHelper.loadThumbnail(stream.thumbnailUrl).into(viewBinding.itemThumbnailView)
|
||||||
stream.thumbnailUrl, viewBinding.itemThumbnailView,
|
|
||||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
if (itemVersion != ItemVersion.MINI) {
|
if (itemVersion != ItemVersion.MINI) {
|
||||||
viewBinding.itemAdditionalDetails.text =
|
viewBinding.itemAdditionalDetails.text =
|
||||||
|
|
|
@ -300,6 +300,12 @@ class FeedLoadService : Service() {
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { _, throwable ->
|
.subscribe { _, throwable ->
|
||||||
|
// There seems to be a bug in the kotlin plugin as it tells you when
|
||||||
|
// building that this can't be null:
|
||||||
|
// "Condition 'throwable != null' is always 'true'"
|
||||||
|
// However it can indeed be null
|
||||||
|
// The suppression may be removed in further versions
|
||||||
|
@Suppress("SENSELESS_COMPARISON")
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
Log.e(TAG, "Error while storing result", throwable)
|
Log.e(TAG, "Error while storing result", throwable)
|
||||||
handleError(throwable)
|
handleError(throwable)
|
||||||
|
|
|
@ -53,8 +53,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
|
||||||
|
|
||||||
public class StatisticsPlaylistFragment
|
public class StatisticsPlaylistFragment
|
||||||
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
|
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
|
||||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
@ -363,10 +361,7 @@ public class StatisticsPlaylistFragment
|
||||||
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||||
entries.add(StreamDialogEntry.play_with_kodi);
|
entries.add(StreamDialogEntry.play_with_kodi);
|
||||||
}
|
}
|
||||||
|
entries.add(StreamDialogEntry.show_channel_details);
|
||||||
if (!isNullOrEmpty(infoItem.getUploaderUrl())) {
|
|
||||||
entries.add(StreamDialogEntry.show_channel_details);
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamDialogEntry.setEnabledEntries(entries);
|
StreamDialogEntry.setEnabledEntries(entries);
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
@ -36,8 +36,7 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
||||||
itemStreamCountView.getContext(), item.streamCount));
|
itemStreamCountView.getContext(), item.streamCount));
|
||||||
itemUploaderView.setVisibility(View.INVISIBLE);
|
itemUploaderView.setVisibility(View.INVISIBLE);
|
||||||
|
|
||||||
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
|
PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView);
|
||||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
|
||||||
|
|
||||||
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
|
@ -81,8 +81,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView,
|
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
|
||||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
.into(itemThumbnailView);
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
|
@ -114,8 +114,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView,
|
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
|
||||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
.into(itemThumbnailView);
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
@ -44,9 +44,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||||
itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId()));
|
itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||||
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
|
||||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
|
||||||
|
|
||||||
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
|
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
|
||||||
|
|
||||||
|
@ -778,10 +777,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||||
entries.add(StreamDialogEntry.play_with_kodi);
|
entries.add(StreamDialogEntry.play_with_kodi);
|
||||||
}
|
}
|
||||||
|
entries.add(StreamDialogEntry.show_channel_details);
|
||||||
if (!isNullOrEmpty(infoItem.getUploaderUrl())) {
|
|
||||||
entries.add(StreamDialogEntry.show_channel_details);
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamDialogEntry.setEnabledEntries(entries);
|
StreamDialogEntry.setEnabledEntries(entries);
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ public class ImportConfirmationDialog extends DialogFragment {
|
||||||
.setMessage(R.string.import_network_expensive_warning)
|
.setMessage(R.string.import_network_expensive_warning)
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.finish, (dialogInterface, i) -> {
|
.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
|
||||||
if (resultServiceIntent != null && getContext() != null) {
|
if (resultServiceIntent != null && getContext() != null) {
|
||||||
getContext().startService(resultServiceIntent);
|
getContext().startService(resultServiceIntent);
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,7 +179,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onImportPreviousSelected() {
|
private fun onImportPreviousSelected() {
|
||||||
requestImportLauncher.launch(StoredFileHelper.getPicker(activity))
|
requestImportLauncher.launch(StoredFileHelper.getPicker(activity, JSON_MIME_TYPE))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onExportSelected() {
|
private fun onExportSelected() {
|
||||||
|
@ -187,7 +187,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||||
val exportName = "newpipe_subscriptions_$date.json"
|
val exportName = "newpipe_subscriptions_$date.json"
|
||||||
|
|
||||||
requestExportLauncher.launch(
|
requestExportLauncher.launch(
|
||||||
StoredFileHelper.getNewPicker(activity, exportName, "application/json", null)
|
StoredFileHelper.getNewPicker(activity, exportName, JSON_MIME_TYPE, null)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,7 +195,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||||
FeedGroupReorderDialog().show(parentFragmentManager, null)
|
FeedGroupReorderDialog().show(parentFragmentManager, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestExportResult(result: ActivityResult) {
|
private fun requestExportResult(result: ActivityResult) {
|
||||||
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
|
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
|
||||||
activity.startService(
|
activity.startService(
|
||||||
Intent(activity, SubscriptionsExportService::class.java)
|
Intent(activity, SubscriptionsExportService::class.java)
|
||||||
|
@ -204,7 +204,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestImportResult(result: ActivityResult) {
|
private fun requestImportResult(result: ActivityResult) {
|
||||||
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
|
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
|
||||||
ImportConfirmationDialog.show(
|
ImportConfirmationDialog.show(
|
||||||
this,
|
this,
|
||||||
|
@ -407,4 +407,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||||
super.hideLoading()
|
super.hideLoading()
|
||||||
binding.itemsList.animate(true, 200)
|
binding.itemsList.animate(true, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val JSON_MIME_TYPE = "application/json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,7 +177,8 @@ public class SubscriptionsImportFragment extends BaseFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onImportFile() {
|
public void onImportFile() {
|
||||||
requestImportFileLauncher.launch(StoredFileHelper.getPicker(activity));
|
// leave */* mime type to support all services with different mime types and file extensions
|
||||||
|
requestImportFileLauncher.launch(StoredFileHelper.getPicker(activity, "*/*"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestImportFileResult(final ActivityResult result) {
|
private void requestImportFileResult(final ActivityResult result) {
|
||||||
|
|
|
@ -143,21 +143,15 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||||
).get(FeedGroupDialogViewModel::class.java)
|
).get(FeedGroupDialogViewModel::class.java)
|
||||||
|
|
||||||
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
|
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
|
||||||
viewModel.subscriptionsLiveData.observe(
|
viewModel.subscriptionsLiveData.observe(viewLifecycleOwner) {
|
||||||
viewLifecycleOwner,
|
setupSubscriptionPicker(it.first, it.second)
|
||||||
Observer {
|
}
|
||||||
setupSubscriptionPicker(it.first, it.second)
|
viewModel.dialogEventLiveData.observe(viewLifecycleOwner) {
|
||||||
|
when (it) {
|
||||||
|
ProcessingEvent -> disableInput()
|
||||||
|
SuccessEvent -> dismiss()
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
viewModel.dialogEventLiveData.observe(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
Observer {
|
|
||||||
when (it) {
|
|
||||||
ProcessingEvent -> disableInput()
|
|
||||||
SuccessEvent -> dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
subscriptionGroupAdapter = GroupAdapter<GroupieViewHolder>().apply {
|
subscriptionGroupAdapter = GroupAdapter<GroupieViewHolder>().apply {
|
||||||
add(subscriptionMainSection)
|
add(subscriptionMainSection)
|
||||||
|
@ -437,7 +431,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||||
feedGroupCreateBinding.confirmButton.setText(
|
feedGroupCreateBinding.confirmButton.setText(
|
||||||
when {
|
when {
|
||||||
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
|
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
|
||||||
else -> android.R.string.ok
|
else -> R.string.ok
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,13 @@ package org.schabi.newpipe.local.subscription.item
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader
|
|
||||||
import com.xwray.groupie.GroupieViewHolder
|
import com.xwray.groupie.GroupieViewHolder
|
||||||
import com.xwray.groupie.Item
|
import com.xwray.groupie.Item
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.OnClickGesture
|
import org.schabi.newpipe.util.OnClickGesture
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper
|
||||||
|
|
||||||
class ChannelItem(
|
class ChannelItem(
|
||||||
private val infoItem: ChannelInfoItem,
|
private val infoItem: ChannelInfoItem,
|
||||||
|
@ -40,10 +39,7 @@ class ChannelItem(
|
||||||
itemChannelDescriptionView.text = infoItem.description
|
itemChannelDescriptionView.text = infoItem.description
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageLoader.getInstance().displayImage(
|
PicassoHelper.loadThumbnail(infoItem.thumbnailUrl).into(itemThumbnailView)
|
||||||
infoItem.thumbnailUrl, itemThumbnailView,
|
|
||||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
gesturesListener?.run {
|
gesturesListener?.run {
|
||||||
viewHolder.root.setOnClickListener { selected(infoItem) }
|
viewHolder.root.setOnClickListener { selected(infoItem) }
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
package org.schabi.newpipe.local.subscription.item
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import android.view.View.OnClickListener
|
|
||||||
import com.xwray.groupie.viewbinding.BindableItem
|
|
||||||
import org.schabi.newpipe.R
|
|
||||||
import org.schabi.newpipe.databinding.HeaderItemBinding
|
|
||||||
|
|
||||||
class HeaderItem(
|
|
||||||
val title: String,
|
|
||||||
private val onClickListener: (() -> Unit)? = null
|
|
||||||
) : BindableItem<HeaderItemBinding>() {
|
|
||||||
override fun getLayout(): Int = R.layout.header_item
|
|
||||||
|
|
||||||
override fun bind(viewBinding: HeaderItemBinding, position: Int) {
|
|
||||||
viewBinding.headerTitle.text = title
|
|
||||||
|
|
||||||
val listener = onClickListener?.let { OnClickListener { onClickListener.invoke() } }
|
|
||||||
viewBinding.root.setOnClickListener(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun initializeViewBinding(view: View) = HeaderItemBinding.bind(view)
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ package org.schabi.newpipe.local.subscription.item
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader
|
|
||||||
import com.xwray.groupie.viewbinding.BindableItem
|
import com.xwray.groupie.viewbinding.BindableItem
|
||||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
|
@ -11,7 +10,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
|
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
|
||||||
import org.schabi.newpipe.ktx.AnimationType
|
import org.schabi.newpipe.ktx.AnimationType
|
||||||
import org.schabi.newpipe.ktx.animate
|
import org.schabi.newpipe.ktx.animate
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
import org.schabi.newpipe.util.PicassoHelper
|
||||||
|
|
||||||
data class PickerSubscriptionItem(
|
data class PickerSubscriptionItem(
|
||||||
val subscriptionEntity: SubscriptionEntity,
|
val subscriptionEntity: SubscriptionEntity,
|
||||||
|
@ -22,11 +21,7 @@ data class PickerSubscriptionItem(
|
||||||
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
|
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
|
||||||
|
|
||||||
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
|
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
|
||||||
ImageLoader.getInstance().displayImage(
|
PicassoHelper.loadAvatar(subscriptionEntity.avatarUrl).into(viewBinding.thumbnailView)
|
||||||
subscriptionEntity.avatarUrl,
|
|
||||||
viewBinding.thumbnailView, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
viewBinding.titleView.text = subscriptionEntity.name
|
viewBinding.titleView.text = subscriptionEntity.name
|
||||||
viewBinding.selectedHighlight.isVisible = isSelected
|
viewBinding.selectedHighlight.isVisible = isSelected
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,9 @@
|
||||||
|
|
||||||
package org.schabi.newpipe.local.subscription.services;
|
package org.schabi.newpipe.local.subscription.services;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
@ -46,6 +49,7 @@ import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
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.Flowable;
|
||||||
|
@ -54,9 +58,6 @@ import io.reactivex.rxjava3.functions.Consumer;
|
||||||
import io.reactivex.rxjava3.functions.Function;
|
import io.reactivex.rxjava3.functions.Function;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
||||||
import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME;
|
|
||||||
|
|
||||||
public class SubscriptionsImportService extends BaseImportExportService {
|
public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
public static final int CHANNEL_URL_MODE = 0;
|
public static final int CHANNEL_URL_MODE = 0;
|
||||||
public static final int INPUT_STREAM_MODE = 1;
|
public static final int INPUT_STREAM_MODE = 1;
|
||||||
|
@ -89,6 +90,8 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
private String channelUrl;
|
private String channelUrl;
|
||||||
@Nullable
|
@Nullable
|
||||||
private InputStream inputStream;
|
private InputStream inputStream;
|
||||||
|
@Nullable
|
||||||
|
private String inputStreamType;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||||
|
@ -111,8 +114,20 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
inputStream = new SharpInputStream(
|
final StoredFileHelper fileHelper = new StoredFileHelper(this, uri, DEFAULT_MIME);
|
||||||
new StoredFileHelper(this, uri, DEFAULT_MIME).getStream());
|
inputStream = new SharpInputStream(fileHelper.getStream());
|
||||||
|
inputStreamType = fileHelper.getType();
|
||||||
|
|
||||||
|
if (inputStreamType == null || inputStreamType.equals(DEFAULT_MIME)) {
|
||||||
|
// mime type could not be determined, just take file extension
|
||||||
|
final String name = fileHelper.getName();
|
||||||
|
final int pointIndex = name.lastIndexOf('.');
|
||||||
|
if (pointIndex == -1 || pointIndex >= name.length() - 1) {
|
||||||
|
inputStreamType = DEFAULT_MIME; // no extension, will fail in the extractor
|
||||||
|
} else {
|
||||||
|
inputStreamType = name.substring(pointIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
handleError(e);
|
handleError(e);
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
|
@ -248,9 +263,9 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
final Throwable error = notification.getError();
|
final Throwable error = notification.getError();
|
||||||
final Throwable cause = error.getCause();
|
final Throwable cause = error.getCause();
|
||||||
if (error instanceof IOException) {
|
if (error instanceof IOException) {
|
||||||
throw (IOException) error;
|
throw error;
|
||||||
} else if (cause instanceof IOException) {
|
} else if (cause instanceof IOException) {
|
||||||
throw (IOException) cause;
|
throw cause;
|
||||||
} else if (ExceptionUtils.isNetworkRelated(error)) {
|
} else if (ExceptionUtils.isNetworkRelated(error)) {
|
||||||
throw new IOException(error);
|
throw new IOException(error);
|
||||||
}
|
}
|
||||||
|
@ -280,9 +295,12 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Flowable<List<SubscriptionItem>> importFromInputStream() {
|
private Flowable<List<SubscriptionItem>> importFromInputStream() {
|
||||||
|
Objects.requireNonNull(inputStream);
|
||||||
|
Objects.requireNonNull(inputStreamType);
|
||||||
|
|
||||||
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
|
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
|
||||||
.getSubscriptionExtractor()
|
.getSubscriptionExtractor()
|
||||||
.fromInputStream(inputStream));
|
.fromInputStream(inputStream, inputStreamType));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Flowable<List<SubscriptionItem>> importFromPreviousExport() {
|
private Flowable<List<SubscriptionItem>> importFromPreviousExport() {
|
||||||
|
|
|
@ -24,7 +24,6 @@ import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -36,6 +35,7 @@ import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import org.schabi.newpipe.App;
|
import org.schabi.newpipe.App;
|
||||||
import org.schabi.newpipe.databinding.PlayerBinding;
|
import org.schabi.newpipe.databinding.PlayerBinding;
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
@ -133,32 +133,29 @@ public final class MainPlayer extends Service {
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop(final boolean autoplayEnabled) {
|
public void stopForImmediateReusing() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "stop() called");
|
Log.d(TAG, "stopForImmediateReusing() called");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!player.exoPlayerIsNull()) {
|
if (!player.exoPlayerIsNull()) {
|
||||||
player.saveWasPlaying();
|
player.saveWasPlaying();
|
||||||
|
|
||||||
// Releases wifi & cpu, disables keepScreenOn, etc.
|
// Releases wifi & cpu, disables keepScreenOn, etc.
|
||||||
if (!autoplayEnabled) {
|
|
||||||
player.pause();
|
|
||||||
}
|
|
||||||
// We can't just pause the player here because it will make transition
|
// We can't just pause the player here because it will make transition
|
||||||
// from one stream to a new stream not smooth
|
// from one stream to a new stream not smooth
|
||||||
player.smoothStopPlayer();
|
player.smoothStopPlayer();
|
||||||
player.setRecovery();
|
player.setRecovery();
|
||||||
|
|
||||||
// Android TV will handle back button in case controls will be visible
|
// Android TV will handle back button in case controls will be visible
|
||||||
// (one more additional unneeded click while the player is hidden)
|
// (one more additional unneeded click while the player is hidden)
|
||||||
player.hideControls(0, 0);
|
player.hideControls(0, 0);
|
||||||
player.closeItemsList();
|
player.closeItemsList();
|
||||||
|
|
||||||
// Notification shows information about old stream but if a user selects
|
// Notification shows information about old stream but if a user selects
|
||||||
// a stream from backStack it's not actual anymore
|
// a stream from backStack it's not actual anymore
|
||||||
// So we should hide the notification at all.
|
// So we should hide the notification at all.
|
||||||
// When autoplay enabled such notification flashing is annoying so skip this case
|
// When autoplay enabled such notification flashing is annoying so skip this case
|
||||||
if (!autoplayEnabled) {
|
|
||||||
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,11 +219,8 @@ public final class MainPlayer extends Service {
|
||||||
boolean isLandscape() {
|
boolean isLandscape() {
|
||||||
// DisplayMetrics from activity context knows about MultiWindow feature
|
// DisplayMetrics from activity context knows about MultiWindow feature
|
||||||
// while DisplayMetrics from app context doesn't
|
// while DisplayMetrics from app context doesn't
|
||||||
final DisplayMetrics metrics = (player != null
|
return DeviceUtils.isLandscape(player != null && player.getParentActivity() != null
|
||||||
&& player.getParentActivity() != null
|
? player.getParentActivity() : this);
|
||||||
? player.getParentActivity().getResources()
|
|
||||||
: getResources()).getDisplayMetrics();
|
|
||||||
return metrics.heightPixels < metrics.widthPixels;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
package org.schabi.newpipe.player;
|
package org.schabi.newpipe.player;
|
||||||
|
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|
||||||
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
|
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
@ -17,7 +13,6 @@ import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.PopupMenu;
|
|
||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
@ -49,9 +44,12 @@ import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
|
||||||
|
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||||
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.SponsorBlockUtils.markSegments;
|
import static org.schabi.newpipe.util.SponsorBlockUtils.markSegments;
|
||||||
|
|
||||||
public final class PlayQueueActivity extends AppCompatActivity
|
public final class PlayQueueActivity extends AppCompatActivity
|
||||||
|
@ -60,7 +58,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
|
|
||||||
private static final String TAG = PlayQueueActivity.class.getSimpleName();
|
private static final String TAG = PlayQueueActivity.class.getSimpleName();
|
||||||
|
|
||||||
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
|
|
||||||
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
||||||
|
|
||||||
protected Player player;
|
protected Player player;
|
||||||
|
@ -289,49 +286,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
queueControlBinding.controlShuffle.setOnClickListener(this);
|
queueControlBinding.controlShuffle.setOnClickListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
|
|
||||||
final PopupMenu popupMenu = new PopupMenu(this, view);
|
|
||||||
final MenuItem remove = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0,
|
|
||||||
Menu.NONE, R.string.play_queue_remove);
|
|
||||||
remove.setOnMenuItemClickListener(menuItem -> {
|
|
||||||
if (player == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int index = player.getPlayQueue().indexOf(item);
|
|
||||||
if (index != -1) {
|
|
||||||
player.getPlayQueue().remove(index);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
final MenuItem detail = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1,
|
|
||||||
Menu.NONE, R.string.play_queue_stream_detail);
|
|
||||||
detail.setOnMenuItemClickListener(menuItem -> {
|
|
||||||
// playQueue is null since we don't want any queue change
|
|
||||||
NavigationHelper.openVideoDetail(this, item.getServiceId(), item.getUrl(),
|
|
||||||
item.getTitle(), null, false);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
final MenuItem append = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 2,
|
|
||||||
Menu.NONE, R.string.append_playlist);
|
|
||||||
append.setOnMenuItemClickListener(menuItem -> {
|
|
||||||
openPlaylistAppendDialog(Collections.singletonList(item));
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
final MenuItem share = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 3,
|
|
||||||
Menu.NONE, R.string.share);
|
|
||||||
share.setOnMenuItemClickListener(menuItem -> {
|
|
||||||
shareText(getApplicationContext(), item.getTitle(), item.getUrl(),
|
|
||||||
item.getThumbnailUrl());
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
popupMenu.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Component Helpers
|
// Component Helpers
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -379,13 +333,9 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void held(final PlayQueueItem item, final View view) {
|
public void held(final PlayQueueItem item, final View view) {
|
||||||
if (player == null) {
|
if (player != null && player.getPlayQueue().indexOf(item) != -1) {
|
||||||
return;
|
openPopupMenu(player.getPlayQueue(), item, view, false,
|
||||||
}
|
getSupportFragmentManager(), PlayQueueActivity.this);
|
||||||
|
|
||||||
final int index = player.getPlayQueue().indexOf(item);
|
|
||||||
if (index != -1) {
|
|
||||||
buildItemPopupMenu(item, view);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import android.graphics.BitmapFactory;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.PorterDuff;
|
import android.graphics.PorterDuff;
|
||||||
import android.graphics.PorterDuffColorFilter;
|
import android.graphics.PorterDuffColorFilter;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
@ -83,9 +84,8 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.android.exoplayer2.video.VideoListener;
|
import com.google.android.exoplayer2.video.VideoListener;
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.squareup.picasso.Picasso;
|
||||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
import com.squareup.picasso.Target;
|
||||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.DownloaderImpl;
|
import org.schabi.newpipe.DownloaderImpl;
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
|
@ -129,11 +129,11 @@ import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
||||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
|
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.SerializedCache;
|
import org.schabi.newpipe.util.SerializedCache;
|
||||||
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.SponsorBlockMode;
|
import org.schabi.newpipe.util.SponsorBlockMode;
|
||||||
import org.schabi.newpipe.util.VideoSegment;
|
import org.schabi.newpipe.util.VideoSegment;
|
||||||
|
@ -164,7 +164,9 @@ import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||||
import static com.google.android.exoplayer2.Player.RepeatMode;
|
import static com.google.android.exoplayer2.Player.RepeatMode;
|
||||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
|
||||||
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
|
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
|
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
|
||||||
|
@ -202,7 +204,6 @@ import static org.schabi.newpipe.util.SponsorBlockUtils.markSegments;
|
||||||
public final class Player implements
|
public final class Player implements
|
||||||
EventListener,
|
EventListener,
|
||||||
PlaybackListener,
|
PlaybackListener,
|
||||||
ImageLoadingListener,
|
|
||||||
VideoListener,
|
VideoListener,
|
||||||
SeekBar.OnSeekBarChangeListener,
|
SeekBar.OnSeekBarChangeListener,
|
||||||
View.OnClickListener,
|
View.OnClickListener,
|
||||||
|
@ -244,7 +245,7 @@ public final class Player implements
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
|
public static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
|
||||||
public static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500; // 500 millis
|
public static final int PROGRESS_LOOP_INTERVAL_MILLIS = 1000; // 1 second
|
||||||
public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis
|
public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis
|
||||||
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
|
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
|
||||||
public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds
|
public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds
|
||||||
|
@ -403,7 +404,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Constructor
|
// Constructor
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Constructor
|
||||||
|
|
||||||
public Player(@NonNull final MainPlayer service) {
|
public Player(@NonNull final MainPlayer service) {
|
||||||
this.service = service;
|
this.service = service;
|
||||||
|
@ -450,7 +451,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Setup and initialization
|
// Setup and initialization
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Setup and initialization
|
||||||
|
|
||||||
public void setupFromView(@NonNull final PlayerBinding playerBinding) {
|
public void setupFromView(@NonNull final PlayerBinding playerBinding) {
|
||||||
initViews(playerBinding);
|
initViews(playerBinding);
|
||||||
|
@ -601,7 +602,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Playback initialization via intent
|
// Playback initialization via intent
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Playback initialization via intent
|
||||||
|
|
||||||
public void handleIntent(@NonNull final Intent intent) {
|
public void handleIntent(@NonNull final Intent intent) {
|
||||||
// fail fast if no play queue was provided
|
// fail fast if no play queue was provided
|
||||||
|
@ -629,13 +630,16 @@ public final class Player implements
|
||||||
playQueue.append(newQueue.getStreams());
|
playQueue.append(newQueue.getStreams());
|
||||||
|
|
||||||
if ((intent.getBooleanExtra(SELECT_ON_APPEND, false)
|
if ((intent.getBooleanExtra(SELECT_ON_APPEND, false)
|
||||||
|| currentState == STATE_COMPLETED) && newQueue.getStreams().size() > 0) {
|
|| currentState == STATE_COMPLETED) && !newQueue.getStreams().isEmpty()) {
|
||||||
playQueue.setIndex(sizeBeforeAppend);
|
playQueue.setIndex(sizeBeforeAppend);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// needed for tablets, check the function for a better explanation
|
||||||
|
directlyOpenFullscreenIfNeeded();
|
||||||
|
|
||||||
final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this);
|
final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this);
|
||||||
final float playbackSpeed = savedParameters.speed;
|
final float playbackSpeed = savedParameters.speed;
|
||||||
final float playbackPitch = savedParameters.pitch;
|
final float playbackPitch = savedParameters.pitch;
|
||||||
|
@ -687,6 +691,7 @@ public final class Player implements
|
||||||
&& isPlaybackResumeEnabled(this)
|
&& isPlaybackResumeEnabled(this)
|
||||||
&& !samePlayQueue
|
&& !samePlayQueue
|
||||||
&& !newQueue.isEmpty()
|
&& !newQueue.isEmpty()
|
||||||
|
&& newQueue.getItem() != null
|
||||||
&& newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
|
&& newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
|
||||||
databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem())
|
databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -760,6 +765,22 @@ public final class Player implements
|
||||||
NavigationHelper.sendPlayerStartedEvent(context);
|
NavigationHelper.sendPlayerStartedEvent(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open fullscreen on tablets where the option to have the main player start automatically in
|
||||||
|
* fullscreen mode is on. Rotating the device to landscape is already done in {@link
|
||||||
|
* VideoDetailFragment#openVideoPlayer(boolean)} when the thumbnail is clicked, and that's
|
||||||
|
* enough for phones, but not for tablets since the mini player can be also shown in landscape.
|
||||||
|
*/
|
||||||
|
private void directlyOpenFullscreenIfNeeded() {
|
||||||
|
if (fragmentListener != null
|
||||||
|
&& PlayerHelper.isStartMainPlayerFullscreenEnabled(service)
|
||||||
|
&& DeviceUtils.isTablet(service)
|
||||||
|
&& videoPlayerSelected()
|
||||||
|
&& PlayerHelper.globalScreenOrientationLocked(service)) {
|
||||||
|
fragmentListener.onScreenRotationButtonClicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void initPlayback(@NonNull final PlayQueue queue,
|
private void initPlayback(@NonNull final PlayQueue queue,
|
||||||
@RepeatMode final int repeatMode,
|
@RepeatMode final int repeatMode,
|
||||||
final float playbackSpeed,
|
final float playbackSpeed,
|
||||||
|
@ -792,7 +813,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Destroy and recovery
|
// Destroy and recovery
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Destroy and recovery
|
||||||
|
|
||||||
private void destroyPlayer() {
|
private void destroyPlayer() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
@ -838,7 +859,7 @@ public final class Player implements
|
||||||
|
|
||||||
databaseUpdateDisposable.clear();
|
databaseUpdateDisposable.clear();
|
||||||
progressUpdateDisposable.set(null);
|
progressUpdateDisposable.set(null);
|
||||||
ImageLoader.getInstance().stop();
|
PicassoHelper.cancelTag(PicassoHelper.PLAYER_THUMBNAIL_TAG); // cancel thumbnail loading
|
||||||
|
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
binding.endScreen.setImageBitmap(null);
|
binding.endScreen.setImageBitmap(null);
|
||||||
|
@ -901,7 +922,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Player type specific setup
|
// Player type specific setup
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Player type specific setup
|
||||||
|
|
||||||
private void initVideoPlayer() {
|
private void initVideoPlayer() {
|
||||||
// restore last resize mode
|
// restore last resize mode
|
||||||
|
@ -963,7 +984,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Elements visibility and size: popup and main players have different look
|
// Elements visibility and size: popup and main players have different look
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Elements visibility and size: popup and main players have different look
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method ensures that popup and main players have different look.
|
* This method ensures that popup and main players have different look.
|
||||||
|
@ -1083,7 +1104,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Broadcast receiver
|
// Broadcast receiver
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Broadcast receiver
|
||||||
|
|
||||||
private void setupBroadcastReceiver() {
|
private void setupBroadcastReceiver() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
@ -1235,18 +1256,49 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Thumbnail loading
|
// Thumbnail loading
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Thumbnail loading
|
||||||
|
|
||||||
private void initThumbnail(final String url) {
|
private void initThumbnail(final String url) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Thumbnail - initThumbnail() called");
|
Log.d(TAG, "Thumbnail - initThumbnail() called with url = ["
|
||||||
|
+ (url == null ? "null" : url) + "]");
|
||||||
}
|
}
|
||||||
if (url == null || url.isEmpty()) {
|
if (isNullOrEmpty(url)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ImageLoader.getInstance().resume();
|
|
||||||
ImageLoader.getInstance()
|
// scale down the notification thumbnail for performance
|
||||||
.loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, this);
|
PicassoHelper.loadScaledDownThumbnail(context, url).into(new Target() {
|
||||||
|
@Override
|
||||||
|
public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Thumbnail - onLoadingComplete() called with: url = [" + url
|
||||||
|
+ "], " + "loadedImage = [" + bitmap + " -> " + bitmap.getWidth() + "x"
|
||||||
|
+ bitmap.getHeight() + "], from = [" + from + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
currentThumbnail = bitmap;
|
||||||
|
NotificationUtil.getInstance()
|
||||||
|
.createNotificationIfNeededAndUpdate(Player.this, false);
|
||||||
|
// there is a new thumbnail, so changed the end screen thumbnail, too.
|
||||||
|
updateEndScreenThumbnail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBitmapFailed(final Exception e, final Drawable errorDrawable) {
|
||||||
|
Log.e(TAG, "Thumbnail - onBitmapFailed() called with: url = [" + url + "]", e);
|
||||||
|
currentThumbnail = null;
|
||||||
|
NotificationUtil.getInstance()
|
||||||
|
.createNotificationIfNeededAndUpdate(Player.this, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrepareLoad(final Drawable placeHolderDrawable) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Thumbnail - onLoadingStarted() called with: url = [" + url + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1320,61 +1372,6 @@ public final class Player implements
|
||||||
return Math.min(currentThumbnail.getHeight(), screenHeight);
|
return Math.min(currentThumbnail.getHeight(), screenHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadingStarted(final String imageUri, final View view) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Thumbnail - onLoadingStarted() called on: "
|
|
||||||
+ "imageUri = [" + imageUri + "], view = [" + view + "]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadingFailed(final String imageUri, final View view,
|
|
||||||
final FailReason failReason) {
|
|
||||||
Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]",
|
|
||||||
failReason.getCause());
|
|
||||||
currentThumbnail = null;
|
|
||||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadingComplete(final String imageUri, final View view,
|
|
||||||
final Bitmap loadedImage) {
|
|
||||||
// scale down the notification thumbnail for performance
|
|
||||||
final float notificationThumbnailWidth = Math.min(
|
|
||||||
context.getResources().getDimension(R.dimen.player_notification_thumbnail_width),
|
|
||||||
loadedImage.getWidth());
|
|
||||||
currentThumbnail = Bitmap.createScaledBitmap(
|
|
||||||
loadedImage,
|
|
||||||
(int) notificationThumbnailWidth,
|
|
||||||
(int) (loadedImage.getHeight()
|
|
||||||
/ (loadedImage.getWidth() / notificationThumbnailWidth)),
|
|
||||||
true);
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Thumbnail - onLoadingComplete() called with: "
|
|
||||||
+ "imageUri = [" + imageUri + "], view = [" + view + "], "
|
|
||||||
+ "loadedImage = [" + loadedImage + "], "
|
|
||||||
+ loadedImage.getWidth() + "x" + loadedImage.getHeight()
|
|
||||||
+ ", scaled notification width = " + notificationThumbnailWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
||||||
|
|
||||||
// there is a new thumbnail, thus the end screen thumbnail needs to be changed, too.
|
|
||||||
updateEndScreenThumbnail();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadingCancelled(final String imageUri, final View view) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: "
|
|
||||||
+ "imageUri = [" + imageUri + "], view = [" + view + "]");
|
|
||||||
}
|
|
||||||
currentThumbnail = null;
|
|
||||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
|
||||||
}
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
@ -1382,7 +1379,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Popup player utils
|
// Popup player utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Popup player utils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if {@link #popupLayoutParams}' position is within a arbitrary boundary
|
* Check if {@link #popupLayoutParams}' position is within a arbitrary boundary
|
||||||
|
@ -1557,7 +1554,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Playback parameters
|
// Playback parameters
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Playback parameters
|
||||||
|
|
||||||
public float getPlaybackSpeed() {
|
public float getPlaybackSpeed() {
|
||||||
return getPlaybackParameters().speed;
|
return getPlaybackParameters().speed;
|
||||||
|
@ -1610,7 +1607,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Progress loop and updates
|
// Progress loop and updates
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Progress loop and updates
|
||||||
|
|
||||||
private void onUpdateProgress(final int currentProgress,
|
private void onUpdateProgress(final int currentProgress,
|
||||||
final int duration,
|
final int duration,
|
||||||
|
@ -1620,8 +1617,7 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
if (duration != binding.playbackSeekBar.getMax()) {
|
if (duration != binding.playbackSeekBar.getMax()) {
|
||||||
binding.playbackEndTime.setText(getTimeString(duration));
|
setVideoDurationToControls(duration);
|
||||||
binding.playbackSeekBar.setMax(duration);
|
|
||||||
}
|
}
|
||||||
if (currentState != STATE_PAUSED) {
|
if (currentState != STATE_PAUSED) {
|
||||||
if (currentState != STATE_PAUSED_SEEK) {
|
if (currentState != STATE_PAUSED_SEEK) {
|
||||||
|
@ -1900,7 +1896,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Controls showing / hiding
|
// Controls showing / hiding
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Controls showing / hiding
|
||||||
|
|
||||||
public boolean isControlsVisible() {
|
public boolean isControlsVisible() {
|
||||||
return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE;
|
return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE;
|
||||||
|
@ -2070,7 +2066,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Playback states
|
// Playback states
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Playback states
|
||||||
|
|
||||||
@Override // exoplayer listener
|
@Override // exoplayer listener
|
||||||
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
|
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
|
||||||
|
@ -2195,8 +2191,8 @@ public final class Player implements
|
||||||
Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
|
Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackSeekBar.setMax((int) simpleExoPlayer.getDuration());
|
setVideoDurationToControls((int) simpleExoPlayer.getDuration());
|
||||||
binding.playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration()));
|
|
||||||
binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
|
binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
|
||||||
|
|
||||||
if (playWhenReady) {
|
if (playWhenReady) {
|
||||||
|
@ -2393,7 +2389,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Repeat and shuffle
|
// Repeat and shuffle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Repeat and shuffle
|
||||||
|
|
||||||
public void onRepeatClicked() {
|
public void onRepeatClicked() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
@ -2430,7 +2426,7 @@ public final class Player implements
|
||||||
Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: "
|
Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: "
|
||||||
+ "repeatMode = [" + repeatMode + "]");
|
+ "repeatMode = [" + repeatMode + "]");
|
||||||
}
|
}
|
||||||
setRepeatModeButton(((AppCompatImageButton) binding.repeatButton), repeatMode);
|
setRepeatModeButton(binding.repeatButton, repeatMode);
|
||||||
onShuffleOrRepeatModeChanged();
|
onShuffleOrRepeatModeChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2482,7 +2478,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Mute / Unmute
|
// Mute / Unmute
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Mute / Unmute
|
||||||
|
|
||||||
public void onMuteUnmuteButtonClicked() {
|
public void onMuteUnmuteButtonClicked() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
@ -2508,7 +2504,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// ExoPlayer listeners (that didn't fit in other categories)
|
// ExoPlayer listeners (that didn't fit in other categories)
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region ExoPlayer listeners (that didn't fit in other categories)
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(@NonNull final Timeline timeline, final int reason) {
|
public void onTimelineChanged(@NonNull final Timeline timeline, final int reason) {
|
||||||
|
@ -2596,7 +2592,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Errors
|
// Errors
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Errors
|
||||||
/**
|
/**
|
||||||
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
|
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
|
||||||
* <p>There are multiple types of errors:</p>
|
* <p>There are multiple types of errors:</p>
|
||||||
|
@ -2697,7 +2693,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Playback position and seek
|
// Playback position and seek
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Playback position and seek
|
||||||
|
|
||||||
@Override // own playback listener (this is a getter)
|
@Override // own playback listener (this is a getter)
|
||||||
public boolean isApproachingPlaybackEdge(final long timeToEndMillis) {
|
public boolean isApproachingPlaybackEdge(final long timeToEndMillis) {
|
||||||
|
@ -2840,6 +2836,20 @@ public final class Player implements
|
||||||
simpleExoPlayer.seekToDefaultPosition();
|
simpleExoPlayer.seekToDefaultPosition();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the video duration time into all control components (e.g. seekbar).
|
||||||
|
* @param duration
|
||||||
|
*/
|
||||||
|
private void setVideoDurationToControls(final int duration) {
|
||||||
|
binding.playbackEndTime.setText(getTimeString(duration));
|
||||||
|
|
||||||
|
binding.playbackSeekBar.setMax(duration);
|
||||||
|
// This is important for Android TVs otherwise it would apply the default from
|
||||||
|
// setMax/Min methods which is (max - min) / 20
|
||||||
|
binding.playbackSeekBar.setKeyProgressIncrement(
|
||||||
|
PlayerHelper.retrieveSeekDurationFromPreferences(this));
|
||||||
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
@ -2847,7 +2857,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Player actions (play, pause, previous, fast-forward, ...)
|
// Player actions (play, pause, previous, fast-forward, ...)
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Player actions (play, pause, previous, fast-forward, ...)
|
||||||
|
|
||||||
public void play() {
|
public void play() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
@ -2889,7 +2899,9 @@ public final class Player implements
|
||||||
Log.d(TAG, "onPlayPause() called");
|
Log.d(TAG, "onPlayPause() called");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getPlayWhenReady()) {
|
if (getPlayWhenReady()
|
||||||
|
// When state is completed (replay button is shown) then (re)play and do not pause
|
||||||
|
&& currentState != STATE_COMPLETED) {
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
play();
|
play();
|
||||||
|
@ -2955,7 +2967,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// StreamInfo history: views and progress
|
// StreamInfo history: views and progress
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region StreamInfo history: views and progress
|
||||||
|
|
||||||
private void registerStreamViewed() {
|
private void registerStreamViewed() {
|
||||||
if (currentMetadata != null) {
|
if (currentMetadata != null) {
|
||||||
|
@ -3013,7 +3025,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Metadata
|
// Metadata
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Metadata
|
||||||
|
|
||||||
private void onMetadataChanged(@NonNull final MediaSourceTag tag) {
|
private void onMetadataChanged(@NonNull final MediaSourceTag tag) {
|
||||||
final StreamInfo info = tag.getMetadata();
|
final StreamInfo info = tag.getMetadata();
|
||||||
|
@ -3137,7 +3149,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Play queue, segments and streams
|
// Play queue, segments and streams
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Play queue, segments and streams
|
||||||
|
|
||||||
private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) {
|
private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) {
|
||||||
if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1
|
if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1
|
||||||
|
@ -3292,7 +3304,7 @@ public final class Player implements
|
||||||
private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
|
private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
|
||||||
return (item, seconds) -> {
|
return (item, seconds) -> {
|
||||||
segmentAdapter.selectSegment(item);
|
segmentAdapter.selectSegment(item);
|
||||||
seekTo(seconds * 1000);
|
seekTo(seconds * 1000L);
|
||||||
triggerProgressUpdate();
|
triggerProgressUpdate();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -3302,7 +3314,7 @@ public final class Player implements
|
||||||
final List<StreamSegment> segments = currentMetadata.getMetadata().getStreamSegments();
|
final List<StreamSegment> segments = currentMetadata.getMetadata().getStreamSegments();
|
||||||
|
|
||||||
for (int i = 0; i < segments.size(); i++) {
|
for (int i = 0; i < segments.size(); i++) {
|
||||||
if (segments.get(i).getStartTimeSeconds() * 1000 > playbackPosition) {
|
if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
nearestPosition++;
|
nearestPosition++;
|
||||||
|
@ -3337,9 +3349,9 @@ public final class Player implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void held(final PlayQueueItem item, final View view) {
|
public void held(final PlayQueueItem item, final View view) {
|
||||||
final int index = playQueue.indexOf(item);
|
if (playQueue.indexOf(item) != -1) {
|
||||||
if (index != -1) {
|
openPopupMenu(playQueue, item, view, true,
|
||||||
playQueue.remove(index);
|
getParentActivity().getSupportFragmentManager(), context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3453,7 +3465,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Popup menus ("popup" means that they pop up, not that they belong to the popup player)
|
// Popup menus ("popup" means that they pop up, not that they belong to the popup player)
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Popup menus ("popup" means that they pop up, not that they belong to the popup player)
|
||||||
|
|
||||||
private void buildQualityMenu() {
|
private void buildQualityMenu() {
|
||||||
if (qualityPopupMenu == null) {
|
if (qualityPopupMenu == null) {
|
||||||
|
@ -3656,7 +3668,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Captions (text tracks)
|
// Captions (text tracks)
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Captions (text tracks)
|
||||||
|
|
||||||
private void setupSubtitleView() {
|
private void setupSubtitleView() {
|
||||||
final float captionScale = PlayerHelper.getCaptionScale(context);
|
final float captionScale = PlayerHelper.getCaptionScale(context);
|
||||||
|
@ -3735,7 +3747,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Click listeners
|
// Click listeners
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Click listeners
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(final View v) {
|
public void onClick(final View v) {
|
||||||
|
@ -3952,7 +3964,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Video size, resize, orientation, fullscreen
|
// Video size, resize, orientation, fullscreen
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Video size, resize, orientation, fullscreen
|
||||||
|
|
||||||
private void setupScreenRotationButton() {
|
private void setupScreenRotationButton() {
|
||||||
binding.screenRotationButton.setVisibility(videoPlayerSelected()
|
binding.screenRotationButton.setVisibility(videoPlayerSelected()
|
||||||
|
@ -4007,11 +4019,9 @@ public final class Player implements
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "toggleFullscreen() called");
|
Log.d(TAG, "toggleFullscreen() called");
|
||||||
}
|
}
|
||||||
if (popupPlayerSelected() || exoPlayerIsNull() || currentMetadata == null
|
if (popupPlayerSelected() || exoPlayerIsNull() || fragmentListener == null) {
|
||||||
|| fragmentListener == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//changeState(STATE_BLOCKED); TODO check what this does
|
|
||||||
|
|
||||||
isFullscreen = !isFullscreen;
|
isFullscreen = !isFullscreen;
|
||||||
if (!isFullscreen) {
|
if (!isFullscreen) {
|
||||||
|
@ -4066,7 +4076,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Gestures
|
// Gestures
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Gestures
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:ParameterNumber")
|
@SuppressWarnings("checkstyle:ParameterNumber")
|
||||||
private void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
|
private void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
|
||||||
|
@ -4130,7 +4140,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Activity / fragment binding
|
// Activity / fragment binding
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Activity / fragment binding
|
||||||
|
|
||||||
public void setFragmentListener(final PlayerServiceEventListener listener) {
|
public void setFragmentListener(final PlayerServiceEventListener listener) {
|
||||||
fragmentListener = listener;
|
fragmentListener = listener;
|
||||||
|
@ -4269,7 +4279,7 @@ public final class Player implements
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Getters
|
// Getters
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region Getters
|
||||||
|
|
||||||
public int getCurrentState() {
|
public int getCurrentState() {
|
||||||
return currentState;
|
return currentState;
|
||||||
|
@ -4544,6 +4554,7 @@ public final class Player implements
|
||||||
// SurfaceHolderCallback helpers
|
// SurfaceHolderCallback helpers
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region SurfaceHolderCallback helpers
|
//region SurfaceHolderCallback helpers
|
||||||
|
|
||||||
private void setupVideoSurface() {
|
private void setupVideoSurface() {
|
||||||
// make sure there is nothing left over from previous calls
|
// make sure there is nothing left over from previous calls
|
||||||
cleanupVideoSurface();
|
cleanupVideoSurface();
|
||||||
|
@ -4571,5 +4582,5 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//endregion SurfaceHolderCallback helpers
|
//endregion
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import androidx.media.AudioManagerCompat;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
||||||
|
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||||
|
|
||||||
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
|
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
|
||||||
|
|
||||||
|
@ -50,6 +51,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
abandonAudioFocus();
|
abandonAudioFocus();
|
||||||
player.removeAnalyticsListener(this);
|
player.removeAnalyticsListener(this);
|
||||||
|
notifyAudioSessionUpdate(false, player.getAudioSessionId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -149,11 +151,21 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) {
|
public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) {
|
||||||
|
notifyAudioSessionUpdate(true, audioSessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAudioDisabled(final EventTime eventTime, final DecoderCounters counters) {
|
||||||
|
notifyAudioSessionUpdate(false, player.getAudioSessionId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) {
|
||||||
if (!PlayerHelper.isUsingDSP()) {
|
if (!PlayerHelper.isUsingDSP()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final Intent intent = new Intent(active
|
||||||
final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
|
? AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION
|
||||||
|
: AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
|
||||||
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId);
|
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId);
|
||||||
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName());
|
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName());
|
||||||
context.sendBroadcast(intent);
|
context.sendBroadcast(intent);
|
||||||
|
|
|
@ -20,18 +20,16 @@ public class LoadController implements LoadControl {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public LoadController() {
|
public LoadController() {
|
||||||
this(PlayerHelper.getPlaybackStartBufferMs(),
|
this(PlayerHelper.getPlaybackStartBufferMs());
|
||||||
PlayerHelper.getPlaybackMinimumBufferMs(),
|
|
||||||
PlayerHelper.getPlaybackOptimalBufferMs());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoadController(final int initialPlaybackBufferMs,
|
private LoadController(final int initialPlaybackBufferMs) {
|
||||||
final int minimumPlaybackBufferMs,
|
|
||||||
final int optimalPlaybackBufferMs) {
|
|
||||||
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
|
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
|
||||||
|
|
||||||
final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder();
|
final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder();
|
||||||
builder.setBufferDurationsMs(minimumPlaybackBufferMs, optimalPlaybackBufferMs,
|
builder.setBufferDurationsMs(
|
||||||
|
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
|
||||||
|
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
|
||||||
initialPlaybackBufferMs,
|
initialPlaybackBufferMs,
|
||||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
|
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
|
||||||
internalLoadControl = builder.build();
|
internalLoadControl = builder.build();
|
||||||
|
|
|
@ -164,7 +164,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence))
|
setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence))
|
||||||
.setNeutralButton(R.string.playback_reset, (dialogInterface, i) ->
|
.setNeutralButton(R.string.playback_reset, (dialogInterface, i) ->
|
||||||
setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE))
|
setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE))
|
||||||
.setPositiveButton(R.string.finish, (dialogInterface, i) ->
|
.setPositiveButton(R.string.ok, (dialogInterface, i) ->
|
||||||
setCurrentPlaybackParameters());
|
setCurrentPlaybackParameters());
|
||||||
|
|
||||||
return dialogBuilder.create();
|
return dialogBuilder.create();
|
||||||
|
|
|
@ -239,6 +239,11 @@ public final class PlayerHelper {
|
||||||
.getBoolean(context.getString(R.string.brightness_gesture_control_key), true);
|
.getBoolean(context.getString(R.string.brightness_gesture_control_key), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isStartMainPlayerFullscreenEnabled(@NonNull final Context context) {
|
||||||
|
return getPreferences(context)
|
||||||
|
.getBoolean(context.getString(R.string.start_main_player_fullscreen_key), false);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isAutoQueueEnabled(@NonNull final Context context) {
|
public static boolean isAutoQueueEnabled(@NonNull final Context context) {
|
||||||
return getPreferences(context)
|
return getPreferences(context)
|
||||||
.getBoolean(context.getString(R.string.auto_queue_key), false);
|
.getBoolean(context.getString(R.string.auto_queue_key), false);
|
||||||
|
@ -307,22 +312,6 @@ public final class PlayerHelper {
|
||||||
return 500;
|
return 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the minimum number of milliseconds the player always buffers to
|
|
||||||
* after starting playback.
|
|
||||||
*/
|
|
||||||
public static int getPlaybackMinimumBufferMs() {
|
|
||||||
return 25000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the maximum/optimal number of milliseconds the player will buffer to once the buffer
|
|
||||||
* hits the point of {@link #getPlaybackMinimumBufferMs()}.
|
|
||||||
*/
|
|
||||||
public static int getPlaybackOptimalBufferMs() {
|
|
||||||
return 60000;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TrackSelection.Factory getQualitySelector() {
|
public static TrackSelection.Factory getQualitySelector() {
|
||||||
return new AdaptiveTrackSelection.Factory(
|
return new AdaptiveTrackSelection.Factory(
|
||||||
1000,
|
1000,
|
||||||
|
|
|
@ -28,6 +28,7 @@ public class PlayQueueItem implements Serializable {
|
||||||
private final String thumbnailUrl;
|
private final String thumbnailUrl;
|
||||||
@NonNull
|
@NonNull
|
||||||
private final String uploader;
|
private final String uploader;
|
||||||
|
private final String uploaderUrl;
|
||||||
@NonNull
|
@NonNull
|
||||||
private final StreamType streamType;
|
private final StreamType streamType;
|
||||||
|
|
||||||
|
@ -40,7 +41,8 @@ public class PlayQueueItem implements Serializable {
|
||||||
|
|
||||||
PlayQueueItem(@NonNull final StreamInfo info) {
|
PlayQueueItem(@NonNull final StreamInfo info) {
|
||||||
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
||||||
info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType());
|
info.getThumbnailUrl(), info.getUploaderName(),
|
||||||
|
info.getUploaderUrl(), info.getStreamType());
|
||||||
|
|
||||||
if (info.getStartPosition() > 0) {
|
if (info.getStartPosition() > 0) {
|
||||||
setRecoveryPosition(info.getStartPosition() * 1000);
|
setRecoveryPosition(info.getStartPosition() * 1000);
|
||||||
|
@ -49,19 +51,21 @@ public class PlayQueueItem implements Serializable {
|
||||||
|
|
||||||
PlayQueueItem(@NonNull final StreamInfoItem item) {
|
PlayQueueItem(@NonNull final StreamInfoItem item) {
|
||||||
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
|
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
|
||||||
item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType());
|
item.getThumbnailUrl(), item.getUploaderName(),
|
||||||
|
item.getUploaderUrl(), item.getStreamType());
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
|
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
|
||||||
final int serviceId, final long duration,
|
final int serviceId, final long duration,
|
||||||
@Nullable final String thumbnailUrl, @Nullable final String uploader,
|
@Nullable final String thumbnailUrl, @Nullable final String uploader,
|
||||||
@NonNull final StreamType streamType) {
|
final String uploaderUrl, @NonNull final StreamType streamType) {
|
||||||
this.title = name != null ? name : EMPTY_STRING;
|
this.title = name != null ? name : EMPTY_STRING;
|
||||||
this.url = url != null ? url : EMPTY_STRING;
|
this.url = url != null ? url : EMPTY_STRING;
|
||||||
this.serviceId = serviceId;
|
this.serviceId = serviceId;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING;
|
this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING;
|
||||||
this.uploader = uploader != null ? uploader : EMPTY_STRING;
|
this.uploader = uploader != null ? uploader : EMPTY_STRING;
|
||||||
|
this.uploaderUrl = uploaderUrl;
|
||||||
this.streamType = streamType;
|
this.streamType = streamType;
|
||||||
|
|
||||||
this.recoveryPosition = RECOVERY_UNSET;
|
this.recoveryPosition = RECOVERY_UNSET;
|
||||||
|
@ -95,6 +99,10 @@ public class PlayQueueItem implements Serializable {
|
||||||
return uploader;
|
return uploader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUploaderUrl() {
|
||||||
|
return uploaderUrl;
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public StreamType getStreamType() {
|
public StreamType getStreamType() {
|
||||||
return streamType;
|
return streamType;
|
||||||
|
|
|
@ -5,11 +5,9 @@ import android.text.TextUtils;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
|
|
||||||
public class PlayQueueItemBuilder {
|
public class PlayQueueItemBuilder {
|
||||||
private static final String TAG = PlayQueueItemBuilder.class.toString();
|
private static final String TAG = PlayQueueItemBuilder.class.toString();
|
||||||
|
@ -35,8 +33,7 @@ public class PlayQueueItemBuilder {
|
||||||
holder.itemDurationView.setVisibility(View.GONE);
|
holder.itemDurationView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView,
|
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(holder.itemThumbnailView);
|
||||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
|
||||||
|
|
||||||
holder.itemRoot.setOnClickListener(view -> {
|
holder.itemRoot.setOnClickListener(view -> {
|
||||||
if (onItemClickListener != null) {
|
if (onItemClickListener != null) {
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
package org.schabi.newpipe.player.seekbarpreview;
|
package org.schabi.newpipe.player.seekbarpreview;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.common.base.Stopwatch;
|
import com.google.common.base.Stopwatch;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.Frameset;
|
import org.schabi.newpipe.extractor.stream.Frameset;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -21,11 +23,8 @@ import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType;
|
|
||||||
|
|
||||||
public class SeekbarPreviewThumbnailHolder {
|
public class SeekbarPreviewThumbnailHolder {
|
||||||
|
|
||||||
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
|
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
|
||||||
|
@ -174,6 +173,7 @@ public class SeekbarPreviewThumbnailHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
private Bitmap getBitMapFrom(final String url) {
|
private Bitmap getBitMapFrom(final String url) {
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
Log.w(TAG, "url is null; This should never happen");
|
Log.w(TAG, "url is null; This should never happen");
|
||||||
|
@ -182,24 +182,11 @@ public class SeekbarPreviewThumbnailHolder {
|
||||||
|
|
||||||
final Stopwatch sw = Log.isLoggable(TAG, Log.DEBUG) ? Stopwatch.createStarted() : null;
|
final Stopwatch sw = Log.isLoggable(TAG, Log.DEBUG) ? Stopwatch.createStarted() : null;
|
||||||
try {
|
try {
|
||||||
final SyncImageLoadingListener syncImageLoadingListener =
|
|
||||||
new SyncImageLoadingListener();
|
|
||||||
|
|
||||||
Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'");
|
Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'");
|
||||||
|
|
||||||
// Ensure that everything is running
|
// Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient
|
||||||
ImageLoader.getInstance().resume();
|
|
||||||
// Load the image
|
|
||||||
// Impl-Note:
|
|
||||||
// Ensure that your are not running on the main-Thread this will otherwise hang
|
// Ensure that your are not running on the main-Thread this will otherwise hang
|
||||||
ImageLoader.getInstance().loadImage(
|
final Bitmap bitmap = PicassoHelper.loadSeekbarThumbnailPreview(url).get();
|
||||||
url,
|
|
||||||
ImageDisplayConstants.DISPLAY_SEEKBAR_PREVIEW_OPTIONS,
|
|
||||||
syncImageLoadingListener);
|
|
||||||
|
|
||||||
// Get the bitmap within the timeout
|
|
||||||
final Bitmap bitmap =
|
|
||||||
syncImageLoadingListener.waitForBitmapOrThrow(30, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
if (sw != null) {
|
if (sw != null) {
|
||||||
Log.d(TAG,
|
Log.d(TAG,
|
||||||
|
|
|
@ -1,87 +0,0 @@
|
||||||
package org.schabi.newpipe.player.seekbarpreview;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
|
||||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
|
||||||
|
|
||||||
import java.util.concurrent.CancellationException;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for synchronously downloading of an image/bitmap.
|
|
||||||
*/
|
|
||||||
public class SyncImageLoadingListener extends SimpleImageLoadingListener {
|
|
||||||
|
|
||||||
private final CountDownLatch countDownLatch = new CountDownLatch(1);
|
|
||||||
|
|
||||||
private Bitmap bitmap;
|
|
||||||
private boolean cancelled = false;
|
|
||||||
private FailReason failReason = null;
|
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:HiddenField")
|
|
||||||
@Override
|
|
||||||
public void onLoadingFailed(
|
|
||||||
final String imageUri,
|
|
||||||
final View view,
|
|
||||||
final FailReason failReason) {
|
|
||||||
|
|
||||||
this.failReason = failReason;
|
|
||||||
countDownLatch.countDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadingComplete(
|
|
||||||
final String imageUri,
|
|
||||||
final View view,
|
|
||||||
final Bitmap loadedImage) {
|
|
||||||
|
|
||||||
bitmap = loadedImage;
|
|
||||||
countDownLatch.countDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadingCancelled(final String imageUri, final View view) {
|
|
||||||
cancelled = true;
|
|
||||||
countDownLatch.countDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap waitForBitmapOrThrow(final long timeout, final TimeUnit timeUnit)
|
|
||||||
throws InterruptedException, TimeoutException {
|
|
||||||
|
|
||||||
// Wait for the download to finish
|
|
||||||
if (!countDownLatch.await(timeout, timeUnit)) {
|
|
||||||
throw new TimeoutException("Couldn't get the image in time");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCancelled()) {
|
|
||||||
throw new CancellationException("Download of image was cancelled");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getFailReason() != null) {
|
|
||||||
throw new RuntimeException("Failed to download image" + getFailReason().getType(),
|
|
||||||
getFailReason().getCause());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getBitmap() == null) {
|
|
||||||
throw new NullPointerException("Bitmap is null");
|
|
||||||
}
|
|
||||||
|
|
||||||
return getBitmap();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap getBitmap() {
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isCancelled() {
|
|
||||||
return cancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FailReason getFailReason() {
|
|
||||||
return failReason;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,8 +17,6 @@ import androidx.core.content.ContextCompat;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.DownloaderImpl;
|
import org.schabi.newpipe.DownloaderImpl;
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
@ -29,6 +27,7 @@ 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.StoredFileHelper;
|
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ZipHelper;
|
import org.schabi.newpipe.util.ZipHelper;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -50,7 +49,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||||
private ContentSettingsManager manager;
|
private ContentSettingsManager manager;
|
||||||
|
|
||||||
private String importExportDataPathKey;
|
private String importExportDataPathKey;
|
||||||
private String thumbnailLoadToggleKey;
|
|
||||||
private String youtubeRestrictedModeEnabledKey;
|
private String youtubeRestrictedModeEnabledKey;
|
||||||
|
|
||||||
private Localization initialSelectedLocalization;
|
private Localization initialSelectedLocalization;
|
||||||
|
@ -69,7 +67,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||||
manager.deleteSettingsFile();
|
manager.deleteSettingsFile();
|
||||||
|
|
||||||
importExportDataPathKey = getString(R.string.import_export_data_path);
|
importExportDataPathKey = getString(R.string.import_export_data_path);
|
||||||
thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key);
|
|
||||||
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
||||||
|
|
||||||
addPreferencesFromResource(R.xml.content_settings);
|
addPreferencesFromResource(R.xml.content_settings);
|
||||||
|
@ -77,7 +74,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||||
final Preference importDataPreference = requirePreference(R.string.import_data);
|
final Preference importDataPreference = requirePreference(R.string.import_data);
|
||||||
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
|
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
|
||||||
requestImportPathLauncher.launch(
|
requestImportPathLauncher.launch(
|
||||||
StoredFileHelper.getPicker(requireContext(), getImportExportDataUri()));
|
StoredFileHelper.getPicker(requireContext(),
|
||||||
|
ZIP_MIME_TYPE, getImportExportDataUri()));
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -95,8 +93,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||||
.getPreferredLocalization(requireContext());
|
.getPreferredLocalization(requireContext());
|
||||||
initialSelectedContentCountry = org.schabi.newpipe.util.Localization
|
initialSelectedContentCountry = org.schabi.newpipe.util.Localization
|
||||||
.getPreferredContentCountry(requireContext());
|
.getPreferredContentCountry(requireContext());
|
||||||
initialLanguage = PreferenceManager
|
initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
|
||||||
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en");
|
|
||||||
|
|
||||||
final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key);
|
final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key);
|
||||||
clearCookiePref.setOnPreferenceClickListener(preference -> {
|
clearCookiePref.setOnPreferenceClickListener(preference -> {
|
||||||
|
@ -112,30 +109,30 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||||
if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) {
|
if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) {
|
||||||
clearCookiePref.setVisible(false);
|
clearCookiePref.setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findPreference(getString(R.string.download_thumbnail_key)).setOnPreferenceChangeListener(
|
||||||
|
(preference, newValue) -> {
|
||||||
|
PicassoHelper.setShouldLoadImages((Boolean) newValue);
|
||||||
|
try {
|
||||||
|
PicassoHelper.clearCache(preference.getContext());
|
||||||
|
Toast.makeText(preference.getContext(),
|
||||||
|
R.string.thumbnail_cache_wipe_complete_notice, Toast.LENGTH_SHORT)
|
||||||
|
.show();
|
||||||
|
} catch (final IOException e) {
|
||||||
|
Log.e(TAG, "Unable to clear Picasso cache", e);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceTreeClick(final Preference preference) {
|
public boolean onPreferenceTreeClick(final Preference preference) {
|
||||||
final String key = preference.getKey();
|
if (preference.getKey().equals(youtubeRestrictedModeEnabledKey)) {
|
||||||
if (key != null) {
|
final Context context = getContext();
|
||||||
if (key.equals(thumbnailLoadToggleKey)) {
|
if (context != null) {
|
||||||
final ImageLoader imageLoader = ImageLoader.getInstance();
|
DownloaderImpl.getInstance().updateYoutubeRestrictedModeCookies(context);
|
||||||
imageLoader.stop();
|
} else {
|
||||||
imageLoader.clearDiskCache();
|
Log.w(TAG, "onPreferenceTreeClick: null context");
|
||||||
imageLoader.clearMemoryCache();
|
|
||||||
imageLoader.resume();
|
|
||||||
Toast.makeText(preference.getContext(),
|
|
||||||
R.string.thumbnail_cache_wipe_complete_notice,
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key.equals(youtubeRestrictedModeEnabledKey)) {
|
|
||||||
final Context context = getContext();
|
|
||||||
if (context != null) {
|
|
||||||
DownloaderImpl.getInstance().updateYoutubeRestrictedModeCookies(context);
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "onPreferenceTreeClick: null context");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,8 +147,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||||
.getPreferredLocalization(requireContext());
|
.getPreferredLocalization(requireContext());
|
||||||
final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization
|
final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization
|
||||||
.getPreferredContentCountry(requireContext());
|
.getPreferredContentCountry(requireContext());
|
||||||
final String selectedLanguage = PreferenceManager
|
final String selectedLanguage =
|
||||||
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en");
|
defaultPreferences.getString(getString(R.string.app_language_key), "en");
|
||||||
|
|
||||||
if (!selectedLocalization.equals(initialSelectedLocalization)
|
if (!selectedLocalization.equals(initialSelectedLocalization)
|
||||||
|| !selectedContentCountry.equals(initialSelectedContentCountry)
|
|| !selectedContentCountry.equals(initialSelectedContentCountry)
|
||||||
|
@ -187,7 +184,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||||
|
|
||||||
new AlertDialog.Builder(requireActivity())
|
new AlertDialog.Builder(requireActivity())
|
||||||
.setMessage(R.string.override_current_data)
|
.setMessage(R.string.override_current_data)
|
||||||
.setPositiveButton(R.string.finish, (d, id) ->
|
.setPositiveButton(R.string.ok, (d, id) ->
|
||||||
importDatabase(file, lastImportDataUri))
|
importDatabase(file, lastImportDataUri))
|
||||||
.setNegativeButton(R.string.cancel, (d, id) ->
|
.setNegativeButton(R.string.cancel, (d, id) ->
|
||||||
d.cancel())
|
d.cancel())
|
||||||
|
@ -235,11 +232,11 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
|
||||||
final AlertDialog.Builder alert = new AlertDialog.Builder(requireContext());
|
final AlertDialog.Builder alert = new AlertDialog.Builder(requireContext());
|
||||||
alert.setTitle(R.string.import_settings);
|
alert.setTitle(R.string.import_settings);
|
||||||
|
|
||||||
alert.setNegativeButton(android.R.string.no, (dialog, which) -> {
|
alert.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
finishImport(importDataUri);
|
finishImport(importDataUri);
|
||||||
});
|
});
|
||||||
alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> {
|
alert.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
manager.loadSharedPreferences(PreferenceManager
|
manager.loadSharedPreferences(PreferenceManager
|
||||||
.getDefaultSharedPreferences(requireContext()));
|
.getDefaultSharedPreferences(requireContext()));
|
||||||
|
|
|
@ -179,7 +179,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||||
final AlertDialog.Builder msg = new AlertDialog.Builder(ctx);
|
final AlertDialog.Builder msg = new AlertDialog.Builder(ctx);
|
||||||
msg.setTitle(title);
|
msg.setTitle(title);
|
||||||
msg.setMessage(message);
|
msg.setMessage(message);
|
||||||
msg.setPositiveButton(getString(R.string.finish), null);
|
msg.setPositiveButton(getString(R.string.ok), null);
|
||||||
msg.show();
|
msg.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.os.Build;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
@ -59,6 +60,10 @@ public final class NewPipeSettings {
|
||||||
isFirstRun = true;
|
isFirstRun = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// first run migrations, then setDefaultValues, since the latter requires the correct types
|
||||||
|
SettingMigrations.initMigrations(context, isFirstRun);
|
||||||
|
|
||||||
|
// 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);
|
||||||
PreferenceManager.setDefaultValues(context, R.xml.video_audio_settings, true);
|
PreferenceManager.setDefaultValues(context, R.xml.video_audio_settings, true);
|
||||||
PreferenceManager.setDefaultValues(context, R.xml.download_settings, true);
|
PreferenceManager.setDefaultValues(context, R.xml.download_settings, true);
|
||||||
|
@ -72,8 +77,6 @@ public final class NewPipeSettings {
|
||||||
|
|
||||||
saveDefaultVideoDownloadDirectory(context);
|
saveDefaultVideoDownloadDirectory(context);
|
||||||
saveDefaultAudioDownloadDirectory(context);
|
saveDefaultAudioDownloadDirectory(context);
|
||||||
|
|
||||||
SettingMigrations.initMigrations(context, isFirstRun);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void saveDefaultVideoDownloadDirectory(final Context context) {
|
static void saveDefaultVideoDownloadDirectory(final Context context) {
|
||||||
|
@ -125,4 +128,29 @@ public final class NewPipeSettings {
|
||||||
|
|
||||||
return prefs.getBoolean(key, true);
|
return prefs.getBoolean(key, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean showSearchSuggestions(final Context context,
|
||||||
|
final SharedPreferences sharedPreferences,
|
||||||
|
@StringRes final int key) {
|
||||||
|
final Set<String> enabledSearchSuggestions = sharedPreferences.getStringSet(
|
||||||
|
context.getString(R.string.show_search_suggestions_key), null);
|
||||||
|
|
||||||
|
if (enabledSearchSuggestions == null) {
|
||||||
|
return true; // defaults to true
|
||||||
|
} else {
|
||||||
|
return enabledSearchSuggestions.contains(context.getString(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean showLocalSearchSuggestions(final Context context,
|
||||||
|
final SharedPreferences sharedPreferences) {
|
||||||
|
return showSearchSuggestions(context, sharedPreferences,
|
||||||
|
R.string.show_local_search_suggestions_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean showRemoteSearchSuggestions(final Context context,
|
||||||
|
final SharedPreferences sharedPreferences) {
|
||||||
|
return showSearchSuggestions(context, sharedPreferences,
|
||||||
|
R.string.show_remote_search_suggestions_key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -218,7 +218,7 @@ public class PeertubeInstanceListFragment extends Fragment {
|
||||||
.setIcon(R.drawable.place_holder_peertube)
|
.setIcon(R.drawable.place_holder_peertube)
|
||||||
.setView(dialogBinding.getRoot())
|
.setView(dialogBinding.getRoot())
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.finish, (dialog1, which) -> {
|
.setPositiveButton(R.string.ok, (dialog1, which) -> {
|
||||||
final String url = dialogBinding.dialogEditText.getText().toString();
|
final String url = dialogBinding.dialogEditText.getText().toString();
|
||||||
addInstance(url);
|
addInstance(url);
|
||||||
})
|
})
|
||||||
|
|
|
@ -14,13 +14,11 @@ import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
import org.schabi.newpipe.error.ErrorActivity;
|
import org.schabi.newpipe.error.ErrorActivity;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -54,13 +52,6 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class SelectChannelFragment extends DialogFragment {
|
public class SelectChannelFragment extends DialogFragment {
|
||||||
/**
|
|
||||||
* This contains the base display options for images.
|
|
||||||
*/
|
|
||||||
private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS
|
|
||||||
= new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
|
||||||
|
|
||||||
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
|
||||||
|
|
||||||
private OnSelectedListener onSelectedListener = null;
|
private OnSelectedListener onSelectedListener = null;
|
||||||
private OnCancelListener onCancelListener = null;
|
private OnCancelListener onCancelListener = null;
|
||||||
|
@ -199,8 +190,7 @@ public class SelectChannelFragment extends DialogFragment {
|
||||||
final SubscriptionEntity entry = subscriptions.get(position);
|
final SubscriptionEntity entry = subscriptions.get(position);
|
||||||
holder.titleView.setText(entry.getName());
|
holder.titleView.setText(entry.getName());
|
||||||
holder.view.setOnClickListener(view -> clickedItem(position));
|
holder.view.setOnClickListener(view -> clickedItem(position));
|
||||||
imageLoader.displayImage(entry.getAvatarUrl(), holder.thumbnailView,
|
PicassoHelper.loadAvatar(entry.getAvatarUrl()).into(holder.thumbnailView);
|
||||||
DISPLAY_IMAGE_OPTIONS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -14,9 +14,6 @@ import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.AppDatabase;
|
import org.schabi.newpipe.database.AppDatabase;
|
||||||
|
@ -29,6 +26,7 @@ import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
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.PicassoHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
|
@ -38,13 +36,6 @@ 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 {
|
||||||
/**
|
|
||||||
* This contains the base display options for images.
|
|
||||||
*/
|
|
||||||
private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS
|
|
||||||
= new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
|
||||||
|
|
||||||
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
|
||||||
|
|
||||||
private OnSelectedListener onSelectedListener = null;
|
private OnSelectedListener onSelectedListener = null;
|
||||||
|
|
||||||
|
@ -170,16 +161,15 @@ public class SelectPlaylistFragment extends DialogFragment {
|
||||||
|
|
||||||
holder.titleView.setText(entry.name);
|
holder.titleView.setText(entry.name);
|
||||||
holder.view.setOnClickListener(view -> clickedItem(position));
|
holder.view.setOnClickListener(view -> clickedItem(position));
|
||||||
imageLoader.displayImage(entry.thumbnailUrl, holder.thumbnailView,
|
PicassoHelper.loadPlaylistThumbnail(entry.thumbnailUrl).into(holder.thumbnailView);
|
||||||
DISPLAY_IMAGE_OPTIONS);
|
|
||||||
|
|
||||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||||
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
||||||
|
|
||||||
holder.titleView.setText(entry.getName());
|
holder.titleView.setText(entry.getName());
|
||||||
holder.view.setOnClickListener(view -> clickedItem(position));
|
holder.view.setOnClickListener(view -> clickedItem(position));
|
||||||
imageLoader.displayImage(entry.getThumbnailUrl(), holder.thumbnailView,
|
PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl())
|
||||||
DISPLAY_IMAGE_OPTIONS);
|
.into(holder.thumbnailView);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,14 +13,22 @@ import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In order to add a migration, follow these steps, given P is the previous version:<br>
|
||||||
|
* - in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put in
|
||||||
|
* the {@code migrate()} method the code that need to be run when migrating from P to P+1<br>
|
||||||
|
* - add {@code MIGRATION_P_P+1} at the end of {@link SettingMigrations#SETTING_MIGRATIONS}<br>
|
||||||
|
* - increment {@link SettingMigrations#VERSION}'s value by 1 (so it should become P+1)
|
||||||
|
*/
|
||||||
public final class SettingMigrations {
|
public final class SettingMigrations {
|
||||||
|
|
||||||
private static final String TAG = SettingMigrations.class.toString();
|
private static final String TAG = SettingMigrations.class.toString();
|
||||||
/**
|
|
||||||
* Version number for preferences. Must be incremented every time a migration is necessary.
|
|
||||||
*/
|
|
||||||
public static final int VERSION = 3;
|
|
||||||
private static SharedPreferences sp;
|
private static SharedPreferences sp;
|
||||||
|
|
||||||
public static final Migration MIGRATION_0_1 = new Migration(0, 1) {
|
public static final Migration MIGRATION_0_1 = new Migration(0, 1) {
|
||||||
|
@ -72,6 +80,35 @@ public final class SettingMigrations {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_3_4 = new Migration(3, 4) {
|
||||||
|
@Override
|
||||||
|
protected void migrate(final Context context) {
|
||||||
|
// Pull request #3546 added support for choosing the type of search suggestions to
|
||||||
|
// show, replacing the on-off switch used before, so migrate the previous user choice
|
||||||
|
|
||||||
|
final String showSearchSuggestionsKey =
|
||||||
|
context.getString(R.string.show_search_suggestions_key);
|
||||||
|
|
||||||
|
boolean addAllSearchSuggestionTypes;
|
||||||
|
try {
|
||||||
|
addAllSearchSuggestionTypes = sp.getBoolean(showSearchSuggestionsKey, true);
|
||||||
|
} catch (final ClassCastException e) {
|
||||||
|
// just in case it was not a boolean for some reason, let's consider it a "true"
|
||||||
|
addAllSearchSuggestionTypes = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Set<String> showSearchSuggestionsValueList = new HashSet<>();
|
||||||
|
if (addAllSearchSuggestionTypes) {
|
||||||
|
// if the preference was true, all suggestions will be shown, otherwise none
|
||||||
|
Collections.addAll(showSearchSuggestionsValueList, context.getResources()
|
||||||
|
.getStringArray(R.array.show_search_suggestions_value_list));
|
||||||
|
}
|
||||||
|
|
||||||
|
sp.edit().putStringSet(
|
||||||
|
showSearchSuggestionsKey, showSearchSuggestionsValueList).apply();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of all implemented migrations.
|
* List of all implemented migrations.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -81,9 +118,15 @@ public final class SettingMigrations {
|
||||||
private static final Migration[] SETTING_MIGRATIONS = {
|
private static final Migration[] SETTING_MIGRATIONS = {
|
||||||
MIGRATION_0_1,
|
MIGRATION_0_1,
|
||||||
MIGRATION_1_2,
|
MIGRATION_1_2,
|
||||||
MIGRATION_2_3
|
MIGRATION_2_3,
|
||||||
|
MIGRATION_3_4,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version number for preferences. Must be incremented every time a migration is necessary.
|
||||||
|
*/
|
||||||
|
public static final int VERSION = 4;
|
||||||
|
|
||||||
|
|
||||||
public static void initMigrations(final Context context, final boolean isFirstRun) {
|
public static void initMigrations(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
|
||||||
|
|
|
@ -459,11 +459,12 @@ public class StoredFileHelper implements Serializable {
|
||||||
return !str1.equals(str2);
|
return !str1.equals(str2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Intent getPicker(@NonNull final Context ctx) {
|
public static Intent getPicker(@NonNull final Context ctx,
|
||||||
|
@NonNull final String mimeType) {
|
||||||
if (NewPipeSettings.useStorageAccessFramework(ctx)) {
|
if (NewPipeSettings.useStorageAccessFramework(ctx)) {
|
||||||
return new Intent(Intent.ACTION_OPEN_DOCUMENT)
|
return new Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
.putExtra("android.content.extra.SHOW_ADVANCED", true)
|
.putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||||
.setType("*/*")
|
.setType(mimeType)
|
||||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||||
| StoredDirectoryHelper.PERMISSION_FLAGS);
|
| StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||||
|
@ -477,8 +478,10 @@ public class StoredFileHelper implements Serializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Intent getPicker(@NonNull final Context ctx, @Nullable final Uri initialPath) {
|
public static Intent getPicker(@NonNull final Context ctx,
|
||||||
return applyInitialPathToPickerIntent(ctx, getPicker(ctx), initialPath, null);
|
@NonNull final String mimeType,
|
||||||
|
@Nullable final Uri initialPath) {
|
||||||
|
return applyInitialPathToPickerIntent(ctx, getPicker(ctx, mimeType), initialPath, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Intent getNewPicker(@NonNull final Context ctx,
|
public static Intent getNewPicker(@NonNull final Context ctx,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import android.view.KeyEvent;
|
||||||
|
|
||||||
import androidx.annotation.Dimension;
|
import androidx.annotation.Dimension;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
@ -130,4 +131,13 @@ public final class DeviceUtils {
|
||||||
&& !HI3798MV200
|
&& !HI3798MV200
|
||||||
&& !CVT_MT5886_EU_1G;
|
&& !CVT_MT5886_EU_1G;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isLandscape(final Context context) {
|
||||||
|
return context.getResources().getDisplayMetrics().heightPixels < context.getResources()
|
||||||
|
.getDisplayMetrics().widthPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isInMultiWindow(final AppCompatActivity activity) {
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
package org.schabi.newpipe.util;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
|
||||||
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
|
|
||||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
|
|
||||||
public final class ImageDisplayConstants {
|
|
||||||
private static final int BITMAP_FADE_IN_DURATION_MILLIS = 250;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This constant contains the base display options.
|
|
||||||
*/
|
|
||||||
private static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
|
|
||||||
new DisplayImageOptions.Builder()
|
|
||||||
.cacheInMemory(true)
|
|
||||||
.cacheOnDisk(true)
|
|
||||||
.resetViewBeforeLoading(true)
|
|
||||||
.bitmapConfig(Bitmap.Config.RGB_565)
|
|
||||||
.imageScaleType(ImageScaleType.EXACTLY)
|
|
||||||
.displayer(new FadeInBitmapDisplayer(BITMAP_FADE_IN_DURATION_MILLIS))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// DisplayImageOptions default configurations
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS =
|
|
||||||
new DisplayImageOptions.Builder()
|
|
||||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
|
||||||
.showImageForEmptyUri(R.drawable.buddy)
|
|
||||||
.showImageOnFail(R.drawable.buddy)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
|
||||||
new DisplayImageOptions.Builder()
|
|
||||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
|
||||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
|
|
||||||
.showImageOnFail(R.drawable.dummy_thumbnail)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS =
|
|
||||||
new DisplayImageOptions.Builder()
|
|
||||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
|
||||||
.showImageForEmptyUri(R.drawable.channel_banner)
|
|
||||||
.showImageOnFail(R.drawable.channel_banner)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public static final DisplayImageOptions DISPLAY_PLAYLIST_OPTIONS =
|
|
||||||
new DisplayImageOptions.Builder()
|
|
||||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
|
||||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
|
|
||||||
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public static final DisplayImageOptions DISPLAY_SEEKBAR_PREVIEW_OPTIONS =
|
|
||||||
new DisplayImageOptions.Builder()
|
|
||||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
private ImageDisplayConstants() { }
|
|
||||||
}
|
|
|
@ -226,6 +226,16 @@ public final class Localization {
|
||||||
shortCount(context, subscriberCount));
|
shortCount(context, subscriberCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String downloadCount(final Context context, final int downloadCount) {
|
||||||
|
return getQuantity(context, R.plurals.download_finished_notification, 0,
|
||||||
|
downloadCount, shortCount(context, downloadCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String deletedDownloadCount(final Context context, final int deletedCount) {
|
||||||
|
return getQuantity(context, R.plurals.deleted_downloads_toast, 0,
|
||||||
|
deletedCount, shortCount(context, deletedCount));
|
||||||
|
}
|
||||||
|
|
||||||
private static String getQuantity(final Context context, @PluralsRes final int pluralId,
|
private static String getQuantity(final Context context, @PluralsRes final int pluralId,
|
||||||
@StringRes final int zeroCaseStringId, final long count,
|
@StringRes final int zeroCaseStringId, final long count,
|
||||||
final String formattedCount) {
|
final String formattedCount) {
|
||||||
|
|
|
@ -18,8 +18,6 @@ import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
@ -60,6 +58,8 @@ import java.util.ArrayList;
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.external_communication.ShareUtils.installApp;
|
import static org.schabi.newpipe.util.external_communication.ShareUtils.installApp;
|
||||||
|
|
||||||
|
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||||
|
|
||||||
public final class NavigationHelper {
|
public final class NavigationHelper {
|
||||||
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
|
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
|
||||||
public static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag";
|
public static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag";
|
||||||
|
@ -259,10 +259,9 @@ public final class NavigationHelper {
|
||||||
if (context instanceof Activity) {
|
if (context instanceof Activity) {
|
||||||
new AlertDialog.Builder(context)
|
new AlertDialog.Builder(context)
|
||||||
.setMessage(R.string.no_player_found)
|
.setMessage(R.string.no_player_found)
|
||||||
.setPositiveButton(R.string.install, (dialog, which) -> {
|
.setPositiveButton(R.string.install,
|
||||||
ShareUtils.openUrlInBrowser(context,
|
(dialog, which) -> ShareUtils.openUrlInBrowser(context,
|
||||||
context.getString(R.string.fdroid_vlc_url), false);
|
context.getString(R.string.fdroid_vlc_url), false))
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.cancel, (dialog, which)
|
.setNegativeButton(R.string.cancel, (dialog, which)
|
||||||
-> Log.i("NavigationHelper", "You unlocked a secret unicorn."))
|
-> Log.i("NavigationHelper", "You unlocked a secret unicorn."))
|
||||||
.show();
|
.show();
|
||||||
|
@ -284,8 +283,6 @@ public final class NavigationHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void gotoMainFragment(final FragmentManager fragmentManager) {
|
public static void gotoMainFragment(final FragmentManager fragmentManager) {
|
||||||
ImageLoader.getInstance().clearMemoryCache();
|
|
||||||
|
|
||||||
final boolean popped = fragmentManager.popBackStackImmediate(MAIN_FRAGMENT_TAG, 0);
|
final boolean popped = fragmentManager.popBackStackImmediate(MAIN_FRAGMENT_TAG, 0);
|
||||||
if (!popped) {
|
if (!popped) {
|
||||||
openMainFragment(fragmentManager);
|
openMainFragment(fragmentManager);
|
||||||
|
@ -365,13 +362,15 @@ public final class NavigationHelper {
|
||||||
autoPlay = false;
|
autoPlay = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final RunnableWithVideoDetailFragment onVideoDetailFragmentReady = (detailFragment) -> {
|
final RunnableWithVideoDetailFragment onVideoDetailFragmentReady = detailFragment -> {
|
||||||
expandMainPlayer(detailFragment.requireActivity());
|
expandMainPlayer(detailFragment.requireActivity());
|
||||||
detailFragment.setAutoPlay(autoPlay);
|
detailFragment.setAutoPlay(autoPlay);
|
||||||
if (switchingPlayers) {
|
if (switchingPlayers) {
|
||||||
// Situation when user switches from players to main player. All needed data is
|
// Situation when user switches from players to main player. All needed data is
|
||||||
// here, we can start watching (assuming newQueue equals playQueue).
|
// here, we can start watching (assuming newQueue equals playQueue).
|
||||||
detailFragment.openVideoPlayer();
|
// Starting directly in fullscreen if the previous player type was popup.
|
||||||
|
detailFragment.openVideoPlayer(playerType == MainPlayer.PlayerType.POPUP
|
||||||
|
|| PlayerHelper.isStartMainPlayerFullscreenEnabled(context));
|
||||||
} else {
|
} else {
|
||||||
detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue);
|
detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue);
|
||||||
}
|
}
|
||||||
|
@ -610,8 +609,7 @@ public final class NavigationHelper {
|
||||||
*/
|
*/
|
||||||
public static void restartApp(final Activity activity) {
|
public static void restartApp(final Activity activity) {
|
||||||
NewPipeDatabase.close();
|
NewPipeDatabase.close();
|
||||||
activity.finishAffinity();
|
|
||||||
final Intent intent = new Intent(activity, MainActivity.class);
|
ProcessPhoenix.triggerRebirth(activity.getApplicationContext());
|
||||||
activity.startActivity(intent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,7 +119,7 @@ public final class PermissionHelper {
|
||||||
|
|
||||||
public static boolean isPopupEnabled(final Context context) {
|
public static boolean isPopupEnabled(final Context context) {
|
||||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|
||||||
|| PermissionHelper.checkSystemAlertWindowPermission(context);
|
|| checkSystemAlertWindowPermission(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showPopupEnablementToast(final Context context) {
|
public static void showPopupEnablementToast(final Context context) {
|
||||||
|
|
171
app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
|
import com.squareup.picasso.Cache;
|
||||||
|
import com.squareup.picasso.LruCache;
|
||||||
|
import com.squareup.picasso.OkHttp3Downloader;
|
||||||
|
import com.squareup.picasso.Picasso;
|
||||||
|
import com.squareup.picasso.RequestCreator;
|
||||||
|
import com.squareup.picasso.Transformation;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||||
|
|
||||||
|
public final class PicassoHelper {
|
||||||
|
public static final String PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG";
|
||||||
|
private static final String PLAYER_THUMBNAIL_TRANSFORMATION_KEY
|
||||||
|
= "PICASSO_PLAYER_THUMBNAIL_TRANSFORMATION_KEY";
|
||||||
|
|
||||||
|
private PicassoHelper() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Cache picassoCache;
|
||||||
|
private static OkHttpClient picassoDownloaderClient;
|
||||||
|
|
||||||
|
// suppress because terminate() is called in App.onTerminate(), preventing leaks
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
private static Picasso picassoInstance;
|
||||||
|
|
||||||
|
private static boolean shouldLoadImages;
|
||||||
|
|
||||||
|
public static void init(final Context context) {
|
||||||
|
picassoCache = new LruCache(10 * 1024 * 1024);
|
||||||
|
picassoDownloaderClient = new OkHttpClient.Builder()
|
||||||
|
.cache(new okhttp3.Cache(new File(context.getExternalCacheDir(), "picasso"),
|
||||||
|
50 * 1024 * 1024))
|
||||||
|
// this should already be the default timeout in OkHttp3, but just to be sure...
|
||||||
|
.callTimeout(15, TimeUnit.SECONDS)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
picassoInstance = new Picasso.Builder(context)
|
||||||
|
.memoryCache(picassoCache) // memory cache
|
||||||
|
.downloader(new OkHttp3Downloader(picassoDownloaderClient)) // disk cache
|
||||||
|
.defaultBitmapConfig(Bitmap.Config.RGB_565)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void terminate() {
|
||||||
|
picassoCache = null;
|
||||||
|
picassoDownloaderClient = null;
|
||||||
|
|
||||||
|
if (picassoInstance != null) {
|
||||||
|
picassoInstance.shutdown();
|
||||||
|
picassoInstance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearCache(final Context context) throws IOException {
|
||||||
|
picassoInstance.shutdown();
|
||||||
|
picassoCache.clear(); // clear memory cache
|
||||||
|
final okhttp3.Cache diskCache = picassoDownloaderClient.cache();
|
||||||
|
if (diskCache != null) {
|
||||||
|
diskCache.delete(); // clear disk cache
|
||||||
|
}
|
||||||
|
init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void cancelTag(final Object tag) {
|
||||||
|
picassoInstance.cancelTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setIndicatorsEnabled(final boolean enabled) {
|
||||||
|
picassoInstance.setIndicatorsEnabled(enabled); // useful for debugging
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setShouldLoadImages(final boolean shouldLoadImages) {
|
||||||
|
PicassoHelper.shouldLoadImages = shouldLoadImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getShouldLoadImages() {
|
||||||
|
return shouldLoadImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static RequestCreator loadAvatar(final String url) {
|
||||||
|
return loadImageDefault(url, R.drawable.buddy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RequestCreator loadThumbnail(final String url) {
|
||||||
|
return loadImageDefault(url, R.drawable.dummy_thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RequestCreator loadBanner(final String url) {
|
||||||
|
return loadImageDefault(url, R.drawable.channel_banner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RequestCreator loadPlaylistThumbnail(final String url) {
|
||||||
|
return loadImageDefault(url, R.drawable.dummy_thumbnail_playlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RequestCreator loadSeekbarThumbnailPreview(final String url) {
|
||||||
|
return picassoInstance.load(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static RequestCreator loadScaledDownThumbnail(final Context context, final String url) {
|
||||||
|
// scale down the notification thumbnail for performance
|
||||||
|
return PicassoHelper.loadThumbnail(url)
|
||||||
|
.tag(PLAYER_THUMBNAIL_TAG)
|
||||||
|
.transform(new Transformation() {
|
||||||
|
@Override
|
||||||
|
public Bitmap transform(final Bitmap source) {
|
||||||
|
final float notificationThumbnailWidth = Math.min(
|
||||||
|
context.getResources()
|
||||||
|
.getDimension(R.dimen.player_notification_thumbnail_width),
|
||||||
|
source.getWidth());
|
||||||
|
|
||||||
|
final Bitmap result = Bitmap.createScaledBitmap(
|
||||||
|
source,
|
||||||
|
(int) notificationThumbnailWidth,
|
||||||
|
(int) (source.getHeight()
|
||||||
|
/ (source.getWidth() / notificationThumbnailWidth)),
|
||||||
|
true);
|
||||||
|
|
||||||
|
if (result == source) {
|
||||||
|
// create a new mutable bitmap to prevent strange crashes on some
|
||||||
|
// devices (see #4638)
|
||||||
|
final Bitmap copied = Bitmap.createScaledBitmap(
|
||||||
|
source,
|
||||||
|
(int) notificationThumbnailWidth - 1,
|
||||||
|
(int) (source.getHeight() / (source.getWidth()
|
||||||
|
/ (notificationThumbnailWidth - 1))),
|
||||||
|
true);
|
||||||
|
source.recycle();
|
||||||
|
return copied;
|
||||||
|
} else {
|
||||||
|
source.recycle();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String key() {
|
||||||
|
return PLAYER_THUMBNAIL_TRANSFORMATION_KEY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static RequestCreator loadImageDefault(final String url, final int placeholderResId) {
|
||||||
|
if (!shouldLoadImages || isBlank(url)) {
|
||||||
|
return picassoInstance
|
||||||
|
.load((String) null)
|
||||||
|
.placeholder(placeholderResId) // show placeholder when no image should load
|
||||||
|
.error(placeholderResId);
|
||||||
|
} else {
|
||||||
|
return picassoInstance
|
||||||
|
.load(url)
|
||||||
|
.error(placeholderResId); // don't show placeholder while loading, only on error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package org.schabi.newpipe.util
|
package org.schabi.newpipe.util
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information about the saved state on the disk.
|
* Information about the saved state on the disk.
|
||||||
|
|
|
@ -2,9 +2,11 @@ package org.schabi.newpipe.util;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||||
|
@ -20,7 +22,9 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.PlayerType.AUDIO;
|
import static org.schabi.newpipe.player.MainPlayer.PlayerType.AUDIO;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.PlayerType.POPUP;
|
import static org.schabi.newpipe.player.MainPlayer.PlayerType.POPUP;
|
||||||
|
|
||||||
|
@ -29,12 +33,30 @@ public enum StreamDialogEntry {
|
||||||
// enum values with DEFAULT actions //
|
// enum values with DEFAULT actions //
|
||||||
//////////////////////////////////////
|
//////////////////////////////////////
|
||||||
|
|
||||||
show_channel_details(R.string.show_channel_details, (fragment, item) ->
|
show_channel_details(R.string.show_channel_details, (fragment, item) -> {
|
||||||
// For some reason `getParentFragmentManager()` doesn't work, but this does.
|
if (isNullOrEmpty(item.getUploaderUrl())) {
|
||||||
NavigationHelper.openChannelFragment(
|
final int serviceId = item.getServiceId();
|
||||||
fragment.requireActivity().getSupportFragmentManager(),
|
final String url = item.getUrl();
|
||||||
item.getServiceId(), item.getUploaderUrl(), item.getUploaderName())
|
Toast.makeText(fragment.getContext(), R.string.loading_channel_details,
|
||||||
),
|
Toast.LENGTH_SHORT).show();
|
||||||
|
ExtractorHelper.getStreamInfo(serviceId, url, false)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(result -> {
|
||||||
|
NewPipeDatabase.getInstance(fragment.getContext()).streamDAO()
|
||||||
|
.setUploaderUrl(serviceId, url, result.getUploaderUrl())
|
||||||
|
.subscribeOn(Schedulers.io()).subscribe();
|
||||||
|
openChannelFragment(fragment, item, result.getUploaderUrl());
|
||||||
|
}, throwable -> Toast.makeText(
|
||||||
|
// TODO: Open the Error Activity
|
||||||
|
fragment.getContext(),
|
||||||
|
R.string.error_show_channel_details,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show());
|
||||||
|
} else {
|
||||||
|
openChannelFragment(fragment, item, item.getUploaderUrl());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueues the stream automatically to the current PlayerType.<br>
|
* Enqueues the stream automatically to the current PlayerType.<br>
|
||||||
|
@ -179,4 +201,17 @@ public enum StreamDialogEntry {
|
||||||
public interface StreamDialogEntryAction {
|
public interface StreamDialogEntryAction {
|
||||||
void onClick(Fragment fragment, StreamInfoItem infoItem);
|
void onClick(Fragment fragment, StreamInfoItem infoItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
// private method to open channel fragment //
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
|
||||||
|
private static void openChannelFragment(final Fragment fragment,
|
||||||
|
final StreamInfoItem item,
|
||||||
|
final String uploaderUrl) {
|
||||||
|
// For some reason `getParentFragmentManager()` doesn't work, but this does.
|
||||||
|
NavigationHelper.openChannelFragment(
|
||||||
|
fragment.requireActivity().getSupportFragmentManager(),
|
||||||
|
item.getServiceId(), uploaderUrl, item.getUploaderName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
package org.schabi.newpipe.util.external_communication;
|
package org.schabi.newpipe.util.external_communication;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.MainActivity;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.error.ErrorPanelHelper;
|
||||||
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.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
@ -24,6 +29,9 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public final class InternalUrlsHandler {
|
public final class InternalUrlsHandler {
|
||||||
|
private static final String TAG = InternalUrlsHandler.class.getSimpleName();
|
||||||
|
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
|
|
||||||
private static final Pattern AMPERSAND_TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)");
|
private static final Pattern AMPERSAND_TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)");
|
||||||
private static final Pattern HASHTAG_TIMESTAMP_PATTERN =
|
private static final Pattern HASHTAG_TIMESTAMP_PATTERN =
|
||||||
Pattern.compile("(.*)#timestamp=(\\d+)");
|
Pattern.compile("(.*)#timestamp=(\\d+)");
|
||||||
|
@ -93,7 +101,12 @@ public final class InternalUrlsHandler {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final String matchedUrl = matcher.group(1);
|
final String matchedUrl = matcher.group(1);
|
||||||
final int seconds = Integer.parseInt(matcher.group(2));
|
final int seconds;
|
||||||
|
if (matcher.group(2) == null) {
|
||||||
|
seconds = -1;
|
||||||
|
} else {
|
||||||
|
seconds = Integer.parseInt(matcher.group(2));
|
||||||
|
}
|
||||||
|
|
||||||
final StreamingService service;
|
final StreamingService service;
|
||||||
final StreamingService.LinkType linkType;
|
final StreamingService.LinkType linkType;
|
||||||
|
@ -146,8 +159,18 @@ public final class InternalUrlsHandler {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(info -> {
|
.subscribe(info -> {
|
||||||
final PlayQueue playQueue
|
final PlayQueue playQueue
|
||||||
= new SinglePlayQueue(info, seconds * 1000);
|
= new SinglePlayQueue(info, seconds * 1000L);
|
||||||
NavigationHelper.playOnPopupPlayer(context, playQueue, false);
|
NavigationHelper.playOnPopupPlayer(context, playQueue, false);
|
||||||
|
}, throwable -> {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.e(TAG, "Could not play on popup: " + url, throwable);
|
||||||
|
}
|
||||||
|
new AlertDialog.Builder(context)
|
||||||
|
.setTitle(R.string.player_stream_failure)
|
||||||
|
.setMessage(
|
||||||
|
ErrorPanelHelper.Companion.getExceptionDescription(throwable))
|
||||||
|
.setPositiveButton(R.string.ok, (v, b) -> { })
|
||||||
|
.show();
|
||||||
}));
|
}));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -248,6 +248,7 @@ public final class ShareUtils {
|
||||||
shareIntent.putExtra(Intent.EXTRA_TEXT, content);
|
shareIntent.putExtra(Intent.EXTRA_TEXT, content);
|
||||||
if (!title.isEmpty()) {
|
if (!title.isEmpty()) {
|
||||||
shareIntent.putExtra(Intent.EXTRA_TITLE, title);
|
shareIntent.putExtra(Intent.EXTRA_TITLE, title);
|
||||||
|
shareIntent.putExtra(Intent.EXTRA_SUBJECT, title);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: add the image of the content to Android share sheet with setClipData after
|
/* TODO: add the image of the content to Android share sheet with setClipData after
|
||||||
|
|
|
@ -32,9 +32,8 @@ import static org.schabi.newpipe.util.external_communication.InternalUrlsHandler
|
||||||
|
|
||||||
public final class TextLinkifier {
|
public final class TextLinkifier {
|
||||||
public static final String TAG = TextLinkifier.class.getSimpleName();
|
public static final String TAG = TextLinkifier.class.getSimpleName();
|
||||||
|
|
||||||
private static final Pattern HASHTAGS_PATTERN = Pattern.compile("(#[A-Za-z0-9_]+)");
|
private static final Pattern HASHTAGS_PATTERN = Pattern.compile("(#[A-Za-z0-9_]+)");
|
||||||
private static final Pattern TIMESTAMPS_PATTERN = Pattern.compile(
|
|
||||||
"(?:^|(?!:)\\W)(?:([0-5]?[0-9]):)?([0-5]?[0-9]):([0-5][0-9])(?=$|(?!:)\\W)");
|
|
||||||
|
|
||||||
private TextLinkifier() {
|
private TextLinkifier() {
|
||||||
}
|
}
|
||||||
|
@ -174,33 +173,34 @@ public final class TextLinkifier {
|
||||||
final Info relatedInfo,
|
final Info relatedInfo,
|
||||||
final CompositeDisposable disposables) {
|
final CompositeDisposable disposables) {
|
||||||
final String descriptionText = spannableDescription.toString();
|
final String descriptionText = spannableDescription.toString();
|
||||||
final Matcher timestampsMatches = TIMESTAMPS_PATTERN.matcher(descriptionText);
|
final Matcher timestampsMatches =
|
||||||
|
TimestampExtractor.TIMESTAMPS_PATTERN.matcher(descriptionText);
|
||||||
|
|
||||||
while (timestampsMatches.find()) {
|
while (timestampsMatches.find()) {
|
||||||
final int timestampStart = timestampsMatches.start(2);
|
final TimestampExtractor.TimestampMatchDTO timestampMatchDTO =
|
||||||
final int timestampEnd = timestampsMatches.end(3);
|
TimestampExtractor.getTimestampFromMatcher(
|
||||||
final String parsedTimestamp = descriptionText.substring(timestampStart, timestampEnd);
|
timestampsMatches,
|
||||||
final String[] timestampParts = parsedTimestamp.split(":");
|
descriptionText);
|
||||||
|
|
||||||
final int seconds;
|
if (timestampMatchDTO == null) {
|
||||||
if (timestampParts.length == 3) { // timestamp format: XX:XX:XX
|
|
||||||
seconds = Integer.parseInt(timestampParts[0]) * 3600 // hours
|
|
||||||
+ Integer.parseInt(timestampParts[1]) * 60 // minutes
|
|
||||||
+ Integer.parseInt(timestampParts[2]); // seconds
|
|
||||||
} else if (timestampParts.length == 2) { // timestamp format: XX:XX
|
|
||||||
seconds = Integer.parseInt(timestampParts[0]) * 60 // minutes
|
|
||||||
+ Integer.parseInt(timestampParts[1]); // seconds
|
|
||||||
} else {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
spannableDescription.setSpan(new ClickableSpan() {
|
spannableDescription.setSpan(
|
||||||
@Override
|
new ClickableSpan() {
|
||||||
public void onClick(@NonNull final View view) {
|
@Override
|
||||||
playOnPopup(context, relatedInfo.getUrl(), relatedInfo.getService(), seconds,
|
public void onClick(@NonNull final View view) {
|
||||||
disposables);
|
playOnPopup(
|
||||||
}
|
context,
|
||||||
}, timestampStart, timestampEnd, 0);
|
relatedInfo.getUrl(),
|
||||||
|
relatedInfo.getService(),
|
||||||
|
timestampMatchDTO.seconds(),
|
||||||
|
disposables);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
timestampMatchDTO.timestampStart(),
|
||||||
|
timestampMatchDTO.timestampEnd(),
|
||||||
|
0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
package org.schabi.newpipe.util.external_communication;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts timestamps.
|
||||||
|
*/
|
||||||
|
public final class TimestampExtractor {
|
||||||
|
public static final Pattern TIMESTAMPS_PATTERN = Pattern.compile(
|
||||||
|
"(?:^|(?!:)\\W)(?:([0-5]?[0-9]):)?([0-5]?[0-9]):([0-5][0-9])(?=$|(?!:)\\W)");
|
||||||
|
|
||||||
|
private TimestampExtractor() {
|
||||||
|
// No impl pls
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get's a single timestamp from a matcher.
|
||||||
|
*
|
||||||
|
* @param timestampMatches The matcher which was created using {@link #TIMESTAMPS_PATTERN}
|
||||||
|
* @param baseText The text where the pattern was applied to /
|
||||||
|
* where the matcher is based upon
|
||||||
|
* @return If a match occurred: a {@link TimestampMatchDTO} filled with information.<br/>
|
||||||
|
* If not <code>null</code>.
|
||||||
|
*/
|
||||||
|
public static TimestampMatchDTO getTimestampFromMatcher(
|
||||||
|
final Matcher timestampMatches,
|
||||||
|
final String baseText) {
|
||||||
|
int timestampStart = timestampMatches.start(1);
|
||||||
|
if (timestampStart == -1) {
|
||||||
|
timestampStart = timestampMatches.start(2);
|
||||||
|
}
|
||||||
|
final int timestampEnd = timestampMatches.end(3);
|
||||||
|
|
||||||
|
final String parsedTimestamp = baseText.substring(timestampStart, timestampEnd);
|
||||||
|
final String[] timestampParts = parsedTimestamp.split(":");
|
||||||
|
|
||||||
|
final int seconds;
|
||||||
|
if (timestampParts.length == 3) { // timestamp format: XX:XX:XX
|
||||||
|
seconds = Integer.parseInt(timestampParts[0]) * 3600 // hours
|
||||||
|
+ Integer.parseInt(timestampParts[1]) * 60 // minutes
|
||||||
|
+ Integer.parseInt(timestampParts[2]); // seconds
|
||||||
|
} else if (timestampParts.length == 2) { // timestamp format: XX:XX
|
||||||
|
seconds = Integer.parseInt(timestampParts[0]) * 60 // minutes
|
||||||
|
+ Integer.parseInt(timestampParts[1]); // seconds
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TimestampMatchDTO(timestampStart, timestampEnd, seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TimestampMatchDTO {
|
||||||
|
private final int timestampStart;
|
||||||
|
private final int timestampEnd;
|
||||||
|
private final int seconds;
|
||||||
|
|
||||||
|
public TimestampMatchDTO(
|
||||||
|
final int timestampStart,
|
||||||
|
final int timestampEnd,
|
||||||
|
final int seconds) {
|
||||||
|
this.timestampStart = timestampStart;
|
||||||
|
this.timestampEnd = timestampEnd;
|
||||||
|
this.seconds = seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int timestampStart() {
|
||||||
|
return timestampStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int timestampEnd() {
|
||||||
|
return timestampEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int seconds() {
|
||||||
|
return seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package us.shandian.giga.get
|
package us.shandian.giga.get
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.schabi.newpipe.extractor.MediaFormat
|
import org.schabi.newpipe.extractor.MediaFormat
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream
|
import org.schabi.newpipe.extractor.stream.AudioStream
|
||||||
import org.schabi.newpipe.extractor.stream.Stream
|
import org.schabi.newpipe.extractor.stream.Stream
|
||||||
|
|
|
@ -53,6 +53,8 @@ import us.shandian.giga.get.DownloadMission;
|
||||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||||
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import us.shandian.giga.postprocessing.Postprocessing;
|
import us.shandian.giga.postprocessing.Postprocessing;
|
||||||
import us.shandian.giga.service.DownloadManager.NetworkState;
|
import us.shandian.giga.service.DownloadManager.NetworkState;
|
||||||
|
|
||||||
|
@ -494,7 +496,8 @@ public class DownloadManagerService extends Service {
|
||||||
.setContentIntent(makePendingIntent(ACTION_OPEN_DOWNLOADS_FINISHED));
|
.setContentIntent(makePendingIntent(ACTION_OPEN_DOWNLOADS_FINISHED));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloadDoneCount < 1) {
|
downloadDoneCount++;
|
||||||
|
if (downloadDoneCount == 1) {
|
||||||
downloadDoneList.append(name);
|
downloadDoneList.append(name);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
@ -503,9 +506,9 @@ public class DownloadManagerService extends Service {
|
||||||
downloadDoneNotification.setContentTitle(null);
|
downloadDoneNotification.setContentTitle(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadDoneNotification.setContentText(getString(R.string.download_finished));
|
downloadDoneNotification.setContentText(Localization.downloadCount(this, downloadDoneCount));
|
||||||
downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle()
|
downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle()
|
||||||
.setBigContentTitle(getString(R.string.download_finished))
|
.setBigContentTitle(Localization.downloadCount(this, downloadDoneCount))
|
||||||
.bigText(name)
|
.bigText(name)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -513,12 +516,11 @@ public class DownloadManagerService extends Service {
|
||||||
downloadDoneList.append(name);
|
downloadDoneList.append(name);
|
||||||
|
|
||||||
downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle().bigText(downloadDoneList));
|
downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle().bigText(downloadDoneList));
|
||||||
downloadDoneNotification.setContentTitle(getString(R.string.download_finished_more, String.valueOf(downloadDoneCount + 1)));
|
downloadDoneNotification.setContentTitle(Localization.downloadCount(this, downloadDoneCount));
|
||||||
downloadDoneNotification.setContentText(downloadDoneList);
|
downloadDoneNotification.setContentText(downloadDoneList);
|
||||||
}
|
}
|
||||||
|
|
||||||
mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
|
mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
|
||||||
downloadDoneCount++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyFailedDownload(DownloadMission mission) {
|
public void notifyFailedDownload(DownloadMission mission) {
|
||||||
|
|
|
@ -47,6 +47,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.error.ErrorActivity;
|
import org.schabi.newpipe.error.ErrorActivity;
|
||||||
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.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
|
@ -580,7 +581,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.setNegativeButton(R.string.finish, (dialog, which) -> dialog.cancel())
|
builder.setNegativeButton(R.string.ok, (dialog, which) -> dialog.cancel())
|
||||||
.setTitle(mission.storage.getName())
|
.setTitle(mission.storage.getName())
|
||||||
.create()
|
.create()
|
||||||
.show();
|
.show();
|
||||||
|
@ -622,7 +623,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||||
}
|
}
|
||||||
applyChanges();
|
applyChanges();
|
||||||
|
|
||||||
String msg = String.format(mContext.getString(R.string.deleted_downloads), mHidden.size());
|
String msg = Localization.deletedDownloadCount(mContext, mHidden.size());
|
||||||
mSnackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
|
mSnackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
|
||||||
mSnackbar.setAction(R.string.undo, s -> {
|
mSnackbar.setAction(R.string.undo, s -> {
|
||||||
Iterator<Mission> i = mHidden.iterator();
|
Iterator<Mission> i = mHidden.iterator();
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:interpolator="@android:interpolator/decelerate_quint">
|
|
||||||
|
|
||||||
<alpha
|
|
||||||
android:duration="150"
|
|
||||||
android:fromAlpha="0.00"
|
|
||||||
android:toAlpha="1.0" />
|
|
||||||
</set>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:interpolator="@android:interpolator/accelerate_quint">
|
|
||||||
|
|
||||||
<alpha
|
|
||||||
android:duration="350"
|
|
||||||
android:fromAlpha="1.0"
|
|
||||||
android:toAlpha="0.00" />
|
|
||||||
</set>
|
|
Before Width: | Height: | Size: 358 B After Width: | Height: | Size: 342 B |
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 252 B |
Before Width: | Height: | Size: 480 B After Width: | Height: | Size: 450 B |
Before Width: | Height: | Size: 581 B After Width: | Height: | Size: 525 B |
Before Width: | Height: | Size: 511 B After Width: | Height: | Size: 486 B |
Before Width: | Height: | Size: 244 B After Width: | Height: | Size: 230 B |