mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-10-14 07:12:16 +02:00
Compare commits
154 Commits
frankenpip
...
v0.27.7
Author | SHA1 | Date | |
---|---|---|---|
![]() |
81b4e3f970 | ||
![]() |
ef068e1eca | ||
![]() |
8407b5aefd | ||
![]() |
b6aa07545a | ||
![]() |
1dcb1953ba | ||
![]() |
c6e1721884 | ||
![]() |
94684fe380 | ||
![]() |
398a2f55ce | ||
![]() |
1f7b3b5b06 | ||
![]() |
909ed616c4 | ||
![]() |
dd223af28d | ||
![]() |
dbee8d8128 | ||
![]() |
b62a09b5b3 | ||
![]() |
87317c6faf | ||
![]() |
53b599b042 | ||
![]() |
21df24abfd | ||
![]() |
ca4592a935 | ||
![]() |
3fc487310b | ||
![]() |
056809cb0d | ||
![]() |
a60bb3e7af | ||
![]() |
ecd3f6c2ee | ||
![]() |
70ff47b810 | ||
![]() |
b8e050f6c4 | ||
![]() |
46d0bc1004 | ||
![]() |
e7fe84f2c7 | ||
![]() |
2b183a0576 | ||
![]() |
f856bd9306 | ||
![]() |
0066b322e1 | ||
![]() |
3bdae81c0a | ||
![]() |
6010c4ea7f | ||
![]() |
690b3410e9 | ||
![]() |
ba86ce137b | ||
![]() |
410c01547c | ||
![]() |
47263f5254 | ||
![]() |
01bf855015 | ||
![]() |
ebf3008729 | ||
![]() |
33ecfb757e | ||
![]() |
ffe26d882b | ||
![]() |
83f8141fe7 | ||
![]() |
9253640fae | ||
![]() |
8b5aa5cd9b | ||
![]() |
58393ad4ef | ||
![]() |
977f7e28b5 | ||
![]() |
99e77249de | ||
![]() |
a955408053 | ||
![]() |
86203d6800 | ||
![]() |
edd19641ac | ||
![]() |
65749cbac0 | ||
![]() |
658ddfc921 | ||
![]() |
f7d0fd545d | ||
![]() |
27e6be792f | ||
![]() |
3fc0147f47 | ||
![]() |
c6b05c6094 | ||
![]() |
240a2fe36b | ||
![]() |
de46e3abb3 | ||
![]() |
70748fa0bc | ||
![]() |
3847b32c11 | ||
![]() |
9054575f6c | ||
![]() |
0dca92dd59 | ||
![]() |
b19cd00dba | ||
![]() |
88d8d90bbd | ||
![]() |
c569f08a32 | ||
![]() |
246fc034c1 | ||
![]() |
52942ffd30 | ||
![]() |
e4b0245530 | ||
![]() |
c6b8bcf0f4 | ||
![]() |
e31a8ad7a2 | ||
![]() |
b21981a9c7 | ||
![]() |
f9711a3402 | ||
![]() |
df941670a8 | ||
![]() |
57e66b17c6 | ||
![]() |
d298a12533 | ||
![]() |
a79bc3db14 | ||
![]() |
661e6155c1 | ||
![]() |
12558172d1 | ||
![]() |
dc3f55674f | ||
![]() |
acf2e88cb3 | ||
![]() |
726c12e934 | ||
![]() |
33b96d238a | ||
![]() |
213f49f5c4 | ||
![]() |
16c79c8219 | ||
![]() |
14081505cd | ||
![]() |
ebd4880188 | ||
![]() |
ffcba175ff | ||
![]() |
c7848e5e86 | ||
![]() |
6d686b93cb | ||
![]() |
2cc38f59d3 | ||
![]() |
8bf24e6b14 | ||
![]() |
10e7a5cf9c | ||
![]() |
9f2f219613 | ||
![]() |
841471bf85 | ||
![]() |
06d25b0310 | ||
![]() |
3c8d81a3c2 | ||
![]() |
cf870add49 | ||
![]() |
a962e6d633 | ||
![]() |
970ef9357b | ||
![]() |
4ba961fe7a | ||
![]() |
e6c03bf4ac | ||
![]() |
1f39523429 | ||
![]() |
b43031fb99 | ||
![]() |
986cd52da0 | ||
![]() |
bcd4579187 | ||
![]() |
6fe417abc6 | ||
![]() |
a229ab68d5 | ||
![]() |
544b30290d | ||
![]() |
cb300724da | ||
![]() |
0ac5a269ff | ||
![]() |
0009613608 | ||
![]() |
7c18d4dd01 | ||
![]() |
fe1c538f9c | ||
![]() |
f08e07873a | ||
![]() |
1193b02ca1 | ||
![]() |
c0b36b86b9 | ||
![]() |
66ec596f67 | ||
![]() |
90404a23ce | ||
![]() |
64ad05d813 | ||
![]() |
734b6e2b67 | ||
![]() |
94f992a2e2 | ||
![]() |
c8550695aa | ||
![]() |
cdac50bab3 | ||
![]() |
23961548c0 | ||
![]() |
ba1e9c8e1b | ||
![]() |
f4baf4628e | ||
![]() |
05a87da827 | ||
![]() |
fef40014a0 | ||
![]() |
1996c1176c | ||
![]() |
0190bcee25 | ||
![]() |
1ed4928f40 | ||
![]() |
63bc982cb2 | ||
![]() |
3a286515f2 | ||
![]() |
2e96b65fda | ||
![]() |
2482615460 | ||
![]() |
9384365061 | ||
![]() |
b1d4b66aa6 | ||
![]() |
ea0da5fdbd | ||
![]() |
d80b6a759c | ||
![]() |
8106ba68b5 | ||
![]() |
ee15a72e4f | ||
![]() |
2eb256799d | ||
![]() |
0cf4732d8a | ||
![]() |
53edd054aa | ||
![]() |
678f0a786a | ||
![]() |
b14f65804d | ||
![]() |
781a69d60d | ||
![]() |
eb9f300e60 | ||
![]() |
063568b620 | ||
![]() |
07c63f794e | ||
![]() |
26dd86e967 | ||
![]() |
71822a47a5 | ||
![]() |
e1bf67c676 | ||
![]() |
c02ceda22f | ||
![]() |
cf21b9feaf | ||
![]() |
b74cab6642 | ||
![]() |
8267d325ed |
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -6,7 +6,7 @@ NewPipe contribution guidelines
|
|||||||
## Crash reporting
|
## Crash reporting
|
||||||
|
|
||||||
Report crashes through the **automated crash report system** of NewPipe.
|
Report crashes through the **automated crash report system** of NewPipe.
|
||||||
This way all the data needed for debugging is included in your bugreport for GitHub.
|
This way all the data needed for debugging is included in your bug report for GitHub.
|
||||||
You'll see *exactly* what is sent, be able to add **your comments**, and then send it.
|
You'll see *exactly* what is sent, be able to add **your comments**, and then send it.
|
||||||
|
|
||||||
## Issue reporting/feature requests
|
## Issue reporting/feature requests
|
||||||
|
38
.github/workflows/build-release-apk.yml
vendored
Normal file
38
.github/workflows/build-release-apk.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: "Build unsigned release APK on master"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: 'master'
|
||||||
|
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '21'
|
||||||
|
cache: 'gradle'
|
||||||
|
|
||||||
|
- name: "Build release APK"
|
||||||
|
run: ./gradlew assembleRelease --stacktrace
|
||||||
|
|
||||||
|
- name: "Rename APK"
|
||||||
|
run: |
|
||||||
|
VERSION_NAME="$(jq -r ".elements[0].versionName" "app/build/outputs/apk/release/output-metadata.json")"
|
||||||
|
echo "Version name: $VERSION_NAME" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo '```json' >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
cat "app/build/outputs/apk/release/output-metadata.json" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
# assume there is only one APK in that folder
|
||||||
|
mv app/build/outputs/apk/release/*.apk "app/build/outputs/apk/release/NewPipe_v$VERSION_NAME.apk"
|
||||||
|
|
||||||
|
- name: "Upload APK"
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: app
|
||||||
|
path: app/build/outputs/apk/release/*.apk
|
4
.github/workflows/image-minimizer.js
vendored
4
.github/workflows/image-minimizer.js
vendored
@@ -32,8 +32,8 @@ module.exports = async ({github, context}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Regex for finding images (simple variant) 
|
// Regex for finding images (simple variant) 
|
||||||
const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[(.*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
|
const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[([^\]]*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
|
||||||
const REGEX_ASSETS_IMAGE_LOCKUP = /\!\[(.*)\]\((https:\/\/github\.com\/[-\w\d]+\/[-\w\d]+\/assets\/\d+\/[\-0-9a-f]{32,512})\)/gm;
|
const REGEX_ASSETS_IMAGE_LOCKUP = /\!\[([^\]]*)\]\((https:\/\/github\.com\/[-\w\d]+\/[-\w\d]+\/assets\/\d+\/[\-0-9a-f]{32,512})\)/gm;
|
||||||
|
|
||||||
// Check if we found something
|
// Check if we found something
|
||||||
let foundSimpleImages = REGEX_USER_CONTENT_IMAGE_LOOKUP.test(initialBody)
|
let foundSimpleImages = REGEX_USER_CONTENT_IMAGE_LOOKUP.test(initialBody)
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,7 +10,6 @@ captures/
|
|||||||
*.class
|
*.class
|
||||||
app/debug/
|
app/debug/
|
||||||
app/release/
|
app/release/
|
||||||
.kotlin/
|
|
||||||
|
|
||||||
# vscode / eclipse files
|
# vscode / eclipse files
|
||||||
*.classpath
|
*.classpath
|
||||||
|
243
app/build.gradle
243
app/build.gradle
File diff suppressed because it is too large
Load Diff
@@ -1,48 +0,0 @@
|
|||||||
tasks.register('checkDependenciesOrder') {
|
|
||||||
group = 'verification'
|
|
||||||
description = 'Checks that each section in libs.versions.toml is sorted alphabetically'
|
|
||||||
|
|
||||||
def tomlFile = file('../gradle/libs.versions.toml')
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
if (!tomlFile.exists()) {
|
|
||||||
throw new GradleException('TOML file not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
def lines = tomlFile.readLines()
|
|
||||||
def nonSortedBlocks = []
|
|
||||||
def currentBlock = []
|
|
||||||
def prevLine = ''
|
|
||||||
def prevIndex = 0
|
|
||||||
|
|
||||||
lines.eachWithIndex { line, lineIndex ->
|
|
||||||
if (line.trim() && !line.startsWith('#')) {
|
|
||||||
if (line.startsWith('[')) {
|
|
||||||
prevLine = ''
|
|
||||||
} else {
|
|
||||||
def currIndex = lineIndex + 1
|
|
||||||
if (prevLine > line) {
|
|
||||||
if (currentBlock && currentBlock[-1] == "${prevIndex}: ${prevLine}") {
|
|
||||||
currentBlock.add("${currIndex}: ${line}")
|
|
||||||
} else {
|
|
||||||
if (!currentBlock.isEmpty()) {
|
|
||||||
nonSortedBlocks.add(currentBlock)
|
|
||||||
currentBlock = []
|
|
||||||
}
|
|
||||||
currentBlock.add("${prevIndex}: ${prevLine}")
|
|
||||||
currentBlock.add("${currIndex}: ${line}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prevLine = line
|
|
||||||
prevIndex = lineIndex + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentBlock.isEmpty()) {
|
|
||||||
nonSortedBlocks.add(currentBlock)
|
|
||||||
throw new GradleException("The following lines were not sorted:\n" +
|
|
||||||
nonSortedBlocks.collect { it.join("\n") }.join("\n\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
7
app/proguard-rules.pro
vendored
7
app/proguard-rules.pro
vendored
@@ -5,10 +5,17 @@
|
|||||||
|
|
||||||
## Rules for NewPipeExtractor
|
## Rules for NewPipeExtractor
|
||||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||||
|
## Rules for Rhino and Rhino Engine
|
||||||
|
-keep class org.mozilla.javascript.* { *; }
|
||||||
-keep class org.mozilla.javascript.** { *; }
|
-keep class org.mozilla.javascript.** { *; }
|
||||||
|
-keep class org.mozilla.javascript.engine.** { *; }
|
||||||
-keep class org.mozilla.classfile.ClassFileWriter
|
-keep class org.mozilla.classfile.ClassFileWriter
|
||||||
-dontwarn org.mozilla.javascript.JavaToJSONConverters
|
-dontwarn org.mozilla.javascript.JavaToJSONConverters
|
||||||
-dontwarn org.mozilla.javascript.tools.**
|
-dontwarn org.mozilla.javascript.tools.**
|
||||||
|
-keep class javax.script.** { *; }
|
||||||
|
-dontwarn javax.script.**
|
||||||
|
-keep class jdk.dynalink.** { *; }
|
||||||
|
-dontwarn jdk.dynalink.**
|
||||||
|
|
||||||
## Rules for ExoPlayer
|
## Rules for ExoPlayer
|
||||||
-keep class com.google.android.exoplayer2.** { *; }
|
-keep class com.google.android.exoplayer2.** { *; }
|
||||||
|
@@ -77,11 +77,6 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/settings" />
|
android:label="@string/settings" />
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".settings.SettingsV2Activity"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="@string/settings" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".about.AboutActivity"
|
android:name=".about.AboutActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
127
app/src/main/assets/po_token.html
Normal file
127
app/src/main/assets/po_token.html
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en"><head><title></title><script>
|
||||||
|
/**
|
||||||
|
* Factory method to create and load a BotGuardClient instance.
|
||||||
|
* @param options - Configuration options for the BotGuardClient.
|
||||||
|
* @returns A promise that resolves to a loaded BotGuardClient instance.
|
||||||
|
*/
|
||||||
|
function loadBotGuard(challengeData) {
|
||||||
|
this.vm = this[challengeData.globalName];
|
||||||
|
this.program = challengeData.program;
|
||||||
|
this.vmFunctions = {};
|
||||||
|
this.syncSnapshotFunction = null;
|
||||||
|
|
||||||
|
if (!this.vm)
|
||||||
|
throw new Error('[BotGuardClient]: VM not found in the global object');
|
||||||
|
|
||||||
|
if (!this.vm.a)
|
||||||
|
throw new Error('[BotGuardClient]: Could not load program');
|
||||||
|
|
||||||
|
const vmFunctionsCallback = function (
|
||||||
|
asyncSnapshotFunction,
|
||||||
|
shutdownFunction,
|
||||||
|
passEventFunction,
|
||||||
|
checkCameraFunction
|
||||||
|
) {
|
||||||
|
this.vmFunctions = {
|
||||||
|
asyncSnapshotFunction: asyncSnapshotFunction,
|
||||||
|
shutdownFunction: shutdownFunction,
|
||||||
|
passEventFunction: passEventFunction,
|
||||||
|
checkCameraFunction: checkCameraFunction
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
this.syncSnapshotFunction = this.vm.a(this.program, vmFunctionsCallback, true, this.userInteractionElement, function () {/** no-op */ }, [ [], [] ])[0]
|
||||||
|
|
||||||
|
// an asynchronous function runs in the background and it will eventually call
|
||||||
|
// `vmFunctionsCallback`, however we need to manually tell JavaScript to pass
|
||||||
|
// control to the things running in the background by interrupting this async
|
||||||
|
// function in any way, e.g. with a delay of 1ms. The loop is most probably not
|
||||||
|
// needed but is there just because.
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
i = 0
|
||||||
|
refreshIntervalId = setInterval(function () {
|
||||||
|
if (!!this.vmFunctions.asyncSnapshotFunction) {
|
||||||
|
resolve(this)
|
||||||
|
clearInterval(refreshIntervalId);
|
||||||
|
}
|
||||||
|
if (i >= 10000) {
|
||||||
|
reject("asyncSnapshotFunction is null even after 10 seconds")
|
||||||
|
clearInterval(refreshIntervalId);
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}, 1);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a snapshot asynchronously.
|
||||||
|
* @returns The snapshot result.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const result = await botguard.snapshot({
|
||||||
|
* contentBinding: {
|
||||||
|
* c: "a=6&a2=10&b=SZWDwKVIuixOp7Y4euGTgwckbJA&c=1729143849&d=1&t=7200&c1a=1&c6a=1&c6b=1&hh=HrMb5mRWTyxGJphDr0nW2Oxonh0_wl2BDqWuLHyeKLo",
|
||||||
|
* e: "ENGAGEMENT_TYPE_VIDEO_LIKE",
|
||||||
|
* encryptedVideoId: "P-vC09ZJcnM"
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* console.log(result);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
function snapshot(args) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (!this.vmFunctions.asyncSnapshotFunction)
|
||||||
|
return reject(new Error('[BotGuardClient]: Async snapshot function not found'));
|
||||||
|
|
||||||
|
this.vmFunctions.asyncSnapshotFunction(function (response) { resolve(response) }, [
|
||||||
|
args.contentBinding,
|
||||||
|
args.signedTimestamp,
|
||||||
|
args.webPoSignalOutput,
|
||||||
|
args.skipPrivacyBuffer
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runBotGuard(challengeData) {
|
||||||
|
const interpreterJavascript = challengeData.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
|
||||||
|
|
||||||
|
if (interpreterJavascript) {
|
||||||
|
new Function(interpreterJavascript)();
|
||||||
|
} else throw new Error('Could not load VM');
|
||||||
|
|
||||||
|
const webPoSignalOutput = [];
|
||||||
|
return loadBotGuard({
|
||||||
|
globalName: challengeData.globalName,
|
||||||
|
globalObj: this,
|
||||||
|
program: challengeData.program
|
||||||
|
}).then(function (botguard) {
|
||||||
|
return botguard.snapshot({ webPoSignalOutput: webPoSignalOutput })
|
||||||
|
}).then(function (botguardResponse) {
|
||||||
|
return { webPoSignalOutput: webPoSignalOutput, botguardResponse: botguardResponse }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function obtainPoToken(webPoSignalOutput, integrityToken, identifier) {
|
||||||
|
const getMinter = webPoSignalOutput[0];
|
||||||
|
|
||||||
|
if (!getMinter)
|
||||||
|
throw new Error('PMD:Undefined');
|
||||||
|
|
||||||
|
const mintCallback = getMinter(integrityToken);
|
||||||
|
|
||||||
|
if (!(mintCallback instanceof Function))
|
||||||
|
throw new Error('APF:Failed');
|
||||||
|
|
||||||
|
const result = mintCallback(identifier);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
throw new Error('YNJ:Undefined');
|
||||||
|
|
||||||
|
if (!(result instanceof Uint8Array))
|
||||||
|
throw new Error('ODM:Invalid');
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
</script></head><body></body></html>
|
275
app/src/main/java/org/schabi/newpipe/App.java
Normal file
275
app/src/main/java/org/schabi/newpipe/App.java
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,22 +0,0 @@
|
|||||||
package org.schabi.newpipe
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
class AppModule {
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun providesSharedPreference(@ApplicationContext context: Context): SharedPreferences {
|
|
||||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -29,7 +29,7 @@ import okhttp3.ResponseBody;
|
|||||||
|
|
||||||
public final class DownloaderImpl extends Downloader {
|
public final class DownloaderImpl extends Downloader {
|
||||||
public static final String USER_AGENT =
|
public static final String USER_AGENT =
|
||||||
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0";
|
||||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
|
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
|
||||||
"youtube_restricted_mode_key";
|
"youtube_restricted_mode_key";
|
||||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
||||||
@@ -48,11 +48,6 @@ public final class DownloaderImpl extends Downloader {
|
|||||||
this.mCookies = new HashMap<>();
|
this.mCookies = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public OkHttpClient getClient() {
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's recommended to call exactly once in the entire lifetime of the application.
|
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||||
*
|
*
|
||||||
@@ -142,7 +137,8 @@ public final class DownloaderImpl extends Downloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||||
.method(httpMethod, requestBody).url(url)
|
.method(httpMethod, requestBody)
|
||||||
|
.url(url)
|
||||||
.addHeader("User-Agent", USER_AGENT);
|
.addHeader("User-Agent", USER_AGENT);
|
||||||
|
|
||||||
final String cookies = getCookies(url);
|
final String cookies = getCookies(url);
|
||||||
@@ -150,38 +146,33 @@ public final class DownloaderImpl extends Downloader {
|
|||||||
requestBuilder.addHeader("Cookie", cookies);
|
requestBuilder.addHeader("Cookie", cookies);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final Map.Entry<String, List<String>> pair : headers.entrySet()) {
|
headers.forEach((headerName, headerValueList) -> {
|
||||||
final String headerName = pair.getKey();
|
requestBuilder.removeHeader(headerName);
|
||||||
final List<String> headerValueList = pair.getValue();
|
headerValueList.forEach(headerValue ->
|
||||||
|
requestBuilder.addHeader(headerName, headerValue));
|
||||||
|
});
|
||||||
|
|
||||||
if (headerValueList.size() > 1) {
|
try (
|
||||||
requestBuilder.removeHeader(headerName);
|
okhttp3.Response response = client.newCall(requestBuilder.build()).execute()
|
||||||
for (final String headerValue : headerValueList) {
|
) {
|
||||||
requestBuilder.addHeader(headerName, headerValue);
|
if (response.code() == 429) {
|
||||||
}
|
throw new ReCaptchaException("reCaptcha Challenge requested", url);
|
||||||
} else if (headerValueList.size() == 1) {
|
|
||||||
requestBuilder.header(headerName, headerValueList.get(0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String responseBodyToReturn = null;
|
||||||
|
try (ResponseBody body = response.body()) {
|
||||||
|
if (body != null) {
|
||||||
|
responseBodyToReturn = body.string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String latestUrl = response.request().url().toString();
|
||||||
|
return new Response(
|
||||||
|
response.code(),
|
||||||
|
response.message(),
|
||||||
|
response.headers().toMultimap(),
|
||||||
|
responseBodyToReturn,
|
||||||
|
latestUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
final okhttp3.Response response = client.newCall(requestBuilder.build()).execute();
|
|
||||||
|
|
||||||
if (response.code() == 429) {
|
|
||||||
response.close();
|
|
||||||
|
|
||||||
throw new ReCaptchaException("reCaptcha Challenge requested", url);
|
|
||||||
}
|
|
||||||
|
|
||||||
final ResponseBody body = response.body();
|
|
||||||
String responseBodyToReturn = null;
|
|
||||||
|
|
||||||
if (body != null) {
|
|
||||||
responseBodyToReturn = body.string();
|
|
||||||
}
|
|
||||||
|
|
||||||
final String latestUrl = response.request().url().toString();
|
|
||||||
return new Response(response.code(), response.message(), response.headers().toMultimap(),
|
|
||||||
responseBodyToReturn, latestUrl);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -44,6 +44,7 @@ import android.widget.FrameLayout;
|
|||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
@@ -51,6 +52,7 @@ import androidx.core.app.ActivityCompat;
|
|||||||
import androidx.core.view.GravityCompat;
|
import androidx.core.view.GravityCompat;
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentContainerView;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
@@ -64,11 +66,13 @@ import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
|||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.MainFragment;
|
import org.schabi.newpipe.fragments.MainFragment;
|
||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||||
|
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
|
||||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
@@ -88,17 +92,13 @@ import org.schabi.newpipe.util.SerializedCache;
|
|||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.views.FocusOverlayView;
|
import org.schabi.newpipe.views.FocusOverlayView;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint;
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
private static final String TAG = "MainActivity";
|
private static final String TAG = "MainActivity";
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
@@ -121,7 +121,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private static final int ITEM_ID_DOWNLOADS = -4;
|
private static final int ITEM_ID_DOWNLOADS = -4;
|
||||||
private static final int ITEM_ID_HISTORY = -5;
|
private static final int ITEM_ID_HISTORY = -5;
|
||||||
private static final int ITEM_ID_SETTINGS = 0;
|
private static final int ITEM_ID_SETTINGS = 0;
|
||||||
private static final int ITEM_ID_ABOUT = 1;
|
private static final int ITEM_ID_DONATION = 1;
|
||||||
|
private static final int ITEM_ID_ABOUT = 2;
|
||||||
|
|
||||||
private static final int ORDER = 0;
|
private static final int ORDER = 0;
|
||||||
|
|
||||||
@@ -171,7 +172,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
NotificationWorker.initialize(this);
|
NotificationWorker.initialize(this);
|
||||||
}
|
}
|
||||||
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
||||||
&& !App.getInstance().isFirstRun()
|
&& !App.getApp().isFirstRun()
|
||||||
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||||
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
||||||
}
|
}
|
||||||
@@ -181,7 +182,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
protected void onPostCreate(final Bundle savedInstanceState) {
|
protected void onPostCreate(final Bundle savedInstanceState) {
|
||||||
super.onPostCreate(savedInstanceState);
|
super.onPostCreate(savedInstanceState);
|
||||||
|
|
||||||
final App app = App.getInstance();
|
final App app = App.getApp();
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||||
|
|
||||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
||||||
@@ -263,6 +264,10 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
drawerLayoutBinding.navigation.getMenu()
|
drawerLayoutBinding.navigation.getMenu()
|
||||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||||
.setIcon(R.drawable.ic_settings);
|
.setIcon(R.drawable.ic_settings);
|
||||||
|
drawerLayoutBinding.navigation.getMenu()
|
||||||
|
.add(R.id.menu_options_about_group, ITEM_ID_DONATION, ORDER,
|
||||||
|
R.string.donation_title)
|
||||||
|
.setIcon(R.drawable.volunteer_activism_ic);
|
||||||
drawerLayoutBinding.navigation.getMenu()
|
drawerLayoutBinding.navigation.getMenu()
|
||||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||||
.setIcon(R.drawable.ic_info_outline);
|
.setIcon(R.drawable.ic_info_outline);
|
||||||
@@ -338,6 +343,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
case ITEM_ID_SETTINGS:
|
case ITEM_ID_SETTINGS:
|
||||||
NavigationHelper.openSettings(this);
|
NavigationHelper.openSettings(this);
|
||||||
break;
|
break;
|
||||||
|
case ITEM_ID_DONATION:
|
||||||
|
ShareUtils.openUrlInBrowser(this, getString(R.string.donation_url));
|
||||||
|
break;
|
||||||
case ITEM_ID_ABOUT:
|
case ITEM_ID_ABOUT:
|
||||||
NavigationHelper.openAbout(this);
|
NavigationHelper.openAbout(this);
|
||||||
break;
|
break;
|
||||||
@@ -558,27 +566,39 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
|
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
|
||||||
// interacts with a fragment inside fragment_holder so all back presses should be
|
// interacts with a fragment inside fragment_holder so all back presses should be
|
||||||
// handled by it
|
// handled by it
|
||||||
final var fragmentManager = getSupportFragmentManager();
|
|
||||||
|
|
||||||
if (bottomSheetHiddenOrCollapsed()) {
|
if (bottomSheetHiddenOrCollapsed()) {
|
||||||
final var fragment = fragmentManager.findFragmentById(R.id.fragment_holder);
|
final FragmentManager fm = getSupportFragmentManager();
|
||||||
|
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
||||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||||
// delegate the back press to it
|
// delegate the back press to it
|
||||||
if (fragment instanceof BackPressable backPressable && backPressable.onBackPressed()) {
|
if (fragment instanceof BackPressable) {
|
||||||
return;
|
if (((BackPressable) fragment).onBackPressed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (fragment instanceof CommentRepliesFragment) {
|
||||||
|
// expand DetailsFragment if CommentRepliesFragment was opened
|
||||||
|
// to show the top level comments again
|
||||||
|
// Expand DetailsFragment if CommentRepliesFragment was opened
|
||||||
|
// and no other CommentRepliesFragments are on top of the back stack
|
||||||
|
// to show the top level comments again.
|
||||||
|
openDetailFragmentFromCommentReplies(fm, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
final var player = fragmentManager.findFragmentById(R.id.fragment_player_holder);
|
final Fragment fragmentPlayer = getSupportFragmentManager()
|
||||||
|
.findFragmentById(R.id.fragment_player_holder);
|
||||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||||
// delegate the back press to it
|
// delegate the back press to it
|
||||||
if (player instanceof BackPressable backPressable && !backPressable.onBackPressed()) {
|
if (fragmentPlayer instanceof BackPressable) {
|
||||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
if (!((BackPressable) fragmentPlayer).onBackPressed()) {
|
||||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
||||||
|
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fragmentManager.getBackStackEntryCount() == 1) {
|
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||||
finish();
|
finish();
|
||||||
} else {
|
} else {
|
||||||
super.onBackPressed();
|
super.onBackPressed();
|
||||||
@@ -637,9 +657,15 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
private void onHomeButtonPressed() {
|
private void onHomeButtonPressed() {
|
||||||
final var fm = getSupportFragmentManager();
|
final FragmentManager fm = getSupportFragmentManager();
|
||||||
|
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
||||||
|
|
||||||
if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
if (fragment instanceof CommentRepliesFragment) {
|
||||||
|
// Expand DetailsFragment if CommentRepliesFragment was opened
|
||||||
|
// and no other CommentRepliesFragments are on top of the back stack
|
||||||
|
// to show the top level comments again.
|
||||||
|
openDetailFragmentFromCommentReplies(fm, true);
|
||||||
|
} else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
||||||
// If search fragment wasn't found in the backstack go to the main fragment
|
// If search fragment wasn't found in the backstack go to the main fragment
|
||||||
NavigationHelper.gotoMainFragment(fm);
|
NavigationHelper.gotoMainFragment(fm);
|
||||||
}
|
}
|
||||||
@@ -837,6 +863,68 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void openDetailFragmentFromCommentReplies(
|
||||||
|
@NonNull final FragmentManager fm,
|
||||||
|
final boolean popBackStack
|
||||||
|
) {
|
||||||
|
// obtain the name of the fragment under the replies fragment that's going to be popped
|
||||||
|
@Nullable final String fragmentUnderEntryName;
|
||||||
|
if (fm.getBackStackEntryCount() < 2) {
|
||||||
|
fragmentUnderEntryName = null;
|
||||||
|
} else {
|
||||||
|
fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
|
||||||
|
.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// the root comment is the comment for which the user opened the replies page
|
||||||
|
@Nullable final CommentRepliesFragment repliesFragment =
|
||||||
|
(CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG);
|
||||||
|
@Nullable final CommentsInfoItem rootComment =
|
||||||
|
repliesFragment == null ? null : repliesFragment.getCommentsInfoItem();
|
||||||
|
|
||||||
|
// sometimes this function pops the backstack, other times it's handled by the system
|
||||||
|
if (popBackStack) {
|
||||||
|
fm.popBackStackImmediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// only expand the bottom sheet back if there are no more nested comment replies fragments
|
||||||
|
// stacked under the one that is currently being popped
|
||||||
|
if (CommentRepliesFragment.TAG.equals(fragmentUnderEntryName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final BottomSheetBehavior<FragmentContainerView> behavior = BottomSheetBehavior
|
||||||
|
.from(mainBinding.fragmentPlayerHolder);
|
||||||
|
// do not return to the comment if the details fragment was closed
|
||||||
|
if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// scroll to the root comment once the bottom sheet expansion animation is finished
|
||||||
|
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
||||||
|
@Override
|
||||||
|
public void onStateChanged(@NonNull final View bottomSheet,
|
||||||
|
final int newState) {
|
||||||
|
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
|
final Fragment detailFragment = fm.findFragmentById(
|
||||||
|
R.id.fragment_player_holder);
|
||||||
|
if (detailFragment instanceof VideoDetailFragment && rootComment != null) {
|
||||||
|
// should always be the case
|
||||||
|
((VideoDetailFragment) detailFragment).scrollToComment(rootComment);
|
||||||
|
}
|
||||||
|
behavior.removeBottomSheetCallback(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
|
||||||
|
// not needed, listener is removed once the sheet is expanded
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean bottomSheetHiddenOrCollapsed() {
|
private boolean bottomSheetHiddenOrCollapsed() {
|
||||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
||||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
||||||
@@ -845,4 +933,5 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
return sheetState == BottomSheetBehavior.STATE_HIDDEN
|
return sheetState == BottomSheetBehavior.STATE_HIDDEN
|
||||||
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED;
|
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,203 @@
|
|||||||
package org.schabi.newpipe.about
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import android.view.LayoutInflater
|
||||||
import androidx.activity.enableEdgeToEdge
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar
|
import org.schabi.newpipe.databinding.ActivityAboutBinding
|
||||||
import org.schabi.newpipe.ui.screens.AboutScreen
|
import org.schabi.newpipe.databinding.FragmentAboutBinding
|
||||||
import org.schabi.newpipe.ui.theme.AppTheme
|
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
class AboutActivity : AppCompatActivity() {
|
class AboutActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Localization.assureCorrectAppLanguage(this)
|
Localization.assureCorrectAppLanguage(this)
|
||||||
enableEdgeToEdge()
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeHelper.setTheme(this)
|
||||||
|
title = getString(R.string.title_activity_about)
|
||||||
|
|
||||||
setContent {
|
val aboutBinding = ActivityAboutBinding.inflate(layoutInflater)
|
||||||
AppTheme {
|
setContentView(aboutBinding.root)
|
||||||
ScaffoldWithToolbar(
|
setSupportActionBar(aboutBinding.aboutToolbar)
|
||||||
title = stringResource(R.string.title_activity_about),
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
onBackClick = { onBackPressedDispatcher.onBackPressed() }
|
|
||||||
) { padding ->
|
// Create the adapter that will return a fragment for each of the three
|
||||||
AboutScreen(padding)
|
// primary sections of the activity.
|
||||||
}
|
val mAboutStateAdapter = AboutStateAdapter(this)
|
||||||
|
// Set up the ViewPager with the sections adapter.
|
||||||
|
aboutBinding.aboutViewPager2.adapter = mAboutStateAdapter
|
||||||
|
TabLayoutMediator(
|
||||||
|
aboutBinding.aboutTabLayout,
|
||||||
|
aboutBinding.aboutViewPager2
|
||||||
|
) { tab, position ->
|
||||||
|
tab.setText(mAboutStateAdapter.getPageTitle(position))
|
||||||
|
}.attach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
if (item.itemId == android.R.id.home) {
|
||||||
|
finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A placeholder fragment containing a simple view.
|
||||||
|
*/
|
||||||
|
class AboutFragment : Fragment() {
|
||||||
|
private fun Button.openLink(@StringRes url: Int) {
|
||||||
|
setOnClickListener {
|
||||||
|
ShareUtils.openUrlInApp(context, requireContext().getString(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
FragmentAboutBinding.inflate(inflater, container, false).apply {
|
||||||
|
aboutAppVersion.text = BuildConfig.VERSION_NAME
|
||||||
|
aboutGithubLink.openLink(R.string.github_url)
|
||||||
|
aboutDonationLink.openLink(R.string.donation_url)
|
||||||
|
aboutWebsiteLink.openLink(R.string.website_url)
|
||||||
|
aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
|
||||||
|
faqLink.openLink(R.string.faq_url)
|
||||||
|
return root
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [FragmentStateAdapter] that returns a fragment corresponding to
|
||||||
|
* one of the sections/tabs/pages.
|
||||||
|
*/
|
||||||
|
private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
||||||
|
private val posAbout = 0
|
||||||
|
private val posLicense = 1
|
||||||
|
private val totalCount = 2
|
||||||
|
|
||||||
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
return when (position) {
|
||||||
|
posAbout -> AboutFragment()
|
||||||
|
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
|
||||||
|
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
// Show 2 total pages.
|
||||||
|
return totalCount
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPageTitle(position: Int): Int {
|
||||||
|
return when (position) {
|
||||||
|
posAbout -> R.string.tab_about
|
||||||
|
posLicense -> R.string.tab_licenses
|
||||||
|
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* List of all software components.
|
||||||
|
*/
|
||||||
|
private val SOFTWARE_COMPONENTS = arrayListOf(
|
||||||
|
SoftwareComponent(
|
||||||
|
"ACRA", "2013", "Kevin Gaudin",
|
||||||
|
"https://github.com/ACRA/acra", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"AndroidX", "2005 - 2011", "The Android Open Source Project",
|
||||||
|
"https://developer.android.com/jetpack", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"ExoPlayer", "2014 - 2020", "Google, Inc.",
|
||||||
|
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"GigaGet", "2014 - 2015", "Peter Cai",
|
||||||
|
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Groupie", "2016", "Lisa Wray",
|
||||||
|
"https://github.com/lisawray/groupie", StandardLicenses.MIT
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Android-State", "2018", "Evernote",
|
||||||
|
"https://github.com/Evernote/android-state", StandardLicenses.EPL1
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Bridge", "2021", "Livefront",
|
||||||
|
"https://github.com/livefront/bridge", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Jsoup", "2009 - 2020", "Jonathan Hedley",
|
||||||
|
"https://github.com/jhy/jsoup", StandardLicenses.MIT
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Markwon", "2019", "Dimitry Ivanov",
|
||||||
|
"https://github.com/noties/Markwon", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Material Components for Android", "2016 - 2020", "Google, Inc.",
|
||||||
|
"https://github.com/material-components/material-components-android",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
|
||||||
|
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
|
||||||
|
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"OkHttp", "2019", "Square, Inc.",
|
||||||
|
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Picasso", "2013", "Square, Inc.",
|
||||||
|
"https://square.github.io/picasso/", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
|
||||||
|
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"ProcessPhoenix", "2015", "Jake Wharton",
|
||||||
|
"https://github.com/JakeWharton/ProcessPhoenix", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"RxAndroid", "2015", "The RxAndroid authors",
|
||||||
|
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"RxBinding", "2015", "Jake Wharton",
|
||||||
|
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"RxJava", "2016 - 2020", "RxJava Contributors",
|
||||||
|
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"SearchPreference", "2018", "ByteHamster",
|
||||||
|
"https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
11
app/src/main/java/org/schabi/newpipe/about/License.kt
Normal file
11
app/src/main/java/org/schabi/newpipe/about/License.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for storing information about a software license.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
class License(val name: String, val abbreviation: String, val filename: String) : Parcelable, Serializable
|
140
app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt
Normal file
140
app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Base64
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.webkit.WebView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
||||||
|
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
||||||
|
import org.schabi.newpipe.ktx.parcelableArrayList
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment containing the software licenses.
|
||||||
|
*/
|
||||||
|
class LicenseFragment : Fragment() {
|
||||||
|
private lateinit var softwareComponents: List<SoftwareComponent>
|
||||||
|
private var activeSoftwareComponent: SoftwareComponent? = null
|
||||||
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
softwareComponents = arguments?.parcelableArrayList<SoftwareComponent>(ARG_COMPONENTS)!!
|
||||||
|
.sortedBy { it.name } // Sort components by name
|
||||||
|
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
compositeDisposable.dispose()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
|
||||||
|
binding.licensesAppReadLicense.setOnClickListener {
|
||||||
|
compositeDisposable.add(
|
||||||
|
showLicense(NEWPIPE_SOFTWARE_COMPONENT)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (component in softwareComponents) {
|
||||||
|
val componentBinding = ItemSoftwareComponentBinding
|
||||||
|
.inflate(inflater, container, false)
|
||||||
|
componentBinding.name.text = component.name
|
||||||
|
componentBinding.copyright.text = getString(
|
||||||
|
R.string.copyright,
|
||||||
|
component.years,
|
||||||
|
component.copyrightOwner,
|
||||||
|
component.license.abbreviation
|
||||||
|
)
|
||||||
|
val root: View = componentBinding.root
|
||||||
|
root.tag = component
|
||||||
|
root.setOnClickListener {
|
||||||
|
compositeDisposable.add(
|
||||||
|
showLicense(component)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
binding.licensesSoftwareComponents.addView(root)
|
||||||
|
registerForContextMenu(root)
|
||||||
|
}
|
||||||
|
activeSoftwareComponent?.let { compositeDisposable.add(showLicense(it)) }
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||||
|
super.onSaveInstanceState(savedInstanceState)
|
||||||
|
activeSoftwareComponent?.let { savedInstanceState.putSerializable(SOFTWARE_COMPONENT_KEY, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLicense(
|
||||||
|
softwareComponent: SoftwareComponent
|
||||||
|
): Disposable {
|
||||||
|
return if (context == null) {
|
||||||
|
Disposable.empty()
|
||||||
|
} else {
|
||||||
|
val context = requireContext()
|
||||||
|
activeSoftwareComponent = softwareComponent
|
||||||
|
Observable.fromCallable { getFormattedLicense(context, softwareComponent.license) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { formattedLicense ->
|
||||||
|
val webViewData = Base64.encodeToString(
|
||||||
|
formattedLicense.toByteArray(), Base64.NO_PADDING
|
||||||
|
)
|
||||||
|
val webView = WebView(context)
|
||||||
|
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
||||||
|
|
||||||
|
Localization.assureCorrectAppLanguage(context)
|
||||||
|
val builder = AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(softwareComponent.name)
|
||||||
|
.setView(webView)
|
||||||
|
.setOnCancelListener { activeSoftwareComponent = null }
|
||||||
|
.setOnDismissListener { activeSoftwareComponent = null }
|
||||||
|
.setPositiveButton(R.string.done) { dialog, _ -> dialog.dismiss() }
|
||||||
|
|
||||||
|
if (softwareComponent != NEWPIPE_SOFTWARE_COMPONENT) {
|
||||||
|
builder.setNeutralButton(R.string.open_website_license) { _, _ ->
|
||||||
|
ShareUtils.openUrlInApp(requireContext(), softwareComponent.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ARG_COMPONENTS = "components"
|
||||||
|
private const val SOFTWARE_COMPONENT_KEY = "ACTIVE_SOFTWARE_COMPONENT"
|
||||||
|
private val NEWPIPE_SOFTWARE_COMPONENT = SoftwareComponent(
|
||||||
|
"NewPipe",
|
||||||
|
"2014-2023",
|
||||||
|
"Team NewPipe",
|
||||||
|
"https://newpipe.net/",
|
||||||
|
StandardLicenses.GPL3,
|
||||||
|
BuildConfig.VERSION_NAME
|
||||||
|
)
|
||||||
|
|
||||||
|
fun newInstance(softwareComponents: ArrayList<SoftwareComponent>): LicenseFragment {
|
||||||
|
val fragment = LicenseFragment()
|
||||||
|
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,52 @@
|
|||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context the context to use
|
||||||
|
* @param license the license
|
||||||
|
* @return String which contains a HTML formatted license page
|
||||||
|
* styled according to the context's theme
|
||||||
|
*/
|
||||||
|
fun getFormattedLicense(context: Context, license: License): String {
|
||||||
|
try {
|
||||||
|
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
||||||
|
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||||
|
.replace("</head>", "<style>${getLicenseStylesheet(context)}</style></head>")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw IllegalArgumentException("Could not get license file: ${license.filename}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context the Android context
|
||||||
|
* @return String which is a CSS stylesheet according to the context's theme
|
||||||
|
*/
|
||||||
|
fun getLicenseStylesheet(context: Context): String {
|
||||||
|
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
||||||
|
val licenseBackgroundColor = getHexRGBColor(
|
||||||
|
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
||||||
|
)
|
||||||
|
val licenseTextColor = getHexRGBColor(
|
||||||
|
context, if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color
|
||||||
|
)
|
||||||
|
val youtubePrimaryColor = getHexRGBColor(
|
||||||
|
context, if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color
|
||||||
|
)
|
||||||
|
return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" +
|
||||||
|
"a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast R.color to a hexadecimal color value.
|
||||||
|
*
|
||||||
|
* @param context the context to use
|
||||||
|
* @param color the color number from R.color
|
||||||
|
* @return a six characters long String with hexadecimal RGB values
|
||||||
|
*/
|
||||||
|
fun getHexRGBColor(context: Context, color: Int): String {
|
||||||
|
return context.getString(color).substring(3)
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class SoftwareComponent
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(
|
||||||
|
val name: String,
|
||||||
|
val years: String,
|
||||||
|
val copyrightOwner: String,
|
||||||
|
val link: String,
|
||||||
|
val license: License,
|
||||||
|
val version: String? = null
|
||||||
|
) : Parcelable, Serializable
|
@@ -0,0 +1,21 @@
|
|||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class containing information about standard software licenses.
|
||||||
|
*/
|
||||||
|
object StandardLicenses {
|
||||||
|
@JvmField
|
||||||
|
val GPL3 = License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html")
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val APACHE2 = License("Apache License, Version 2.0", "ALv2", "apache2.html")
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val MPL2 = License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html")
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val MIT = License("MIT License", "MIT", "mit.html")
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val EPL1 = License("Eclipse Public License, Version 1.0", "EPL 1.0", "epl1.html")
|
||||||
|
}
|
@@ -154,6 +154,6 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||||||
+ " AND :streamUrl = :streamUrl"
|
+ " AND :streamUrl = :streamUrl"
|
||||||
|
|
||||||
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
||||||
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX + ", " + PLAYLIST_NAME)
|
||||||
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,6 @@ import androidx.room.Query
|
|||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import io.reactivex.rxjava3.core.Completable
|
import io.reactivex.rxjava3.core.Completable
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import io.reactivex.rxjava3.core.Maybe
|
|
||||||
import 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
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
||||||
@@ -28,7 +27,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
|||||||
abstract override fun listByService(serviceId: Int): Flowable<List<StreamEntity>>
|
abstract override fun listByService(serviceId: Int): Flowable<List<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): Maybe<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")
|
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
|
||||||
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
|
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
|
||||||
|
@@ -1,8 +1,5 @@
|
|||||||
package org.schabi.newpipe.database.stream.dao;
|
package org.schabi.newpipe.database.stream.dao;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
|
||||||
|
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Insert;
|
import androidx.room.Insert;
|
||||||
import androidx.room.OnConflictStrategy;
|
import androidx.room.OnConflictStrategy;
|
||||||
@@ -15,7 +12,9 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
import io.reactivex.rxjava3.core.Maybe;
|
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
|
public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
|
||||||
@@ -33,7 +32,7 @@ public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
Maybe<StreamStateEntity> getState(long streamId);
|
Flowable<List<StreamStateEntity>> getState(long streamId);
|
||||||
|
|
||||||
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
int deleteState(long streamId);
|
int deleteState(long streamId);
|
||||||
|
@@ -26,7 +26,7 @@ import org.schabi.newpipe.util.Localization;
|
|||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -67,10 +67,6 @@ public class ErrorActivity extends AppCompatActivity {
|
|||||||
public static final String ERROR_GITHUB_ISSUE_URL =
|
public static final String ERROR_GITHUB_ISSUE_URL =
|
||||||
"https://github.com/TeamNewPipe/NewPipe/issues";
|
"https://github.com/TeamNewPipe/NewPipe/issues";
|
||||||
|
|
||||||
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER =
|
|
||||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
|
||||||
|
|
||||||
|
|
||||||
private ErrorInfo errorInfo;
|
private ErrorInfo errorInfo;
|
||||||
private String currentTimeStamp;
|
private String currentTimeStamp;
|
||||||
|
|
||||||
@@ -107,7 +103,9 @@ public class ErrorActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
// important add guru meditation
|
// important add guru meditation
|
||||||
addGuruMeditation();
|
addGuruMeditation();
|
||||||
currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now());
|
// print current time, as zoned ISO8601 timestamp
|
||||||
|
final ZonedDateTime now = ZonedDateTime.now();
|
||||||
|
currentTimeStamp = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
||||||
|
|
||||||
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
|
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
|
||||||
openPrivacyPolicyDialog(this, "EMAIL"));
|
openPrivacyPolicyDialog(this, "EMAIL"));
|
||||||
@@ -250,6 +248,9 @@ public class ErrorActivity extends AppCompatActivity {
|
|||||||
.append("\n* __Content Language:__ ").append(getContentLanguageString())
|
.append("\n* __Content Language:__ ").append(getContentLanguageString())
|
||||||
.append("\n* __App Language:__ ").append(getAppLanguage())
|
.append("\n* __App Language:__ ").append(getAppLanguage())
|
||||||
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
||||||
|
.append("\n* __Timestamp:__ ").append(currentTimeStamp)
|
||||||
|
.append("\n* __Package:__ ").append(getPackageName())
|
||||||
|
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
||||||
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
|
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
|
||||||
.append("\n* __OS:__ ").append(getOsString()).append("\n");
|
.append("\n* __OS:__ ").append(getOsString()).append("\n");
|
||||||
|
|
||||||
|
@@ -6,11 +6,9 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.compose.ui.platform.ComposeView;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.BaseFragment;
|
import org.schabi.newpipe.BaseFragment;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
|
||||||
|
|
||||||
public class EmptyFragment extends BaseFragment {
|
public class EmptyFragment extends BaseFragment {
|
||||||
private static final String SHOW_MESSAGE = "SHOW_MESSAGE";
|
private static final String SHOW_MESSAGE = "SHOW_MESSAGE";
|
||||||
@@ -28,10 +26,8 @@ public class EmptyFragment extends BaseFragment {
|
|||||||
final Bundle savedInstanceState) {
|
final Bundle savedInstanceState) {
|
||||||
final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE);
|
final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE);
|
||||||
final View view = inflater.inflate(R.layout.fragment_empty, container, false);
|
final View view = inflater.inflate(R.layout.fragment_empty, container, false);
|
||||||
|
view.findViewById(R.id.empty_state_view).setVisibility(
|
||||||
final ComposeView composeView = view.findViewById(R.id.empty_state_view);
|
showMessage ? View.VISIBLE : View.GONE);
|
||||||
EmptyStateUtil.setEmptyStateComposable(composeView);
|
|
||||||
composeView.setVisibility(showMessage ? View.VISIBLE : View.GONE);
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ import android.graphics.Color;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.TypedValue;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
@@ -44,8 +45,6 @@ import org.schabi.newpipe.fragments.detail.TabAdapter;
|
|||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
|
|
||||||
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
|
||||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
@@ -54,14 +53,13 @@ import org.schabi.newpipe.util.NavigationHelper;
|
|||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.image.CoilHelper;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import coil3.util.CoilUtils;
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Observable;
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
@@ -75,6 +73,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
implements StateSaver.WriteRead {
|
implements StateSaver.WriteRead {
|
||||||
|
|
||||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||||
|
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||||
@@ -200,11 +199,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
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);
|
||||||
|
|
||||||
EmptyStateUtil.setEmptyStateComposable(
|
|
||||||
binding.emptyStateView,
|
|
||||||
EmptyStateSpec.Companion.getContentNotSupported()
|
|
||||||
);
|
|
||||||
|
|
||||||
tabAdapter = new TabAdapter(getChildFragmentManager());
|
tabAdapter = new TabAdapter(getChildFragmentManager());
|
||||||
binding.viewPager.setAdapter(tabAdapter);
|
binding.viewPager.setAdapter(tabAdapter);
|
||||||
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||||
@@ -582,9 +576,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
@Override
|
@Override
|
||||||
public void showLoading() {
|
public void showLoading() {
|
||||||
super.showLoading();
|
super.showLoading();
|
||||||
CoilUtils.dispose(binding.channelAvatarView);
|
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
||||||
CoilUtils.dispose(binding.channelBannerImage);
|
|
||||||
CoilUtils.dispose(binding.subChannelAvatarView);
|
|
||||||
animate(binding.channelSubscribeButton, false, 100);
|
animate(binding.channelSubscribeButton, false, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,15 +587,17 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
|
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
|
||||||
|
|
||||||
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
|
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
|
||||||
CoilHelper.INSTANCE.loadBanner(binding.channelBannerImage, result.getBanners());
|
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
|
||||||
|
.into(binding.channelBannerImage);
|
||||||
} else {
|
} else {
|
||||||
// do not waste space for the banner, if the user disabled images or there is not one
|
// do not waste space for the banner, if the user disabled images or there is not one
|
||||||
binding.channelBannerImage.setImageDrawable(null);
|
binding.channelBannerImage.setImageDrawable(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
CoilHelper.INSTANCE.loadAvatar(binding.channelAvatarView, result.getAvatars());
|
PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG)
|
||||||
CoilHelper.INSTANCE.loadAvatar(binding.subChannelAvatarView,
|
.into(binding.channelAvatarView);
|
||||||
result.getParentChannelAvatars());
|
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG)
|
||||||
|
.into(binding.subChannelAvatarView);
|
||||||
|
|
||||||
binding.channelTitleView.setText(result.getName());
|
binding.channelTitleView.setText(result.getName());
|
||||||
binding.channelSubscriberView.setVisibility(View.VISIBLE);
|
binding.channelSubscriberView.setVisibility(View.VISIBLE);
|
||||||
@@ -651,6 +645,8 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.emptyStateView.setVisibility(View.VISIBLE);
|
binding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
||||||
|
binding.channelKaomoji.setText("(︶︹︺)");
|
||||||
|
binding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -26,7 +26,6 @@ import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
|||||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
|
|
||||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
@@ -80,12 +79,6 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
|
|||||||
return inflater.inflate(R.layout.fragment_channel_tab, container, false);
|
return inflater.inflate(R.layout.fragment_channel_tab, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(rootView, savedInstanceState);
|
|
||||||
EmptyStateUtil.setEmptyStateComposable(rootView.findViewById(R.id.empty_state_view));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
|
@@ -0,0 +1,171 @@
|
|||||||
|
package org.schabi.newpipe.fragments.list.comments;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
|
import androidx.core.text.HtmlCompat;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.CommentRepliesHeaderBinding;
|
||||||
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
|
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
|
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||||
|
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
|
||||||
|
public final class CommentRepliesFragment
|
||||||
|
extends BaseListInfoFragment<CommentsInfoItem, CommentRepliesInfo> {
|
||||||
|
|
||||||
|
public static final String TAG = CommentRepliesFragment.class.getSimpleName();
|
||||||
|
|
||||||
|
@State
|
||||||
|
CommentsInfoItem commentsInfoItem; // the comment to show replies of
|
||||||
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Constructors and lifecycle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
// only called by the Android framework, after which readFrom is called and restores all data
|
||||||
|
public CommentRepliesFragment() {
|
||||||
|
super(UserAction.REQUESTED_COMMENT_REPLIES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommentRepliesFragment(@NonNull final CommentsInfoItem commentsInfoItem) {
|
||||||
|
this();
|
||||||
|
this.commentsInfoItem = commentsInfoItem;
|
||||||
|
// setting "" as title since the title will be properly set right after
|
||||||
|
setInitialData(commentsInfoItem.getServiceId(), commentsInfoItem.getUrl(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup container,
|
||||||
|
@Nullable final Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_comments, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
disposables.clear();
|
||||||
|
super.onDestroyView();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
|
return () -> {
|
||||||
|
final CommentRepliesHeaderBinding binding = CommentRepliesHeaderBinding
|
||||||
|
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||||
|
final CommentsInfoItem item = commentsInfoItem;
|
||||||
|
|
||||||
|
// load the author avatar
|
||||||
|
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar);
|
||||||
|
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
|
||||||
|
? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
// setup author name and comment date
|
||||||
|
binding.authorName.setText(item.getUploaderName());
|
||||||
|
binding.uploadDate.setText(Localization.relativeTimeOrTextual(
|
||||||
|
getContext(), item.getUploadDate(), item.getTextualUploadDate()));
|
||||||
|
binding.authorTouchArea.setOnClickListener(
|
||||||
|
v -> NavigationHelper.openCommentAuthorIfPresent(requireActivity(), item));
|
||||||
|
|
||||||
|
// setup like count, hearted and pinned
|
||||||
|
binding.thumbsUpCount.setText(
|
||||||
|
Localization.likeCount(requireContext(), item.getLikeCount()));
|
||||||
|
// for heartImage goneMarginEnd was used, but there is no way to tell ConstraintLayout
|
||||||
|
// not to use a different margin only when both the next two views are gone
|
||||||
|
((ConstraintLayout.LayoutParams) binding.thumbsUpCount.getLayoutParams())
|
||||||
|
.setMarginEnd(DeviceUtils.dpToPx(
|
||||||
|
(item.isHeartedByUploader() || item.isPinned() ? 8 : 16),
|
||||||
|
requireContext()));
|
||||||
|
binding.heartImage.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
||||||
|
binding.pinnedImage.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
// setup comment content
|
||||||
|
TextLinkifier.fromDescription(binding.commentContent, item.getCommentText(),
|
||||||
|
HtmlCompat.FROM_HTML_MODE_LEGACY, getServiceById(item.getServiceId()),
|
||||||
|
item.getUrl(), disposables, null);
|
||||||
|
|
||||||
|
return binding.getRoot();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// State saving
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(final Queue<Object> objectsToSave) {
|
||||||
|
super.writeTo(objectsToSave);
|
||||||
|
objectsToSave.add(commentsInfoItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
||||||
|
super.readFrom(savedObjects);
|
||||||
|
commentsInfoItem = (CommentsInfoItem) savedObjects.poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Data loading
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<CommentRepliesInfo> loadResult(final boolean forceLoad) {
|
||||||
|
return Single.fromCallable(() -> new CommentRepliesInfo(commentsInfoItem,
|
||||||
|
// the reply count string will be shown as the activity title
|
||||||
|
Localization.replyCount(requireContext(), commentsInfoItem.getReplyCount())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
|
||||||
|
// commentsInfoItem.getUrl() should contain the url of the original
|
||||||
|
// ListInfo<CommentsInfoItem>, which should be the stream url
|
||||||
|
return ExtractorHelper.getMoreCommentItems(
|
||||||
|
serviceId, commentsInfoItem.getUrl(), currentNextPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ItemViewMode getItemViewMode() {
|
||||||
|
return ItemViewMode.LIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the comment to which the replies are shown
|
||||||
|
*/
|
||||||
|
public CommentsInfoItem getCommentsInfoItem() {
|
||||||
|
return commentsInfoItem;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
package org.schabi.newpipe.fragments.list.comments;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public final class CommentRepliesInfo extends ListInfo<CommentsInfoItem> {
|
||||||
|
/**
|
||||||
|
* This class is used to wrap the comment replies page into a ListInfo object.
|
||||||
|
*
|
||||||
|
* @param comment the comment from which to get replies
|
||||||
|
* @param name will be shown as the fragment title
|
||||||
|
*/
|
||||||
|
public CommentRepliesInfo(final CommentsInfoItem comment, final String name) {
|
||||||
|
super(comment.getServiceId(),
|
||||||
|
new ListLinkHandler("", "", "", Collections.emptyList(), null), name);
|
||||||
|
setNextPage(comment.getReplies());
|
||||||
|
setRelatedItems(Collections.emptyList()); // since it must be non-null
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user