mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-28 02:33:03 +02:00
Compare commits
178 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1075ded170 | |||
| 80bb15f3fb | |||
| 27a86a67f0 | |||
| 284b2a24f8 | |||
| 854d1506a6 | |||
| 811fd4e73e | |||
| 335988aa67 | |||
| 29a54fbed4 | |||
| 3a11d0d9d1 | |||
| bda534e485 | |||
| 09fd4c0881 | |||
| 1667866a35 | |||
| 035125d0f8 | |||
| 1bb0cdc405 | |||
| 86019c80a1 | |||
| 8c640d3def | |||
| 7ed1e8a28b | |||
| 3dcfe8c340 | |||
| 042ced81ef | |||
| b37f48380b | |||
| 0a02169782 | |||
| f12e4390f3 | |||
| 82ab45d04e | |||
| 7f77c39296 | |||
| 99eee4f6ee | |||
| 68886502d1 | |||
| 26461c21c4 | |||
| 300466f722 | |||
| 961710cc8b | |||
| eba995f87d | |||
| a67244e79a | |||
| 70502a7651 | |||
| 36b4f5b41d | |||
| def39ba397 | |||
| 49d59f4466 | |||
| 1c9becc2ba | |||
| 1cde591061 | |||
| 8ac18f053c | |||
| 56bdae9ff1 | |||
| 74ddfe9f0e | |||
| acb9500e2a | |||
| 45f621763a | |||
| 0abc65a9bd | |||
| 6d6309973e | |||
| 92ec085d25 | |||
| 767a8befaa | |||
| 09763320dd | |||
| 27fb2997f9 | |||
| 0f46bc5888 | |||
| dccf4fcf3c | |||
| da7fef1ecd | |||
| 58a89a00ef | |||
| f2efc603ba | |||
| efe074d272 | |||
| 8a9efd3a0f | |||
| 251302b9c3 | |||
| 5cdac1405e | |||
| 894e400819 | |||
| 565ea7cb8b | |||
| 9fa3e22d2e | |||
| 5548783337 | |||
| 0dca8798cb | |||
| d902306fe4 | |||
| baa2a4fcf3 | |||
| 8be7ad9f68 | |||
| 992cbcb3a0 | |||
| e0857aea9b | |||
| 50cd0723c9 | |||
| 4c4b322682 | |||
| 7cff8568c0 | |||
| 801c646a09 | |||
| df4ec87613 | |||
| b08a79b7cb | |||
| 396e9f9f43 | |||
| 0e5a87a911 | |||
| 64d72f6d10 | |||
| 5b40da109b | |||
| 949294952f | |||
| 40a058e369 | |||
| a070d78dd9 | |||
| 105ac538bb | |||
| ce2029774e | |||
| 50c63d7e8d | |||
| d3534080d7 | |||
| b5025193a5 | |||
| 3f85b7ed78 | |||
| 98d008ef6c | |||
| 20eb53fc38 | |||
| 1ea7b307fa | |||
| f18571e0b2 | |||
| 70872d429a | |||
| cbf3db6e30 | |||
| 0be0dcfadc | |||
| abd226c33d | |||
| 89dbdc99a0 | |||
| f89ed18a49 | |||
| 77ac2b537c | |||
| 8ab03b6b66 | |||
| dad70e57c6 | |||
| eb9c6c8330 | |||
| 68da797f4d | |||
| 25948dd296 | |||
| 10d39d6ed1 | |||
| 85e8e674dd | |||
| 0d70392bf0 | |||
| f89b074d28 | |||
| ee2af411aa | |||
| 9ffbe6dd03 | |||
| 0ae6ac2fac | |||
| fd835cc54e | |||
| 07f3140038 | |||
| 3e753d70de | |||
| d578c47975 | |||
| b7a61425ca | |||
| 727f977672 | |||
| fc9d5eeb27 | |||
| f17e147b4e | |||
| 1c569b465b | |||
| 6289c85bd5 | |||
| 098599853b | |||
| 68d11f6d58 | |||
| 74f6b9aa62 | |||
| fc2aba0120 | |||
| e4f51bb130 | |||
| 9e9d26c752 | |||
| 5c5dd3af44 | |||
| 4433364cd8 | |||
| 2c957d7188 | |||
| f229f4ed1f | |||
| e8d1f73e29 | |||
| dd2cf18cb2 | |||
| 5355602577 | |||
| 8cc82e4d16 | |||
| d6468ba283 | |||
| 4b5ed38175 | |||
| 75eb7359de | |||
| fd519d48cf | |||
| 6f1866ac27 | |||
| 0dc0f07785 | |||
| bae8cb7bc4 | |||
| d5a696289b | |||
| 75ef7085eb | |||
| 347ef855b3 | |||
| aac19aef86 | |||
| 33efc5c21d | |||
| fc7001c295 | |||
| 9b68394f70 | |||
| e2ef8c2593 | |||
| 551bfe44ac | |||
| 6fbfa98ad3 | |||
| 9b97e05e3b | |||
| 62a2f42d68 | |||
| da44e86163 | |||
| 682b86330e | |||
| c9ba8a09e2 | |||
| 7d19c2357c | |||
| 64030a038c | |||
| 87d93c2ed8 | |||
| 9d9ad52535 | |||
| b10cf6a323 | |||
| 4407e82d8a | |||
| 3113dc53a6 | |||
| bd25276720 | |||
| 29d3a9986e | |||
| bce93b8e0f | |||
| 9a950958f9 | |||
| b6676e7763 | |||
| 35fe093e5c | |||
| 7cad4fbe07 | |||
| 240772790d | |||
| d659ecc518 | |||
| 7d8bb20b71 | |||
| 1cf5f776d5 | |||
| 137ba85538 | |||
| 642d218c54 | |||
| 26b5470200 | |||
| 547fe7bc13 | |||
| 7c70e58129 |
@@ -1,2 +1,6 @@
|
|||||||
aar/* filter=lfs diff=lfs merge=lfs -text
|
aar/* filter=lfs diff=lfs merge=lfs -text
|
||||||
app/aar/* filter=lfs diff=lfs merge=lfs -text
|
app/aar/* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
app/src/main/jniLibs/arm64-v8a filter=lfs diff=lfs merge=lfs -text
|
||||||
|
app/src/main/jniLibs/armeabi-v7a filter=lfs diff=lfs merge=lfs -text
|
||||||
|
app/src/main/jniLibs/x86 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
app/src/main/jniLibs/x86_64 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|||||||
@@ -64,12 +64,6 @@
|
|||||||
[submodule "app/src/stable/assets/sources/bilibili"]
|
[submodule "app/src/stable/assets/sources/bilibili"]
|
||||||
path = app/src/stable/assets/sources/bilibili
|
path = app/src/stable/assets/sources/bilibili
|
||||||
url = ../plugins/bilibili.git
|
url = ../plugins/bilibili.git
|
||||||
[submodule "app/src/stable/assets/sources/spotify"]
|
|
||||||
path = app/src/stable/assets/sources/spotify
|
|
||||||
url = ../plugins/spotify.git
|
|
||||||
[submodule "app/src/unstable/assets/sources/spotify"]
|
|
||||||
path = app/src/unstable/assets/sources/spotify
|
|
||||||
url = ../plugins/spotify.git
|
|
||||||
[submodule "app/src/stable/assets/sources/bitchute"]
|
[submodule "app/src/stable/assets/sources/bitchute"]
|
||||||
path = app/src/stable/assets/sources/bitchute
|
path = app/src/stable/assets/sources/bitchute
|
||||||
url = ../plugins/bitchute.git
|
url = ../plugins/bitchute.git
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:ea10d3c5562c9f449a4e89e9c3dfcf881ed79a952f3409bc005bcc62c2cf4b81
|
|
||||||
size 65512557
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:22c06ca0d1a5808b2fc0a12227d5915b3126bc0b9b1305cf6bab855f2ec6fcbb
|
||||||
|
size 36133152
|
||||||
+44
-45
@@ -1,8 +1,8 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.21'
|
||||||
id 'org.ajoberstar.grgit' version '5.2.2'
|
id 'org.ajoberstar.grgit' version '5.3.3'
|
||||||
id 'com.google.protobuf'
|
id 'com.google.protobuf'
|
||||||
id 'kotlin-parcelize'
|
id 'kotlin-parcelize'
|
||||||
id 'com.google.devtools.ksp'
|
id 'com.google.devtools.ksp'
|
||||||
@@ -97,7 +97,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 28
|
minSdk 28
|
||||||
targetSdk 34
|
targetSdk 36
|
||||||
versionCode gitVersionCode
|
versionCode gitVersionCode
|
||||||
versionName gitVersionName
|
versionName gitVersionName
|
||||||
|
|
||||||
@@ -146,6 +146,7 @@ android {
|
|||||||
}
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
|
jniLibs.srcDirs = ['src/main/jniLibs']
|
||||||
assets {
|
assets {
|
||||||
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
|
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
|
||||||
}
|
}
|
||||||
@@ -155,85 +156,83 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
//implementation 'com.google.dagger:dagger:2.48'
|
//implementation 'com.google.dagger:dagger:2.48'
|
||||||
implementation 'androidx.test:monitor:1.7.2'
|
implementation 'androidx.test:monitor:1.8.0'
|
||||||
implementation 'com.google.android.material:material:1.12.0'
|
implementation 'com.google.android.material:material:1.13.0'
|
||||||
//annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
//annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||||
|
|
||||||
//Core
|
//Core
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.17.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.7.1'
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.documentfile:documentfile:1.1.0'
|
||||||
|
|
||||||
//Images
|
//Images
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
annotationProcessor 'com.github.bumptech.glide:compiler:5.0.5'
|
||||||
implementation 'com.github.bumptech.glide:glide:4.16.0'
|
implementation 'com.github.bumptech.glide:glide:5.0.5'
|
||||||
|
|
||||||
//Async
|
//Async
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
|
||||||
|
|
||||||
//HTTP
|
//HTTP
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.11.0"
|
implementation "com.squareup.okhttp3:okhttp:5.3.0"
|
||||||
|
|
||||||
//JSON
|
//JSON
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" //Used for structured json
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0" //Used for structured json
|
||||||
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
implementation 'com.google.code.gson:gson:2.13.2' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
||||||
|
|
||||||
//JS
|
//JS
|
||||||
implementation("com.caoccao.javet:javet-android:3.0.2")
|
implementation 'com.caoccao.javet:javet-v8-android:4.1.5'
|
||||||
//implementation 'com.caoccao.javet:javet-v8-android:4.1.4' //Change after extensive testing the freezing edge cases are solved.
|
|
||||||
|
|
||||||
//Exoplayer
|
//Exoplayer
|
||||||
implementation 'androidx.media3:media3-exoplayer:1.2.1'
|
implementation 'androidx.media3:media3-exoplayer:1.8.0'
|
||||||
implementation 'androidx.media3:media3-exoplayer-dash:1.2.1'
|
implementation 'androidx.media3:media3-exoplayer-dash:1.8.0'
|
||||||
implementation 'androidx.media3:media3-ui:1.2.1'
|
implementation 'androidx.media3:media3-ui:1.8.0'
|
||||||
implementation 'androidx.media3:media3-exoplayer-hls:1.2.1'
|
implementation 'androidx.media3:media3-exoplayer-hls:1.8.0'
|
||||||
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1'
|
implementation 'androidx.media3:media3-exoplayer-rtsp:1.8.0'
|
||||||
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1'
|
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.8.0'
|
||||||
implementation 'androidx.media3:media3-transformer:1.2.1'
|
implementation 'androidx.media3:media3-transformer:1.8.0'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.9.6'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.9.6'
|
||||||
implementation 'androidx.media:media:1.7.0'
|
implementation 'androidx.media:media:1.7.1'
|
||||||
|
|
||||||
//Other
|
//Other
|
||||||
implementation 'org.jsoup:jsoup:1.15.3'
|
implementation 'org.jsoup:jsoup:1.21.2'
|
||||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation fileTree(dir: 'aar', include: ['*.aar'])
|
implementation fileTree(dir: 'aar', include: ['*.aar'])
|
||||||
implementation 'com.arthenica:smart-exception-java:0.2.1'
|
implementation 'com.arthenica:smart-exception-java:0.2.1'
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
implementation 'org.jetbrains.kotlin:kotlin-reflect:2.2.0'
|
||||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||||
implementation 'com.google.zxing:core:3.4.1'
|
implementation 'com.google.zxing:core:3.5.3'
|
||||||
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
||||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||||
|
|
||||||
//Protobuf
|
//Protobuf
|
||||||
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
|
implementation 'com.google.protobuf:protobuf-javalite:4.33.0'
|
||||||
|
|
||||||
implementation 'com.polycentric.core:app:1.0'
|
implementation 'com.polycentric.core:app:1.0'
|
||||||
implementation 'com.futo.futopay:app:1.0'
|
implementation 'com.futo.futopay:app:1.0'
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
implementation 'androidx.work:work-runtime-ktx:2.11.0'
|
||||||
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
|
implementation 'androidx.concurrent:concurrent-futures-ktx:1.3.0'
|
||||||
|
|
||||||
//Database
|
//Database
|
||||||
implementation("androidx.room:room-runtime:2.6.1")
|
implementation("androidx.room:room-runtime:2.8.3")
|
||||||
annotationProcessor("androidx.room:room-compiler:2.6.1")
|
ksp("androidx.room:room-compiler:2.8.3")
|
||||||
ksp("androidx.room:room-compiler:2.6.1")
|
implementation("androidx.room:room-ktx:2.8.3")
|
||||||
implementation("androidx.room:room-ktx:2.6.1")
|
|
||||||
|
|
||||||
//Payment
|
//Payment
|
||||||
implementation 'com.stripe:stripe-android:20.35.1'
|
implementation 'com.stripe:stripe-android:22.0.0'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
|
||||||
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.22"
|
testImplementation "org.jetbrains.kotlin:kotlin-test:2.0.21"
|
||||||
testImplementation "org.xmlunit:xmlunit-core:2.9.1"
|
testImplementation "org.xmlunit:xmlunit-core:2.11.0"
|
||||||
testImplementation "org.mockito:mockito-core:5.4.0"
|
testImplementation "org.mockito:mockito-core:5.20.0"
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
|
||||||
|
|
||||||
//Rust casting SDK
|
//Rust casting SDK
|
||||||
implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.3.1') {
|
implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.4.0') {
|
||||||
// Polycentricandroid includes this
|
// Polycentricandroid includes this
|
||||||
exclude group: 'net.java.dev.jna'
|
exclude group: 'net.java.dev.jna'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@@ -26,6 +29,8 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.FutoVideo"
|
android:theme="@style/Theme.FutoVideo"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
|
tools:replace="android:enableOnBackInvokedCallback"
|
||||||
|
android:enableOnBackInvokedCallback="false"
|
||||||
tools:targetApi="31"
|
tools:targetApi="31"
|
||||||
android:largeHeap="true">
|
android:largeHeap="true">
|
||||||
<provider
|
<provider
|
||||||
@@ -58,6 +63,7 @@
|
|||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
||||||
|
android:windowSoftInputMode="adjustPan"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsPictureInPicture="true">
|
android:supportsPictureInPicture="true">
|
||||||
@@ -153,30 +159,30 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.TestActivity"
|
android:name=".activities.TestActivity"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SettingsActivity"
|
android:name=".activities.SettingsActivity"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.DeveloperActivity"
|
android:name=".activities.DeveloperActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ExceptionActivity"
|
android:name=".activities.ExceptionActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.CaptchaActivity"
|
android:name=".activities.CaptchaActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.LoginActivity"
|
android:name=".activities.LoginActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceActivity"
|
android:name=".activities.AddSourceActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@@ -189,54 +195,78 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceOptionsActivity"
|
android:name=".activities.AddSourceOptionsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricHomeActivity"
|
android:name=".activities.PolycentricHomeActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricBackupActivity"
|
android:name=".activities.PolycentricBackupActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricCreateProfileActivity"
|
android:name=".activities.PolycentricCreateProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricProfileActivity"
|
android:name=".activities.PolycentricProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricWhyActivity"
|
android:name=".activities.PolycentricWhyActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricImportProfileActivity"
|
android:name=".activities.PolycentricImportProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ManageTabsActivity"
|
android:name=".activities.ManageTabsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.QRCaptureActivity"
|
android:name=".activities.QRCaptureActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.FCastGuideActivity"
|
android:name=".activities.FCastGuideActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SyncHomeActivity"
|
android:name=".activities.SyncHomeActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SyncPairActivity"
|
android:name=".activities.SyncPairActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SyncShowPairingCodeActivity"
|
android:name=".activities.SyncShowPairingCodeActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.PolycentricModerationActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.QRCodeFullscreenActivity"
|
||||||
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
<service
|
||||||
|
android:name=".UpdateDownloadService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".UpdateActionReceiver"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activities.InstallUpdateActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/Theme.App.TransparentNoUi"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:finishOnTaskLaunch="true" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1025,18 +1025,21 @@
|
|||||||
|
|
||||||
let settingsToUse = __DEV_SETTINGS ?? {};
|
let settingsToUse = __DEV_SETTINGS ?? {};
|
||||||
if (true) {
|
if (true) {
|
||||||
for (let setting of this.Plugin?.currentPlugin?.settings) {
|
const settings = this.Plugin?.currentPlugin?.settings;
|
||||||
if (typeof settingsToUse[setting.variable] == "undefined") {
|
if (settings) {
|
||||||
switch (setting?.type?.toLowerCase()) {
|
for (let setting of settings) {
|
||||||
case "boolean":
|
if (typeof settingsToUse[setting.variable] == "undefined") {
|
||||||
settingsToUse[setting.variable] = setting.default === 'true';
|
switch (setting?.type?.toLowerCase()) {
|
||||||
break;
|
case "boolean":
|
||||||
case "dropdown":
|
settingsToUse[setting.variable] = setting.default === 'true';
|
||||||
let dropDownIndex = parseInt(setting.default);
|
break;
|
||||||
if (dropDownIndex) {
|
case "dropdown":
|
||||||
settingsToUse[setting.variable] = setting.options[dropDownIndex];
|
let dropDownIndex = parseInt(setting.default);
|
||||||
}
|
if (dropDownIndex) {
|
||||||
break;
|
settingsToUse[setting.variable] = setting.options[dropDownIndex];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -415,6 +415,8 @@ class VideoUrlSource {
|
|||||||
this.url = obj.url;
|
this.url = obj.url;
|
||||||
if(obj.requestModifier)
|
if(obj.requestModifier)
|
||||||
this.requestModifier = obj.requestModifier;
|
this.requestModifier = obj.requestModifier;
|
||||||
|
this.language = obj?.language;
|
||||||
|
this.original = obj?.original;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class VideoUrlWidevineSource extends VideoUrlSource {
|
class VideoUrlWidevineSource extends VideoUrlSource {
|
||||||
@@ -512,6 +514,8 @@ class HLSSource {
|
|||||||
this.language = obj.language;
|
this.language = obj.language;
|
||||||
if(obj.requestModifier)
|
if(obj.requestModifier)
|
||||||
this.requestModifier = obj.requestModifier;
|
this.requestModifier = obj.requestModifier;
|
||||||
|
this.language = obj?.language;
|
||||||
|
this.original = obj?.original;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class DashSource {
|
class DashSource {
|
||||||
@@ -525,6 +529,8 @@ class DashSource {
|
|||||||
this.language = obj.language;
|
this.language = obj.language;
|
||||||
if(obj.requestModifier)
|
if(obj.requestModifier)
|
||||||
this.requestModifier = obj.requestModifier;
|
this.requestModifier = obj.requestModifier;
|
||||||
|
this.language = obj?.language;
|
||||||
|
this.original = obj?.original;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class DashWidevineSource extends DashSource {
|
class DashWidevineSource extends DashSource {
|
||||||
@@ -550,6 +556,7 @@ class DashManifestRawSource {
|
|||||||
this.language = obj.language ?? Language.UNKNOWN;
|
this.language = obj.language ?? Language.UNKNOWN;
|
||||||
if(obj.requestModifier)
|
if(obj.requestModifier)
|
||||||
this.requestModifier = obj.requestModifier;
|
this.requestModifier = obj.requestModifier;
|
||||||
|
this.original = obj?.original;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
object AppCaUpdater {
|
||||||
|
private const val CA_URL = "https://curl.se/ca/cacert.pem"
|
||||||
|
private const val CACHE_FILENAME = "curl-ca-bundle.pem"
|
||||||
|
private const val MAX_AGE_DAYS = 30
|
||||||
|
|
||||||
|
suspend fun ensureCaBundle(context: Context): File = withContext(Dispatchers.IO) {
|
||||||
|
val file = File(context.noBackupFilesDir, CACHE_FILENAME)
|
||||||
|
val needsUpdate = !file.exists() || isOlderThanDays(file, MAX_AGE_DAYS)
|
||||||
|
if (needsUpdate) {
|
||||||
|
downloadToFile(CA_URL, file)
|
||||||
|
}
|
||||||
|
return@withContext file
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isOlderThanDays(file: File, days: Int): Boolean {
|
||||||
|
val ageMs = System.currentTimeMillis() - file.lastModified()
|
||||||
|
return ageMs > days * 24L * 60L * 60L * 1000L
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadToFile(urlStr: String, dest: File) {
|
||||||
|
val conn = (URL(urlStr).openConnection() as HttpURLConnection).apply {
|
||||||
|
connectTimeout = 15000
|
||||||
|
readTimeout = 15000
|
||||||
|
instanceFollowRedirects = true
|
||||||
|
}
|
||||||
|
conn.inputStream.use { input ->
|
||||||
|
dest.parentFile?.mkdirs()
|
||||||
|
dest.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -216,10 +216,9 @@ private fun ByteArray.toInetAddress(): InetAddress {
|
|||||||
return InetAddress.getByAddress(this);
|
return InetAddress.getByAddress(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
|
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs: Int = 10_000): Socket? {
|
||||||
ensureNotMainThread()
|
ensureNotMainThread()
|
||||||
|
|
||||||
val timeout = 10000
|
|
||||||
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
|
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
|
||||||
if(addresses.isEmpty())
|
if(addresses.isEmpty())
|
||||||
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
|
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
|
||||||
@@ -232,7 +231,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
|
|||||||
val socket = Socket()
|
val socket = Socket()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
|
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeoutMs) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
|
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
|
||||||
socket.close()
|
socket.close()
|
||||||
@@ -263,7 +262,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.connect(InetSocketAddress(address, port), timeout);
|
socket.connect(InetSocketAddress(address, port), timeoutMs);
|
||||||
|
|
||||||
synchronized(syncObject) {
|
synchronized(syncObject) {
|
||||||
if (connectedSocket == null) {
|
if (connectedSocket == null) {
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import com.caoccao.javet.values.reference.V8ValueArray
|
|||||||
import com.caoccao.javet.values.reference.V8ValueError
|
import com.caoccao.javet.values.reference.V8ValueError
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.caoccao.javet.values.reference.V8ValuePromise
|
import com.caoccao.javet.values.reference.V8ValuePromise
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
@@ -21,7 +24,6 @@ import kotlinx.coroutines.async
|
|||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.selects.SelectClause0
|
import kotlinx.coroutines.selects.SelectClause0
|
||||||
import kotlinx.coroutines.selects.SelectClause1
|
import kotlinx.coroutines.selects.SelectClause1
|
||||||
import java.util.concurrent.CancellationException
|
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import kotlin.coroutines.AbstractCoroutineContextElement
|
import kotlin.coroutines.AbstractCoroutineContextElement
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
@@ -268,12 +270,25 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
|
|||||||
underlyingDef.complete(p0 as T);
|
underlyingDef.complete(p0 as T);
|
||||||
}
|
}
|
||||||
override fun onRejected(p0: V8Value?) {
|
override fun onRejected(p0: V8Value?) {
|
||||||
plugin.resolvePromise(promise);
|
try {
|
||||||
underlyingDef.completeExceptionally(p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented.."));
|
plugin.resolvePromise(promise);
|
||||||
|
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented..");
|
||||||
|
Logger.i("V8", "Promise rejected, setting exception");
|
||||||
|
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("V8", "Rejection handling failed?" , ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
override fun onCatch(p0: V8Value?) {
|
override fun onCatch(p0: V8Value?) {
|
||||||
plugin.resolvePromise(promise);
|
try {
|
||||||
underlyingDef.completeExceptionally(p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented.."));
|
plugin.resolvePromise(promise);
|
||||||
|
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented..");
|
||||||
|
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("V8", "Catching handling failed?" , ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -287,13 +302,16 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
|
|||||||
fun V8Value.toException(config: IV8PluginConfig): Throwable {
|
fun V8Value.toException(config: IV8PluginConfig): Throwable {
|
||||||
val p0 = this;
|
val p0 = this;
|
||||||
if(p0 is V8ValueObject) {
|
if(p0 is V8ValueObject) {
|
||||||
|
return V8Plugin.getExceptionFromPlugin(config, p0, null, null, null, "P:");
|
||||||
|
/*
|
||||||
val pluginType = p0.getOrDefault(config, "plugin_type", "Promise Exception", "")?.let { if(!it.isNullOrBlank()) it + "" else "" }
|
val pluginType = p0.getOrDefault(config, "plugin_type", "Promise Exception", "")?.let { if(!it.isNullOrBlank()) it + "" else "" }
|
||||||
val msg = p0.getOrDefault<String?>(config, "msg", "Promise Exception", null)
|
val msg = p0.getOrDefault<String?>(config, "msg", "Promise Exception", null)
|
||||||
?: p0.getOrDefault(config, "message", "Promise Exception", "");
|
?: p0.getOrDefault(config, "message", "Promise Exception", "");
|
||||||
return Exception("Promise Failed: " + pluginType + msg);
|
return Throwable("Promise Failed: " + pluginType + msg);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
else if(p0 is V8ValueString)
|
else if(p0 is V8ValueString)
|
||||||
return Exception("Promise Failed:" + p0.value);
|
return Throwable("Promise Failed:" + p0.value);
|
||||||
else
|
else
|
||||||
return NotImplementedError("onCatch promise not implemented..");
|
return NotImplementedError("onCatch promise not implemented..");
|
||||||
}
|
}
|
||||||
@@ -358,4 +376,27 @@ fun V8ValueObject.invokeV8VoidAsync(method: String, vararg obj: Any?): V8Deferre
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
return V8Deferred(CompletableDeferred(result));
|
return V8Deferred(CompletableDeferred(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> Deferred<T>.awaitCancelConverted(): T {
|
||||||
|
try {
|
||||||
|
return this.await();
|
||||||
|
}
|
||||||
|
catch(ex: CancellationException) {
|
||||||
|
if(ex.cause != null) {
|
||||||
|
throw ex.cause!!;
|
||||||
|
}
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> IPager<T>.toList(): List<T> {
|
||||||
|
val list = this.getResults().toMutableList();
|
||||||
|
|
||||||
|
while(this.hasMorePages()) {
|
||||||
|
this.nextPage();
|
||||||
|
list.addAll(this.getResults());
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.toList();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.Window
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat.Type
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import androidx.core.view.doOnAttach
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class RootInsetsController private constructor(
|
||||||
|
private val activity: Activity,
|
||||||
|
private val window: Window,
|
||||||
|
private val root: ViewGroup
|
||||||
|
) {
|
||||||
|
private val controller by lazy { WindowInsetsControllerCompat(window, root) }
|
||||||
|
|
||||||
|
private val basePaddingLeft = root.paddingLeft
|
||||||
|
private val basePaddingTop = root.paddingTop
|
||||||
|
private val basePaddingRight = root.paddingRight
|
||||||
|
private val basePaddingBottom = root.paddingBottom
|
||||||
|
|
||||||
|
private var currentInsets: WindowInsetsCompat = WindowInsetsCompat.CONSUMED
|
||||||
|
private var fullscreen = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
window.statusBarColor = Color.TRANSPARENT
|
||||||
|
window.navigationBarColor = Color.TRANSPARENT
|
||||||
|
controller.systemBarsBehavior =
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(root) { _, insets ->
|
||||||
|
currentInsets = insets
|
||||||
|
applyPadding()
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
|
root.doOnAttach { ViewCompat.requestApplyInsets(root) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun effectiveInsets(): Insets {
|
||||||
|
if (fullscreen) return Insets.NONE
|
||||||
|
|
||||||
|
val sys = currentInsets.getInsets(Type.systemBars())
|
||||||
|
val cut = currentInsets.getInsetsIgnoringVisibility(Type.displayCutout())
|
||||||
|
val portrait = activity.resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_PORTRAIT
|
||||||
|
|
||||||
|
val top = if (portrait) max(sys.top, cut.top) else sys.top
|
||||||
|
return Insets.of(sys.left, top, sys.right, sys.bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun applyPadding() {
|
||||||
|
val e = effectiveInsets()
|
||||||
|
root.updatePadding(
|
||||||
|
left = basePaddingLeft + e.left,
|
||||||
|
top = basePaddingTop + e.top,
|
||||||
|
right = basePaddingRight + e.right,
|
||||||
|
bottom = basePaddingBottom + e.bottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun forceRelayoutAndInsets() {
|
||||||
|
root.post {
|
||||||
|
ViewCompat.requestApplyInsets(root)
|
||||||
|
applyPadding()
|
||||||
|
root.post {
|
||||||
|
ViewCompat.requestApplyInsets(root)
|
||||||
|
applyPadding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enterFullscreen(allowCutoutShortEdges: Boolean = true) {
|
||||||
|
fullscreen = true
|
||||||
|
if (allowCutoutShortEdges) {
|
||||||
|
window.attributes = window.attributes.apply {
|
||||||
|
layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
controller.hide(Type.systemBars())
|
||||||
|
forceRelayoutAndInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exitFullscreen() {
|
||||||
|
fullscreen = false
|
||||||
|
window.attributes = window.attributes.apply {
|
||||||
|
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
|
||||||
|
}
|
||||||
|
controller.show(Type.systemBars())
|
||||||
|
forceRelayoutAndInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onConfigurationChanged() {
|
||||||
|
forceRelayoutAndInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLightSystemBarAppearance(lightStatus: Boolean, lightNav: Boolean) {
|
||||||
|
controller.isAppearanceLightStatusBars = lightStatus
|
||||||
|
controller.isAppearanceLightNavigationBars = lightNav
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun attach(activity: Activity, root: ViewGroup): RootInsetsController {
|
||||||
|
return RootInsetsController(activity, activity.window, root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,11 +10,11 @@ import com.futo.platformplayer.activities.MainActivity
|
|||||||
import com.futo.platformplayer.activities.ManageTabsActivity
|
import com.futo.platformplayer.activities.ManageTabsActivity
|
||||||
import com.futo.platformplayer.activities.PolycentricHomeActivity
|
import com.futo.platformplayer.activities.PolycentricHomeActivity
|
||||||
import com.futo.platformplayer.activities.PolycentricProfileActivity
|
import com.futo.platformplayer.activities.PolycentricProfileActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
|
||||||
import com.futo.platformplayer.activities.SyncHomeActivity
|
import com.futo.platformplayer.activities.SyncHomeActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
@@ -42,7 +42,6 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
@@ -64,7 +63,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
|
@FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
|
||||||
@FormFieldButton(R.drawable.ic_update)
|
@FormFieldButton(R.drawable.ic_update)
|
||||||
fun syncGrayjay() {
|
fun syncGrayjay() {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp?.instance?.activity?.let {
|
||||||
it.startActivity(Intent(it, SyncHomeActivity::class.java))
|
it.startActivity(Intent(it, SyncHomeActivity::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +72,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
|
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
|
||||||
@FormFieldButton(R.drawable.ic_person)
|
@FormFieldButton(R.drawable.ic_person)
|
||||||
fun managePolycentricIdentity() {
|
fun managePolycentricIdentity() {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp?.instance?.activity?.let {
|
||||||
if (StatePolycentric.instance.enabled) {
|
if (StatePolycentric.instance.enabled) {
|
||||||
if (StatePolycentric.instance.processHandle != null) {
|
if (StatePolycentric.instance.processHandle != null) {
|
||||||
it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
|
it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
|
||||||
@@ -91,7 +90,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun openFAQ() {
|
fun openFAQ() {
|
||||||
try {
|
try {
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Settings.URL_FAQ))
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Settings.URL_FAQ))
|
||||||
SettingsActivity.getActivity()?.startActivity(browserIntent);
|
StateApp?.instance?.activity?.startActivity(browserIntent);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
@@ -101,7 +100,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun openIssues() {
|
fun openIssues() {
|
||||||
try {
|
try {
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/futo-org/grayjay-android/issues"))
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/futo-org/grayjay-android/issues"))
|
||||||
SettingsActivity.getActivity()?.startActivity(browserIntent);
|
StateApp?.instance?.activity?.startActivity(browserIntent);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
@@ -132,7 +131,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormFieldButton(R.drawable.ic_tabs)
|
@FormFieldButton(R.drawable.ic_tabs)
|
||||||
fun manageTabs() {
|
fun manageTabs() {
|
||||||
try {
|
try {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp?.instance?.activity?.let {
|
||||||
it.startActivity(Intent(it, ManageTabsActivity::class.java));
|
it.startActivity(Intent(it, ManageTabsActivity::class.java));
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -145,7 +144,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
|
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
|
||||||
@FormFieldButton(R.drawable.ic_move_up)
|
@FormFieldButton(R.drawable.ic_move_up)
|
||||||
fun import() {
|
fun import() {
|
||||||
val act = SettingsActivity.getActivity() ?: return;
|
val act = StateApp.instance.activity ?: return;
|
||||||
val intent = MainActivity.getImportOptionsIntent(act);
|
val intent = MainActivity.getImportOptionsIntent(act);
|
||||||
act.startActivity(intent);
|
act.startActivity(intent);
|
||||||
}
|
}
|
||||||
@@ -154,7 +153,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormFieldButton(R.drawable.ic_link)
|
@FormFieldButton(R.drawable.ic_link)
|
||||||
fun manageLinks() {
|
fun manageLinks() {
|
||||||
try {
|
try {
|
||||||
SettingsActivity.getActivity()?.let { UIDialogs.showUrlHandlingPrompt(it) }
|
StateApp.instance.activity?.let { UIDialogs.showUrlHandlingPrompt(it) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to show url handling prompt", e)
|
Logger.e(TAG, "Failed to show url handling prompt", e)
|
||||||
}
|
}
|
||||||
@@ -163,7 +162,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
/*@FormField(R.string.disable_battery_optimization, FieldForm.BUTTON, R.string.click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions, -1)
|
/*@FormField(R.string.disable_battery_optimization, FieldForm.BUTTON, R.string.click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions, -1)
|
||||||
@FormFieldButton(R.drawable.battery_full_24px)
|
@FormFieldButton(R.drawable.battery_full_24px)
|
||||||
fun ignoreBatteryOptimization() {
|
fun ignoreBatteryOptimization() {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
val packageName = it.packageName
|
val packageName = it.packageName
|
||||||
val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
|
val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
|
||||||
@@ -244,7 +243,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun clearHidden() {
|
fun clearHidden() {
|
||||||
StateMeta.instance.removeAllHiddenCreators();
|
StateMeta.instance.removeAllHiddenCreators();
|
||||||
StateMeta.instance.removeAllHiddenVideos();
|
StateMeta.instance.removeAllHiddenVideos();
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
UIDialogs.toast(it, "Creators and videos should show up again");
|
UIDialogs.toast(it, "Creators and videos should show up again");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,9 +373,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
|
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
|
||||||
fun clearChannelCache() {
|
fun clearChannelCache() {
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
UIDialogs.toast(StateApp.instance.activity!!, "Started clearing..");
|
||||||
StateCache.instance.clear();
|
StateCache.instance.clear();
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Finished clearing");
|
UIDialogs.toast(StateApp.instance.activity!!, "Finished clearing");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,6 +407,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.sticky_subtitles, FieldForm.TOGGLE, R.string.sticky_subtitles_description, -1)
|
||||||
|
var stickySubtitles: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
|
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
|
||||||
var preferOriginalAudio: Boolean = true;
|
var preferOriginalAudio: Boolean = true;
|
||||||
|
|
||||||
@@ -426,6 +429,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
6 -> 1.75f;
|
6 -> 1.75f;
|
||||||
7 -> 2.0f;
|
7 -> 2.0f;
|
||||||
8 -> 2.25f;
|
8 -> 2.25f;
|
||||||
|
9 -> 2.5f;
|
||||||
|
10 -> 2.75f;
|
||||||
|
11 -> 3.0f;
|
||||||
else -> 1.0f;
|
else -> 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -722,7 +728,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@AdvancedField
|
@AdvancedField
|
||||||
@FormField(R.string.experimental_cast, FieldForm.TOGGLE, R.string.experimental_cast_description, 6)
|
@FormField(R.string.experimental_cast, FieldForm.TOGGLE, R.string.experimental_cast_description, 6)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var experimentalCasting: Boolean = false
|
var experimentalCasting: Boolean = true
|
||||||
|
|
||||||
/*TODO: Should we have a different casting quality?
|
/*TODO: Should we have a different casting quality?
|
||||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||||
@@ -756,7 +762,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
try {
|
try {
|
||||||
if (!Logger.submitLogs()) {
|
if (!Logger.submitLogs()) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.please_enable_logging_to_submit_logs)) }
|
StateApp.instance.activity?.let { UIDialogs.toast(it, it.getString(R.string.please_enable_logging_to_submit_logs)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -773,7 +779,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.reset_announcements, FieldForm.BUTTON, R.string.reset_hidden_announcements, 1)
|
@FormField(R.string.reset_announcements, FieldForm.BUTTON, R.string.reset_hidden_announcements, 1)
|
||||||
fun resetAnnouncements() {
|
fun resetAnnouncements() {
|
||||||
StateAnnouncement.instance.resetAnnouncements();
|
StateAnnouncement.instance.resetAnnouncements();
|
||||||
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.announcements_reset)); };
|
StateApp.instance.activity?.let { UIDialogs.toast(it, it.getString(R.string.announcements_reset)); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -841,13 +847,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.change_external_general_directory, FieldForm.BUTTON, R.string.change_the_external_directory_for_general_files, 3)
|
@FormField(R.string.change_external_general_directory, FieldForm.BUTTON, R.string.change_the_external_directory_for_general_files, 3)
|
||||||
fun changeStorageGeneral() {
|
fun changeStorageGeneral() {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
StateApp.instance.changeExternalGeneralDirectory(it);
|
StateApp.instance.changeExternalGeneralDirectory(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@FormField(R.string.change_external_downloads_directory, FieldForm.BUTTON, R.string.change_the_external_storage_for_download_files, 4)
|
@FormField(R.string.change_external_downloads_directory, FieldForm.BUTTON, R.string.change_the_external_storage_for_download_files, 4)
|
||||||
fun changeStorageDownload() {
|
fun changeStorageDownload() {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
StateApp.instance.changeExternalDownloadDirectory(it);
|
StateApp.instance.changeExternalDownloadDirectory(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -856,7 +862,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun clearStorageDownload() {
|
fun clearStorageDownload() {
|
||||||
Settings.instance.storage.storage_download = null;
|
Settings.instance.storage.storage_download = null;
|
||||||
Settings.instance.save();
|
Settings.instance.save();
|
||||||
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, "Cleared download storage directory") };
|
StateApp.instance.activity?.let { UIDialogs.toast(it, "Cleared download storage directory") };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -869,9 +875,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@DropdownFieldOptionsId(R.array.auto_update_when_array)
|
@DropdownFieldOptionsId(R.array.auto_update_when_array)
|
||||||
var check: Int = 0;
|
var check: Int = 0;
|
||||||
|
|
||||||
@FormField(R.string.background_download, FieldForm.DROPDOWN, R.string.configure_if_background_download_should_be_used, 1)
|
@FormField(R.string.background_download, FieldForm.TOGGLE, R.string.configure_if_background_download_should_be_used, 1)
|
||||||
@DropdownFieldOptionsId(R.array.background_download)
|
//@DropdownFieldOptionsId(R.array.background_download)
|
||||||
var backgroundDownload: Int = 0;
|
var shouldBackgroundDownload: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.download_when, FieldForm.DROPDOWN, R.string.configure_when_updates_should_be_downloaded, 2)
|
@FormField(R.string.download_when, FieldForm.DROPDOWN, R.string.configure_when_updates_should_be_downloaded, 2)
|
||||||
@DropdownFieldOptionsId(R.array.when_download)
|
@DropdownFieldOptionsId(R.array.when_download)
|
||||||
@@ -893,13 +899,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.manual_check, FieldForm.BUTTON, R.string.manually_check_for_updates, 3)
|
@FormField(R.string.manual_check, FieldForm.BUTTON, R.string.manually_check_for_updates, 3)
|
||||||
fun manualCheck() {
|
fun manualCheck() {
|
||||||
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
StateUpdate.instance.checkForUpdates(it, true)
|
StateUpdate.instance.checkForUpdates(it, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
try {
|
try {
|
||||||
it.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${it.packageName}")))
|
it.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${it.packageName}")))
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
@@ -911,7 +917,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.view_changelog, FieldForm.BUTTON, R.string.review_the_current_and_past_changelogs, 4)
|
@FormField(R.string.view_changelog, FieldForm.BUTTON, R.string.review_the_current_and_past_changelogs, 4)
|
||||||
fun viewChangelog() {
|
fun viewChangelog() {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
UIDialogs.toast(it.getString(R.string.retrieving_changelog));
|
UIDialogs.toast(it.getString(R.string.retrieving_changelog));
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
@@ -951,7 +957,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
class Backup {
|
class Backup {
|
||||||
@Serializable(with = OffsetDateTimeSerializer::class)
|
@Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
var lastAutoBackupTime: OffsetDateTime = OffsetDateTime.MIN;
|
var lastAutoBackupTime: OffsetDateTime = OffsetDateTime.MIN;
|
||||||
var didAskAutoBackup: Boolean = false;
|
var didAskAutoBackup: Boolean = true;
|
||||||
var autoBackupPassword: String? = null;
|
var autoBackupPassword: String? = null;
|
||||||
fun shouldAutomaticBackup() = autoBackupPassword != null;
|
fun shouldAutomaticBackup() = autoBackupPassword != null;
|
||||||
|
|
||||||
@@ -960,13 +966,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.set_automatic_backup, FieldForm.BUTTON, R.string.configure_daily_backup_in_case_of_catastrophic_failure, 1)
|
@FormField(R.string.set_automatic_backup, FieldForm.BUTTON, R.string.configure_daily_backup_in_case_of_catastrophic_failure, 1)
|
||||||
fun configureAutomaticBackup() {
|
fun configureAutomaticBackup() {
|
||||||
UIDialogs.showAutomaticBackupDialog(SettingsActivity.getActivity()!!, autoBackupPassword != null) {
|
UIDialogs.showAutomaticBackupDialog(StateApp.instance.activity!!, autoBackupPassword != null) {
|
||||||
SettingsActivity.getActivity()?.reloadSettings();
|
SettingsFragment.currentView?.reloadSettings();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@FormField(R.string.restore_automatic_backup, FieldForm.BUTTON, R.string.restore_a_previous_automatic_backup, 2)
|
@FormField(R.string.restore_automatic_backup, FieldForm.BUTTON, R.string.restore_a_previous_automatic_backup, 2)
|
||||||
fun restoreAutomaticBackup() {
|
fun restoreAutomaticBackup() {
|
||||||
val activity = SettingsActivity.getActivity()!!
|
val activity = StateApp.instance.activity!!
|
||||||
|
|
||||||
if(!StateBackup.hasAutomaticBackup())
|
if(!StateBackup.hasAutomaticBackup())
|
||||||
UIDialogs.toast(activity, activity.getString(R.string.you_don_t_have_any_automatic_backups), false);
|
UIDialogs.toast(activity, activity.getString(R.string.you_don_t_have_any_automatic_backups), false);
|
||||||
@@ -977,8 +983,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.export_data, FieldForm.BUTTON, R.string.creates_a_zip_file_with_your_data_which_can_be_imported_by_opening_it_with_grayjay, 3)
|
@FormField(R.string.export_data, FieldForm.BUTTON, R.string.creates_a_zip_file_with_your_data_which_can_be_imported_by_opening_it_with_grayjay, 3)
|
||||||
fun export() {
|
fun export() {
|
||||||
val activity = SettingsActivity.getActivity() ?: return;
|
val activity = StateApp.instance.activity ?: return;
|
||||||
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
|
val fragView = SettingsFragment.currentView ?: return;
|
||||||
|
UISlideOverlays.showOverlay(fragView.overlay, "Select export type", null, {},
|
||||||
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
|
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
|
||||||
StateBackup.shareExternalBackup();
|
StateBackup.shareExternalBackup();
|
||||||
}),
|
}),
|
||||||
@@ -994,11 +1001,11 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable
|
@Serializable
|
||||||
class Payment {
|
class Payment {
|
||||||
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
|
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
|
||||||
val paymentStatus: String get() = SettingsActivity.getActivity()?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
|
val paymentStatus: String get() = StateApp.instance.activity?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
|
||||||
|
|
||||||
@FormField(R.string.license_status, FieldForm.BUTTON, R.string.view_license_status, 2)
|
@FormField(R.string.license_status, FieldForm.BUTTON, R.string.view_license_status, 2)
|
||||||
fun viewLicenseStatus() {
|
fun viewLicenseStatus() {
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
try {
|
try {
|
||||||
if (StatePayment.instance.hasPaid) {
|
if (StatePayment.instance.hasPaid) {
|
||||||
val paymentKey = StatePayment.instance.getPaymentKey()
|
val paymentKey = StatePayment.instance.getPaymentKey()
|
||||||
@@ -1014,12 +1021,12 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 3)
|
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 3)
|
||||||
fun clearPayment() {
|
fun clearPayment() {
|
||||||
SettingsActivity.getActivity()?.let { context ->
|
StateApp.instance.activity?.let { context ->
|
||||||
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
|
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
|
||||||
StatePayment.instance.clearLicenses();
|
StatePayment.instance.clearLicenses();
|
||||||
SettingsActivity.getActivity()?.let {
|
StateApp.instance.activity?.let {
|
||||||
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
||||||
it.reloadSettings();
|
SettingsFragment.currentView?.reloadSettings();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1045,6 +1052,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7)
|
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7)
|
||||||
var polycentricLocalCache: Boolean = true;
|
var polycentricLocalCache: Boolean = true;
|
||||||
|
|
||||||
|
var showPrivacyModeDialog: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
||||||
@@ -1116,7 +1125,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@AdvancedField
|
@AdvancedField
|
||||||
@FormField(R.string.configure_sync_server, FieldForm.BUTTON, R.string.configure_sync_server_description, 7)
|
@FormField(R.string.configure_sync_server, FieldForm.BUTTON, R.string.configure_sync_server_description, 7)
|
||||||
fun configureSyncServer() {
|
fun configureSyncServer() {
|
||||||
SettingsActivity.getActivity()?.let { context ->
|
StateApp.instance.activity?.let { context ->
|
||||||
UIDialogs.showDialog(context, R.drawable.device_sync, false,
|
UIDialogs.showDialog(context, R.drawable.device_sync, false,
|
||||||
"Enter the url to your relay server",
|
"Enter the url to your relay server",
|
||||||
"Using your own relay server requires a proper setup with portforwarding.\nUse at your own risk.",
|
"Using your own relay server requires a proper setup with portforwarding.\nUse at your own risk.",
|
||||||
@@ -1127,13 +1136,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
UIDialogs.Action("Reset", {
|
UIDialogs.Action("Reset", {
|
||||||
syncServerUrl = null;
|
syncServerUrl = null;
|
||||||
instance.save();
|
instance.save();
|
||||||
context.reloadSettings();
|
SettingsFragment.currentView?.reloadSettings();
|
||||||
UIDialogs.toast("Sync server changes require a restart");
|
UIDialogs.toast("Sync server changes require a restart");
|
||||||
}, UIDialogs.ActionStyle.ACCENT),
|
}, UIDialogs.ActionStyle.ACCENT),
|
||||||
UIDialogs.Action.withInput("Configure", {
|
UIDialogs.Action.withInput("Configure", {
|
||||||
syncServerUrl = it?.text
|
syncServerUrl = it?.text
|
||||||
instance.save();
|
instance.save();
|
||||||
context.reloadSettings();
|
SettingsFragment.currentView?.reloadSettings();
|
||||||
UIDialogs.toast("Sync server changes require a restart");
|
UIDialogs.toast("Sync server changes require a restart");
|
||||||
}, UIDialogs.ActionStyle.PRIMARY),
|
}, UIDialogs.ActionStyle.PRIMARY),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import androidx.work.OneTimeWorkRequestBuilder
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
import com.futo.platformplayer.activities.DeveloperActivity
|
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
@@ -20,6 +18,8 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
|||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.background.BackgroundWorker
|
import com.futo.platformplayer.background.BackgroundWorker
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.DeveloperFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
@@ -97,10 +97,10 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
fun subscriptionsCache5000() {
|
fun subscriptionsCache5000() {
|
||||||
Logger.i("SettingsDev", "Started caching 5000 sub items");
|
Logger.i("SettingsDev", "Started caching 5000 sub items");
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
SettingsActivity.getActivity()!!,
|
StateApp.instance.activity!!,
|
||||||
"Started caching 5000 sub items"
|
"Started caching 5000 sub items"
|
||||||
);
|
);
|
||||||
val button = DeveloperActivity.getActivity()?.getField("subscription_cache_button");
|
val button = DeveloperFragment.currentView?.getField("subscription_cache_button");
|
||||||
if(button is ButtonField)
|
if(button is ButtonField)
|
||||||
button.setButtonEnabled(false);
|
button.setButtonEnabled(false);
|
||||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
@@ -121,7 +121,7 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
val diff = System.currentTimeMillis() - lastToast;
|
val diff = System.currentTimeMillis() - lastToast;
|
||||||
lastToast = System.currentTimeMillis();
|
lastToast = System.currentTimeMillis();
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
SettingsActivity.getActivity()!!,
|
StateApp.instance.activity!!,
|
||||||
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
SettingsActivity.getActivity()!!,
|
StateApp.instance.activity!!,
|
||||||
"FINISHED Page: ${page}, Total: ${total}"
|
"FINISHED Page: ${page}, Total: ${total}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -152,10 +152,10 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
fun historyCache100() {
|
fun historyCache100() {
|
||||||
Logger.i("SettingsDev", "Started caching 100 history items (from home)");
|
Logger.i("SettingsDev", "Started caching 100 history items (from home)");
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
SettingsActivity.getActivity()!!,
|
StateApp.instance.activity!!,
|
||||||
"Started caching 100 history items (from home)"
|
"Started caching 100 history items (from home)"
|
||||||
);
|
);
|
||||||
val button = DeveloperActivity.getActivity()?.getField("history_cache_button");
|
val button = DeveloperFragment.currentView?.getField("history_cache_button");
|
||||||
if(button is ButtonField)
|
if(button is ButtonField)
|
||||||
button.setButtonEnabled(false);
|
button.setButtonEnabled(false);
|
||||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
@@ -186,7 +186,7 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
val diff = System.currentTimeMillis() - lastToast;
|
val diff = System.currentTimeMillis() - lastToast;
|
||||||
lastToast = System.currentTimeMillis();
|
lastToast = System.currentTimeMillis();
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
SettingsActivity.getActivity()!!,
|
StateApp.instance.activity!!,
|
||||||
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -195,7 +195,7 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
SettingsActivity.getActivity()!!,
|
StateApp.instance.activity!!,
|
||||||
"FINISHED Page: ${page}, Total: ${total}"
|
"FINISHED Page: ${page}, Total: ${total}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -235,9 +235,9 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
|
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
|
||||||
R.string.test_background_worker_description, 4)
|
R.string.test_background_worker_description, 4)
|
||||||
fun triggerBackgroundUpdate() {
|
fun triggerBackgroundUpdate() {
|
||||||
val act = SettingsActivity.getActivity()!!;
|
val act = StateApp.instance.activity!!;
|
||||||
try {
|
try {
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
UIDialogs.toast(StateApp.instance.activity!!, "Starting test background worker");
|
||||||
|
|
||||||
val wm = WorkManager.getInstance(act);
|
val wm = WorkManager.getInstance(act);
|
||||||
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
||||||
@@ -251,9 +251,9 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
|
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
|
||||||
R.string.test_background_worker_description, 4)
|
R.string.test_background_worker_description, 4)
|
||||||
fun clearChannelContentCache() {
|
fun clearChannelContentCache() {
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Clearing cache");
|
UIDialogs.toast(StateApp.instance.activity!!, "Clearing cache");
|
||||||
StateCache.instance.clearToday();
|
StateCache.instance.clearToday();
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Cleared");
|
UIDialogs.toast(StateApp.instance.activity!!, "Cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -370,17 +370,19 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null) {
|
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, dismissAction: (() -> Unit)? = null): AlertDialog {
|
||||||
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
||||||
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
|
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
|
||||||
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
return showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction).apply {
|
||||||
|
setOnDismissListener { dismissAction?.invoke() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null) {
|
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, dismissAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null): AlertDialog {
|
||||||
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
||||||
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
|
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
|
||||||
val doNotAskAgain = Action(context.getString(R.string.do_not_ask_again), doNotAskAgainAction ?: {}, ActionStyle.NONE)
|
val doNotAskAgain = Action(context.getString(R.string.do_not_ask_again), doNotAskAgainAction ?: {}, ActionStyle.NONE)
|
||||||
showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction)
|
return showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
|
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
|
||||||
@@ -403,13 +405,6 @@ class UIDialogs {
|
|||||||
dialog.setMaxVersion(lastVersion);
|
dialog.setMaxVersion(lastVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showInstallDownloadedUpdateDialog(context: Context, apkFile: File) {
|
|
||||||
val dialog = AutoUpdateDialog(context);
|
|
||||||
registerDialogOpened(dialog);
|
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
|
||||||
dialog.showPredownloaded(apkFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showMigrateDialog(context: Context, store: ManagedStore<*>, onConcluded: ()->Unit) {
|
fun showMigrateDialog(context: Context, store: ManagedStore<*>, onConcluded: ()->Unit) {
|
||||||
if(!store.hasMissingReconstructions())
|
if(!store.hasMissingReconstructions())
|
||||||
onConcluded();
|
onConcluded();
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist
|
|||||||
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist
|
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
@@ -74,6 +73,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
|
||||||
|
|
||||||
class UISlideOverlays {
|
class UISlideOverlays {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -331,15 +331,9 @@ class UISlideOverlays {
|
|||||||
0,
|
0,
|
||||||
UIDialogs.Action("Cancel", {}),
|
UIDialogs.Action("Cancel", {}),
|
||||||
UIDialogs.Action("Configure", {
|
UIDialogs.Action("Configure", {
|
||||||
val intent = Intent(
|
StateApp.instance.activity?.let {
|
||||||
mainContext,
|
it.navigate(it.getFragment<SettingsFragment>(), mainContext.getString(R.string.background_update))
|
||||||
SettingsActivity::class.java
|
}
|
||||||
);
|
|
||||||
intent.putExtra(
|
|
||||||
"query",
|
|
||||||
mainContext.getString(R.string.background_update)
|
|
||||||
);
|
|
||||||
mainContext.startActivity(intent);
|
|
||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class UpdateActionReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (intent.action) {
|
||||||
|
UpdateNotificationManager.ACTION_UPDATE_YES -> handleUpdateYes(context, intent)
|
||||||
|
UpdateNotificationManager.ACTION_UPDATE_NO -> handleUpdateNo(context)
|
||||||
|
UpdateNotificationManager.ACTION_UPDATE_NEVER -> handleUpdateNever(context)
|
||||||
|
UpdateNotificationManager.ACTION_DOWNLOAD_CANCEL -> handleDownloadCancel(context, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUpdateYes(context: Context, intent: Intent) {
|
||||||
|
AutoUpdateDialog.currentDialog?.dismiss()
|
||||||
|
|
||||||
|
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
||||||
|
if (version == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_AVAILABLE)
|
||||||
|
|
||||||
|
val serviceIntent = Intent(context, UpdateDownloadService::class.java).apply {
|
||||||
|
putExtra(UpdateDownloadService.EXTRA_VERSION, version)
|
||||||
|
}
|
||||||
|
ContextCompat.startForegroundService(context, serviceIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUpdateNo(context: Context) {
|
||||||
|
AutoUpdateDialog.currentDialog?.dismiss()
|
||||||
|
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_AVAILABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUpdateNever(context: Context) {
|
||||||
|
AutoUpdateDialog.currentDialog?.dismiss()
|
||||||
|
Settings.instance.autoUpdate.check = 1
|
||||||
|
Settings.instance.save()
|
||||||
|
|
||||||
|
UpdateNotificationManager.cancelAll(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDownloadCancel(context: Context, intent: Intent) {
|
||||||
|
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
||||||
|
|
||||||
|
val cancelIntent = Intent(context, UpdateDownloadService::class.java).apply {
|
||||||
|
putExtra(UpdateDownloadService.EXTRA_CANCEL, true)
|
||||||
|
putExtra(UpdateDownloadService.EXTRA_VERSION, version)
|
||||||
|
}
|
||||||
|
ContextCompat.startForegroundService(context, cancelIntent)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_DOWNLOADING)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class UpdateCheckWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) {
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
if (!Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
|
||||||
|
Logger.i(TAG, "Auto-update disabled, skipping worker run")
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val client = ManagedHttpClient()
|
||||||
|
val latestVersion = StateUpdate.Companion.instance.downloadVersionCode(client)
|
||||||
|
|
||||||
|
if (latestVersion == null) {
|
||||||
|
Logger.w(TAG, "Failed to fetch latest version in worker")
|
||||||
|
return@withContext Result.retry()
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentVersion = BuildConfig.VERSION_CODE
|
||||||
|
Logger.i(TAG, "Worker check: current=$currentVersion, latest=$latestVersion")
|
||||||
|
|
||||||
|
if (latestVersion <= currentVersion) {
|
||||||
|
return@withContext Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateNotificationManager.showUpdateAvailableNotification(applicationContext, latestVersion)
|
||||||
|
|
||||||
|
if (StateApp.instance.isMainActive) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
StateApp.withContext { ctx ->
|
||||||
|
try {
|
||||||
|
UIDialogs.showUpdateAvailableDialog(ctx, latestVersion, false)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to show in-app update dialog from worker", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result.success()
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Exception in UpdateCheckWorker", t)
|
||||||
|
Result.retry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "UpdateCheckWorker"
|
||||||
|
const val UNIQUE_WORK_NAME = "updateCheck"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.SystemClock
|
||||||
|
import com.futo.platformplayer.UIDialogs.ActionStyle
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class UpdateDownloadService : Service() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "UpdateDownloadService"
|
||||||
|
const val EXTRA_VERSION = "version"
|
||||||
|
const val EXTRA_CANCEL = "cancel"
|
||||||
|
private const val MAX_RETRIES = 5
|
||||||
|
private const val INITIAL_BACKOFF_MS = 5_000L
|
||||||
|
private const val BUFFER_SIZE = 8 * 1024
|
||||||
|
private const val MIN_PROGRESS_UPDATE_INTERVAL_MS = 500L
|
||||||
|
|
||||||
|
var updateDownloadedDialog: Dialog? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private val job = SupervisorJob()
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO + job)
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var isDownloading: Boolean = false
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var cancelRequested: Boolean = false
|
||||||
|
|
||||||
|
private var lastProgressUpdateElapsedMs: Long = 0L
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (intent == null) {
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intent.getBooleanExtra(EXTRA_CANCEL, false)) {
|
||||||
|
cancelRequested = true
|
||||||
|
Logger.i(TAG, "Download cancel requested")
|
||||||
|
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
val version = intent.getIntExtra(EXTRA_VERSION, 0)
|
||||||
|
if (version == 0) {
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDownloading) {
|
||||||
|
Logger.i(TAG, "Download already in progress, ignoring new start")
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
isDownloading = true
|
||||||
|
cancelRequested = false
|
||||||
|
|
||||||
|
val notification = UpdateNotificationManager.buildDownloadProgressNotification(this, version, 0, true)
|
||||||
|
startForeground(UpdateNotificationManager.NOTIF_ID_DOWNLOADING, notification)
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
downloadApk(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun throttledUpdateDownloadProgress(version: Int, progress: Int, indeterminate: Boolean) {
|
||||||
|
val now = SystemClock.elapsedRealtime()
|
||||||
|
val force = progress == 100 && !indeterminate
|
||||||
|
|
||||||
|
if (force || now - lastProgressUpdateElapsedMs >= MIN_PROGRESS_UPDATE_INTERVAL_MS) {
|
||||||
|
lastProgressUpdateElapsedMs = now
|
||||||
|
UpdateNotificationManager.updateDownloadProgress(this, version, progress, indeterminate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun downloadApk(version: Int) {
|
||||||
|
val apkFile = StateUpdate.getApkFile(this, version)
|
||||||
|
val partialFile = StateUpdate.getPartialApkFile(this, version)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (apkFile.exists() && apkFile.length() > 0L) {
|
||||||
|
Logger.i(TAG, "APK already downloaded at ${apkFile.absolutePath}")
|
||||||
|
onDownloadComplete(version, apkFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var backoffMs = INITIAL_BACKOFF_MS
|
||||||
|
|
||||||
|
for (attempt in 0 until MAX_RETRIES) {
|
||||||
|
if (cancelRequested) {
|
||||||
|
Logger.i(TAG, "Download cancelled before attempt ${attempt + 1}")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
performDownload(StateUpdate.APK_URL, partialFile, version)
|
||||||
|
|
||||||
|
if (!cancelRequested) {
|
||||||
|
if (apkFile.exists()) {
|
||||||
|
apkFile.delete()
|
||||||
|
}
|
||||||
|
if (!partialFile.renameTo(apkFile)) {
|
||||||
|
throw IllegalStateException("Failed to rename partial APK file")
|
||||||
|
}
|
||||||
|
onDownloadComplete(version, apkFile)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
if (cancelRequested) {
|
||||||
|
Logger.i(TAG, "Download cancelled by user", t)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt == MAX_RETRIES - 1) {
|
||||||
|
Logger.e(TAG, "Download failed after ${attempt + 1} attempts", t)
|
||||||
|
UpdateNotificationManager.showDownloadFailedNotification(this, version, t)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
Logger.w(TAG, "Download attempt ${attempt + 1} failed, retrying in ${backoffMs / 1000}s", t)
|
||||||
|
delay(backoffMs)
|
||||||
|
backoffMs *= 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isDownloading = false
|
||||||
|
cancelRequested = false
|
||||||
|
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performDownload(url: String, partialFile: File, version: Int) {
|
||||||
|
var startOffset = if (partialFile.exists()) partialFile.length() else 0L
|
||||||
|
Logger.i(TAG, "Starting download. url=$url, existingBytes=$startOffset")
|
||||||
|
|
||||||
|
var connection: HttpURLConnection? = null
|
||||||
|
try {
|
||||||
|
connection = (URL(url).openConnection() as HttpURLConnection).apply {
|
||||||
|
connectTimeout = 15_000
|
||||||
|
readTimeout = 30_000
|
||||||
|
if (startOffset > 0L) {
|
||||||
|
setRequestProperty("Range", "bytes=$startOffset-")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.connect()
|
||||||
|
val responseCode = connection.responseCode
|
||||||
|
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_OK && startOffset > 0L) {
|
||||||
|
Logger.w(TAG, "Server ignored Range header, restarting download from scratch")
|
||||||
|
partialFile.delete()
|
||||||
|
startOffset = 0L
|
||||||
|
} else if (responseCode != HttpURLConnection.HTTP_OK &&
|
||||||
|
responseCode != HttpURLConnection.HTTP_PARTIAL) {
|
||||||
|
throw IllegalStateException("Unexpected HTTP response code $responseCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
val contentLength = connection.contentLengthLong
|
||||||
|
val totalBytes = if (contentLength > 0L) startOffset + contentLength else -1L
|
||||||
|
|
||||||
|
val buffer = ByteArray(BUFFER_SIZE)
|
||||||
|
var downloaded = 0L
|
||||||
|
var lastProgress = -1
|
||||||
|
|
||||||
|
connection.inputStream.use { input ->
|
||||||
|
FileOutputStream(partialFile, startOffset > 0L).use { output ->
|
||||||
|
while (!cancelRequested) {
|
||||||
|
val read = input.read(buffer)
|
||||||
|
if (read == -1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
output.write(buffer, 0, read)
|
||||||
|
downloaded += read
|
||||||
|
|
||||||
|
if (totalBytes > 0L) {
|
||||||
|
val progress = (((startOffset + downloaded) * 100L) / totalBytes).toInt()
|
||||||
|
if (progress != lastProgress) {
|
||||||
|
lastProgress = progress
|
||||||
|
val safeProgress = when {
|
||||||
|
progress < 0 -> 0
|
||||||
|
progress > 100 -> 100
|
||||||
|
else -> progress
|
||||||
|
}
|
||||||
|
throttledUpdateDownloadProgress(version, safeProgress, indeterminate = false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throttledUpdateDownloadProgress(version, progress = 0, indeterminate = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cancelRequested && totalBytes > 0L) {
|
||||||
|
val finalProgress = 100
|
||||||
|
throttledUpdateDownloadProgress(version, finalProgress, indeterminate = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelRequested) {
|
||||||
|
throw CancellationException("Download cancelled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalBytes > 0L && startOffset + downloaded < totalBytes) {
|
||||||
|
throw IllegalStateException("Download incomplete: expected=$totalBytes, got=${startOffset + downloaded}")
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
connection?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDownloadComplete(version: Int, apkFile: File) {
|
||||||
|
Logger.i(TAG, "Download complete for version=$version, file=${apkFile.absolutePath}")
|
||||||
|
UpdateNotificationManager.showDownloadCompleteNotification(this, version, apkFile)
|
||||||
|
|
||||||
|
if (StateApp.instance.isMainActive) {
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
|
StateApp.withContext { ctx ->
|
||||||
|
try {
|
||||||
|
updateDownloadedDialog = UIDialogs.showDialog(ctx, R.drawable.foreground,
|
||||||
|
"Update downloaded",
|
||||||
|
"Would you like to install it now?", null, 0,
|
||||||
|
UIDialogs.Action("Not now", {
|
||||||
|
updateDownloadedDialog = null
|
||||||
|
}, ActionStyle.NONE, true),
|
||||||
|
UIDialogs.Action("Install", {
|
||||||
|
UpdateNotificationManager.cancelAll(ctx)
|
||||||
|
UpdateInstaller.startInstall(ctx, version, apkFile)
|
||||||
|
}, ActionStyle.PRIMARY, true));
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to show in-app update downloaded dialog", t)
|
||||||
|
updateDownloadedDialog = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent.FLAG_MUTABLE
|
||||||
|
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
import android.app.PendingIntent.getBroadcast
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.view.View
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.receivers.InstallReceiver
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
|
||||||
|
object UpdateInstaller {
|
||||||
|
private const val TAG = "UpdateInstaller"
|
||||||
|
|
||||||
|
@SuppressLint("RequestInstallPackagesPolicy")
|
||||||
|
fun startInstall(context: Context, version: Int, apkFile: File) {
|
||||||
|
if (!apkFile.exists()) {
|
||||||
|
Logger.w(TAG, "APK file does not exist: ${apkFile.absolutePath}")
|
||||||
|
UIDialogs.toast(context, "Update file missing")
|
||||||
|
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, "APK file does not exist.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||||
|
UIDialogs.toast(context, "Updates are managed by the Play Store")
|
||||||
|
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, "Updates are managed by the Play Store.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val pm = context.packageManager
|
||||||
|
if (!pm.canRequestPackageInstalls()) {
|
||||||
|
UIDialogs.toast(context, "Allow this app to install updates, then try again")
|
||||||
|
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, "Install update permission was missing.")
|
||||||
|
|
||||||
|
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
||||||
|
data = "package:${context.packageName}".toUri()
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to check unknown sources permission", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
var inputStream: InputStream? = null
|
||||||
|
var session: PackageInstaller.Session? = null
|
||||||
|
try {
|
||||||
|
|
||||||
|
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller
|
||||||
|
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||||
|
val sessionId = packageInstaller.createSession(params)
|
||||||
|
session = packageInstaller.openSession(sessionId)
|
||||||
|
|
||||||
|
inputStream = apkFile.inputStream()
|
||||||
|
val dataLength = apkFile.length()
|
||||||
|
|
||||||
|
session.openWrite("package", 0, dataLength).use { sessionStream ->
|
||||||
|
inputStream.copyToOutputStream(dataLength, sessionStream) { _ -> }
|
||||||
|
session.fsync(sessionStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(context, InstallReceiver::class.java).apply {
|
||||||
|
putExtra(UpdateNotificationManager.EXTRA_VERSION, version)
|
||||||
|
putExtra(UpdateNotificationManager.EXTRA_APK_PATH, apkFile.absolutePath)
|
||||||
|
}
|
||||||
|
val pendingIntent = getBroadcast(context, 0, intent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
||||||
|
val statusReceiver = pendingIntent.intentSender
|
||||||
|
|
||||||
|
InstallReceiver.onReceiveResult.subscribe(this) { message ->
|
||||||
|
InstallReceiver.onReceiveResult.clear();
|
||||||
|
onReceiveResult(context, version, apkFile, message);
|
||||||
|
};
|
||||||
|
Logger.i(TAG, "Committing install session for ${apkFile.absolutePath}")
|
||||||
|
session.commit(statusReceiver)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Exception while installing update", e)
|
||||||
|
session?.abandon()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(context, "Failed to install update: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, e.message)
|
||||||
|
} finally {
|
||||||
|
session?.close()
|
||||||
|
inputStream?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onReceiveResult(context: Context, version: Int, apkFile: File, result: String?) {
|
||||||
|
try {
|
||||||
|
InstallReceiver.onReceiveResult.remove(this)
|
||||||
|
|
||||||
|
if (result.isNullOrEmpty()) {
|
||||||
|
Logger.i(TAG, "Update install finished successfully")
|
||||||
|
UpdateNotificationManager.showInstallSucceededNotification(context, version)
|
||||||
|
} else {
|
||||||
|
Logger.w(TAG, "Update install failed: $result")
|
||||||
|
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, result)
|
||||||
|
UIDialogs.showGeneralErrorDialog(context, "Install failed due to:\n$result")
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to handle install result", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.PendingIntent.FLAG_MUTABLE
|
||||||
|
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
import android.app.PendingIntent.getBroadcast
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.futo.platformplayer.activities.InstallUpdateActivity
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object UpdateNotificationManager {
|
||||||
|
private const val CHANNEL_ID = "app_updates"
|
||||||
|
private const val CHANNEL_NAME = "App updates"
|
||||||
|
private const val CHANNEL_DESCRIPTION = "Notifications about new app versions"
|
||||||
|
|
||||||
|
const val ACTION_UPDATE_YES = "com.futo.platformplayer.UPDATE_YES"
|
||||||
|
const val ACTION_UPDATE_NO = "com.futo.platformplayer.UPDATE_NO"
|
||||||
|
const val ACTION_UPDATE_NEVER = "com.futo.platformplayer.UPDATE_NEVER"
|
||||||
|
const val ACTION_DOWNLOAD_CANCEL = "com.futo.platformplayer.UPDATE_CANCEL"
|
||||||
|
const val ACTION_INSTALL_NOW = "com.futo.platformplayer.UPDATE_INSTALL"
|
||||||
|
private const val REQUEST_CODE_INSTALL = 1001
|
||||||
|
|
||||||
|
const val EXTRA_VERSION = "version"
|
||||||
|
const val EXTRA_APK_PATH = "apk_path"
|
||||||
|
|
||||||
|
const val NOTIF_ID_AVAILABLE = 2001
|
||||||
|
const val NOTIF_ID_DOWNLOADING = 2002
|
||||||
|
const val NOTIF_ID_READY = 2003
|
||||||
|
const val NOTIF_ID_INSTALL_FAILED = 2004
|
||||||
|
const val NOTIF_ID_INSTALL_SUCCEEDED = 2005
|
||||||
|
|
||||||
|
fun ensureChannel(context: Context) {
|
||||||
|
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
if (manager.getNotificationChannel(CHANNEL_ID) == null) {
|
||||||
|
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
|
||||||
|
description = CHANNEL_DESCRIPTION
|
||||||
|
enableVibration(false)
|
||||||
|
enableLights(false)
|
||||||
|
setSound(null, null)
|
||||||
|
}
|
||||||
|
manager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showInstallSucceededNotification(context: Context, version: Int) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureChannel(context)
|
||||||
|
|
||||||
|
val launchIntent = context.packageManager
|
||||||
|
.getLaunchIntentForPackage(context.packageName)
|
||||||
|
?.apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
|
||||||
|
}
|
||||||
|
|
||||||
|
val launchPendingIntent = launchIntent?.let {
|
||||||
|
PendingIntent.getActivity(context, REQUEST_CODE_INSTALL, it, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.foreground)
|
||||||
|
.setContentTitle("Update installed")
|
||||||
|
.setContentText("Version $version installed. Tap to open.")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setSilent(true)
|
||||||
|
|
||||||
|
if (launchPendingIntent != null) {
|
||||||
|
builder.setContentIntent(launchPendingIntent)
|
||||||
|
builder.addAction(0, "Open app", launchPendingIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(context).notify(NOTIF_ID_INSTALL_SUCCEEDED, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showUpdateAvailableNotification(context: Context, version: Int) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureChannel(context)
|
||||||
|
|
||||||
|
val yesIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
||||||
|
action = ACTION_UPDATE_YES
|
||||||
|
putExtra(EXTRA_VERSION, version)
|
||||||
|
}
|
||||||
|
val yesPendingIntent = getBroadcast(context, 0, yesIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
||||||
|
val noIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
||||||
|
action = ACTION_UPDATE_NO
|
||||||
|
putExtra(EXTRA_VERSION, version)
|
||||||
|
}
|
||||||
|
val noPendingIntent = getBroadcast(context, 1, noIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
||||||
|
val neverIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
||||||
|
action = ACTION_UPDATE_NEVER
|
||||||
|
putExtra(EXTRA_VERSION, version)
|
||||||
|
}
|
||||||
|
val neverPendingIntent = getBroadcast(context, 2, neverIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
||||||
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.foreground)
|
||||||
|
.setContentTitle("Update available")
|
||||||
|
.setContentText("A new version ($version) is available.")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(yesPendingIntent)
|
||||||
|
.setSilent(true)
|
||||||
|
.addAction(0, "Never", neverPendingIntent)
|
||||||
|
.addAction(0, "Not now", noPendingIntent)
|
||||||
|
.addAction(0, "Download", yesPendingIntent)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(context).notify(NOTIF_ID_AVAILABLE, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildDownloadProgressNotification(context: Context, version: Int, progress: Int, indeterminate: Boolean): Notification {
|
||||||
|
ensureChannel(context)
|
||||||
|
|
||||||
|
val cancelIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
||||||
|
action = ACTION_DOWNLOAD_CANCEL
|
||||||
|
putExtra(EXTRA_VERSION, version)
|
||||||
|
}
|
||||||
|
val cancelPendingIntent = getBroadcast(
|
||||||
|
context,
|
||||||
|
3,
|
||||||
|
cancelIntent,
|
||||||
|
FLAG_MUTABLE or FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.foreground)
|
||||||
|
.setContentTitle("Downloading update")
|
||||||
|
.setContentText("Downloading version $version")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setSilent(true)
|
||||||
|
.addAction(0, "Cancel", cancelPendingIntent)
|
||||||
|
|
||||||
|
if (indeterminate) {
|
||||||
|
builder.setProgress(0, 0, true)
|
||||||
|
} else {
|
||||||
|
builder.setProgress(100, progress, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDownloadProgress(context: Context, version: Int, progress: Int, indeterminate: Boolean) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val notification = buildDownloadProgressNotification(context, version, progress, indeterminate)
|
||||||
|
NotificationManagerCompat.from(context).notify(NOTIF_ID_DOWNLOADING, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun showDownloadCompleteNotification(context: Context, version: Int, apkFile: File) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ensureChannel(context)
|
||||||
|
|
||||||
|
val installIntent = InstallUpdateActivity.createIntent(context, version, apkFile.absolutePath)
|
||||||
|
val installPendingIntent = PendingIntent.getActivity(context, REQUEST_CODE_INSTALL, installIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.foreground)
|
||||||
|
.setContentTitle("Update downloaded")
|
||||||
|
.setContentText("Tap to install version $version.")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setContentIntent(installPendingIntent)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setSilent(true)
|
||||||
|
.addAction(0, "Install", installPendingIntent)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(context).notify(NOTIF_ID_READY, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun showDownloadFailedNotification(context: Context, version: Int, error: Throwable?) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ensureChannel(context)
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.foreground)
|
||||||
|
.setContentTitle("Failed to download update")
|
||||||
|
.setContentText(error?.message ?: "Unknown error")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setSilent(true)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(context).notify(NOTIF_ID_READY, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showInstallFailedNotification(context: Context, version: Int, apkFile: File, error: String?) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED)
|
||||||
|
return
|
||||||
|
|
||||||
|
ensureChannel(context)
|
||||||
|
|
||||||
|
val installIntent = InstallUpdateActivity.createIntent(context, version, apkFile.absolutePath)
|
||||||
|
val installPendingIntent = PendingIntent.getActivity(context, REQUEST_CODE_INSTALL, installIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.foreground)
|
||||||
|
.setContentTitle("Failed to install update")
|
||||||
|
.setContentText(if (error != null && error.isNotBlank()) "$error Tap to try again." else "Tap to try again.")
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setSilent(true)
|
||||||
|
.setContentIntent(installPendingIntent)
|
||||||
|
.addAction(0, "Install again", installPendingIntent)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(context).notify(NOTIF_ID_INSTALL_FAILED, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelAll(context: Context) {
|
||||||
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_AVAILABLE)
|
||||||
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_DOWNLOADING)
|
||||||
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_READY)
|
||||||
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_INSTALL_FAILED)
|
||||||
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_INSTALL_SUCCEEDED)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.icu.util.Output
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.OperationCanceledException
|
import android.os.OperationCanceledException
|
||||||
@@ -44,6 +42,9 @@ import java.util.*
|
|||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
|
import androidx.core.graphics.scale
|
||||||
|
import com.bumptech.glide.RequestBuilder
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||||
|
|
||||||
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
|
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
|
||||||
fun getRandomString(sizeOfRandomString: Int): String {
|
fun getRandomString(sizeOfRandomString: Int): String {
|
||||||
@@ -101,7 +102,7 @@ fun String.isHexColor(): Boolean {
|
|||||||
|
|
||||||
fun IPlatformClient.fromPool(pool: PlatformMultiClientPool) = pool.getClientPooled(this);
|
fun IPlatformClient.fromPool(pool: PlatformMultiClientPool) = pool.getClientPooled(this);
|
||||||
|
|
||||||
fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec);
|
fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec);
|
||||||
|
|
||||||
fun DocumentFile.getInputStream(context: Context) = context.contentResolver.openInputStream(this.uri);
|
fun DocumentFile.getInputStream(context: Context) = context.contentResolver.openInputStream(this.uri);
|
||||||
fun DocumentFile.getOutputStream(context: Context) = context.contentResolver.openOutputStream(this.uri);
|
fun DocumentFile.getOutputStream(context: Context) = context.contentResolver.openOutputStream(this.uri);
|
||||||
@@ -114,23 +115,6 @@ fun DocumentFile.writeBytes(context: Context, byteArray: ByteArray) = context.co
|
|||||||
it.flush();
|
it.flush();
|
||||||
};
|
};
|
||||||
|
|
||||||
fun loadBitmap(url: String): Bitmap {
|
|
||||||
try {
|
|
||||||
val client = ManagedHttpClient();
|
|
||||||
val response = client.get(url);
|
|
||||||
if (response.isOk && response.body != null) {
|
|
||||||
val bitmapStream = response.body.byteStream();
|
|
||||||
val bitmap = BitmapFactory.decodeStream(bitmapStream);
|
|
||||||
return bitmap;
|
|
||||||
} else {
|
|
||||||
throw Exception("Failed to find data at URL.");
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.w("Utility", "Exception thrown while downloading bitmap.", e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun TextView.setPlatformPlayerLinkMovementMethod(context: Context) {
|
fun TextView.setPlatformPlayerLinkMovementMethod(context: Context) {
|
||||||
this.movementMethod = PlatformLinkMovementMethod(context);
|
this.movementMethod = PlatformLinkMovementMethod(context);
|
||||||
}
|
}
|
||||||
@@ -458,4 +442,11 @@ fun addressScore(addr: InetAddress): Int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> Enumeration<T>.toList(): List<T> = Collections.list(this)
|
fun <T> Enumeration<T>.toList(): List<T> = Collections.list(this)
|
||||||
|
|
||||||
|
fun <T> RequestBuilder<T>.withMaxSizePx(maxSizePx: Int = 1920): RequestBuilder<T> {
|
||||||
|
return this;
|
||||||
|
//.downsample(DownsampleStrategy.AT_MOST)
|
||||||
|
//.override(maxSizePx, maxSizePx)
|
||||||
|
//.centerInside()
|
||||||
|
}
|
||||||
@@ -107,10 +107,9 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
onNewIntent(intent);
|
onNewIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
var url = intent?.dataString;
|
var url = intent.dataString;
|
||||||
|
|
||||||
if(url == null)
|
if(url == null)
|
||||||
UIDialogs.showDialog(this, R.drawable.ic_error, getString(R.string.no_valid_url_provided), null, null,
|
UIDialogs.showDialog(this, R.drawable.ic_error, getString(R.string.no_valid_url_provided), null, null,
|
||||||
0, UIDialogs.Action(getString(R.string.ok), { finish() }, UIDialogs.ActionStyle.PRIMARY));
|
0, UIDialogs.Action(getString(R.string.ok), { finish() }, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
package com.futo.platformplayer.activities
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.ImageButton
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.futo.platformplayer.*
|
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
|
||||||
import com.futo.platformplayer.views.fields.IField
|
|
||||||
|
|
||||||
class DeveloperActivity : AppCompatActivity() {
|
|
||||||
private lateinit var _form: FieldForm;
|
|
||||||
private lateinit var _buttonBack: ImageButton;
|
|
||||||
|
|
||||||
fun getField(id: String): IField? {
|
|
||||||
return _form.findField(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
DeveloperActivity._lastActivity = this;
|
|
||||||
setContentView(R.layout.activity_dev);
|
|
||||||
setNavigationBarColorAndIcons();
|
|
||||||
|
|
||||||
_buttonBack = findViewById(R.id.button_back);
|
|
||||||
_form = findViewById(R.id.settings_form);
|
|
||||||
|
|
||||||
_form.fromObject(SettingsDev.instance);
|
|
||||||
_form.onChanged.subscribe { _, _ ->
|
|
||||||
_form.setObjectValues();
|
|
||||||
SettingsDev.instance.save();
|
|
||||||
};
|
|
||||||
|
|
||||||
_buttonBack.setOnClickListener {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finish() {
|
|
||||||
super.finish()
|
|
||||||
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
//TODO: Temporary for solving Settings issues
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
private var _lastActivity: DeveloperActivity? = null;
|
|
||||||
|
|
||||||
fun getActivity(): DeveloperActivity? {
|
|
||||||
val act = _lastActivity;
|
|
||||||
if(act != null)
|
|
||||||
return act;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.UpdateInstaller
|
||||||
|
import com.futo.platformplayer.UpdateNotificationManager
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class InstallUpdateActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
UpdateNotificationManager.cancelAll(this)
|
||||||
|
|
||||||
|
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
||||||
|
val apkPath = intent.getStringExtra(UpdateNotificationManager.EXTRA_APK_PATH)
|
||||||
|
|
||||||
|
if (version == 0 || apkPath.isNullOrEmpty()) {
|
||||||
|
Logger.w("InstallUpdateActivity", "Missing version or apkPath")
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val apkFile = File(apkPath)
|
||||||
|
if (!apkFile.exists()) {
|
||||||
|
Logger.w("InstallUpdateActivity", "APK file does not exist: $apkPath")
|
||||||
|
UIDialogs.Companion.toast(this, "Update file missing")
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateInstaller.startInstall(this, version, apkFile)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun createIntent(context: Context, version: Int, apkPath: String): Intent =
|
||||||
|
Intent(context, InstallUpdateActivity::class.java).apply {
|
||||||
|
putExtra(UpdateNotificationManager.EXTRA_VERSION, version)
|
||||||
|
putExtra(UpdateNotificationManager.EXTRA_APK_PATH, apkPath)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import android.content.Intent
|
|||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -16,7 +17,6 @@ import android.os.StrictMode.VmPolicy
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
@@ -36,6 +36,7 @@ import androidx.lifecycle.withStateAtLeast
|
|||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.RootInsetsController
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
@@ -52,17 +53,28 @@ import com.futo.platformplayer.fragment.mainactivity.main.CommentsFragment
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.main.ContentSearchResultsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ContentSearchResultsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.CreatorsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.CreatorsFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.DeveloperFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.DownloadsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.DownloadsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.HistoryFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.HistoryFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ImportPlaylistsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ImportPlaylistsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumsFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryArtistFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryArtistsFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFilesFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibrarySearchFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryVideosFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LoginFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ShortsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ShortsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
|
||||||
@@ -76,6 +88,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment.St
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.WebDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.WebDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.topbar.FilesTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
||||||
@@ -147,6 +160,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragTopBarNavigation: NavigationTopBarFragment;
|
lateinit var _fragTopBarNavigation: NavigationTopBarFragment;
|
||||||
lateinit var _fragTopBarImport: ImportTopBarFragment;
|
lateinit var _fragTopBarImport: ImportTopBarFragment;
|
||||||
lateinit var _fragTopBarAdd: AddTopBarFragment;
|
lateinit var _fragTopBarAdd: AddTopBarFragment;
|
||||||
|
lateinit var _fragTopBarFiles: FilesTopBarFragment;
|
||||||
|
|
||||||
//Frags BotBar
|
//Frags BotBar
|
||||||
lateinit var _fragBotBarMenu: MenuBottomBarFragment;
|
lateinit var _fragBotBarMenu: MenuBottomBarFragment;
|
||||||
@@ -179,6 +193,17 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragBuy: BuyFragment;
|
lateinit var _fragBuy: BuyFragment;
|
||||||
lateinit var _fragSubGroup: SubscriptionGroupFragment;
|
lateinit var _fragSubGroup: SubscriptionGroupFragment;
|
||||||
lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
|
lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
|
||||||
|
lateinit var _fragLibrary: LibraryFragment;
|
||||||
|
lateinit var _fragLibraryAlbums: LibraryAlbumsFragment;
|
||||||
|
lateinit var _fragLibraryAlbum: LibraryAlbumFragment;
|
||||||
|
lateinit var _fragLibraryArtists: LibraryArtistsFragment;
|
||||||
|
lateinit var _fragLibraryArtist: LibraryArtistFragment;
|
||||||
|
lateinit var _fragLibraryVideos: LibraryVideosFragment;
|
||||||
|
lateinit var _fragLibrarySearch: LibrarySearchFragment;
|
||||||
|
lateinit var _fragLibraryFiles: LibraryFilesFragment;
|
||||||
|
lateinit var _fragSettings: SettingsFragment;
|
||||||
|
lateinit var _fragDeveloper: DeveloperFragment;
|
||||||
|
lateinit var _fragLogin: LoginFragment;
|
||||||
|
|
||||||
lateinit var _fragBrowser: BrowserFragment;
|
lateinit var _fragBrowser: BrowserFragment;
|
||||||
|
|
||||||
@@ -187,7 +212,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
//State
|
//State
|
||||||
private val _queue: LinkedList<Pair<MainFragment, Any?>> = LinkedList();
|
private val _queue: LinkedList<Pair<MainFragment, Any?>> = LinkedList();
|
||||||
lateinit var fragCurrent: MainFragment private set;
|
var fragCurrent: MainFragment? = null; private set;
|
||||||
private var _parameterCurrent: Any? = null;
|
private var _parameterCurrent: Any? = null;
|
||||||
|
|
||||||
var fragBeforeOverlay: MainFragment? = null; private set;
|
var fragBeforeOverlay: MainFragment? = null; private set;
|
||||||
@@ -199,6 +224,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
private var _privateModeEnabled = false
|
private var _privateModeEnabled = false
|
||||||
private var _pictureInPictureEnabled = false
|
private var _pictureInPictureEnabled = false
|
||||||
private var _isFullscreen = false
|
private var _isFullscreen = false
|
||||||
|
private lateinit var _rootInsetsController: RootInsetsController
|
||||||
|
|
||||||
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
@@ -274,6 +300,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Logger.w(TAG, "MainActivity Starting [$mainId]");
|
Logger.w(TAG, "MainActivity Starting [$mainId]");
|
||||||
|
|
||||||
StateApp.instance.setGlobalContext(this, lifecycleScope, mainId);
|
StateApp.instance.setGlobalContext(this, lifecycleScope, mainId);
|
||||||
StateApp.instance.mainAppStarting(this);
|
StateApp.instance.mainAppStarting(this);
|
||||||
|
|
||||||
@@ -284,9 +311,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
if (Settings.instance.playback.allowVideoToGoUnderCutout)
|
|
||||||
window.attributes.layoutInDisplayCutoutMode =
|
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
try {
|
try {
|
||||||
@@ -296,11 +320,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
window.isNavigationBarContrastEnforced = false
|
||||||
|
}
|
||||||
|
|
||||||
//Preload common files to memory
|
//Preload common files to memory
|
||||||
FragmentedStorage.get<SubscriptionStorage>();
|
FragmentedStorage.get<SubscriptionStorage>();
|
||||||
FragmentedStorage.get<Settings>();
|
FragmentedStorage.get<Settings>();
|
||||||
|
|
||||||
rootView = findViewById(R.id.rootView);
|
rootView = findViewById(R.id.rootView);
|
||||||
|
_rootInsetsController = RootInsetsController.attach(this, rootView)
|
||||||
|
_rootInsetsController.setLightSystemBarAppearance(lightStatus = false, lightNav = false)
|
||||||
|
|
||||||
_fragContainerTopBar = findViewById(R.id.fragment_top_bar);
|
_fragContainerTopBar = findViewById(R.id.fragment_top_bar);
|
||||||
_fragContainerMain = findViewById(R.id.fragment_main);
|
_fragContainerMain = findViewById(R.id.fragment_main);
|
||||||
_fragContainerBotBar = findViewById(R.id.fragment_bottom_bar);
|
_fragContainerBotBar = findViewById(R.id.fragment_bottom_bar);
|
||||||
@@ -317,6 +348,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragTopBarNavigation = NavigationTopBarFragment.newInstance();
|
_fragTopBarNavigation = NavigationTopBarFragment.newInstance();
|
||||||
_fragTopBarImport = ImportTopBarFragment.newInstance();
|
_fragTopBarImport = ImportTopBarFragment.newInstance();
|
||||||
_fragTopBarAdd = AddTopBarFragment.newInstance();
|
_fragTopBarAdd = AddTopBarFragment.newInstance();
|
||||||
|
_fragTopBarFiles = FilesTopBarFragment.newInstance();
|
||||||
|
|
||||||
//BotBars
|
//BotBars
|
||||||
_fragBotBarMenu = MenuBottomBarFragment.newInstance();
|
_fragBotBarMenu = MenuBottomBarFragment.newInstance();
|
||||||
@@ -349,6 +381,17 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragBuy = BuyFragment.newInstance();
|
_fragBuy = BuyFragment.newInstance();
|
||||||
_fragSubGroup = SubscriptionGroupFragment.newInstance();
|
_fragSubGroup = SubscriptionGroupFragment.newInstance();
|
||||||
_fragSubGroupList = SubscriptionGroupListFragment.newInstance();
|
_fragSubGroupList = SubscriptionGroupListFragment.newInstance();
|
||||||
|
_fragLibrary = LibraryFragment.newInstance();
|
||||||
|
_fragLibraryAlbums = LibraryAlbumsFragment.newInstance();
|
||||||
|
_fragLibraryAlbum = LibraryAlbumFragment.newInstance();
|
||||||
|
_fragLibraryArtists = LibraryArtistsFragment.newInstance();
|
||||||
|
_fragLibraryArtist = LibraryArtistFragment.newInstance();
|
||||||
|
_fragLibraryVideos = LibraryVideosFragment.newInstance();
|
||||||
|
_fragLibraryFiles = LibraryFilesFragment.newInstance();
|
||||||
|
_fragLibrarySearch = LibrarySearchFragment.newInstance();
|
||||||
|
_fragSettings = SettingsFragment.newInstance();
|
||||||
|
_fragDeveloper = DeveloperFragment.newInstance();
|
||||||
|
_fragLogin = LoginFragment.newInstance();
|
||||||
|
|
||||||
_fragBrowser = BrowserFragment.newInstance();
|
_fragBrowser = BrowserFragment.newInstance();
|
||||||
|
|
||||||
@@ -367,12 +410,17 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
updateSegmentPaddings();
|
updateSegmentPaddings();
|
||||||
};
|
};
|
||||||
_fragVideoDetail.onTransitioning.subscribe {
|
_fragVideoDetail.onTransitioning.subscribe {
|
||||||
if (it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED)
|
if (it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED) {
|
||||||
|
Logger.i(TAG, "onTransition Setting elevation higher");
|
||||||
_fragContainerOverlay.elevation =
|
_fragContainerOverlay.elevation =
|
||||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics);
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics);
|
||||||
else
|
}
|
||||||
|
else {
|
||||||
|
Logger.i(TAG, "onTransition Setting elevation lower");
|
||||||
_fragContainerOverlay.elevation =
|
_fragContainerOverlay.elevation =
|
||||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_fragVideoDetail.onCloseEvent.subscribe {
|
_fragVideoDetail.onCloseEvent.subscribe {
|
||||||
@@ -411,6 +459,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.i(TAG, "onFullscreenChanged ${it}");
|
Logger.i(TAG, "onFullscreenChanged ${it}");
|
||||||
_isFullscreen = it
|
_isFullscreen = it
|
||||||
updatePrivateModeVisibility()
|
updatePrivateModeVisibility()
|
||||||
|
if (it) {
|
||||||
|
_rootInsetsController.enterFullscreen(allowCutoutShortEdges = Settings.instance.playback.allowVideoToGoUnderCutout)
|
||||||
|
} else {
|
||||||
|
_rootInsetsController.exitFullscreen()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_fragVideoDetail.onMinimize.subscribe {
|
_fragVideoDetail.onMinimize.subscribe {
|
||||||
@@ -475,6 +528,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
||||||
_fragImportPlaylists.topBar = _fragTopBarImport;
|
_fragImportPlaylists.topBar = _fragTopBarImport;
|
||||||
_fragSubGroupList.topBar = _fragTopBarAdd;
|
_fragSubGroupList.topBar = _fragTopBarAdd;
|
||||||
|
_fragLibrary.topBar = _fragTopBarGeneral;
|
||||||
|
_fragLibraryAlbums.topBar = _fragTopBarNavigation;
|
||||||
|
_fragLibraryAlbum.topBar = _fragTopBarNavigation;
|
||||||
|
_fragLibraryArtists.topBar = _fragTopBarNavigation;
|
||||||
|
_fragLibraryArtist.topBar = _fragTopBarNavigation;
|
||||||
|
_fragLibraryVideos.topBar = _fragTopBarNavigation;
|
||||||
|
_fragLibraryFiles.topBar = _fragTopBarFiles;
|
||||||
|
_fragLibrarySearch.topBar = _fragTopBarSearch;
|
||||||
|
_fragSettings.topBar = _fragTopBarNavigation;
|
||||||
|
_fragDeveloper.topBar = _fragTopBarNavigation;
|
||||||
|
|
||||||
_fragBrowser.topBar = _fragTopBarNavigation;
|
_fragBrowser.topBar = _fragTopBarNavigation;
|
||||||
|
|
||||||
@@ -500,7 +563,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
defaultTab.action(_fragBotBarMenu);
|
defaultTab.action(_fragBotBarMenu);
|
||||||
StateSubscriptions.instance;
|
StateSubscriptions.instance;
|
||||||
|
|
||||||
fragCurrent.onShown(null, false);
|
fragCurrent?.onShown(null, false);
|
||||||
|
|
||||||
//Other stuff
|
//Other stuff
|
||||||
rootView.progress = 0f;
|
rootView.progress = 0f;
|
||||||
@@ -555,6 +618,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
|
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && Settings.instance.autoUpdate.isAutoUpdateEnabled() && Settings.instance.autoUpdate.shouldBackgroundDownload) {
|
||||||
|
requestNotificationPermissions("You have enabled background updating.\n\nGrayjay uses notifications to inform you when a new app update is available.");
|
||||||
|
}
|
||||||
|
|
||||||
val submissionStatus = FragmentedStorage.get<StringStorage>("subscriptionSubmissionStatus")
|
val submissionStatus = FragmentedStorage.get<StringStorage>("subscriptionSubmissionStatus")
|
||||||
|
|
||||||
val numSubscriptions = StateSubscriptions.instance.getSubscriptionCount()
|
val numSubscriptions = StateSubscriptions.instance.getSubscriptionCount()
|
||||||
@@ -639,6 +706,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
private var _qrCodeLoadingDialog: AlertDialog? = null
|
private var _qrCodeLoadingDialog: AlertDialog? = null
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
_rootInsetsController.onConfigurationChanged()
|
||||||
|
}
|
||||||
|
|
||||||
fun showUrlQrCodeScanner() {
|
fun showUrlQrCodeScanner() {
|
||||||
try {
|
try {
|
||||||
_qrCodeLoadingDialog = UIDialogs.showDialog(this, R.drawable.ic_loader_animated, true,
|
_qrCodeLoadingDialog = UIDialogs.showDialog(this, R.drawable.ic_loader_animated, true,
|
||||||
@@ -697,17 +769,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_wasStopped = true;
|
_wasStopped = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent);
|
super.onNewIntent(intent);
|
||||||
handleIntent(intent);
|
handleIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleIntent(intent: Intent?) {
|
private fun handleIntent(intent: Intent) {
|
||||||
if (intent == null)
|
|
||||||
return;
|
|
||||||
Logger.i(TAG, "handleIntent started by " + intent.action);
|
Logger.i(TAG, "handleIntent started by " + intent.action);
|
||||||
|
|
||||||
|
|
||||||
var targetData: String? = null;
|
var targetData: String? = null;
|
||||||
|
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
@@ -1086,7 +1154,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
|
if (_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!fragCurrent.onBackPressed())
|
if (!(fragCurrent?.onBackPressed() ?: true))
|
||||||
closeSegment();
|
closeSegment();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1137,6 +1205,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Fragment> navigate(parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
|
||||||
|
val segment = getFragment<T>();
|
||||||
|
navigate(segment as MainFragment, parameter, withHistory, isBack);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate takes a MainFragment, and makes them the current main visible view
|
* Navigate takes a MainFragment, and makes them the current main visible view
|
||||||
* A parameter can be provided which becomes available in the onShow of said fragment
|
* A parameter can be provided which becomes available in the onShow of said fragment
|
||||||
@@ -1159,27 +1232,27 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fragCurrent.onHide();
|
fragCurrent?.onHide();
|
||||||
|
|
||||||
if (segment.isMainView) {
|
if (segment.isMainView) {
|
||||||
var transaction = supportFragmentManager.beginTransaction();
|
var transaction = supportFragmentManager.beginTransaction();
|
||||||
if (segment.topBar != null) {
|
if (segment.topBar != null) {
|
||||||
if (segment.topBar != fragCurrent.topBar) {
|
if (segment.topBar != fragCurrent?.topBar) {
|
||||||
transaction = transaction
|
transaction = transaction
|
||||||
.show(segment.topBar as Fragment)
|
.show(segment.topBar as Fragment)
|
||||||
.replace(R.id.fragment_top_bar, segment.topBar as Fragment);
|
.replace(R.id.fragment_top_bar, segment.topBar as Fragment);
|
||||||
fragCurrent.topBar?.onHide();
|
fragCurrent?.topBar?.onHide();
|
||||||
}
|
}
|
||||||
} else if (fragCurrent.topBar != null)
|
} else if (fragCurrent?.topBar != null)
|
||||||
transaction.hide(fragCurrent.topBar as Fragment);
|
transaction.hide(fragCurrent?.topBar as Fragment);
|
||||||
|
|
||||||
transaction = transaction.replace(R.id.fragment_main, segment);
|
transaction = transaction.replace(R.id.fragment_main, segment);
|
||||||
|
|
||||||
if (segment.hasBottomBar) {
|
if (segment.hasBottomBar) {
|
||||||
if (!fragCurrent.hasBottomBar)
|
if (!(fragCurrent?.hasBottomBar ?: false))
|
||||||
transaction = transaction.show(_fragBotBarMenu);
|
transaction = transaction.show(_fragBotBarMenu);
|
||||||
} else {
|
} else {
|
||||||
if (fragCurrent.hasBottomBar)
|
if (fragCurrent?.hasBottomBar ?: false)
|
||||||
transaction = transaction.hide(_fragBotBarMenu);
|
transaction = transaction.hide(_fragBotBarMenu);
|
||||||
}
|
}
|
||||||
transaction.commitNow();
|
transaction.commitNow();
|
||||||
@@ -1192,10 +1265,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fragCurrent.isHistory && withHistory && _queue.lastOrNull() != fragCurrent)
|
if (fragCurrent?.isHistory ?: false && withHistory && _queue.lastOrNull() != fragCurrent)
|
||||||
_queue.add(Pair(fragCurrent, _parameterCurrent));
|
_queue.add(Pair(fragCurrent!!, _parameterCurrent));
|
||||||
|
|
||||||
if (segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory)
|
if (segment.isOverlay && !(fragCurrent?.isOverlay ?: false) && withHistory)// && fragCurrent.isHistory)
|
||||||
fragBeforeOverlay = fragCurrent;
|
fragBeforeOverlay = fragCurrent;
|
||||||
|
|
||||||
fragCurrent = segment;
|
fragCurrent = segment;
|
||||||
@@ -1226,11 +1299,24 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
navigate(last.first, last.second, false, true);
|
navigate(last.first, last.second, false, true);
|
||||||
} else {
|
} else {
|
||||||
if (_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
if (_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
||||||
|
Logger.i(TAG, "Closing activity because _fragVideoDetail.state == closed");
|
||||||
finish();
|
finish();
|
||||||
} else {
|
} else {
|
||||||
|
//UIDialogs.toast("Grayjay continues in background because of an open video.")
|
||||||
|
if(Settings.instance.playback.isBackgroundPictureInPicture()) {
|
||||||
|
try {
|
||||||
|
_fragVideoDetail._viewDetail?.startPictureInPicture();
|
||||||
|
_fragVideoDetail?.forcePictureInPicture();
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
} //Fail silently
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moveTaskToBack(false);
|
||||||
|
/*
|
||||||
UIDialogs.showConfirmationDialog(this, "There is a video playing, are you sure you want to exit the app?", {
|
UIDialogs.showConfirmationDialog(this, "There is a video playing, are you sure you want to exit the app?", {
|
||||||
finish();
|
finish();
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1249,6 +1335,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
VideoDetailFragment::class -> _fragVideoDetail as T;
|
VideoDetailFragment::class -> _fragVideoDetail as T;
|
||||||
MenuBottomBarFragment::class -> _fragBotBarMenu as T;
|
MenuBottomBarFragment::class -> _fragBotBarMenu as T;
|
||||||
GeneralTopBarFragment::class -> _fragTopBarGeneral as T;
|
GeneralTopBarFragment::class -> _fragTopBarGeneral as T;
|
||||||
|
FilesTopBarFragment::class -> _fragTopBarFiles as T;
|
||||||
SearchTopBarFragment::class -> _fragTopBarSearch as T;
|
SearchTopBarFragment::class -> _fragTopBarSearch as T;
|
||||||
CreatorsFragment::class -> _fragMainSubscriptions as T;
|
CreatorsFragment::class -> _fragMainSubscriptions as T;
|
||||||
CommentsFragment::class -> _fragMainComments as T;
|
CommentsFragment::class -> _fragMainComments as T;
|
||||||
@@ -1273,6 +1360,17 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
BuyFragment::class -> _fragBuy as T;
|
BuyFragment::class -> _fragBuy as T;
|
||||||
SubscriptionGroupFragment::class -> _fragSubGroup as T;
|
SubscriptionGroupFragment::class -> _fragSubGroup as T;
|
||||||
SubscriptionGroupListFragment::class -> _fragSubGroupList as T;
|
SubscriptionGroupListFragment::class -> _fragSubGroupList as T;
|
||||||
|
LibraryFragment::class -> _fragLibrary as T;
|
||||||
|
LibraryAlbumsFragment::class -> _fragLibraryAlbums as T;
|
||||||
|
LibraryAlbumFragment::class -> _fragLibraryAlbum as T;
|
||||||
|
LibraryArtistsFragment::class -> _fragLibraryArtists as T;
|
||||||
|
LibraryArtistFragment::class -> _fragLibraryArtist as T;
|
||||||
|
LibraryVideosFragment::class -> _fragLibraryVideos as T;
|
||||||
|
LibraryFilesFragment::class -> _fragLibraryFiles as T;
|
||||||
|
LibrarySearchFragment::class -> _fragLibrarySearch as T;
|
||||||
|
SettingsFragment:: class -> _fragSettings as T;
|
||||||
|
DeveloperFragment::class -> _fragDeveloper as T;
|
||||||
|
LoginFragment::class -> _fragLogin as T;
|
||||||
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
|
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1280,7 +1378,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
private fun updateSegmentPaddings() {
|
private fun updateSegmentPaddings() {
|
||||||
var paddingBottom = 0f;
|
var paddingBottom = 0f;
|
||||||
if (fragCurrent.hasBottomBar)
|
if (fragCurrent?.hasBottomBar ?: false)
|
||||||
paddingBottom += HEIGHT_MENU_DP;
|
paddingBottom += HEIGHT_MENU_DP;
|
||||||
|
|
||||||
_fragContainerOverlay.setPadding(
|
_fragContainerOverlay.setPadding(
|
||||||
@@ -1297,6 +1395,23 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _callbackPermissionAudio: ((Boolean)->Unit)? = null;
|
||||||
|
var _callbackPermissionVideo: ((Boolean)->Unit)? = null;
|
||||||
|
val permissionReqAudio = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
|
||||||
|
_callbackPermissionAudio?.invoke(isGranted);
|
||||||
|
});
|
||||||
|
val permissionReqVideo = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
|
||||||
|
_callbackPermissionVideo?.invoke(isGranted);
|
||||||
|
});
|
||||||
|
fun requestPermissionAudio(cb: ((Boolean)->Unit)? = null) {
|
||||||
|
_callbackPermissionAudio = cb;
|
||||||
|
permissionReqAudio.launch(android.Manifest.permission.READ_MEDIA_AUDIO);
|
||||||
|
}
|
||||||
|
fun requestPermissionVideo(cb: ((Boolean)->Unit)? = null) {
|
||||||
|
_callbackPermissionVideo = cb;
|
||||||
|
permissionReqVideo.launch(android.Manifest.permission.READ_MEDIA_VIDEO);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
||||||
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||||
|
|||||||
@@ -13,15 +13,18 @@ import android.view.View
|
|||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateApp.Companion.withContext
|
import com.futo.platformplayer.states.StateApp.Companion.withContext
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import com.futo.platformplayer.activities.QRCodeFullscreenActivity
|
||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.ContentType
|
||||||
import com.futo.polycentric.core.SignedEvent
|
import com.futo.polycentric.core.SignedEvent
|
||||||
import com.futo.polycentric.core.StorageTypeCRDTItem
|
import com.futo.polycentric.core.StorageTypeCRDTItem
|
||||||
@@ -29,8 +32,10 @@ import com.futo.polycentric.core.StorageTypeCRDTSetItem
|
|||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.toBase64Url
|
import com.futo.polycentric.core.toBase64Url
|
||||||
import com.google.zxing.BarcodeFormat
|
import com.google.zxing.BarcodeFormat
|
||||||
|
import com.google.zxing.EncodeHintType
|
||||||
import com.google.zxing.MultiFormatWriter
|
import com.google.zxing.MultiFormatWriter
|
||||||
import com.google.zxing.common.BitMatrix
|
import com.google.zxing.common.BitMatrix
|
||||||
|
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -41,11 +46,27 @@ import userpackage.Protocol.URLInfo
|
|||||||
class PolycentricBackupActivity : AppCompatActivity() {
|
class PolycentricBackupActivity : AppCompatActivity() {
|
||||||
private lateinit var _buttonShare: BigButton;
|
private lateinit var _buttonShare: BigButton;
|
||||||
private lateinit var _buttonCopy: BigButton;
|
private lateinit var _buttonCopy: BigButton;
|
||||||
|
private lateinit var _buttonExportFile: BigButton;
|
||||||
private lateinit var _imageQR: ImageView;
|
private lateinit var _imageQR: ImageView;
|
||||||
private lateinit var _exportBundle: String;
|
private lateinit var _exportBundle: String;
|
||||||
private lateinit var _textQR: TextView;
|
private lateinit var _textQR: TextView;
|
||||||
|
private lateinit var _textQRHint: TextView;
|
||||||
private lateinit var _loader: View
|
private lateinit var _loader: View
|
||||||
|
|
||||||
|
private val _createDocumentLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
||||||
|
uri?.let { fileUri ->
|
||||||
|
try {
|
||||||
|
contentResolver.openOutputStream(fileUri)?.use { outputStream ->
|
||||||
|
outputStream.write(_exportBundle.toByteArray())
|
||||||
|
}
|
||||||
|
UIDialogs.toast(this, getString(R.string.profile_saved_successfully))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.e(TAG, "Failed to write to document", e)
|
||||||
|
UIDialogs.toast(this, "Failed to save profile: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
}
|
}
|
||||||
@@ -57,8 +78,10 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_buttonShare = findViewById(R.id.button_share)
|
_buttonShare = findViewById(R.id.button_share)
|
||||||
_buttonCopy = findViewById(R.id.button_copy)
|
_buttonCopy = findViewById(R.id.button_copy)
|
||||||
|
_buttonExportFile = findViewById(R.id.button_export_file)
|
||||||
_imageQR = findViewById(R.id.image_qr)
|
_imageQR = findViewById(R.id.image_qr)
|
||||||
_textQR = findViewById(R.id.text_qr)
|
_textQR = findViewById(R.id.text_qr)
|
||||||
|
_textQRHint = findViewById(R.id.text_qr_hint)
|
||||||
_loader = findViewById(R.id.progress_loader)
|
_loader = findViewById(R.id.progress_loader)
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
@@ -66,14 +89,23 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_imageQR.visibility = View.INVISIBLE
|
_imageQR.visibility = View.INVISIBLE
|
||||||
_textQR.visibility = View.INVISIBLE
|
_textQR.visibility = View.INVISIBLE
|
||||||
|
_textQRHint.visibility = View.INVISIBLE
|
||||||
_loader.visibility = View.VISIBLE
|
_loader.visibility = View.VISIBLE
|
||||||
_buttonShare.visibility = View.INVISIBLE
|
_buttonShare.visibility = View.INVISIBLE
|
||||||
_buttonCopy.visibility = View.INVISIBLE
|
_buttonCopy.visibility = View.INVISIBLE
|
||||||
|
_buttonExportFile.visibility = View.INVISIBLE
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
val bundle = withContext(Dispatchers.IO) { createExportBundle() }
|
||||||
|
_exportBundle = bundle
|
||||||
|
Logger.i(TAG, "Export bundle created, length: ${bundle.length}")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val pair = withContext(Dispatchers.IO) {
|
val pair = withContext(Dispatchers.IO) {
|
||||||
val bundle = createExportBundle()
|
if (!isContentSuitableForQRCode(bundle)) {
|
||||||
|
throw Exception("Data too big for QR code generation")
|
||||||
|
}
|
||||||
|
|
||||||
val dimension = TypedValue.applyDimension(
|
val dimension = TypedValue.applyDimension(
|
||||||
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
|
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
|
||||||
).toInt()
|
).toInt()
|
||||||
@@ -81,18 +113,35 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
Pair(bundle, qr)
|
Pair(bundle, qr)
|
||||||
}
|
}
|
||||||
|
|
||||||
_exportBundle = pair.first
|
|
||||||
_imageQR.setImageBitmap(pair.second)
|
_imageQR.setImageBitmap(pair.second)
|
||||||
_imageQR.visibility = View.VISIBLE
|
_imageQR.visibility = View.VISIBLE
|
||||||
_textQR.visibility = View.VISIBLE
|
_textQR.visibility = View.VISIBLE
|
||||||
|
_textQRHint.visibility = View.VISIBLE
|
||||||
_buttonShare.visibility = View.VISIBLE
|
_buttonShare.visibility = View.VISIBLE
|
||||||
_buttonCopy.visibility = View.VISIBLE
|
_buttonCopy.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
_imageQR.setOnClickListener {
|
||||||
|
val intent = QRCodeFullscreenActivity.createIntent(this@PolycentricBackupActivity, _exportBundle)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
|
val byteSize = bundle.toByteArray(Charsets.UTF_8).size
|
||||||
|
Logger.e(TAG, "QR code generation failed. Bundle length: ${bundle.length} chars, ${byteSize} bytes, Error: ${e.message}", e)
|
||||||
|
|
||||||
|
if (e.message?.contains("Data too big") == true) {
|
||||||
|
_textQR.text = getString(R.string.qr_code_too_large_use_file_export)
|
||||||
|
_buttonExportFile.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
_textQR.text = getString(R.string.failed_to_generate_qr_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
_textQR.visibility = View.VISIBLE
|
||||||
|
_textQRHint.visibility = View.INVISIBLE
|
||||||
|
_buttonShare.visibility = View.VISIBLE
|
||||||
|
_buttonCopy.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
// Hide QR image since generation failed
|
||||||
_imageQR.visibility = View.INVISIBLE
|
_imageQR.visibility = View.INVISIBLE
|
||||||
_textQR.visibility = View.INVISIBLE
|
|
||||||
_buttonShare.visibility = View.INVISIBLE
|
|
||||||
_buttonCopy.visibility = View.INVISIBLE
|
|
||||||
} finally {
|
} finally {
|
||||||
_loader.visibility = View.GONE
|
_loader.visibility = View.GONE
|
||||||
}
|
}
|
||||||
@@ -108,11 +157,29 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
|
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
|
||||||
clipboard.setPrimaryClip(clip);
|
clipboard.setPrimaryClip(clip);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_buttonExportFile.onClick.subscribe {
|
||||||
|
val fileName = "polycentric_profile_${System.currentTimeMillis()}.txt"
|
||||||
|
_createDocumentLauncher.launch(fileName)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isContentSuitableForQRCode(content: String): Boolean {
|
||||||
|
val bytes = content.toByteArray(Charsets.UTF_8)
|
||||||
|
return bytes.size <= 2300 // QR Code Version 40 with Error Correction Level M can hold ~2331 bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
|
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
|
||||||
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
|
if (!isContentSuitableForQRCode(content)) {
|
||||||
return bitMatrixToBitmap(bitMatrix);
|
throw Exception("Data too big for QR code generation")
|
||||||
|
}
|
||||||
|
|
||||||
|
val hints = java.util.EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
||||||
|
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
|
||||||
|
hints[EncodeHintType.MARGIN] = 1
|
||||||
|
|
||||||
|
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
|
||||||
|
return bitMatrixToBitmap(bitMatrix)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
|
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
|
||||||
@@ -203,7 +270,8 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
.setBody(exportBundle.toByteString())
|
.setBody(exportBundle.toByteString())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return "polycentric://" + urlInfo.toByteArray().toBase64Url()
|
val data = urlInfo.toByteArray()
|
||||||
|
return "polycentric://" + data.toBase64Url()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
+133
-61
@@ -32,100 +32,166 @@ import userpackage.Protocol
|
|||||||
import userpackage.Protocol.ExportBundle
|
import userpackage.Protocol.ExportBundle
|
||||||
|
|
||||||
class PolycentricImportProfileActivity : AppCompatActivity() {
|
class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||||
private lateinit var _buttonHelp: ImageButton;
|
private lateinit var _buttonHelp: ImageButton
|
||||||
private lateinit var _buttonScanProfile: LinearLayout;
|
private lateinit var _buttonScanProfile: LinearLayout
|
||||||
private lateinit var _buttonImportProfile: LinearLayout;
|
private lateinit var _buttonImportFile: LinearLayout
|
||||||
private lateinit var _editProfile: EditText;
|
private lateinit var _buttonImportProfile: LinearLayout
|
||||||
private lateinit var _loaderOverlay: LoaderOverlay;
|
private lateinit var _editProfile: EditText
|
||||||
|
private lateinit var _loaderOverlay: LoaderOverlay
|
||||||
|
|
||||||
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val _qrCodeResultLauncher =
|
||||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
scanResult?.let {
|
val scanResult =
|
||||||
if (it.contents != null) {
|
IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
val scannedUrl = it.contents
|
scanResult?.let {
|
||||||
import(scannedUrl)
|
if (it.contents != null) {
|
||||||
|
val scannedUrl = it.contents
|
||||||
|
import(scannedUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _filePickerLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||||
|
uri?.let { fileUri ->
|
||||||
|
try {
|
||||||
|
// Check file size before reading
|
||||||
|
val fileSize =
|
||||||
|
contentResolver.openFileDescriptor(fileUri, "r")?.statSize ?: 0
|
||||||
|
val maxFileSize = 10 * 1024 * 1024 // 10MB limit
|
||||||
|
|
||||||
|
if (fileSize > maxFileSize) {
|
||||||
|
UIDialogs.toast(this, "File too large. Maximum size is 10MB.")
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileSize == 0L) {
|
||||||
|
UIDialogs.toast(this, "Selected file is empty.")
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
|
||||||
|
val content =
|
||||||
|
contentResolver
|
||||||
|
.openInputStream(fileUri)
|
||||||
|
?.bufferedReader()
|
||||||
|
?.readText()
|
||||||
|
content?.let { fileContent ->
|
||||||
|
val trimmedContent = fileContent.trim()
|
||||||
|
|
||||||
|
// Check if content is empty after trimming
|
||||||
|
if (trimmedContent.isEmpty()) {
|
||||||
|
UIDialogs.toast(this, "Selected file contains no data.")
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if content looks like a valid polycentric URL
|
||||||
|
if (!trimmedContent.startsWith("polycentric://")) {
|
||||||
|
UIDialogs.toast(
|
||||||
|
this,
|
||||||
|
"Selected file does not contain a valid polycentric profile URL."
|
||||||
|
)
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
|
||||||
|
import(trimmedContent)
|
||||||
|
}
|
||||||
|
?: run { UIDialogs.toast(this, "Could not read file content.") }
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Logger.e(TAG, "Security exception reading file", e)
|
||||||
|
UIDialogs.toast(this, "Permission denied to read file.")
|
||||||
|
} catch (e: OutOfMemoryError) {
|
||||||
|
Logger.e(TAG, "Out of memory reading file", e)
|
||||||
|
UIDialogs.toast(this, "File too large to process.")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.e(TAG, "Failed to read file", e)
|
||||||
|
UIDialogs.toast(this, "Failed to read file: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_polycentric_import_profile);
|
setContentView(R.layout.activity_polycentric_import_profile)
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons()
|
||||||
|
|
||||||
_buttonHelp = findViewById(R.id.button_help);
|
_buttonHelp = findViewById(R.id.button_help)
|
||||||
_buttonScanProfile = findViewById(R.id.button_scan_profile);
|
_buttonScanProfile = findViewById(R.id.button_scan_profile)
|
||||||
_buttonImportProfile = findViewById(R.id.button_import_profile);
|
_buttonImportFile = findViewById(R.id.button_import_file)
|
||||||
_loaderOverlay = findViewById(R.id.loader_overlay);
|
_buttonImportProfile = findViewById(R.id.button_import_profile)
|
||||||
_editProfile = findViewById(R.id.edit_profile);
|
_loaderOverlay = findViewById(R.id.loader_overlay)
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
_editProfile = findViewById(R.id.edit_profile)
|
||||||
finish();
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener { finish() }
|
||||||
};
|
|
||||||
|
|
||||||
_buttonHelp.setOnClickListener {
|
_buttonHelp.setOnClickListener {
|
||||||
startActivity(Intent(this, PolycentricWhyActivity::class.java));
|
startActivity(Intent(this, PolycentricWhyActivity::class.java))
|
||||||
};
|
}
|
||||||
|
|
||||||
_buttonScanProfile.setOnClickListener {
|
_buttonScanProfile.setOnClickListener {
|
||||||
val integrator = IntentIntegrator(this)
|
val integrator = IntentIntegrator(this)
|
||||||
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||||
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
||||||
integrator.setOrientationLocked(true);
|
integrator.setOrientationLocked(true)
|
||||||
integrator.setCameraId(0)
|
integrator.setCameraId(0)
|
||||||
integrator.setBeepEnabled(false)
|
integrator.setBeepEnabled(false)
|
||||||
integrator.setBarcodeImageEnabled(true)
|
integrator.setBarcodeImageEnabled(true)
|
||||||
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
integrator.setCaptureActivity(QRCaptureActivity::class.java)
|
||||||
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||||
};
|
}
|
||||||
|
|
||||||
|
_buttonImportFile.setOnClickListener { _filePickerLauncher.launch("text/plain") }
|
||||||
|
|
||||||
_buttonImportProfile.setOnClickListener {
|
_buttonImportProfile.setOnClickListener {
|
||||||
if (_editProfile.text.isEmpty()) {
|
if (_editProfile.text.isEmpty()) {
|
||||||
UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data));
|
UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data))
|
||||||
return@setOnClickListener;
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
|
|
||||||
import(_editProfile.text.toString());
|
import(_editProfile.text.toString())
|
||||||
};
|
}
|
||||||
|
|
||||||
val url = intent.getStringExtra("url");
|
val url = intent.getStringExtra("url")
|
||||||
if (url != null) {
|
if (url != null) {
|
||||||
import(url);
|
import(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun import(url: String) {
|
private fun import(url: String) {
|
||||||
if (!url.startsWith("polycentric://")) {
|
if (!url.startsWith("polycentric://")) {
|
||||||
UIDialogs.toast(this, getString(R.string.not_a_valid_url));
|
UIDialogs.toast(this, getString(R.string.not_a_valid_url))
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_loaderOverlay.show()
|
_loaderOverlay.show()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val data = url.substring("polycentric://".length).base64UrlToByteArray();
|
val data = url.substring("polycentric://".length).base64UrlToByteArray()
|
||||||
val urlInfo = Protocol.URLInfo.parseFrom(data);
|
val urlInfo = Protocol.URLInfo.parseFrom(data)
|
||||||
|
|
||||||
if (urlInfo.urlType != 3L) {
|
if (urlInfo.urlType != 3L) {
|
||||||
throw Exception("Expected urlInfo struct of type ExportBundle")
|
throw Exception("Expected urlInfo struct of type ExportBundle")
|
||||||
}
|
}
|
||||||
|
|
||||||
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
val exportBundle = ExportBundle.parseFrom(urlInfo.body)
|
||||||
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
val keyPair = KeyPair.fromProto(exportBundle.keyPair)
|
||||||
|
|
||||||
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey)
|
||||||
if (existingProcessSecret != null) {
|
if (existingProcessSecret != null) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported));
|
UIDialogs.toast(
|
||||||
|
this@PolycentricImportProfileActivity,
|
||||||
|
getString(R.string.this_profile_is_already_imported)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return@launch;
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val processSecret = ProcessSecret(keyPair, Process.random());
|
val processSecret = ProcessSecret(keyPair, Process.random())
|
||||||
Store.instance.addProcessSecret(processSecret);
|
Store.instance.addProcessSecret(processSecret)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PolycentricStorage.instance.addProcessSecret(processSecret)
|
PolycentricStorage.instance.addProcessSecret(processSecret)
|
||||||
@@ -133,37 +199,43 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
val processHandle = processSecret.toProcessHandle();
|
val processHandle = processSecret.toProcessHandle()
|
||||||
|
|
||||||
for (e in exportBundle.events.eventsList) {
|
for (e in exportBundle.events.eventsList) {
|
||||||
try {
|
try {
|
||||||
val se = SignedEvent.fromProto(e);
|
val se = SignedEvent.fromProto(e)
|
||||||
Store.instance.putSignedEvent(se);
|
Store.instance.putSignedEvent(se)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Ignored invalid event", e);
|
Logger.w(TAG, "Ignored invalid event", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle)
|
||||||
processHandle.fullyBackfillClient(ApiMethods.SERVER);
|
processHandle.fullyBackfillClient(ApiMethods.SERVER)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
startActivity(
|
||||||
finish();
|
Intent(
|
||||||
|
this@PolycentricImportProfileActivity,
|
||||||
|
PolycentricProfileActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to import profile", e);
|
Logger.w(TAG, "Failed to import profile", e)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.failed_to_import_profile) + " '${e.message}'");
|
UIDialogs.toast(
|
||||||
|
this@PolycentricImportProfileActivity,
|
||||||
|
getString(R.string.failed_to_import_profile) + " '${e.message}'"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) { _loaderOverlay.hide() }
|
||||||
_loaderOverlay.hide();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PolycentricImportProfileActivity";
|
private const val TAG = "PolycentricImportProfileActivity"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+147
@@ -0,0 +1,147 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.SeekBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.futo.platformplayer.polycentric.ModerationsManager
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
|
||||||
|
class PolycentricModerationActivity : AppCompatActivity() {
|
||||||
|
private lateinit var _seekbarOffensive: SeekBar
|
||||||
|
private lateinit var _seekbarExplicit: SeekBar
|
||||||
|
private lateinit var _seekbarViolence: SeekBar
|
||||||
|
private lateinit var _textOffensiveDesc: TextView
|
||||||
|
private lateinit var _textExplicitDesc: TextView
|
||||||
|
private lateinit var _textViolenceDesc: TextView
|
||||||
|
private lateinit var _textOffensiveValue: TextView
|
||||||
|
private lateinit var _textExplicitValue: TextView
|
||||||
|
private lateinit var _textViolenceValue: TextView
|
||||||
|
private lateinit var _moderationsManager: ModerationsManager
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_polycentric_moderation)
|
||||||
|
setNavigationBarColorAndIcons()
|
||||||
|
|
||||||
|
_moderationsManager = ModerationsManager.getInstance()
|
||||||
|
try {
|
||||||
|
_moderationsManager = ModerationsManager.getInstance()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_seekbarOffensive = findViewById(R.id.seekbar_offensive)
|
||||||
|
_seekbarExplicit = findViewById(R.id.seekbar_explicit)
|
||||||
|
_seekbarViolence = findViewById(R.id.seekbar_violence)
|
||||||
|
_textOffensiveDesc = findViewById(R.id.text_offensive_desc)
|
||||||
|
_textExplicitDesc = findViewById(R.id.text_explicit_desc)
|
||||||
|
_textViolenceDesc = findViewById(R.id.text_violence_desc)
|
||||||
|
_textOffensiveValue = findViewById(R.id.text_offensive_value)
|
||||||
|
_textExplicitValue = findViewById(R.id.text_explicit_value)
|
||||||
|
_textViolenceValue = findViewById(R.id.text_violence_value)
|
||||||
|
|
||||||
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSettings()
|
||||||
|
setupListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSettings() {
|
||||||
|
val levels = _moderationsManager.moderationLevels.value ?: mapOf()
|
||||||
|
|
||||||
|
val offensiveLevel = levels["hate"] ?: 2
|
||||||
|
val explicitLevel = levels["sexual"] ?: 1
|
||||||
|
val violenceLevel = levels["violence"] ?: 1
|
||||||
|
|
||||||
|
_seekbarOffensive.progress = offensiveLevel
|
||||||
|
_seekbarExplicit.progress = explicitLevel
|
||||||
|
_seekbarViolence.progress = violenceLevel
|
||||||
|
|
||||||
|
updateDescriptionText(_seekbarOffensive, _textOffensiveDesc, _textOffensiveValue, getOffensiveDescriptions())
|
||||||
|
updateDescriptionText(_seekbarExplicit, _textExplicitDesc, _textExplicitValue, getExplicitDescriptions())
|
||||||
|
updateDescriptionText(_seekbarViolence, _textViolenceDesc, _textViolenceValue, getViolenceDescriptions())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupListeners() {
|
||||||
|
_seekbarOffensive.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||||
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||||
|
updateDescriptionText(seekBar, _textOffensiveDesc, _textOffensiveValue, getOffensiveDescriptions())
|
||||||
|
if (fromUser) {
|
||||||
|
_moderationsManager.setModerationLevel("hate", progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
||||||
|
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
_seekbarExplicit.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||||
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||||
|
updateDescriptionText(seekBar, _textExplicitDesc, _textExplicitValue, getExplicitDescriptions())
|
||||||
|
if (fromUser) {
|
||||||
|
_moderationsManager.setModerationLevel("sexual", progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
||||||
|
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
_seekbarViolence.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||||
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||||
|
updateDescriptionText(seekBar, _textViolenceDesc, _textViolenceValue, getViolenceDescriptions())
|
||||||
|
if (fromUser) {
|
||||||
|
_moderationsManager.setModerationLevel("violence", progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
||||||
|
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDescriptionText(seekBar: SeekBar?, textDesc: TextView, textValue: TextView, descriptions: Array<String>) {
|
||||||
|
val progress = seekBar?.progress ?: 0
|
||||||
|
textDesc.text = descriptions[progress]
|
||||||
|
textValue.text = progress.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOffensiveDescriptions(): Array<String> {
|
||||||
|
return arrayOf(
|
||||||
|
"Neutral, general terms, no bias or hate.",
|
||||||
|
"Mildly sensitive, factual.",
|
||||||
|
"Potentially offensive content",
|
||||||
|
"Offensive content"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getExplicitDescriptions(): Array<String> {
|
||||||
|
return arrayOf(
|
||||||
|
"No explicit content",
|
||||||
|
"Mildly suggestive, factual or educational",
|
||||||
|
"Moderate sexual content, non-graphic",
|
||||||
|
"Explicit sexual content"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getViolenceDescriptions(): Array<String> {
|
||||||
|
return arrayOf(
|
||||||
|
"Non-violent",
|
||||||
|
"Mild violence, factual or contextual",
|
||||||
|
"Moderate violence, some graphic content.",
|
||||||
|
"Graphic violence"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonHelp: ImageButton;
|
private lateinit var _buttonHelp: ImageButton;
|
||||||
private lateinit var _editName: EditText;
|
private lateinit var _editName: EditText;
|
||||||
private lateinit var _buttonExport: BigButton;
|
private lateinit var _buttonExport: BigButton;
|
||||||
private lateinit var _buttonOpenHarborProfile: BigButton;
|
private lateinit var _buttonModeration: BigButton;
|
||||||
private lateinit var _buttonLogout: BigButton;
|
private lateinit var _buttonLogout: BigButton;
|
||||||
private lateinit var _buttonDelete: BigButton;
|
private lateinit var _buttonDelete: BigButton;
|
||||||
private lateinit var _username: String;
|
private lateinit var _username: String;
|
||||||
@@ -71,7 +71,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
_imagePolycentric = findViewById(R.id.image_polycentric);
|
_imagePolycentric = findViewById(R.id.image_polycentric);
|
||||||
_editName = findViewById(R.id.edit_profile_name);
|
_editName = findViewById(R.id.edit_profile_name);
|
||||||
_buttonExport = findViewById(R.id.button_export);
|
_buttonExport = findViewById(R.id.button_export);
|
||||||
_buttonOpenHarborProfile = findViewById(R.id.button_open_harbor_profile);
|
_buttonModeration = findViewById(R.id.button_moderation);
|
||||||
_buttonLogout = findViewById(R.id.button_logout);
|
_buttonLogout = findViewById(R.id.button_logout);
|
||||||
_buttonDelete = findViewById(R.id.button_delete);
|
_buttonDelete = findViewById(R.id.button_delete);
|
||||||
_loaderOverlay = findViewById(R.id.loader_overlay);
|
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||||
@@ -99,15 +99,9 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
startActivity(Intent(this, PolycentricBackupActivity::class.java));
|
startActivity(Intent(this, PolycentricBackupActivity::class.java));
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonOpenHarborProfile.onClick.subscribe {
|
_buttonModeration.onClick.subscribe {
|
||||||
val processHandle = StatePolycentric.instance.processHandle!!;
|
startActivity(Intent(this, PolycentricModerationActivity::class.java));
|
||||||
processHandle?.let {
|
};
|
||||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(it.system));
|
|
||||||
val url = it.system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
|
|
||||||
val navUrl = "https://harbor.social/" + url.substring("polycentric://".length)
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_buttonLogout.onClick.subscribe {
|
_buttonLogout.onClick.subscribe {
|
||||||
StatePolycentric.instance.setProcessHandle(null);
|
StatePolycentric.instance.setProcessHandle(null);
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.google.zxing.BarcodeFormat
|
||||||
|
import com.google.zxing.EncodeHintType
|
||||||
|
import com.google.zxing.MultiFormatWriter
|
||||||
|
import com.google.zxing.common.BitMatrix
|
||||||
|
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||||
|
|
||||||
|
class QRCodeFullscreenActivity : AppCompatActivity() {
|
||||||
|
companion object {
|
||||||
|
private const val EXTRA_QR_TEXT = "qr_text"
|
||||||
|
|
||||||
|
fun createIntent(context: Context, qrText: String): android.content.Intent {
|
||||||
|
return android.content.Intent(context, QRCodeFullscreenActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_QR_TEXT, qrText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_qr_code_fullscreen)
|
||||||
|
setNavigationBarColorAndIcons()
|
||||||
|
|
||||||
|
val qrText = intent.getStringExtra(EXTRA_QR_TEXT)
|
||||||
|
|
||||||
|
val imageQR = findViewById<ImageView>(R.id.image_qr_fullscreen)
|
||||||
|
val buttonBack = findViewById<ImageButton>(R.id.button_back_fullscreen)
|
||||||
|
val buttonClose = findViewById<ImageButton>(R.id.button_close_fullscreen)
|
||||||
|
|
||||||
|
// Generate QR code bitmap from text
|
||||||
|
qrText?.let { text ->
|
||||||
|
try {
|
||||||
|
if (!isContentSuitableForQRCode(text)) {
|
||||||
|
throw Exception("Data too big for QR code generation")
|
||||||
|
}
|
||||||
|
|
||||||
|
val dimension = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP, 300f, resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
val qrBitmap = generateQRCode(text, dimension, dimension)
|
||||||
|
imageQR.setImageBitmap(qrBitmap)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// If QR generation fails, show error or fallback
|
||||||
|
imageQR.setImageResource(R.drawable.ic_qr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonBack.setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonClose.setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
imageQR.setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isContentSuitableForQRCode(content: String): Boolean {
|
||||||
|
val bytes = content.toByteArray(Charsets.UTF_8)
|
||||||
|
return bytes.size <= 2300 // QR Code Version 40 with Error Correction Level M can hold ~2331 bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
|
||||||
|
if (!isContentSuitableForQRCode(content)) {
|
||||||
|
throw Exception("Data too big for QR code generation")
|
||||||
|
}
|
||||||
|
|
||||||
|
val hints = java.util.EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
||||||
|
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
|
||||||
|
hints[EncodeHintType.MARGIN] = 1
|
||||||
|
|
||||||
|
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
|
||||||
|
return bitMatrixToBitmap(bitMatrix)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
|
||||||
|
val width = matrix.width
|
||||||
|
val height = matrix.height
|
||||||
|
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
|
||||||
|
|
||||||
|
for (x in 0 until width) {
|
||||||
|
for (y in 0 until height) {
|
||||||
|
bmp.setPixel(x, y, if (matrix[x, y]) Color.BLACK else Color.WHITE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bmp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
package com.futo.platformplayer.activities
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.activity.result.ActivityResult
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.futo.platformplayer.*
|
|
||||||
import com.futo.platformplayer.constructs.Event0
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import com.futo.platformplayer.views.LoaderView
|
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
|
||||||
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
|
||||||
import com.google.android.material.button.MaterialButton
|
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|
||||||
private lateinit var _form: FieldForm;
|
|
||||||
private lateinit var _buttonBack: ImageButton;
|
|
||||||
private lateinit var _loaderView: LoaderView;
|
|
||||||
|
|
||||||
private lateinit var _devSets: LinearLayout;
|
|
||||||
private lateinit var _buttonDev: MaterialButton;
|
|
||||||
|
|
||||||
private var _isFinished = false;
|
|
||||||
|
|
||||||
lateinit var overlay: FrameLayout;
|
|
||||||
|
|
||||||
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
|
||||||
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
|
||||||
if (isGranted)
|
|
||||||
UIDialogs.toast(this, "Notification permission granted");
|
|
||||||
else
|
|
||||||
UIDialogs.toast(this, "Notification permission denied");
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
|
||||||
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
|
|
||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
|
||||||
}
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_settings);
|
|
||||||
setNavigationBarColorAndIcons();
|
|
||||||
|
|
||||||
_form = findViewById(R.id.settings_form);
|
|
||||||
_buttonBack = findViewById(R.id.button_back);
|
|
||||||
_buttonDev = findViewById(R.id.button_dev);
|
|
||||||
_devSets = findViewById(R.id.dev_settings);
|
|
||||||
_loaderView = findViewById(R.id.loader);
|
|
||||||
overlay = findViewById(R.id.overlay_container);
|
|
||||||
|
|
||||||
_form.onChanged.subscribe { field, _ ->
|
|
||||||
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
|
||||||
_form.setObjectValues();
|
|
||||||
Settings.instance.save();
|
|
||||||
|
|
||||||
if(field.descriptor?.id == "app_language") {
|
|
||||||
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
|
|
||||||
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(field.descriptor?.id == "background_update") {
|
|
||||||
Logger.i("SettingsActivity", "Detected change in background work ${field.value}");
|
|
||||||
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval > 0) {
|
|
||||||
val notifManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
|
||||||
if(!notifManager.areNotificationsEnabled()) {
|
|
||||||
UIDialogs.toast(this, "Notifications aren't enabled");
|
|
||||||
|
|
||||||
when {
|
|
||||||
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
|
|
||||||
|
|
||||||
}
|
|
||||||
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
|
|
||||||
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
|
|
||||||
"Notifications need to be enabled for background updating to function", null, 0,
|
|
||||||
UIDialogs.Action("Cancel", {}),
|
|
||||||
UIDialogs.Action("Enable", {
|
|
||||||
requestPermissionLauncher.launch(notifPermission);
|
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
requestPermissionLauncher.launch(notifPermission);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
_buttonBack.setOnClickListener {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
_buttonDev.setOnClickListener {
|
|
||||||
startActivity(Intent(this, DeveloperActivity::class.java));
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastActivity = this;
|
|
||||||
|
|
||||||
reloadSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
var isFirstLoad = true;
|
|
||||||
fun reloadSettings() {
|
|
||||||
val firstLoad = isFirstLoad;
|
|
||||||
isFirstLoad = false;
|
|
||||||
_form.setSearchVisible(false);
|
|
||||||
_loaderView.start();
|
|
||||||
_form.fromObject(lifecycleScope, Settings.instance) {
|
|
||||||
_loaderView.stop();
|
|
||||||
_form.setSearchVisible(true);
|
|
||||||
|
|
||||||
var devCounter = 0;
|
|
||||||
_form.findField("code")?.assume<ReadOnlyTextField>()?.setOnClickListener {
|
|
||||||
devCounter++;
|
|
||||||
if(devCounter > 5) {
|
|
||||||
devCounter = 0;
|
|
||||||
SettingsDev.instance.developerMode = true;
|
|
||||||
SettingsDev.instance.save();
|
|
||||||
updateDevMode();
|
|
||||||
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if(firstLoad) {
|
|
||||||
val query = intent.getStringExtra("query");
|
|
||||||
if(!query.isNullOrEmpty()) {
|
|
||||||
_form.setSearchQuery(query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
updateDevMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateDevMode() {
|
|
||||||
if(SettingsDev.instance.developerMode)
|
|
||||||
_devSets.visibility = View.VISIBLE;
|
|
||||||
else
|
|
||||||
_devSets.visibility = View.GONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finish() {
|
|
||||||
super.finish()
|
|
||||||
_isFinished = true;
|
|
||||||
if(_lastActivity == this)
|
|
||||||
_lastActivity = null;
|
|
||||||
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
|
|
||||||
private var requestCode: Int? = -1;
|
|
||||||
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
result: ActivityResult ->
|
|
||||||
val handler = synchronized(resultLauncherMap) {
|
|
||||||
resultLauncherMap.remove(requestCode);
|
|
||||||
}
|
|
||||||
if(handler != null)
|
|
||||||
handler(result);
|
|
||||||
};
|
|
||||||
override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit) {
|
|
||||||
synchronized(resultLauncherMap) {
|
|
||||||
resultLauncherMap[code] = handler;
|
|
||||||
}
|
|
||||||
requestCode = code;
|
|
||||||
resultLauncher.launch(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
settingsActivityClosed.emit()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
//TODO: Temporary for solving Settings issues
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
private var _lastActivity: SettingsActivity? = null;
|
|
||||||
|
|
||||||
val settingsActivityClosed = Event0()
|
|
||||||
|
|
||||||
fun getActivity(): SettingsActivity? {
|
|
||||||
val act = _lastActivity;
|
|
||||||
if(act != null && !act._isFinished)
|
|
||||||
return act;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -110,7 +110,19 @@ class SyncPairActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
StateSync.instance.syncService?.connect(deviceInfo) { complete, message ->
|
var wasCompleted = false
|
||||||
|
|
||||||
|
StateSync.instance.syncService?.connect(deviceInfo, true) { complete, message ->
|
||||||
|
if (wasCompleted) {
|
||||||
|
Logger.i(TAG, "onStatusUpdate(complete = ${complete}, message = '${message} ignored because wasCompleted')")
|
||||||
|
return@connect
|
||||||
|
}
|
||||||
|
|
||||||
|
if (complete == true) {
|
||||||
|
wasCompleted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "onStatusUpdate(complete = ${complete}, message = '${message}')")
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
if (complete != null) {
|
if (complete != null) {
|
||||||
if (complete) {
|
if (complete) {
|
||||||
|
|||||||
+318
@@ -0,0 +1,318 @@
|
|||||||
|
package com.futo.platformplayer.api.http.server.handlers
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import com.futo.platformplayer.api.http.server.HttpContext
|
||||||
|
import com.futo.platformplayer.api.http.server.HttpHeaders
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class HttpContentUriHandler(
|
||||||
|
method: String,
|
||||||
|
path: String,
|
||||||
|
private val contentResolver: ContentResolver,
|
||||||
|
private val uri: Uri,
|
||||||
|
private val explicitContentType: String? = null
|
||||||
|
) : HttpHandler(method, path) {
|
||||||
|
|
||||||
|
override fun handle(httpContext: HttpContext) {
|
||||||
|
val resolver = contentResolver
|
||||||
|
val requestHeaders = httpContext.headers
|
||||||
|
val responseHeaders = this.headers.clone()
|
||||||
|
|
||||||
|
val meta = try {
|
||||||
|
queryMetadata(resolver, uri)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.e(TAG, "Failed to query metadata for $uri", e)
|
||||||
|
httpContext.respondCode(404, responseHeaders)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val contentType = explicitContentType
|
||||||
|
?: resolver.getType(uri)
|
||||||
|
?: "application/octet-stream"
|
||||||
|
responseHeaders["Content-Type"] = contentType
|
||||||
|
|
||||||
|
meta.lastModifiedMillis?.let { lastModified ->
|
||||||
|
responseHeaders["Last-Modified"] = httpDateFormat.format(Date(lastModified))
|
||||||
|
|
||||||
|
val ifModifiedSinceHeader = requestHeaders["If-Modified-Since"]
|
||||||
|
if (ifModifiedSinceHeader != null) {
|
||||||
|
val ifModifiedSince = try {
|
||||||
|
httpDateFormat.parse(ifModifiedSinceHeader)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ifModifiedSince != null && lastModified <= ifModifiedSince.time) {
|
||||||
|
httpContext.respondCode(304, responseHeaders)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val safeName = (meta.displayName ?: "content.bin").replace("\"", "\\\"")
|
||||||
|
responseHeaders["Content-Disposition"] = "attachment; filename=\"$safeName\""
|
||||||
|
|
||||||
|
val length = meta.size
|
||||||
|
if (length == null) {
|
||||||
|
Logger.i(TAG, "Streaming $uri with unknown length; Range not supported")
|
||||||
|
responseHeaders.remove("Content-Length")
|
||||||
|
responseHeaders.remove("Content-Range")
|
||||||
|
responseHeaders.remove("Accept-Ranges")
|
||||||
|
|
||||||
|
stream(
|
||||||
|
httpContext = httpContext,
|
||||||
|
resolver = resolver,
|
||||||
|
uri = uri,
|
||||||
|
statusCode = 200,
|
||||||
|
headers = responseHeaders,
|
||||||
|
start = null,
|
||||||
|
length = null
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseHeaders["Accept-Ranges"] = "bytes"
|
||||||
|
|
||||||
|
val rangeHeader = requestHeaders["Range"]
|
||||||
|
if (rangeHeader.isNullOrBlank()) {
|
||||||
|
responseHeaders["Content-Length"] = length.toString()
|
||||||
|
Logger.i(TAG, "Sending full content for $uri, length=$length")
|
||||||
|
|
||||||
|
stream(
|
||||||
|
httpContext = httpContext,
|
||||||
|
resolver = resolver,
|
||||||
|
uri = uri,
|
||||||
|
statusCode = 200,
|
||||||
|
headers = responseHeaders,
|
||||||
|
start = 0L,
|
||||||
|
length = length
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val range = parseRange(rangeHeader, length)
|
||||||
|
if (range == null) {
|
||||||
|
Logger.w(TAG, "Invalid Range '$rangeHeader' for $uri (length=$length)")
|
||||||
|
responseHeaders["Content-Range"] = "bytes */$length"
|
||||||
|
httpContext.respondCode(416, responseHeaders)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val start = range.first
|
||||||
|
val endInclusive = range.last
|
||||||
|
val bytesToSend = endInclusive - start + 1
|
||||||
|
|
||||||
|
responseHeaders["Content-Range"] = "bytes $start-$endInclusive/$length"
|
||||||
|
responseHeaders["Content-Length"] = bytesToSend.toString()
|
||||||
|
Logger.i(TAG, "Sending range $start-$endInclusive (length=$bytesToSend) of $length for $uri")
|
||||||
|
|
||||||
|
stream(
|
||||||
|
httpContext = httpContext,
|
||||||
|
resolver = resolver,
|
||||||
|
uri = uri,
|
||||||
|
statusCode = 206,
|
||||||
|
headers = responseHeaders,
|
||||||
|
start = start,
|
||||||
|
length = bytesToSend
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ContentMeta(
|
||||||
|
val displayName: String?,
|
||||||
|
val size: Long?,
|
||||||
|
val lastModifiedMillis: Long?
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun queryMetadata(resolver: ContentResolver, uri: Uri): ContentMeta {
|
||||||
|
var displayName: String? = null
|
||||||
|
var size: Long? = null
|
||||||
|
var lastModifiedMillis: Long? = null
|
||||||
|
|
||||||
|
resolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||||
|
if (nameIndex != -1 && !cursor.isNull(nameIndex)) {
|
||||||
|
displayName = cursor.getString(nameIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||||
|
if (sizeIndex != -1 && !cursor.isNull(sizeIndex)) {
|
||||||
|
val s = cursor.getLong(sizeIndex)
|
||||||
|
if (s >= 0) size = s // -1 means unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
val dateModifiedIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED)
|
||||||
|
if (dateModifiedIndex != -1 && !cursor.isNull(dateModifiedIndex)) {
|
||||||
|
val seconds = cursor.getLong(dateModifiedIndex)
|
||||||
|
if (seconds > 0) {
|
||||||
|
lastModifiedMillis = seconds * 1000L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastModifiedMillis == null) {
|
||||||
|
val dateAddedIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED)
|
||||||
|
if (dateAddedIndex != -1 && !cursor.isNull(dateAddedIndex)) {
|
||||||
|
val seconds = cursor.getLong(dateAddedIndex)
|
||||||
|
if (seconds > 0) {
|
||||||
|
lastModifiedMillis = seconds * 1000L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayName == null) {
|
||||||
|
displayName = uri.lastPathSegment
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size == null) {
|
||||||
|
try {
|
||||||
|
resolver.openAssetFileDescriptor(uri, "r")?.use { afd ->
|
||||||
|
val assetLen = afd.length
|
||||||
|
if (assetLen >= 0) {
|
||||||
|
size = assetLen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
return ContentMeta(
|
||||||
|
displayName = displayName,
|
||||||
|
size = size,
|
||||||
|
lastModifiedMillis = lastModifiedMillis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseRange(header: String, totalLength: Long): LongRange? {
|
||||||
|
if (totalLength <= 0L) return null
|
||||||
|
|
||||||
|
val prefix = "bytes="
|
||||||
|
if (!header.startsWith(prefix, ignoreCase = true)) return null
|
||||||
|
|
||||||
|
val spec = header.substring(prefix.length).trim()
|
||||||
|
if (spec.isEmpty()) return null
|
||||||
|
|
||||||
|
if (spec.contains(",")) return null
|
||||||
|
|
||||||
|
val dashIndex = spec.indexOf('-')
|
||||||
|
if (dashIndex < 0) return null
|
||||||
|
|
||||||
|
val startPart = spec.substring(0, dashIndex).trim()
|
||||||
|
val endPart = spec.substring(dashIndex + 1).trim()
|
||||||
|
|
||||||
|
return when {
|
||||||
|
startPart.isNotEmpty() -> {
|
||||||
|
val start = startPart.toLongOrNull() ?: return null
|
||||||
|
if (start < 0 || start >= totalLength) return null
|
||||||
|
|
||||||
|
val end = if (endPart.isNotEmpty()) {
|
||||||
|
val rawEnd = endPart.toLongOrNull() ?: return null
|
||||||
|
if (rawEnd < start) return null
|
||||||
|
rawEnd.coerceAtMost(totalLength - 1)
|
||||||
|
} else {
|
||||||
|
totalLength - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
start..end
|
||||||
|
}
|
||||||
|
|
||||||
|
endPart.isNotEmpty() -> {
|
||||||
|
val suffixLen = endPart.toLongOrNull() ?: return null
|
||||||
|
if (suffixLen <= 0L) return null
|
||||||
|
|
||||||
|
if (suffixLen >= totalLength) {
|
||||||
|
0L..(totalLength - 1)
|
||||||
|
} else {
|
||||||
|
val start = totalLength - suffixLen
|
||||||
|
val end = totalLength - 1
|
||||||
|
start..end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stream(httpContext: HttpContext, resolver: ContentResolver, uri: Uri, statusCode: Int, headers: HttpHeaders, start: Long?, length: Long?) {
|
||||||
|
try {
|
||||||
|
val input = resolver.openInputStream(uri)
|
||||||
|
if (input == null) {
|
||||||
|
Logger.w(TAG, "Content not found: $uri")
|
||||||
|
httpContext.respondCode(404, headers)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
input.use { inputStream ->
|
||||||
|
httpContext.respond(statusCode, headers) { outputStream ->
|
||||||
|
try {
|
||||||
|
val offset = start ?: 0L
|
||||||
|
if (offset > 0L) {
|
||||||
|
skipFully(inputStream, offset)
|
||||||
|
}
|
||||||
|
copyStream(inputStream, outputStream, length)
|
||||||
|
outputStream.flush()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.e(TAG, "Error while streaming $uri (start=$start, length=$length)", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
Logger.w(TAG, "Content not found: $uri", e)
|
||||||
|
httpContext.respondCode(404, headers)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.e(TAG, "Failed to open stream for $uri", e)
|
||||||
|
httpContext.respondCode(500, headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyStream(input: InputStream, output: OutputStream, limit: Long?) {
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
if (limit == null) {
|
||||||
|
while (true) {
|
||||||
|
val read = input.read(buffer)
|
||||||
|
if (read < 0) break
|
||||||
|
output.write(buffer, 0, read)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var remaining = limit
|
||||||
|
while (remaining > 0L) {
|
||||||
|
val toRead = remaining.coerceAtMost(buffer.size.toLong()).toInt()
|
||||||
|
val read = input.read(buffer, 0, toRead)
|
||||||
|
if (read < 0) break
|
||||||
|
output.write(buffer, 0, read)
|
||||||
|
remaining -= read.toLong()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun skipFully(input: InputStream, bytesToSkip: Long) {
|
||||||
|
var remaining = bytesToSkip
|
||||||
|
while (remaining > 0L) {
|
||||||
|
val skipped = input.skip(remaining)
|
||||||
|
if (skipped <= 0L) {
|
||||||
|
val b = input.read()
|
||||||
|
if (b == -1) break
|
||||||
|
remaining -= 1L
|
||||||
|
} else {
|
||||||
|
remaining -= skipped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "HttpContentUriHandler"
|
||||||
|
|
||||||
|
private val httpDateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US).apply {
|
||||||
|
timeZone = TimeZone.getTimeZone("GMT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
-9
@@ -5,6 +5,7 @@ import android.util.Log
|
|||||||
import com.futo.platformplayer.api.http.server.HttpContext
|
import com.futo.platformplayer.api.http.server.HttpContext
|
||||||
import com.futo.platformplayer.api.http.server.HttpHeaders
|
import com.futo.platformplayer.api.http.server.HttpHeaders
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.models.modifier.IRequest
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.parsers.HttpResponseParser
|
import com.futo.platformplayer.parsers.HttpResponseParser
|
||||||
import com.futo.platformplayer.readLine
|
import com.futo.platformplayer.readLine
|
||||||
@@ -27,6 +28,7 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
|
|||||||
private var _injectReferer = false;
|
private var _injectReferer = false;
|
||||||
|
|
||||||
private val _client = ManagedHttpClient();
|
private val _client = ManagedHttpClient();
|
||||||
|
private var _requestModifier: ((String, Map<String, String>) -> IRequest)? = null;
|
||||||
|
|
||||||
override fun handle(context: HttpContext) {
|
override fun handle(context: HttpContext) {
|
||||||
if (useTcp) {
|
if (useTcp) {
|
||||||
@@ -43,21 +45,33 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
|
|||||||
for (injectHeader in _injectRequestHeader)
|
for (injectHeader in _injectRequestHeader)
|
||||||
proxyHeaders[injectHeader.first] = injectHeader.second;
|
proxyHeaders[injectHeader.first] = injectHeader.second;
|
||||||
|
|
||||||
val parsed = Uri.parse(targetUrl);
|
val req = _requestModifier?.invoke(targetUrl, proxyHeaders)
|
||||||
|
var url = targetUrl
|
||||||
|
if (req != null) {
|
||||||
|
req.url?.let {
|
||||||
|
url = it
|
||||||
|
}
|
||||||
|
req.headers.let {
|
||||||
|
proxyHeaders.clear()
|
||||||
|
proxyHeaders.putAll(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val parsed = Uri.parse(url);
|
||||||
if(_injectHost)
|
if(_injectHost)
|
||||||
proxyHeaders.put("Host", parsed.host!!);
|
proxyHeaders.put("Host", parsed.host!!);
|
||||||
if(_injectReferer)
|
if(_injectReferer)
|
||||||
proxyHeaders.put("Referer", targetUrl);
|
proxyHeaders.put("Referer", url);
|
||||||
|
|
||||||
val useMethod = if (method == "inherit") context.method else method;
|
val useMethod = if (method == "inherit") context.method else method;
|
||||||
Logger.i(TAG, "handleWithOkHttp Proxied Request ${useMethod}: ${targetUrl}");
|
Logger.i(TAG, "handleWithOkHttp Proxied Request ${useMethod}: ${url}");
|
||||||
Logger.i(TAG, "handleWithOkHttp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
|
Logger.i(TAG, "handleWithOkHttp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
|
||||||
|
|
||||||
val resp = when (useMethod) {
|
val resp = when (useMethod) {
|
||||||
"GET" -> _client.get(targetUrl, proxyHeaders);
|
"GET" -> _client.get(url, proxyHeaders);
|
||||||
"POST" -> _client.post(targetUrl, content ?: "", proxyHeaders);
|
"POST" -> _client.post(url, content ?: "", proxyHeaders);
|
||||||
"HEAD" -> _client.head(targetUrl, proxyHeaders)
|
"HEAD" -> _client.head(url, proxyHeaders)
|
||||||
else -> _client.requestMethod(useMethod, targetUrl, proxyHeaders);
|
else -> _client.requestMethod(useMethod, url, proxyHeaders);
|
||||||
};
|
};
|
||||||
|
|
||||||
Logger.i(TAG, "Proxied Response [${resp.code}]");
|
Logger.i(TAG, "Proxied Response [${resp.code}]");
|
||||||
@@ -91,11 +105,23 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
|
|||||||
for (injectHeader in _injectRequestHeader)
|
for (injectHeader in _injectRequestHeader)
|
||||||
proxyHeaders[injectHeader.first] = injectHeader.second;
|
proxyHeaders[injectHeader.first] = injectHeader.second;
|
||||||
|
|
||||||
val parsed = Uri.parse(targetUrl);
|
val req = _requestModifier?.invoke(targetUrl, proxyHeaders)
|
||||||
|
var url = targetUrl
|
||||||
|
if (req != null) {
|
||||||
|
req.url?.let {
|
||||||
|
url = it
|
||||||
|
}
|
||||||
|
req.headers.let {
|
||||||
|
proxyHeaders.clear()
|
||||||
|
proxyHeaders.putAll(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val parsed = Uri.parse(url);
|
||||||
if(_injectHost)
|
if(_injectHost)
|
||||||
proxyHeaders.put("Host", parsed.host!!);
|
proxyHeaders.put("Host", parsed.host!!);
|
||||||
if(_injectReferer)
|
if(_injectReferer)
|
||||||
proxyHeaders.put("Referer", targetUrl);
|
proxyHeaders.put("Referer", url);
|
||||||
|
|
||||||
val useMethod = if (method == "inherit") context.method else method;
|
val useMethod = if (method == "inherit") context.method else method;
|
||||||
Logger.i(TAG, "handleWithTcp Proxied Request ${useMethod}: ${parsed}");
|
Logger.i(TAG, "handleWithTcp Proxied Request ${useMethod}: ${parsed}");
|
||||||
@@ -242,6 +268,10 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
|
|||||||
_ignoreRequestHeaders.add("referer");
|
_ignoreRequestHeaders.add("referer");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
fun withRequestModifier(modifier: (String, Map<String, String>) -> IRequest) : HttpProxyHandler {
|
||||||
|
_requestModifier = modifier;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "HttpProxyHandler"
|
private const val TAG = "HttpProxyHandler"
|
||||||
|
|||||||
@@ -54,14 +54,16 @@ interface IPlatformChannelContent : IPlatformContent {
|
|||||||
val subscribers: Long?
|
val subscribers: Long?
|
||||||
}
|
}
|
||||||
|
|
||||||
open class JSChannelContent : JSContent, IPlatformChannelContent {
|
open class JSChannelContent(
|
||||||
override val contentType: ContentType get() = ContentType.CHANNEL
|
config: SourcePluginConfig,
|
||||||
override val thumbnail: String?
|
obj: V8ValueObject
|
||||||
override val subscribers: Long?
|
) : JSContent(config, obj), IPlatformChannelContent {
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
|
final override val contentType: ContentType = ContentType.CHANNEL
|
||||||
val contextName = "Channel";
|
|
||||||
thumbnail = obj.getOrDefault<String>(config, "thumbnail", contextName, null)
|
override val thumbnail: String? =
|
||||||
subscribers = if(obj.has("subscribers")) obj.getOrThrow(config,"subscribers", contextName) else null
|
_content.getOrDefault<String>(_pluginConfig, "thumbnail", "Channel", null)
|
||||||
}
|
|
||||||
}
|
override val subscribers: Long? =
|
||||||
|
_content.getOrDefault<Long>(_pluginConfig, "subscribers", "Channel", null)?.toLong()
|
||||||
|
}
|
||||||
|
|||||||
+11
-21
@@ -6,25 +6,15 @@ import com.futo.platformplayer.api.media.models.ratings.IRating
|
|||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
open class PlatformComment : IPlatformComment {
|
open class PlatformComment(
|
||||||
override val contextUrl: String;
|
override val contextUrl: String,
|
||||||
override val author: PlatformAuthorLink;
|
override val author: PlatformAuthorLink,
|
||||||
override val message: String;
|
override val message: String,
|
||||||
override val rating: IRating;
|
override val rating: IRating,
|
||||||
override val date: OffsetDateTime;
|
override val date: OffsetDateTime,
|
||||||
|
override val replyCount: Int? = null
|
||||||
|
) : IPlatformComment {
|
||||||
|
|
||||||
override val replyCount: Int?;
|
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> =
|
||||||
|
NoCommentsPager()
|
||||||
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, replyCount: Int? = null) {
|
}
|
||||||
this.contextUrl = contextUrl;
|
|
||||||
this.author = author;
|
|
||||||
this.message = msg;
|
|
||||||
this.rating = rating;
|
|
||||||
this.date = date;
|
|
||||||
this.replyCount = replyCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
|
||||||
return NoCommentsPager();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+3
@@ -12,6 +12,9 @@ class DashManifestSource : IVideoSource, IDashManifestSource {
|
|||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
|
||||||
|
override val language: String? = null;
|
||||||
|
override val original: Boolean? = false;
|
||||||
|
|
||||||
constructor(url : String) {
|
constructor(url : String) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -12,6 +12,9 @@ class HLSManifestSource : IVideoSource, IHLSManifestSource {
|
|||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
|
||||||
|
override val language: String? = null;
|
||||||
|
override val original: Boolean? = false;
|
||||||
|
|
||||||
constructor(url : String) {
|
constructor(url : String) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -14,6 +14,9 @@ class HLSVariantVideoUrlSource(
|
|||||||
override val priority: Boolean,
|
override val priority: Boolean,
|
||||||
val url: String
|
val url: String
|
||||||
) : IVideoUrlSource {
|
) : IVideoUrlSource {
|
||||||
|
override val language: String? = null;
|
||||||
|
override val original: Boolean? = false;
|
||||||
|
|
||||||
override fun getVideoUrl(): String {
|
override fun getVideoUrl(): String {
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
@@ -41,6 +44,7 @@ class HLSVariantSubtitleUrlSource(
|
|||||||
override val format: String,
|
override val format: String,
|
||||||
) : ISubtitleSource {
|
) : ISubtitleSource {
|
||||||
override val hasFetch: Boolean = false
|
override val hasFetch: Boolean = false
|
||||||
|
override val language: String? = null
|
||||||
|
|
||||||
override fun getSubtitles(): String? {
|
override fun getSubtitles(): String? {
|
||||||
return null
|
return null
|
||||||
|
|||||||
+2
@@ -9,4 +9,6 @@ interface IVideoSource {
|
|||||||
val bitrate : Int?;
|
val bitrate : Int?;
|
||||||
val duration: Long;
|
val duration: Long;
|
||||||
val priority: Boolean;
|
val priority: Boolean;
|
||||||
|
val language: String?;
|
||||||
|
val original: Boolean?;
|
||||||
}
|
}
|
||||||
+4
-1
@@ -9,13 +9,15 @@ class LocalSubtitleSource : ISubtitleSource {
|
|||||||
override val name: String;
|
override val name: String;
|
||||||
override val url: String?;
|
override val url: String?;
|
||||||
override val format: String?;
|
override val format: String?;
|
||||||
|
override val language: String?
|
||||||
override val hasFetch: Boolean get() = false;
|
override val hasFetch: Boolean get() = false;
|
||||||
|
|
||||||
val filePath: String;
|
val filePath: String;
|
||||||
|
|
||||||
constructor(name: String, format: String?, filePath: String) {
|
constructor(name: String, language: String?, format: String?, filePath: String) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.format = format;
|
this.format = format;
|
||||||
|
this.language = language
|
||||||
this.filePath = filePath;
|
this.filePath = filePath;
|
||||||
this.url = Uri.fromFile(File(filePath)).toString();
|
this.url = Uri.fromFile(File(filePath)).toString();
|
||||||
}
|
}
|
||||||
@@ -32,6 +34,7 @@ class LocalSubtitleSource : ISubtitleSource {
|
|||||||
fun fromSource(source: SubtitleRawSource, path: String): LocalSubtitleSource {
|
fun fromSource(source: SubtitleRawSource, path: String): LocalSubtitleSource {
|
||||||
return LocalSubtitleSource(
|
return LocalSubtitleSource(
|
||||||
source.name,
|
source.name,
|
||||||
|
source.language,
|
||||||
source.format,
|
source.format,
|
||||||
path
|
path
|
||||||
);
|
);
|
||||||
|
|||||||
+4
@@ -16,6 +16,10 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
|||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
|
||||||
|
override val language: String? = null;
|
||||||
|
override val original: Boolean? = false;
|
||||||
|
|
||||||
|
|
||||||
val filePath : String;
|
val filePath : String;
|
||||||
val fileSize : Long;
|
val fileSize : Long;
|
||||||
|
|
||||||
|
|||||||
+1
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
|||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class SubtitleRawSource(
|
class SubtitleRawSource(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
|
override val language: String?,
|
||||||
override val format: String?,
|
override val format: String?,
|
||||||
val _subtitles: String,
|
val _subtitles: String,
|
||||||
override val url: String? = null,
|
override val url: String? = null,
|
||||||
|
|||||||
+3
@@ -19,6 +19,9 @@ open class VideoUrlSource(
|
|||||||
) : IVideoUrlSource, IStreamMetaDataSource {
|
) : IVideoUrlSource, IStreamMetaDataSource {
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
|
override val language: String? = null;
|
||||||
|
override val original: Boolean? = false;
|
||||||
|
|
||||||
override fun getVideoUrl() : String {
|
override fun getVideoUrl() : String {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -7,6 +7,7 @@ interface ISubtitleSource {
|
|||||||
val url: String?;
|
val url: String?;
|
||||||
val format: String?;
|
val format: String?;
|
||||||
val hasFetch: Boolean;
|
val hasFetch: Boolean;
|
||||||
|
val language: String?
|
||||||
|
|
||||||
fun getSubtitles(): String?;
|
fun getSubtitles(): String?;
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -73,10 +73,10 @@ open class LocalVideoDetails(
|
|||||||
override val video: IVideoSourceDescriptor = (if(mimeType?.startsWith("audio/") ?: false)
|
override val video: IVideoSourceDescriptor = (if(mimeType?.startsWith("audio/") ?: false)
|
||||||
(LocalVideoUnMuxedSourceDescriptor(
|
(LocalVideoUnMuxedSourceDescriptor(
|
||||||
arrayOf(),
|
arrayOf(),
|
||||||
arrayOf(LocalAudioContentSource(url, mimeType ?: "", name))
|
arrayOf(LocalAudioContentSource(url, mimeType ?: "", name, duration))
|
||||||
))
|
))
|
||||||
else (LocalVideoMuxedSourceDescriptor(
|
else (LocalVideoMuxedSourceDescriptor(
|
||||||
LocalVideoContentSource(url, mimeType ?: "", name)
|
LocalVideoContentSource(url, mimeType ?: "", name, duration)
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
override val preview: ISerializedVideoSourceDescriptor? = null;
|
override val preview: ISerializedVideoSourceDescriptor? = null;
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
override val id: String get() = config.id;
|
override val id: String get() = config.id;
|
||||||
override val name: String get() = config.name;
|
override val name: String get() = config.name;
|
||||||
override val icon: ImageVariable;
|
override val icon: ImageVariable get() = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null)
|
||||||
override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities();
|
override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities();
|
||||||
|
|
||||||
private var _busyAction = "";
|
private var _busyAction = "";
|
||||||
@@ -147,15 +147,14 @@ open class JSClient : IPlatformClient {
|
|||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) {
|
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this.config = descriptor.config;
|
this.config = descriptor.config;
|
||||||
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
|
|
||||||
this.descriptor = descriptor;
|
this.descriptor = descriptor;
|
||||||
_injectedSaveState = saveState;
|
_injectedSaveState = saveState;
|
||||||
_auth = descriptor.getAuth();
|
_auth = descriptor.getAuth();
|
||||||
_captcha = descriptor.getCaptchaData();
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
_httpClient = JSHttpClient(this, null, _captcha);
|
_httpClient = JSHttpClient(this, null, _captcha, config);
|
||||||
_httpClientAuth = JSHttpClient(this, _auth, _captcha);
|
_httpClientAuth = JSHttpClient(this, _auth, _captcha, config);
|
||||||
_plugin = V8Plugin(context, descriptor.config, null, _httpClient, _httpClientAuth);
|
_plugin = V8Plugin(context, descriptor.config, null, _httpClient, _httpClientAuth);
|
||||||
_plugin.withDependency(context, "scripts/polyfil.js");
|
_plugin.withDependency(context, "scripts/polyfil.js");
|
||||||
_plugin.withDependency(context, "scripts/source.js");
|
_plugin.withDependency(context, "scripts/source.js");
|
||||||
@@ -178,7 +177,6 @@ open class JSClient : IPlatformClient {
|
|||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) {
|
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this.config = descriptor.config;
|
this.config = descriptor.config;
|
||||||
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
|
|
||||||
this.descriptor = descriptor;
|
this.descriptor = descriptor;
|
||||||
_injectedSaveState = saveState;
|
_injectedSaveState = saveState;
|
||||||
if(!withoutCredentials)
|
if(!withoutCredentials)
|
||||||
@@ -188,8 +186,8 @@ open class JSClient : IPlatformClient {
|
|||||||
_captcha = descriptor.getCaptchaData();
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
_httpClient = JSHttpClient(this, null, _captcha);
|
_httpClient = JSHttpClient(this, null, _captcha, config);
|
||||||
_httpClientAuth = JSHttpClient(this, _auth, _captcha);
|
_httpClientAuth = JSHttpClient(this, _auth, _captcha, config);
|
||||||
_plugin = V8Plugin(context, descriptor.config, script, _httpClient, _httpClientAuth);
|
_plugin = V8Plugin(context, descriptor.config, script, _httpClient, _httpClientAuth);
|
||||||
_plugin.withDependency(context, "scripts/polyfil.js");
|
_plugin.withDependency(context, "scripts/polyfil.js");
|
||||||
_plugin.withDependency(context, "scripts/source.js");
|
_plugin.withDependency(context, "scripts/source.js");
|
||||||
|
|||||||
+2
-1
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.platforms.js
|
|||||||
|
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
import java.util.Dictionary
|
import java.util.Dictionary
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -27,7 +28,7 @@ class SourcePluginAuthConfig(
|
|||||||
val details: String? = null,
|
val details: String? = null,
|
||||||
val once: Boolean? = true
|
val once: Boolean? = true
|
||||||
) {
|
) {
|
||||||
@Contextual
|
@Transient
|
||||||
private var _regex: Regex? = null;
|
private var _regex: Regex? = null;
|
||||||
|
|
||||||
fun getRegex(): Regex {
|
fun getRegex(): Regex {
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ class SourcePluginConfig(
|
|||||||
//Script
|
//Script
|
||||||
val repositoryUrl: String? = null,
|
val repositoryUrl: String? = null,
|
||||||
val scriptUrl: String = "",
|
val scriptUrl: String = "",
|
||||||
val version: Int = -1,
|
var version: Int = -1,
|
||||||
|
|
||||||
val iconUrl: String? = null,
|
val iconUrl: String? = null,
|
||||||
var id: String = UUID.randomUUID().toString(),
|
var id: String = UUID.randomUUID().toString(),
|
||||||
|
|||||||
+71
@@ -23,6 +23,7 @@ import java.util.UUID
|
|||||||
class JSHttpClient : ManagedHttpClient {
|
class JSHttpClient : ManagedHttpClient {
|
||||||
private val _jsClient: JSClient?;
|
private val _jsClient: JSClient?;
|
||||||
private val _jsConfig: SourcePluginConfig?;
|
private val _jsConfig: SourcePluginConfig?;
|
||||||
|
val config get() = _jsConfig
|
||||||
private val _auth: SourceAuth?;
|
private val _auth: SourceAuth?;
|
||||||
private val _captcha: SourceCaptchaData?;
|
private val _captcha: SourceCaptchaData?;
|
||||||
|
|
||||||
@@ -254,6 +255,76 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
fun processRequest(method: String, responseCode: Int, url: Uri, headers: Map<String, List<String>>) {
|
||||||
|
if(doUpdateCookies) {
|
||||||
|
val domain = url.host?.lowercase() ?: return;
|
||||||
|
val domainParts = domain.split(".");
|
||||||
|
val defaultCookieDomain =
|
||||||
|
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||||
|
for (header in headers) {
|
||||||
|
if(header.key.lowercase() == "set-cookie") {
|
||||||
|
var domainToUse = domain;
|
||||||
|
val cookie = cookieStringToPair(header.value.first());
|
||||||
|
var cookieValue = cookie.second;
|
||||||
|
|
||||||
|
if (cookie.first.isNotEmpty() && cookie.second.isNotEmpty()) {
|
||||||
|
val cookieParts = cookie.second.split(";");
|
||||||
|
if (cookieParts.size == 0)
|
||||||
|
continue;
|
||||||
|
cookieValue = cookieParts[0].trim();
|
||||||
|
|
||||||
|
val cookieVariables = cookieParts.drop(1).map {
|
||||||
|
val splitIndex = it.indexOf("=");
|
||||||
|
if (splitIndex < 0)
|
||||||
|
return@map Pair(it.trim().lowercase(), "");
|
||||||
|
return@map Pair<String, String>(
|
||||||
|
it.substring(0, splitIndex).lowercase().trim(),
|
||||||
|
it.substring(splitIndex + 1).trim()
|
||||||
|
);
|
||||||
|
}.toMap();
|
||||||
|
domainToUse = if (cookieVariables.containsKey("domain"))
|
||||||
|
cookieVariables["domain"]!!.lowercase();
|
||||||
|
else defaultCookieDomain;
|
||||||
|
//TODO: Make sure this has no negative effect besides apply cookies to root domain
|
||||||
|
if(!domainToUse.startsWith("."))
|
||||||
|
domainToUse = ".${domainToUse}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_auth != null || _currentCookieMap.isNotEmpty())) {
|
||||||
|
val cookieMap = if (_currentCookieMap.containsKey(domainToUse))
|
||||||
|
_currentCookieMap[domainToUse]!!;
|
||||||
|
else {
|
||||||
|
val newMap = hashMapOf<String, String>();
|
||||||
|
_currentCookieMap[domainToUse] = newMap
|
||||||
|
newMap;
|
||||||
|
}
|
||||||
|
if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
|
||||||
|
cookieMap[cookie.first] = cookieValue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val cookieMap = if (_otherCookieMap.containsKey(domainToUse))
|
||||||
|
_otherCookieMap[domainToUse]!!;
|
||||||
|
else {
|
||||||
|
val newMap = hashMapOf<String, String>();
|
||||||
|
_otherCookieMap[domainToUse] = newMap
|
||||||
|
newMap;
|
||||||
|
}
|
||||||
|
if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
|
||||||
|
cookieMap[cookie.first] = cookieValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_jsClient is DevJSClient) {
|
||||||
|
//val peekBody = resp.peekBody(1000 * 1000).string();
|
||||||
|
StateDeveloper.instance.addDevHttpExchange(
|
||||||
|
StateDeveloper.DevHttpExchange(
|
||||||
|
StateDeveloper.DevHttpRequest(method, url.toString(), mapOf(*headers.map { Pair(it.key, it.value.joinToString(";")) }.toTypedArray()), ""),
|
||||||
|
StateDeveloper.DevHttpRequest("RESP", url.toString(), mapOf(*headers.map { Pair(it.key, it.value.joinToString(";")) }.toTypedArray()), "", responseCode)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun cookieStringToPair(cookie: String): Pair<String, String> {
|
private fun cookieStringToPair(cookie: String): Pair<String, String> {
|
||||||
val cookieKey = cookie.substring(0, cookie.indexOf("="));
|
val cookieKey = cookie.substring(0, cookie.indexOf("="));
|
||||||
|
|||||||
+16
-11
@@ -23,17 +23,22 @@ import com.futo.platformplayer.getOrThrow
|
|||||||
import com.futo.platformplayer.getOrThrowNullableList
|
import com.futo.platformplayer.getOrThrowNullableList
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
open class JSArticle : JSContent, IPlatformArticle, IPluginSourced {
|
open class JSArticle(
|
||||||
final override val contentType: ContentType get() = ContentType.ARTICLE;
|
config: SourcePluginConfig,
|
||||||
|
obj: V8ValueObject
|
||||||
|
) : JSContent(config, obj), IPlatformArticle, IPluginSourced {
|
||||||
|
|
||||||
override val summary: String;
|
final override val contentType: ContentType = ContentType.ARTICLE
|
||||||
override val thumbnails: Thumbnails?;
|
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
override val summary: String =
|
||||||
val contextName = "PlatformArticle";
|
obj.getOrDefault<String>(config, "summary", "PlatformArticle", "") ?: ""
|
||||||
|
|
||||||
summary = _content.getOrDefault(config, "summary", contextName, "") ?: "";
|
override val thumbnails: Thumbnails? =
|
||||||
thumbnails = Thumbnails.fromV8(config, _content.getOrThrow(config, "thumbnails", contextName));
|
if (obj.has("thumbnails"))
|
||||||
|
Thumbnails.fromV8(
|
||||||
}
|
config,
|
||||||
}
|
obj.getOrThrow<V8ValueObject>(config, "thumbnails", "PlatformArticle")
|
||||||
|
)
|
||||||
|
else
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|||||||
+24
-23
@@ -24,36 +24,37 @@ import com.futo.platformplayer.getOrThrowNullableList
|
|||||||
import com.futo.platformplayer.invokeV8
|
import com.futo.platformplayer.invokeV8
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
|
open class JSArticleDetails(
|
||||||
final override val contentType: ContentType get() = ContentType.ARTICLE;
|
private val client: JSClient,
|
||||||
|
obj: V8ValueObject
|
||||||
|
) : JSContent(client.config, obj), IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
|
||||||
|
|
||||||
private val _hasGetComments: Boolean;
|
final override val contentType: ContentType = ContentType.ARTICLE
|
||||||
private val _hasGetContentRecommendations: Boolean;
|
|
||||||
|
|
||||||
override val rating: IRating;
|
private val _hasGetComments: Boolean = _content.has("getComments")
|
||||||
|
private val _hasGetContentRecommendations: Boolean = _content.has("getContentRecommendations")
|
||||||
|
|
||||||
override val summary: String;
|
override val rating: IRating =
|
||||||
override val thumbnails: Thumbnails?;
|
obj.getOrDefault<V8ValueObject>(client.config, "rating", "PlatformArticle", null)
|
||||||
override val segments: List<IJSArticleSegment>;
|
?.let { IRating.fromV8(client.config, it, "PlatformArticle") }
|
||||||
|
?: RatingLikes(0)
|
||||||
|
|
||||||
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
|
override val summary: String =
|
||||||
val contextName = "PlatformArticle";
|
_content.getOrThrow(client.config, "summary", "PlatformArticle")
|
||||||
|
|
||||||
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
|
override val thumbnails: Thumbnails? =
|
||||||
summary = _content.getOrThrow(client.config, "summary", contextName);
|
if (_content.has("thumbnails"))
|
||||||
if(_content.has("thumbnails"))
|
Thumbnails.fromV8(
|
||||||
thumbnails = Thumbnails.fromV8(client.config, _content.getOrThrow(client.config, "thumbnails", contextName));
|
client.config,
|
||||||
|
_content.getOrThrow(client.config, "thumbnails", "PlatformArticle")
|
||||||
|
)
|
||||||
else
|
else
|
||||||
thumbnails = null;
|
null
|
||||||
|
|
||||||
|
override val segments: List<IJSArticleSegment> =
|
||||||
segments = (obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", contextName)
|
obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", "PlatformArticle")
|
||||||
?.map { fromV8Segment(client, it) }
|
?.mapNotNull { fromV8Segment(client, it) }
|
||||||
?.filterNotNull() ?: listOf());
|
?: emptyList()
|
||||||
|
|
||||||
_hasGetComments = _content.has("getComments");
|
|
||||||
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||||
if(!_hasGetComments || _content.isClosed)
|
if(!_hasGetComments || _content.isClosed)
|
||||||
|
|||||||
+34
-36
@@ -16,51 +16,49 @@ import java.time.LocalDateTime
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
open class JSContent : IPlatformContent, IPluginSourced {
|
open class JSContent(
|
||||||
protected val _pluginConfig: SourcePluginConfig;
|
protected val _pluginConfig: SourcePluginConfig,
|
||||||
protected val _content : V8ValueObject;
|
protected val _content: V8ValueObject
|
||||||
|
) : IPlatformContent, IPluginSourced {
|
||||||
|
|
||||||
protected val _hasGetDetails: Boolean;
|
override val contentType: ContentType = ContentType.UNKNOWN
|
||||||
|
|
||||||
override val contentType: ContentType get() = ContentType.UNKNOWN;
|
protected val _hasGetDetails: Boolean = _content.has("getDetails")
|
||||||
|
|
||||||
override val id: PlatformID;
|
override val id: PlatformID =
|
||||||
override val name: String;
|
PlatformID.fromV8(_pluginConfig, _content.getOrThrow(_pluginConfig, "id", CTX))
|
||||||
override val author: PlatformAuthorLink;
|
|
||||||
override val datetime: OffsetDateTime?;
|
|
||||||
|
|
||||||
override val url: String;
|
override val name: String =
|
||||||
override val shareUrl: String;
|
HtmlCompat.fromHtml(
|
||||||
|
_content.getOrThrow<String>(_pluginConfig, "name", CTX).decodeUnicode(),
|
||||||
|
HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||||
|
).toString()
|
||||||
|
|
||||||
override val sourceConfig: SourcePluginConfig get() = _pluginConfig;
|
override val author: PlatformAuthorLink =
|
||||||
|
_content.getOrDefault<V8ValueObject>(_pluginConfig, "author", CTX, null)
|
||||||
|
?.let { PlatformAuthorLink.fromV8(_pluginConfig, it) }
|
||||||
|
?: PlatformAuthorLink.UNKNOWN
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
|
private val _epoch: Long? =
|
||||||
_pluginConfig = config;
|
_content.getOrDefault<Long>(_pluginConfig, "datetime", CTX, null)?.toLong()
|
||||||
_content = obj;
|
|
||||||
|
|
||||||
val contextName = "PlatformContent";
|
override val datetime: OffsetDateTime? =
|
||||||
|
_epoch?.takeIf { it != 0L }?.let {
|
||||||
|
OffsetDateTime.of(LocalDateTime.ofEpochSecond(it, 0, ZoneOffset.UTC), ZoneOffset.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
|
override val url: String =
|
||||||
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
|
_content.getOrThrow<String>(_pluginConfig, "url", CTX)
|
||||||
|
|
||||||
val authorObj = _content.getOrDefault<V8ValueObject>(config, "author", contextName, null);
|
override val shareUrl: String =
|
||||||
if(authorObj != null)
|
_content.getOrDefault<String>(_pluginConfig, "shareUrl", CTX, null) ?: url
|
||||||
author = PlatformAuthorLink.fromV8(_pluginConfig, authorObj);
|
|
||||||
else
|
|
||||||
author = PlatformAuthorLink.UNKNOWN;
|
|
||||||
|
|
||||||
val datetimeInt = _content.getOrDefault<Int>(config, "datetime", contextName, null)?.toLong();
|
override val sourceConfig: SourcePluginConfig
|
||||||
if(datetimeInt == null || datetimeInt == 0.toLong())
|
get() = _pluginConfig
|
||||||
datetime = null;
|
|
||||||
else
|
|
||||||
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
|
||||||
url = _content.getOrThrow(config, "url", contextName);
|
|
||||||
shareUrl = _content.getOrDefault<String>(config, "shareUrl", contextName, null) ?: url;
|
|
||||||
|
|
||||||
_hasGetDetails = _content.has("getDetails");
|
fun getUnderlyingObject(): V8ValueObject? = _content
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CTX = "PlatformContent"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fun getUnderlyingObject(): V8ValueObject? {
|
|
||||||
return _content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+12
-10
@@ -6,14 +6,16 @@ import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
|
||||||
open class JSPlaylist : JSContent, IPlatformPlaylist {
|
open class JSPlaylist(
|
||||||
override val contentType: ContentType get() = ContentType.PLAYLIST;
|
config: SourcePluginConfig,
|
||||||
override val thumbnail: String?;
|
obj: V8ValueObject
|
||||||
override val videoCount: Int;
|
) : JSContent(config, obj), IPlatformPlaylist {
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
|
override val contentType: ContentType = ContentType.PLAYLIST
|
||||||
val contextName = "Playlist";
|
|
||||||
thumbnail = obj.getOrDefault(config, "thumbnail", contextName, null);
|
override val thumbnail: String? =
|
||||||
videoCount = obj.getOrDefault(config, "videoCount", contextName, -1)!!;
|
_content.getOrDefault<String>(_pluginConfig, "thumbnail", "Playlist", null)
|
||||||
}
|
|
||||||
}
|
override val videoCount: Int =
|
||||||
|
_content.getOrDefault<Int>(_pluginConfig, "videoCount", "Playlist", null)?.toInt() ?: -1
|
||||||
|
}
|
||||||
|
|||||||
+30
-4
@@ -17,11 +17,14 @@ import com.futo.platformplayer.getOrThrow
|
|||||||
import com.futo.platformplayer.invokeV8
|
import com.futo.platformplayer.invokeV8
|
||||||
import com.futo.platformplayer.invokeV8Void
|
import com.futo.platformplayer.invokeV8Void
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
|
|
||||||
class JSRequestExecutor {
|
class JSRequestExecutor: AutoCloseable {
|
||||||
private val _plugin: JSClient;
|
private val _plugin: JSClient;
|
||||||
private val _config: IV8PluginConfig;
|
private val _config: IV8PluginConfig;
|
||||||
private var _executor: V8ValueObject;
|
private var _executor: V8ValueObject;
|
||||||
@@ -29,6 +32,9 @@ class JSRequestExecutor {
|
|||||||
|
|
||||||
private val hasCleanup: Boolean;
|
private val hasCleanup: Boolean;
|
||||||
|
|
||||||
|
private var _cleanLock = Any();
|
||||||
|
private var _cleaned: Boolean = false;
|
||||||
|
|
||||||
constructor(plugin: JSClient, executor: V8ValueObject) {
|
constructor(plugin: JSClient, executor: V8ValueObject) {
|
||||||
this._plugin = plugin;
|
this._plugin = plugin;
|
||||||
this._executor = executor;
|
this._executor = executor;
|
||||||
@@ -102,8 +108,12 @@ class JSRequestExecutor {
|
|||||||
|
|
||||||
|
|
||||||
open fun cleanup() {
|
open fun cleanup() {
|
||||||
if (!hasCleanup || _executor.isClosed)
|
synchronized(_cleanLock) {
|
||||||
return;
|
if (!hasCleanup || _executor.isClosed || _cleaned)
|
||||||
|
return;
|
||||||
|
_cleaned = true;
|
||||||
|
}
|
||||||
|
Logger.i("JSRequestExecutor", "JSRequestExecutor cleanup requested");
|
||||||
_plugin.busy {
|
_plugin.busy {
|
||||||
if(_plugin is DevJSClient)
|
if(_plugin is DevJSClient)
|
||||||
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
|
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
|
||||||
@@ -125,9 +135,25 @@ class JSRequestExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun finalize() {
|
override fun close() {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun closeAsync() {
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
|
||||||
|
try {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("JSRequestExecutor", "Cleanup failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
protected fun finalize() {
|
||||||
|
cleanup();
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: are these available..?
|
//TODO: are these available..?
|
||||||
|
|||||||
+3
@@ -5,6 +5,7 @@ import com.caoccao.javet.values.primitive.V8ValueString
|
|||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.getSourcePlugin
|
import com.futo.platformplayer.getSourcePlugin
|
||||||
import com.futo.platformplayer.invokeV8
|
import com.futo.platformplayer.invokeV8
|
||||||
@@ -22,6 +23,7 @@ class JSSubtitleSource : ISubtitleSource {
|
|||||||
override val name: String;
|
override val name: String;
|
||||||
override val url: String?;
|
override val url: String?;
|
||||||
override val format: String?;
|
override val format: String?;
|
||||||
|
override val language: String?
|
||||||
override val hasFetch: Boolean;
|
override val hasFetch: Boolean;
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, v8Value: V8ValueObject) {
|
constructor(config: SourcePluginConfig, v8Value: V8ValueObject) {
|
||||||
@@ -29,6 +31,7 @@ class JSSubtitleSource : ISubtitleSource {
|
|||||||
|
|
||||||
val context = "JSSubtitles";
|
val context = "JSSubtitles";
|
||||||
name = v8Value.getOrThrow(config, "name", context, false);
|
name = v8Value.getOrThrow(config, "name", context, false);
|
||||||
|
language = v8Value.getOrDefault(config, "language", context, null);
|
||||||
url = v8Value.getOrThrow(config, "url", context, true);
|
url = v8Value.getOrThrow(config, "url", context, true);
|
||||||
format = v8Value.getOrThrow(config, "format", context, true);
|
format = v8Value.getOrThrow(config, "format", context, true);
|
||||||
hasFetch = v8Value.has("getSubtitles");
|
hasFetch = v8Value.has("getSubtitles");
|
||||||
|
|||||||
+31
-30
@@ -8,43 +8,44 @@ import com.futo.platformplayer.engine.V8Plugin
|
|||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
open class JSAudioUrlSource(
|
||||||
override val name: String;
|
plugin: JSClient,
|
||||||
override val bitrate : Int;
|
obj: V8ValueObject
|
||||||
override val container : String;
|
) : JSSource(TYPE_AUDIOURL, plugin, obj), IAudioUrlSource {
|
||||||
override val codec: String;
|
|
||||||
private val url : String;
|
|
||||||
|
|
||||||
override val language: String;
|
private val ctx = "AudioUrlSource"
|
||||||
|
private val cfg = plugin.config
|
||||||
|
|
||||||
override val duration: Long?;
|
override val bitrate: Int =
|
||||||
|
_obj.getOrThrow<Int>(cfg, "bitrate", ctx)
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
override val container: String =
|
||||||
|
_obj.getOrThrow<String>(cfg, "container", ctx)
|
||||||
|
|
||||||
override var original: Boolean = false;
|
override val codec: String =
|
||||||
|
_obj.getOrThrow<String>(cfg, "codec", ctx)
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
|
private val url: String =
|
||||||
val contextName = "AudioUrlSource";
|
_obj.getOrThrow<String>(cfg, "url", ctx)
|
||||||
val config = plugin.config;
|
|
||||||
|
|
||||||
bitrate = _obj.getOrThrow(config, "bitrate", contextName);
|
override val language: String =
|
||||||
container = _obj.getOrThrow(config, "container", contextName);
|
_obj.getOrThrow<String>(cfg, "language", ctx)
|
||||||
codec = _obj.getOrThrow(config, "codec", contextName);
|
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
|
||||||
language = _obj.getOrThrow(config, "language", contextName);
|
|
||||||
duration = _obj.getOrDefault(config, "duration", contextName, null);
|
|
||||||
|
|
||||||
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
|
override val duration: Long? =
|
||||||
|
_obj.getOrDefault<Long>(cfg, "duration", ctx, null)?.toLong()
|
||||||
|
|
||||||
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
|
override val name: String =
|
||||||
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
_obj.getOrDefault<String>(cfg, "name", ctx, null)
|
||||||
}
|
?: "$container $bitrate"
|
||||||
|
|
||||||
override fun getAudioUrl() : String {
|
override var priority: Boolean =
|
||||||
return url;
|
if (_obj.has("priority")) _obj.getOrThrow<Boolean>(cfg, "priority", ctx) else false
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override var original: Boolean =
|
||||||
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration)";
|
if (_obj.has("original")) _obj.getOrThrow<Boolean>(cfg, "original", ctx) else false
|
||||||
}
|
|
||||||
}
|
override fun getAudioUrl(): String = url
|
||||||
|
|
||||||
|
override fun toString(): String =
|
||||||
|
"(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration)"
|
||||||
|
}
|
||||||
|
|||||||
+48
-31
@@ -31,42 +31,56 @@ interface IJSDashManifestRawSource {
|
|||||||
fun generateAsync(scope: CoroutineScope): Deferred<String?>;
|
fun generateAsync(scope: CoroutineScope): Deferred<String?>;
|
||||||
fun generate(): String?;
|
fun generate(): String?;
|
||||||
}
|
}
|
||||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
open class JSDashManifestRawSource(
|
||||||
override val container : String;
|
plugin: JSClient,
|
||||||
override val name : String;
|
obj: V8ValueObject
|
||||||
override val width: Int;
|
) : JSSource(TYPE_DASH_RAW, plugin, obj), IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||||
override val height: Int;
|
|
||||||
override val codec: String;
|
|
||||||
override val bitrate: Int?;
|
|
||||||
override val duration: Long;
|
|
||||||
override val priority: Boolean;
|
|
||||||
|
|
||||||
val url: String?;
|
private val ctx = "DashRawSource"
|
||||||
override var manifest: String?;
|
private val cfg = plugin.config
|
||||||
|
|
||||||
override val hasGenerate: Boolean;
|
override val language: String? = _obj.getOrDefault(cfg, "language", ctx, null);
|
||||||
val canMerge: Boolean;
|
override val original: Boolean? = _obj.getOrDefault(cfg, "original", ctx, null);
|
||||||
|
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
override val container: String =
|
||||||
val contextName = "DashRawSource";
|
_obj.getOrDefault<String>(cfg, "container", ctx, null) ?: "application/dash+xml"
|
||||||
val config = plugin.config;
|
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
|
||||||
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
|
||||||
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
|
||||||
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
|
||||||
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
|
||||||
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
|
||||||
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
|
||||||
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
|
||||||
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
|
||||||
canMerge = _obj.getOrDefault(config, "canMerge", contextName, false) ?: false;
|
|
||||||
hasGenerate = _obj.has("generate");
|
|
||||||
}
|
|
||||||
|
|
||||||
private var _pregenerate: V8Deferred<String?>? = null;
|
override val name: String =
|
||||||
|
_obj.getOrThrow<String>(cfg, "name", ctx)
|
||||||
|
|
||||||
|
override val width: Int =
|
||||||
|
_obj.getOrDefault<Int>(cfg, "width", ctx, null)?.toInt() ?: 0
|
||||||
|
|
||||||
|
override val height: Int =
|
||||||
|
_obj.getOrDefault<Int>(cfg, "height", ctx, null)?.toInt() ?: 0
|
||||||
|
|
||||||
|
override val codec: String =
|
||||||
|
_obj.getOrDefault<String>(cfg, "codec", ctx, "") ?: ""
|
||||||
|
|
||||||
|
override val bitrate: Int? =
|
||||||
|
_obj.getOrDefault<Int>(cfg, "bitrate", ctx, null)?.toInt()
|
||||||
|
|
||||||
|
override val duration: Long =
|
||||||
|
_obj.getOrDefault<Long>(cfg, "duration", ctx, 0)?.toLong() ?: 0L
|
||||||
|
|
||||||
|
override val priority: Boolean =
|
||||||
|
_obj.getOrDefault<Boolean>(cfg, "priority", ctx, false) ?: false
|
||||||
|
|
||||||
|
val url: String? =
|
||||||
|
_obj.getOrDefault<String>(cfg, "url", ctx, null)
|
||||||
|
|
||||||
|
override var manifest: String? =
|
||||||
|
_obj.getOrDefault<String>(cfg, "manifest", ctx, null)
|
||||||
|
|
||||||
|
override val hasGenerate: Boolean = _obj.has("generate")
|
||||||
|
|
||||||
|
val canMerge: Boolean =
|
||||||
|
_obj.getOrDefault<Boolean>(cfg, "canMerge", ctx, false) ?: false
|
||||||
|
|
||||||
|
override var streamMetaData: StreamMetaData? = null
|
||||||
|
|
||||||
|
private var _pregenerate: V8Deferred<String?>? = null
|
||||||
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
|
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
|
||||||
_pregenerate = generateAsync(scope);
|
_pregenerate = generateAsync(scope);
|
||||||
return _pregenerate;
|
return _pregenerate;
|
||||||
@@ -175,6 +189,9 @@ class JSDashManifestMergingRawSource(
|
|||||||
override val priority: Boolean
|
override val priority: Boolean
|
||||||
get() = video.priority;
|
get() = video.priority;
|
||||||
|
|
||||||
|
override val language: String? get() = audio.language
|
||||||
|
override val original: Boolean? get() = audio.original;
|
||||||
|
|
||||||
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
|
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
|
||||||
val videoDashDef = video.generateAsync(scope);
|
val videoDashDef = video.generateAsync(scope);
|
||||||
val audioDashDef = audio.generateAsync(scope);
|
val audioDashDef = audio.generateAsync(scope);
|
||||||
|
|||||||
+6
@@ -21,6 +21,9 @@ class JSDashManifestSource : IVideoUrlSource, IDashManifestSource, JSSource {
|
|||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
|
||||||
|
override val language: String?;
|
||||||
|
override val original: Boolean?;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) {
|
||||||
val contextName = "DashSource";
|
val contextName = "DashSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
@@ -29,6 +32,9 @@ class JSDashManifestSource : IVideoUrlSource, IDashManifestSource, JSSource {
|
|||||||
duration = _obj.getOrThrow(config, "duration", contextName);
|
duration = _obj.getOrThrow(config, "duration", contextName);
|
||||||
|
|
||||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||||
|
|
||||||
|
language = obj.getOrNull(config, "language", contextName);
|
||||||
|
original = obj.getOrNull(config, "original", contextName);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getVideoUrl(): String {
|
override fun getVideoUrl(): String {
|
||||||
|
|||||||
+6
@@ -28,6 +28,9 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
|
|||||||
override val licenseUri: String
|
override val licenseUri: String
|
||||||
override val hasLicenseRequestExecutor: Boolean
|
override val hasLicenseRequestExecutor: Boolean
|
||||||
|
|
||||||
|
override val language: String?;
|
||||||
|
override val original: Boolean?;
|
||||||
|
|
||||||
@Suppress("ConvertSecondaryConstructorToPrimary")
|
@Suppress("ConvertSecondaryConstructorToPrimary")
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) {
|
||||||
val contextName = "DashWidevineSource"
|
val contextName = "DashWidevineSource"
|
||||||
@@ -40,6 +43,9 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
|
|||||||
|
|
||||||
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||||
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
||||||
|
|
||||||
|
language = _obj.getOrNull(config, "language", contextName);
|
||||||
|
original = _obj.getOrNull(config, "original", contextName);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
||||||
|
|||||||
+6
@@ -21,6 +21,9 @@ class JSHLSManifestSource : IHLSManifestSource, JSSource {
|
|||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
|
||||||
|
override val language: String?;
|
||||||
|
override val original: Boolean?;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
|
||||||
val contextName = "HLSSource";
|
val contextName = "HLSSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
@@ -30,5 +33,8 @@ class JSHLSManifestSource : IHLSManifestSource, JSSource {
|
|||||||
duration = _obj.getOrThrow<Int>(config, "duration", contextName).toLong();
|
duration = _obj.getOrThrow<Int>(config, "duration", contextName).toLong();
|
||||||
|
|
||||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||||
|
|
||||||
|
language = _obj.getOrNull(config, "language", contextName);
|
||||||
|
original = _obj.getOrNull(config, "original", contextName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+38
-30
@@ -5,42 +5,50 @@ import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrNull
|
import com.futo.platformplayer.getOrNull
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
open class JSVideoUrlSource : IVideoUrlSource, JSSource {
|
open class JSVideoUrlSource(
|
||||||
override val width : Int;
|
plugin: JSClient,
|
||||||
override val height : Int;
|
obj: V8ValueObject
|
||||||
override val container : String;
|
) : JSSource(TYPE_VIDEOURL, plugin, obj), IVideoUrlSource {
|
||||||
override val codec: String;
|
|
||||||
override val name : String;
|
|
||||||
override val bitrate : Int;
|
|
||||||
override val duration: Long;
|
|
||||||
private val url : String;
|
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
private val ctx = "JSVideoUrlSource"
|
||||||
|
private val cfg = plugin.config
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject): super(TYPE_VIDEOURL, plugin, obj) {
|
override val width: Int =
|
||||||
val contextName = "JSVideoUrlSource";
|
_obj.getOrThrow<Int>(cfg, "width", ctx)
|
||||||
val config = plugin.config;
|
|
||||||
|
|
||||||
width = _obj.getOrThrow(config, "width", contextName);
|
override val height: Int =
|
||||||
height = _obj.getOrThrow(config, "height", contextName);
|
_obj.getOrThrow<Int>(cfg, "height", ctx)
|
||||||
container = _obj.getOrThrow(config, "container", contextName);
|
|
||||||
codec = _obj.getOrThrow(config, "codec", contextName);
|
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
|
||||||
bitrate = _obj.getOrThrow(config, "bitrate", contextName);
|
|
||||||
duration = _obj.getOrThrow<Int>(config, "duration", contextName).toLong();
|
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
|
||||||
|
|
||||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
override val container: String =
|
||||||
}
|
_obj.getOrThrow<String>(cfg, "container", ctx)
|
||||||
|
|
||||||
override fun getVideoUrl() : String {
|
override val codec: String =
|
||||||
return url;
|
_obj.getOrThrow<String>(cfg, "codec", ctx)
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override val name: String =
|
||||||
return "(width=$width, height=$height, container=$container, codec=$codec, name=$name, bitrate=$bitrate, duration=$duration, url=$url)"
|
_obj.getOrThrow<String>(cfg, "name", ctx)
|
||||||
}
|
|
||||||
}
|
override val bitrate: Int =
|
||||||
|
_obj.getOrThrow<Int>(cfg, "bitrate", ctx)
|
||||||
|
|
||||||
|
override val duration: Long =
|
||||||
|
_obj.getOrThrow<Long>(cfg, "duration", ctx)
|
||||||
|
|
||||||
|
private val url: String =
|
||||||
|
_obj.getOrThrow<String>(cfg, "url", ctx)
|
||||||
|
|
||||||
|
override var priority: Boolean =
|
||||||
|
_obj.getOrDefault<Boolean>(cfg, "priority", ctx, false) ?: false
|
||||||
|
|
||||||
|
override val language: String? = _obj.getOrDefault(cfg, "language", ctx, null);
|
||||||
|
override val original: Boolean? = _obj.getOrDefault(cfg, "original", ctx, null);
|
||||||
|
|
||||||
|
override fun getVideoUrl(): String = url
|
||||||
|
|
||||||
|
override fun toString(): String =
|
||||||
|
"(width=$width, height=$height, container=$container, codec=$codec, name=$name, bitrate=$bitrate, duration=$duration, url=$url)"
|
||||||
|
}
|
||||||
|
|||||||
+157
-2
@@ -1,5 +1,160 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.local
|
package com.futo.platformplayer.api.media.platforms.local
|
||||||
|
|
||||||
class LocalClient {
|
import android.content.ContentResolver
|
||||||
//TODO
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||||
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||||
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import java.net.MalformedURLException
|
||||||
|
|
||||||
|
class LocalClient: IPlatformClient {
|
||||||
|
override val id: String = "LOCAL"
|
||||||
|
override val name: String = "Local"
|
||||||
|
override val icon: ImageVariable? = ImageVariable.fromResource(R.drawable.ic_library)
|
||||||
|
override val capabilities: PlatformClientCapabilities = PlatformClientCapabilities()
|
||||||
|
|
||||||
|
override fun initialize() {}
|
||||||
|
|
||||||
|
override fun disable() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHome(): IPager<IPlatformContent>
|
||||||
|
= EmptyPager();
|
||||||
|
|
||||||
|
override fun isContentDetailsUrl(url: String): Boolean {
|
||||||
|
try {
|
||||||
|
val uri = Uri.parse(url);
|
||||||
|
return ContentResolver.SCHEME_CONTENT == uri.scheme
|
||||||
|
&& (
|
||||||
|
MediaStore.AUTHORITY == uri.authority ||
|
||||||
|
uri.authority == "com.android.externalstorage.documents"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
catch(ex: MalformedURLException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val audioExtensions = listOf(".mp3", ".wav", ".flac", ".mp4a", ".m4a");
|
||||||
|
override fun getContentDetails(url: String): IPlatformContentDetails {
|
||||||
|
val uri = Uri.parse(url);
|
||||||
|
|
||||||
|
if("audio" in uri.pathSegments) {
|
||||||
|
return StateLibrary.getAudioTrack(url) ?: throw Exception("Failed to find ${url}");
|
||||||
|
}
|
||||||
|
else if("video" in uri.pathSegments) {
|
||||||
|
return StateLibrary.getVideoTrack(url) ?: throw Exception("Failed to find ${url}");
|
||||||
|
}
|
||||||
|
else if(uri.toString().contains("com.android.externalstorage.documents")) {
|
||||||
|
if(audioExtensions.any { uri.lastPathSegment?.lowercase()?.endsWith(it) ?: false })
|
||||||
|
return StateLibrary.getAudioTrack(url) ?: throw Exception("Failed to find ${url}");
|
||||||
|
else
|
||||||
|
return StateLibrary.getVideoTrack(url) ?: throw Exception("Failed to find ${url}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw Exception("Unknown content url [${url}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSearchCapabilities(): ResultCapabilities
|
||||||
|
= ResultCapabilities();
|
||||||
|
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> {
|
||||||
|
return EmptyPager(); //TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSearchChannelContentsCapabilities(): ResultCapabilities
|
||||||
|
= ResultCapabilities();
|
||||||
|
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> {
|
||||||
|
return EmptyPager(); //TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchChannels(query: String): IPager<PlatformAuthorLink> {
|
||||||
|
return EmptyPager(); //TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchChannelsAsContent(query: String): IPager<IPlatformContent> {
|
||||||
|
return EmptyPager(); //TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isChannelUrl(url: String): Boolean {
|
||||||
|
return false //TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChannel(channelUrl: String): IPlatformChannel {
|
||||||
|
throw NotImplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChannelCapabilities(): ResultCapabilities
|
||||||
|
= ResultCapabilities();
|
||||||
|
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> {
|
||||||
|
return EmptyPager();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist> {
|
||||||
|
return EmptyPager();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPeekChannelTypes(): List<String> = listOf();
|
||||||
|
|
||||||
|
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent>
|
||||||
|
= listOf();
|
||||||
|
|
||||||
|
override fun getShorts(): IPager<IPlatformVideo> = EmptyPager();
|
||||||
|
|
||||||
|
override fun searchSuggestions(query: String): Array<String> = arrayOf();
|
||||||
|
|
||||||
|
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String?
|
||||||
|
= null;
|
||||||
|
|
||||||
|
override fun getContentChapters(url: String): List<IChapter>
|
||||||
|
= listOf();
|
||||||
|
|
||||||
|
override fun getPlaybackTracker(url: String): IPlaybackTracker?
|
||||||
|
= null;
|
||||||
|
|
||||||
|
override fun getContentRecommendations(url: String): IPager<IPlatformContent>?
|
||||||
|
= null;
|
||||||
|
|
||||||
|
override fun getComments(url: String): IPager<IPlatformComment>
|
||||||
|
= EmptyPager();
|
||||||
|
|
||||||
|
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment>
|
||||||
|
= EmptyPager();
|
||||||
|
|
||||||
|
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor?
|
||||||
|
= null;
|
||||||
|
|
||||||
|
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>?
|
||||||
|
= null;
|
||||||
|
|
||||||
|
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent>
|
||||||
|
= throw NotImplementedError();
|
||||||
|
|
||||||
|
override fun isPlaylistUrl(url: String): Boolean = false;
|
||||||
|
|
||||||
|
override fun getPlaylist(url: String): IPlatformPlaylistDetails
|
||||||
|
= throw NotImplementedError();
|
||||||
|
override fun getUserPlaylists(): Array<String> = throw NotImplementedError();
|
||||||
|
override fun getUserSubscriptions(): Array<String> = throw NotImplementedError();
|
||||||
|
override fun getUserHistory(): IPager<IPlatformContent> = throw NotImplementedError();
|
||||||
|
override fun isClaimTypeSupported(claimType: Int): Boolean = false;
|
||||||
}
|
}
|
||||||
+2
-2
@@ -23,10 +23,10 @@ class LocalAudioContentSource : IAudioSource {
|
|||||||
|
|
||||||
var contentUrl: String;
|
var contentUrl: String;
|
||||||
|
|
||||||
constructor(contentUrl: String, mime: String, name: String? = null) {
|
constructor(contentUrl: String, mime: String, name: String? = null, duration: Long = 0) {
|
||||||
this.name = name ?: "File";
|
this.name = name ?: "File";
|
||||||
container = mime;
|
container = mime;
|
||||||
duration = 0;
|
this.duration = duration;
|
||||||
|
|
||||||
this.contentUrl = contentUrl;
|
this.contentUrl = contentUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -20,14 +20,17 @@ class LocalVideoContentSource: IVideoSource {
|
|||||||
override val duration: Long;
|
override val duration: Long;
|
||||||
override val priority: Boolean = false;
|
override val priority: Boolean = false;
|
||||||
|
|
||||||
|
override val language: String? = null;
|
||||||
|
override val original: Boolean? = false;
|
||||||
|
|
||||||
var contentUrl: String;
|
var contentUrl: String;
|
||||||
|
|
||||||
constructor(contentUrl: String, mime: String, name: String? = null) {
|
constructor(contentUrl: String, mime: String, name: String? = null, duration: Long = 0) {
|
||||||
this.name = name ?: "File";
|
this.name = name ?: "File";
|
||||||
width = 0;
|
width = 0;
|
||||||
height = 0;
|
height = 0;
|
||||||
container = mime;
|
container = mime;
|
||||||
duration = 0;
|
this.duration = duration;
|
||||||
this.contentUrl = contentUrl;
|
this.contentUrl = contentUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+3
@@ -20,6 +20,9 @@ class LocalVideoFileSource: IVideoSource {
|
|||||||
override val duration: Long;
|
override val duration: Long;
|
||||||
override val priority: Boolean = false;
|
override val priority: Boolean = false;
|
||||||
|
|
||||||
|
override val language: String? = null;
|
||||||
|
override val original: Boolean? = null;
|
||||||
|
|
||||||
var file: File;
|
var file: File;
|
||||||
|
|
||||||
constructor(file: File) {
|
constructor(file: File) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.casting
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import org.fcast.sender_sdk.Metadata
|
import org.fcast.sender_sdk.Metadata
|
||||||
@@ -16,6 +17,7 @@ abstract class CastingDevice {
|
|||||||
abstract val onDurationChanged: Event1<Double>
|
abstract val onDurationChanged: Event1<Double>
|
||||||
abstract val onVolumeChanged: Event1<Double>
|
abstract val onVolumeChanged: Event1<Double>
|
||||||
abstract val onSpeedChanged: Event1<Double>
|
abstract val onSpeedChanged: Event1<Double>
|
||||||
|
abstract val onMediaItemEnd: Event0
|
||||||
abstract var connectionState: CastConnectionState
|
abstract var connectionState: CastConnectionState
|
||||||
abstract val protocolType: CastProtocolType
|
abstract val protocolType: CastProtocolType
|
||||||
abstract var isPlaying: Boolean
|
abstract var isPlaying: Boolean
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package com.futo.platformplayer.casting
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
|
import com.futo.polycentric.core.Event
|
||||||
import org.fcast.sender_sdk.ApplicationInfo
|
import org.fcast.sender_sdk.ApplicationInfo
|
||||||
import org.fcast.sender_sdk.GenericKeyEvent
|
import org.fcast.sender_sdk.KeyEvent
|
||||||
import org.fcast.sender_sdk.GenericMediaEvent
|
import org.fcast.sender_sdk.MediaEvent
|
||||||
import org.fcast.sender_sdk.PlaybackState
|
import org.fcast.sender_sdk.PlaybackState
|
||||||
import org.fcast.sender_sdk.Source
|
import org.fcast.sender_sdk.Source
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
@@ -15,8 +17,10 @@ import org.fcast.sender_sdk.CastingDevice as RsCastingDevice;
|
|||||||
import org.fcast.sender_sdk.DeviceEventHandler as RsDeviceEventHandler;
|
import org.fcast.sender_sdk.DeviceEventHandler as RsDeviceEventHandler;
|
||||||
import org.fcast.sender_sdk.DeviceConnectionState
|
import org.fcast.sender_sdk.DeviceConnectionState
|
||||||
import org.fcast.sender_sdk.DeviceFeature
|
import org.fcast.sender_sdk.DeviceFeature
|
||||||
|
import org.fcast.sender_sdk.EventSubscription
|
||||||
import org.fcast.sender_sdk.IpAddr
|
import org.fcast.sender_sdk.IpAddr
|
||||||
import org.fcast.sender_sdk.LoadRequest
|
import org.fcast.sender_sdk.LoadRequest
|
||||||
|
import org.fcast.sender_sdk.MediaItemEventType
|
||||||
import org.fcast.sender_sdk.Metadata
|
import org.fcast.sender_sdk.Metadata
|
||||||
import org.fcast.sender_sdk.ProtocolType
|
import org.fcast.sender_sdk.ProtocolType
|
||||||
import org.fcast.sender_sdk.urlFormatIpAddr
|
import org.fcast.sender_sdk.urlFormatIpAddr
|
||||||
@@ -63,6 +67,7 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
|||||||
var onDurationChanged = Event1<Double>()
|
var onDurationChanged = Event1<Double>()
|
||||||
var onVolumeChanged = Event1<Double>()
|
var onVolumeChanged = Event1<Double>()
|
||||||
var onSpeedChanged = Event1<Double>()
|
var onSpeedChanged = Event1<Double>()
|
||||||
|
var onMediaItemEnd = Event0()
|
||||||
|
|
||||||
override fun connectionStateChanged(state: DeviceConnectionState) {
|
override fun connectionStateChanged(state: DeviceConnectionState) {
|
||||||
onConnectionStateChanged.emit(state)
|
onConnectionStateChanged.emit(state)
|
||||||
@@ -92,12 +97,14 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
|||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun keyEvent(event: GenericKeyEvent) {
|
override fun keyEvent(event: KeyEvent) {
|
||||||
// Unreachable
|
// Unreachable
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mediaEvent(event: GenericMediaEvent) {
|
override fun mediaEvent(event: MediaEvent) {
|
||||||
// Unreachable
|
if (event.type == MediaItemEventType.END) {
|
||||||
|
onMediaItemEnd.emit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun playbackError(message: String) {
|
override fun playbackError(message: String) {
|
||||||
@@ -127,6 +134,8 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
|||||||
get() = eventHandler.onVolumeChanged
|
get() = eventHandler.onVolumeChanged
|
||||||
override val onSpeedChanged: Event1<Double>
|
override val onSpeedChanged: Event1<Double>
|
||||||
get() = eventHandler.onSpeedChanged
|
get() = eventHandler.onSpeedChanged
|
||||||
|
override val onMediaItemEnd: Event0
|
||||||
|
get() = eventHandler.onMediaItemEnd
|
||||||
|
|
||||||
override fun resumePlayback() = device.resumePlayback()
|
override fun resumePlayback() = device.resumePlayback()
|
||||||
override fun pausePlayback() = device.pausePlayback()
|
override fun pausePlayback() = device.pausePlayback()
|
||||||
@@ -181,7 +190,8 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
|||||||
resumePosition = resumePosition,
|
resumePosition = resumePosition,
|
||||||
speed = speed,
|
speed = speed,
|
||||||
volume = volume,
|
volume = volume,
|
||||||
metadata = metadata
|
metadata = metadata,
|
||||||
|
requestHeaders = null,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -200,6 +210,7 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
|||||||
speed = speed,
|
speed = speed,
|
||||||
volume = volume,
|
volume = volume,
|
||||||
metadata = metadata,
|
metadata = metadata,
|
||||||
|
requestHeaders = null,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -227,6 +238,13 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
|||||||
eventHandler.onConnectionStateChanged.subscribe { newState ->
|
eventHandler.onConnectionStateChanged.subscribe { newState ->
|
||||||
when (newState) {
|
when (newState) {
|
||||||
is DeviceConnectionState.Connected -> {
|
is DeviceConnectionState.Connected -> {
|
||||||
|
if (device.supportsFeature(DeviceFeature.MEDIA_EVENT_SUBSCRIPTION)) {
|
||||||
|
try {
|
||||||
|
device.subscribeEvent(EventSubscription.MediaItemEnd)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.e(TAG, "Failed to subscribe to MediaItemEnd events: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
usedRemoteAddress = ipAddrToInetAddress(newState.usedRemoteAddr)
|
usedRemoteAddress = ipAddrToInetAddress(newState.usedRemoteAddr)
|
||||||
localAddress = ipAddrToInetAddress(newState.localAddr)
|
localAddress = ipAddrToInetAddress(newState.localAddr)
|
||||||
connectionState = CastConnectionState.CONNECTED
|
connectionState = CastConnectionState.CONNECTED
|
||||||
@@ -239,7 +257,7 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DeviceConnectionState.Disconnected -> {
|
DeviceConnectionState.Disconnected -> {
|
||||||
connectionState = CastConnectionState.CONNECTING
|
connectionState = CastConnectionState.DISCONNECTED
|
||||||
onConnectionStateChanged.emit(CastConnectionState.DISCONNECTED)
|
onConnectionStateChanged.emit(CastConnectionState.DISCONNECTED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,4 +286,4 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
|||||||
companion object {
|
companion object {
|
||||||
private val TAG = "CastingDeviceExp"
|
private val TAG = "CastingDeviceExp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.casting
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
@@ -181,6 +182,7 @@ class CastingDeviceLegacyWrapper(val inner: CastingDeviceLegacy) : CastingDevice
|
|||||||
override val onDurationChanged: Event1<Double> get() = inner.onDurationChanged
|
override val onDurationChanged: Event1<Double> get() = inner.onDurationChanged
|
||||||
override val onVolumeChanged: Event1<Double> get() = inner.onVolumeChanged
|
override val onVolumeChanged: Event1<Double> get() = inner.onVolumeChanged
|
||||||
override val onSpeedChanged: Event1<Double> get() = inner.onSpeedChanged
|
override val onSpeedChanged: Event1<Double> get() = inner.onSpeedChanged
|
||||||
|
override val onMediaItemEnd: Event0 = Event0()
|
||||||
override var connectionState: CastConnectionState
|
override var connectionState: CastConnectionState
|
||||||
get() = inner.connectionState
|
get() = inner.connectionState
|
||||||
set(_) = Unit
|
set(_) = Unit
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.content.Context
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
@@ -14,9 +15,11 @@ import com.futo.platformplayer.api.http.ManagedHttpClient
|
|||||||
import com.futo.platformplayer.api.http.server.HttpHeaders
|
import com.futo.platformplayer.api.http.server.HttpHeaders
|
||||||
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
|
||||||
|
import com.futo.platformplayer.api.http.server.handlers.HttpContentUriHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFileHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFileHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler
|
||||||
|
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||||
@@ -33,7 +36,11 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManif
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
|
||||||
|
import com.futo.platformplayer.awaitCancelConverted
|
||||||
import com.futo.platformplayer.builders.DashBuilder
|
import com.futo.platformplayer.builders.DashBuilder
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
@@ -76,6 +83,7 @@ abstract class StateCasting {
|
|||||||
val onActiveDeviceTimeChanged = Event1<Double>();
|
val onActiveDeviceTimeChanged = Event1<Double>();
|
||||||
val onActiveDeviceDurationChanged = Event1<Double>();
|
val onActiveDeviceDurationChanged = Event1<Double>();
|
||||||
val onActiveDeviceVolumeChanged = Event1<Double>();
|
val onActiveDeviceVolumeChanged = Event1<Double>();
|
||||||
|
val onActiveDeviceMediaItemEnd = Event0()
|
||||||
var activeDevice: CastingDevice? = null;
|
var activeDevice: CastingDevice? = null;
|
||||||
private var _videoExecutor: JSRequestExecutor? = null
|
private var _videoExecutor: JSRequestExecutor? = null
|
||||||
private var _audioExecutor: JSRequestExecutor? = null
|
private var _audioExecutor: JSRequestExecutor? = null
|
||||||
@@ -89,8 +97,7 @@ abstract class StateCasting {
|
|||||||
abstract fun start(context: Context)
|
abstract fun start(context: Context)
|
||||||
abstract fun stop()
|
abstract fun stop()
|
||||||
|
|
||||||
@Throws
|
abstract fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDevice?
|
||||||
abstract fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDevice
|
|
||||||
abstract fun startUpdateTimeJob(
|
abstract fun startUpdateTimeJob(
|
||||||
onTimeJobTimeChanged_s: Event1<Long>, setTime: (Long) -> Unit
|
onTimeJobTimeChanged_s: Event1<Long>, setTime: (Long) -> Unit
|
||||||
): Job?
|
): Job?
|
||||||
@@ -140,6 +147,7 @@ abstract class StateCasting {
|
|||||||
device.onTimeChanged.clear();
|
device.onTimeChanged.clear();
|
||||||
device.onVolumeChanged.clear();
|
device.onVolumeChanged.clear();
|
||||||
device.onDurationChanged.clear();
|
device.onDurationChanged.clear();
|
||||||
|
device.onMediaItemEnd.clear();
|
||||||
ad.disconnect()
|
ad.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +162,7 @@ abstract class StateCasting {
|
|||||||
device.onTimeChanged.clear();
|
device.onTimeChanged.clear();
|
||||||
device.onVolumeChanged.clear();
|
device.onVolumeChanged.clear();
|
||||||
device.onDurationChanged.clear();
|
device.onDurationChanged.clear();
|
||||||
|
device.onMediaItemEnd.clear();
|
||||||
activeDevice = null;
|
activeDevice = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +226,9 @@ abstract class StateCasting {
|
|||||||
device.onTimeChanged.subscribe {
|
device.onTimeChanged.subscribe {
|
||||||
invokeInMainScopeIfRequired { onActiveDeviceTimeChanged.emit(it) };
|
invokeInMainScopeIfRequired { onActiveDeviceTimeChanged.emit(it) };
|
||||||
};
|
};
|
||||||
|
device.onMediaItemEnd.subscribe {
|
||||||
|
invokeInMainScopeIfRequired { onActiveDeviceMediaItemEnd.emit() }
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
device.connect();
|
device.connect();
|
||||||
@@ -227,6 +239,7 @@ abstract class StateCasting {
|
|||||||
device.onTimeChanged.clear();
|
device.onTimeChanged.clear();
|
||||||
device.onVolumeChanged.clear();
|
device.onVolumeChanged.clear();
|
||||||
device.onDurationChanged.clear();
|
device.onDurationChanged.clear();
|
||||||
|
device.onMediaItemEnd.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,9 +247,9 @@ abstract class StateCasting {
|
|||||||
Logger.i(TAG, "Connect to device ${device.name}")
|
Logger.i(TAG, "Connect to device ${device.name}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun metadataFromVideo(video: IPlatformVideoDetails): Metadata {
|
fun metadataFromVideo(video: IPlatformVideoDetails, videoThumbnailOverrideUrl: String? = null): Metadata {
|
||||||
return Metadata(
|
return Metadata(
|
||||||
title = video.name, thumbnailUrl = video.thumbnails.getHQThumbnail()
|
title = video.name, thumbnailUrl = videoThumbnailOverrideUrl ?: video.thumbnails.getHQThumbnail()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,20 +308,63 @@ abstract class StateCasting {
|
|||||||
val url = getLocalUrl(ad);
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
|
|
||||||
if (videoSource is IVideoUrlSource) {
|
if (videoSource is IVideoUrlSource) {
|
||||||
val videoPath = "/video-${id}"
|
val videoPath = "/video-$id"
|
||||||
val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl();
|
val upstreamUrl = videoSource.getVideoUrl()
|
||||||
Logger.i(TAG, "Casting as singular video");
|
val videoUrl = if (proxyStreams) url + videoPath else upstreamUrl
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video));
|
val jsReqMod = (videoSource as? JSSource)?.getRequestModifier()
|
||||||
|
|
||||||
|
if (proxyStreams) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpProxyHandler("GET", videoPath, upstreamUrl, true)
|
||||||
|
.withIRequestModifier(jsReqMod)
|
||||||
|
.withInjectedHost()
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"),
|
||||||
|
true
|
||||||
|
).withTag("castSingular")
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "Casting as singular video (proxy=$proxyStreams, url=$videoUrl)")
|
||||||
|
ad.loadVideo(
|
||||||
|
if (video.isLive) "LIVE" else "BUFFERED",
|
||||||
|
videoSource.container,
|
||||||
|
videoUrl,
|
||||||
|
resumePosition,
|
||||||
|
video.duration.toDouble(),
|
||||||
|
speed,
|
||||||
|
metadataFromVideo(video)
|
||||||
|
)
|
||||||
} else if (audioSource is IAudioUrlSource) {
|
} else if (audioSource is IAudioUrlSource) {
|
||||||
val audioPath = "/audio-${id}"
|
val audioPath = "/audio-$id"
|
||||||
val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl();
|
val upstreamUrl = audioSource.getAudioUrl()
|
||||||
Logger.i(TAG, "Casting as singular audio");
|
val audioUrl = if (proxyStreams) url + audioPath else upstreamUrl
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video));
|
val jsReqMod = (audioSource as? JSSource)?.getRequestModifier()
|
||||||
|
|
||||||
|
if (proxyStreams) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpProxyHandler("GET", audioPath, upstreamUrl, true)
|
||||||
|
.withIRequestModifier(jsReqMod)
|
||||||
|
.withInjectedHost()
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"),
|
||||||
|
true
|
||||||
|
).withTag("castSingular")
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "Casting as singular audio (proxy=$proxyStreams, url=$audioUrl)")
|
||||||
|
ad.loadVideo(
|
||||||
|
if (video.isLive) "LIVE" else "BUFFERED",
|
||||||
|
audioSource.container,
|
||||||
|
audioUrl,
|
||||||
|
resumePosition,
|
||||||
|
video.duration.toDouble(),
|
||||||
|
speed,
|
||||||
|
metadataFromVideo(video)
|
||||||
|
)
|
||||||
} else if (videoSource is IHLSManifestSource) {
|
} else if (videoSource is IHLSManifestSource) {
|
||||||
if (proxyStreams || deviceProto == CastProtocolType.CHROMECAST) {
|
if (proxyStreams || deviceProto == CastProtocolType.CHROMECAST) {
|
||||||
Logger.i(TAG, "Casting as proxied HLS");
|
Logger.i(TAG, "Casting as proxied HLS");
|
||||||
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
|
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed, (videoSource as JSSource?)?.getRequestModifier());
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "Casting as non-proxied HLS");
|
Logger.i(TAG, "Casting as non-proxied HLS");
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video));
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video));
|
||||||
@@ -316,7 +372,7 @@ abstract class StateCasting {
|
|||||||
} else if (audioSource is IHLSManifestAudioSource) {
|
} else if (audioSource is IHLSManifestAudioSource) {
|
||||||
if (proxyStreams || deviceProto == CastProtocolType.CHROMECAST) {
|
if (proxyStreams || deviceProto == CastProtocolType.CHROMECAST) {
|
||||||
Logger.i(TAG, "Casting as proxied audio HLS");
|
Logger.i(TAG, "Casting as proxied audio HLS");
|
||||||
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
|
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed, (audioSource as JSSource?)?.getRequestModifier());
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "Casting as non-proxied audio HLS");
|
Logger.i(TAG, "Casting as non-proxied audio HLS");
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video));
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video));
|
||||||
@@ -327,6 +383,12 @@ abstract class StateCasting {
|
|||||||
} else if (audioSource is LocalAudioSource) {
|
} else if (audioSource is LocalAudioSource) {
|
||||||
Logger.i(TAG, "Casting as local audio");
|
Logger.i(TAG, "Casting as local audio");
|
||||||
castLocalAudio(video, audioSource, resumePosition, speed);
|
castLocalAudio(video, audioSource, resumePosition, speed);
|
||||||
|
} else if (videoSource is LocalVideoContentSource) {
|
||||||
|
Logger.i(TAG, "Casting as local video");
|
||||||
|
castLocalVideo(contentResolver, video, videoSource, resumePosition, speed);
|
||||||
|
} else if (audioSource is LocalAudioContentSource) {
|
||||||
|
Logger.i(TAG, "Casting as local audio");
|
||||||
|
castLocalAudio(contentResolver, video, audioSource, resumePosition, speed);
|
||||||
} else if (videoSource is JSDashManifestRawSource) {
|
} else if (videoSource is JSDashManifestRawSource) {
|
||||||
Logger.i(TAG, "Casting as JSDashManifestRawSource video");
|
Logger.i(TAG, "Casting as JSDashManifestRawSource video");
|
||||||
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed, castId, onLoadingEstimate, onLoading);
|
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed, castId, onLoadingEstimate, onLoading);
|
||||||
@@ -347,6 +409,11 @@ abstract class StateCasting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun HttpProxyHandler.withIRequestModifier(requestModifier: IRequestModifier?): HttpProxyHandler {
|
||||||
|
if (requestModifier == null) return this
|
||||||
|
return withRequestModifier { url, headers -> requestModifier.modifyRequest(url, headers) }
|
||||||
|
}
|
||||||
|
|
||||||
fun resumeVideo(): Boolean {
|
fun resumeVideo(): Boolean {
|
||||||
val ad = activeDevice ?: return false;
|
val ad = activeDevice ?: return false;
|
||||||
try {
|
try {
|
||||||
@@ -412,6 +479,65 @@ abstract class StateCasting {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun castLocalVideo(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: LocalVideoContentSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
|
val url = getLocalUrl(ad);
|
||||||
|
val id = UUID.randomUUID();
|
||||||
|
val videoPath = "/video-${id}"
|
||||||
|
val videoUrl = url + videoPath;
|
||||||
|
val thumbnailPath = "/thumbnail-${id}"
|
||||||
|
val thumbnailUrl = url + thumbnailPath;
|
||||||
|
val thumbnailContentUrl = video.thumbnails.getHQThumbnail()
|
||||||
|
|
||||||
|
if (thumbnailContentUrl != null) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpContentUriHandler("GET", thumbnailPath, contentResolver, thumbnailContentUrl.toUri())
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("cast");
|
||||||
|
}
|
||||||
|
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpContentUriHandler("GET", videoPath, contentResolver, videoSource.contentUrl.toUri())
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("cast");
|
||||||
|
|
||||||
|
Logger.i(TAG, "Casting local video (videoUrl: $videoUrl).");
|
||||||
|
ad.loadVideo("BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video, if (thumbnailContentUrl != null) thumbnailUrl else null));
|
||||||
|
|
||||||
|
return listOf(videoUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun castLocalAudio(contentResolver: ContentResolver, video: IPlatformVideoDetails, audioSource: LocalAudioContentSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
|
val url = getLocalUrl(ad);
|
||||||
|
val id = UUID.randomUUID();
|
||||||
|
val audioPath = "/audio-${id}"
|
||||||
|
val audioUrl = url + audioPath;
|
||||||
|
val thumbnailPath = "/thumbnail-${id}"
|
||||||
|
val thumbnailUrl = url + thumbnailPath;
|
||||||
|
val thumbnailContentUrl = video.thumbnails.getHQThumbnail()
|
||||||
|
|
||||||
|
if (thumbnailContentUrl != null) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpContentUriHandler("GET", thumbnailPath, contentResolver, thumbnailContentUrl.toUri())
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("cast");
|
||||||
|
}
|
||||||
|
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpContentUriHandler("GET", audioPath, contentResolver, audioSource.contentUrl.toUri())
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("cast");
|
||||||
|
|
||||||
|
Logger.i(TAG, "Casting local audio (audioUrl: $audioUrl).");
|
||||||
|
ad.loadVideo("BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video, if (thumbnailContentUrl != null) thumbnailUrl else null));
|
||||||
|
|
||||||
|
return listOf(audioUrl);
|
||||||
|
}
|
||||||
|
|
||||||
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
@@ -665,7 +791,8 @@ abstract class StateCasting {
|
|||||||
sourceUrl: String,
|
sourceUrl: String,
|
||||||
codec: String?,
|
codec: String?,
|
||||||
resumePosition: Double,
|
resumePosition: Double,
|
||||||
speed: Double?
|
speed: Double?,
|
||||||
|
requestModifier: IRequestModifier?
|
||||||
): List<String> {
|
): List<String> {
|
||||||
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
||||||
|
|
||||||
@@ -686,7 +813,9 @@ abstract class StateCasting {
|
|||||||
val headers = masterContext.headers.clone()
|
val headers = masterContext.headers.clone()
|
||||||
headers["Content-Type"] = "application/vnd.apple.mpegurl";
|
headers["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||||
|
|
||||||
val masterPlaylistResponse = _client.get(sourceUrl)
|
val req = requestModifier?.modifyRequest(sourceUrl, mapOf())
|
||||||
|
val masterPlaylistResponse = _client.get(req?.url ?: sourceUrl, (req?.headers ?: mapOf()).toMutableMap())
|
||||||
|
|
||||||
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||||
|
|
||||||
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||||
@@ -706,7 +835,7 @@ abstract class StateCasting {
|
|||||||
val variantPlaylist =
|
val variantPlaylist =
|
||||||
HLS.parseVariantPlaylist(masterPlaylistContent, sourceUrl)
|
HLS.parseVariantPlaylist(masterPlaylistContent, sourceUrl)
|
||||||
val proxiedVariantPlaylist =
|
val proxiedVariantPlaylist =
|
||||||
proxyVariantPlaylist(url, id, variantPlaylist, video.isLive)
|
proxyVariantPlaylist(url, id, variantPlaylist, video.isLive, requestModifier)
|
||||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||||
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||||
return@HttpFunctionHandler
|
return@HttpFunctionHandler
|
||||||
@@ -747,7 +876,7 @@ abstract class StateCasting {
|
|||||||
val variantPlaylist =
|
val variantPlaylist =
|
||||||
HLS.parseVariantPlaylist(vpContent, variantPlaylistRef.url)
|
HLS.parseVariantPlaylist(vpContent, variantPlaylistRef.url)
|
||||||
val proxiedVariantPlaylist =
|
val proxiedVariantPlaylist =
|
||||||
proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive)
|
proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive, requestModifier)
|
||||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||||
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||||
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
@@ -784,7 +913,7 @@ abstract class StateCasting {
|
|||||||
val variantPlaylist =
|
val variantPlaylist =
|
||||||
HLS.parseVariantPlaylist(vpContent, mediaRendition.uri)
|
HLS.parseVariantPlaylist(vpContent, mediaRendition.uri)
|
||||||
val proxiedVariantPlaylist = proxyVariantPlaylist(
|
val proxiedVariantPlaylist = proxyVariantPlaylist(
|
||||||
url, playlistId, variantPlaylist, video.isLive
|
url, playlistId, variantPlaylist, video.isLive, requestModifier
|
||||||
)
|
)
|
||||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||||
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||||
@@ -826,13 +955,13 @@ abstract class StateCasting {
|
|||||||
return listOf(hlsUrl);
|
return listOf(hlsUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun proxyVariantPlaylist(url: String, playlistId: UUID, variantPlaylist: HLS.VariantPlaylist, isLive: Boolean, proxySegments: Boolean = true): HLS.VariantPlaylist {
|
private fun proxyVariantPlaylist(url: String, playlistId: UUID, variantPlaylist: HLS.VariantPlaylist, isLive: Boolean, requestModifier: IRequestModifier?, proxySegments: Boolean = true): HLS.VariantPlaylist {
|
||||||
val newSegments = arrayListOf<HLS.Segment>()
|
val newSegments = arrayListOf<HLS.Segment>()
|
||||||
|
|
||||||
if (proxySegments) {
|
if (proxySegments) {
|
||||||
variantPlaylist.segments.forEachIndexed { index, segment ->
|
variantPlaylist.segments.forEachIndexed { index, segment ->
|
||||||
val sequenceNumber = (variantPlaylist.mediaSequence ?: 0) + index.toLong()
|
val sequenceNumber = (variantPlaylist.mediaSequence ?: 0) + index.toLong()
|
||||||
newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber))
|
newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber, requestModifier))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newSegments.addAll(variantPlaylist.segments)
|
newSegments.addAll(variantPlaylist.segments)
|
||||||
@@ -850,7 +979,7 @@ abstract class StateCasting {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun proxySegment(url: String, playlistId: UUID, segment: HLS.Segment, index: Long): HLS.Segment {
|
private fun proxySegment(url: String, playlistId: UUID, segment: HLS.Segment, index: Long, requestModifier: IRequestModifier?): HLS.Segment {
|
||||||
if (segment is HLS.MediaSegment) {
|
if (segment is HLS.MediaSegment) {
|
||||||
val newSegmentPath = "/hls-playlist-${playlistId}-segment-${index}"
|
val newSegmentPath = "/hls-playlist-${playlistId}-segment-${index}"
|
||||||
val newSegmentUrl = url + newSegmentPath;
|
val newSegmentUrl = url + newSegmentPath;
|
||||||
@@ -858,6 +987,7 @@ abstract class StateCasting {
|
|||||||
if (_castServer.getHandler("GET", newSegmentPath) == null) {
|
if (_castServer.getHandler("GET", newSegmentPath) == null) {
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", newSegmentPath, segment.uri, true)
|
HttpProxyHandler("GET", newSegmentPath, segment.uri, true)
|
||||||
|
.withIRequestModifier(requestModifier)
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castProxiedHlsVariant")
|
).withTag("castProxiedHlsVariant")
|
||||||
@@ -1183,7 +1313,7 @@ abstract class StateCasting {
|
|||||||
onLoading?.invoke(true)
|
onLoading?.invoke(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deferred.await()
|
deferred.awaitCancelConverted()
|
||||||
} finally {
|
} finally {
|
||||||
if (castId == _castId.get()) {
|
if (castId == _castId.get()) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -1201,8 +1331,14 @@ abstract class StateCasting {
|
|||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hasAudioInDash = false
|
||||||
for (representation in representationRegex.findAll(dashContent)) {
|
for (representation in representationRegex.findAll(dashContent)) {
|
||||||
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
|
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
|
||||||
|
|
||||||
|
if (mediaType.startsWith("audio/")) {
|
||||||
|
hasAudioInDash = true
|
||||||
|
}
|
||||||
|
|
||||||
dashContent = mediaInitializationRegex.replace(dashContent) {
|
dashContent = mediaInitializationRegex.replace(dashContent) {
|
||||||
if (it.range.first < representation.range.first || it.range.last > representation.range.last) {
|
if (it.range.first < representation.range.first || it.range.last > representation.range.last) {
|
||||||
return@replace it.value
|
return@replace it.value
|
||||||
@@ -1226,12 +1362,20 @@ abstract class StateCasting {
|
|||||||
throw Exception("Audio source without request executor not supported")
|
throw Exception("Audio source without request executor not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioSource != null && audioSource.hasRequestExecutor) {
|
if (videoSource != null && videoSource.hasRequestExecutor) {
|
||||||
_audioExecutor = audioSource.getRequestExecutor()
|
val oldVideoExecutor = _videoExecutor
|
||||||
|
oldVideoExecutor?.closeAsync()
|
||||||
|
_videoExecutor = videoSource.getRequestExecutor()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoSource != null && videoSource.hasRequestExecutor) {
|
if (audioSource != null) {
|
||||||
_videoExecutor = videoSource.getRequestExecutor()
|
val oldExecutor = _audioExecutor
|
||||||
|
oldExecutor?.closeAsync()
|
||||||
|
_audioExecutor = audioSource.getRequestExecutor()
|
||||||
|
} else if (hasAudioInDash && videoSource != null) {
|
||||||
|
val oldExecutor = _audioExecutor
|
||||||
|
oldExecutor?.closeAsync()
|
||||||
|
_audioExecutor = _videoExecutor
|
||||||
}
|
}
|
||||||
|
|
||||||
//TOOD: Else also handle the non request executor case, perhaps add ?url=$originalUrl to the query parameters, ... propagate this for all other flows also
|
//TOOD: Else also handle the non request executor case, perhaps add ?url=$originalUrl to the query parameters, ... propagate this for all other flows also
|
||||||
@@ -1262,7 +1406,7 @@ abstract class StateCasting {
|
|||||||
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castDashRaw");
|
).withTag("castDashRaw");
|
||||||
}
|
}
|
||||||
if (audioSource != null) {
|
if (audioSource != null || (audioSource == null && hasAudioInDash)) {
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpFunctionHandler("GET", audioPath) { httpContext ->
|
HttpFunctionHandler("GET", audioPath) { httpContext ->
|
||||||
val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||||
@@ -1287,9 +1431,11 @@ abstract class StateCasting {
|
|||||||
return listOf()
|
return listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addRememberedDevice(deviceInfo: CastingDeviceInfo): CastingDeviceInfo {
|
fun addRememberedDevice(deviceInfo: CastingDeviceInfo): CastingDeviceInfo? {
|
||||||
val device = deviceFromInfo(deviceInfo);
|
return when (val device = deviceFromInfo(deviceInfo)) {
|
||||||
return addRememberedDevice(device);
|
null -> null
|
||||||
|
else -> addRememberedDevice(device)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addRememberedDevice(device: CastingDevice): CastingDeviceInfo {
|
fun addRememberedDevice(device: CastingDevice): CastingDeviceInfo {
|
||||||
@@ -1298,7 +1444,7 @@ abstract class StateCasting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getRememberedCastingDevices(): List<CastingDevice> {
|
fun getRememberedCastingDevices(): List<CastingDevice> {
|
||||||
return _storage.getDevices().map { deviceFromInfo(it) }
|
return _storage.getDevices().map { deviceFromInfo(it) }.filterNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getRememberedCastingDeviceNames(): List<String> {
|
fun getRememberedCastingDeviceNames(): List<String> {
|
||||||
|
|||||||
@@ -151,21 +151,25 @@ class StateCastingExp : StateCasting() {
|
|||||||
setTime: (Long) -> Unit
|
setTime: (Long) -> Unit
|
||||||
): Job? = null
|
): Job? = null
|
||||||
|
|
||||||
override fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDeviceExp {
|
override fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDeviceExp? {
|
||||||
val rsAddrs =
|
try {
|
||||||
deviceInfo.addresses.map { org.fcast.sender_sdk.tryIpAddrFromStr(it) } // Throws!
|
val rsAddrs =
|
||||||
val rsDeviceInfo = RsDeviceInfo(
|
deviceInfo.addresses.map { org.fcast.sender_sdk.tryIpAddrFromStr(it) }
|
||||||
name = deviceInfo.name,
|
val rsDeviceInfo = RsDeviceInfo(
|
||||||
protocol = when (deviceInfo.type) {
|
name = deviceInfo.name,
|
||||||
com.futo.platformplayer.casting.CastProtocolType.CHROMECAST -> ProtocolType.CHROMECAST
|
protocol = when (deviceInfo.type) {
|
||||||
com.futo.platformplayer.casting.CastProtocolType.FCAST -> ProtocolType.F_CAST
|
com.futo.platformplayer.casting.CastProtocolType.CHROMECAST -> ProtocolType.CHROMECAST
|
||||||
else -> throw IllegalArgumentException()
|
com.futo.platformplayer.casting.CastProtocolType.FCAST -> ProtocolType.F_CAST
|
||||||
},
|
else -> throw IllegalArgumentException()
|
||||||
addresses = rsAddrs,
|
},
|
||||||
port = deviceInfo.port.toUShort(),
|
addresses = rsAddrs,
|
||||||
)
|
port = deviceInfo.port.toUShort(),
|
||||||
|
)
|
||||||
|
|
||||||
return CastingDeviceExp(_context.createDeviceFromInfo(rsDeviceInfo))
|
return CastingDeviceExp(_context.createDeviceFromInfo(rsDeviceInfo))
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ class StateCastingLegacy : StateCasting() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
connectDevice(deviceFromInfo(foundInfo))
|
if (foundInfo != null) {
|
||||||
|
connectDevice(deviceFromInfo(foundInfo))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.futo.platformplayer.engine.dev.V8RemoteObject
|
|||||||
import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.gsonStandard
|
import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.gsonStandard
|
||||||
import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.serialize
|
import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.serialize
|
||||||
import com.futo.platformplayer.engine.packages.PackageHttp
|
import com.futo.platformplayer.engine.packages.PackageHttp
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LoginFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateAssets
|
import com.futo.platformplayer.states.StateAssets
|
||||||
@@ -28,6 +29,8 @@ import com.google.gson.FieldAttributes
|
|||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
@@ -268,11 +271,17 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
context.respondCode(403, "This plugin doesn't support auth");
|
context.respondCode(403, "This plugin doesn't support auth");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
|
LoginFragment.showLogin(config){
|
||||||
|
_testPluginVariables.clear();
|
||||||
|
_testPlugin = V8Plugin(StateApp.instance.context, config, null, JSHttpClient(null, null, null, config), JSHttpClient(null, it, null, config));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/*
|
||||||
LoginActivity.showLogin(StateApp.instance.context, config) {
|
LoginActivity.showLogin(StateApp.instance.context, config) {
|
||||||
_testPluginVariables.clear();
|
_testPluginVariables.clear();
|
||||||
_testPlugin = V8Plugin(StateApp.instance.context, config, null, JSHttpClient(null, null, null, config), JSHttpClient(null, it, null, config));
|
_testPlugin = V8Plugin(StateApp.instance.context, config, null, JSHttpClient(null, null, null, config), JSHttpClient(null, it, null, config));
|
||||||
|
}; */
|
||||||
};
|
|
||||||
context.respondCode(200, "Login started");
|
context.respondCode(200, "Login started");
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ import android.widget.Button
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.UpdateDownloadService
|
||||||
|
import com.futo.platformplayer.UpdateNotificationManager
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.copyToOutputStream
|
import com.futo.platformplayer.copyToOutputStream
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -34,6 +37,8 @@ import java.io.InputStream
|
|||||||
class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "AutoUpdateDialog";
|
private val TAG = "AutoUpdateDialog";
|
||||||
|
|
||||||
|
var currentDialog: AutoUpdateDialog? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var _buttonNever: Button;
|
private lateinit var _buttonNever: Button;
|
||||||
@@ -46,7 +51,6 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
|||||||
private var _maxVersion: Int = 0;
|
private var _maxVersion: Int = 0;
|
||||||
|
|
||||||
private var _updating: Boolean = false;
|
private var _updating: Boolean = false;
|
||||||
private var _apkFile: File? = null;
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -61,12 +65,14 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
|||||||
_buttonShowChangelog = findViewById(R.id.button_show_changelog);
|
_buttonShowChangelog = findViewById(R.id.button_show_changelog);
|
||||||
|
|
||||||
_buttonNever.setOnClickListener {
|
_buttonNever.setOnClickListener {
|
||||||
|
UpdateNotificationManager.cancelAll(context)
|
||||||
Settings.instance.autoUpdate.check = 1;
|
Settings.instance.autoUpdate.check = 1;
|
||||||
Settings.instance.save();
|
Settings.instance.save();
|
||||||
dismiss();
|
dismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonClose.setOnClickListener {
|
_buttonClose.setOnClickListener {
|
||||||
|
UpdateNotificationManager.cancelAll(context)
|
||||||
dismiss();
|
dismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,23 +82,32 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_buttonUpdate.setOnClickListener {
|
_buttonUpdate.setOnClickListener {
|
||||||
|
UpdateNotificationManager.cancelAll(context)
|
||||||
|
|
||||||
if (_updating) {
|
if (_updating) {
|
||||||
return@setOnClickListener;
|
return@setOnClickListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
_updating = true;
|
if (Settings.instance.autoUpdate.shouldBackgroundDownload) {
|
||||||
update();
|
val ctx = context.applicationContext;
|
||||||
|
val intent = Intent(ctx, UpdateDownloadService::class.java);
|
||||||
|
intent.putExtra(UpdateDownloadService.EXTRA_VERSION, _maxVersion);
|
||||||
|
ContextCompat.startForegroundService(ctx, intent);
|
||||||
|
UIDialogs.toast(context, "Downloading update in background");
|
||||||
|
dismiss();
|
||||||
|
} else {
|
||||||
|
_updating = true;
|
||||||
|
update();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
fun showPredownloaded(apkFile: File) {
|
currentDialog = this
|
||||||
_apkFile = apkFile;
|
|
||||||
super.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dismiss() {
|
override fun dismiss() {
|
||||||
super.dismiss()
|
super.dismiss()
|
||||||
InstallReceiver.onReceiveResult.clear();
|
InstallReceiver.onReceiveResult.clear();
|
||||||
|
currentDialog = null
|
||||||
Logger.i(TAG, "Cleared InstallReceiver.onReceiveResult handler.")
|
Logger.i(TAG, "Cleared InstallReceiver.onReceiveResult handler.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,21 +133,14 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
|||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
var inputStream: InputStream? = null;
|
var inputStream: InputStream? = null;
|
||||||
try {
|
try {
|
||||||
val apkFile = _apkFile;
|
val client = ManagedHttpClient();
|
||||||
if (apkFile != null) {
|
val response = client.get(StateUpdate.APK_URL);
|
||||||
inputStream = apkFile.inputStream();
|
if (response.isOk && response.body != null) {
|
||||||
val dataLength = apkFile.length();
|
inputStream = response.body.byteStream();
|
||||||
|
val dataLength = response.body.contentLength();
|
||||||
install(inputStream, dataLength);
|
install(inputStream, dataLength);
|
||||||
} else {
|
} else {
|
||||||
val client = ManagedHttpClient();
|
throw Exception("Failed to download latest version of app.");
|
||||||
val response = client.get(StateUpdate.APK_URL);
|
|
||||||
if (response.isOk && response.body != null) {
|
|
||||||
inputStream = response.body.byteStream();
|
|
||||||
val dataLength = response.body.contentLength();
|
|
||||||
install(inputStream, dataLength);
|
|
||||||
} else {
|
|
||||||
throw Exception("Failed to download latest version of app.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Exception thrown while downloading and installing latest version of app.", e);
|
Logger.w(TAG, "Exception thrown while downloading and installing latest version of app.", e);
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
_buttonClose.setOnClickListener { dismiss(); };
|
_buttonClose.setOnClickListener { dismiss(); };
|
||||||
_buttonDisconnect.setOnClickListener {
|
_buttonDisconnect.setOnClickListener {
|
||||||
try {
|
try {
|
||||||
|
StateCasting.instance.stopVideo()
|
||||||
StateCasting.instance.activeDevice?.disconnect()
|
StateCasting.instance.activeDevice?.disconnect()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Active device failed to disconnect: $e")
|
Logger.e(TAG, "Active device failed to disconnect: $e")
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
|
|
||||||
private lateinit var _buttonCancel1: Button;
|
private lateinit var _buttonCancel1: Button;
|
||||||
private lateinit var _buttonCancel2: Button;
|
private lateinit var _buttonCancel2: Button;
|
||||||
|
private lateinit var _buttonAlways: LinearLayout;
|
||||||
private lateinit var _buttonUpdate: LinearLayout;
|
private lateinit var _buttonUpdate: LinearLayout;
|
||||||
|
|
||||||
private lateinit var _buttonOk: LinearLayout;
|
private lateinit var _buttonOk: LinearLayout;
|
||||||
@@ -58,6 +59,7 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
private lateinit var _textProgres: TextView;
|
private lateinit var _textProgres: TextView;
|
||||||
private lateinit var _textError: TextView;
|
private lateinit var _textError: TextView;
|
||||||
private lateinit var _textResult: TextView;
|
private lateinit var _textResult: TextView;
|
||||||
|
private lateinit var _textChangelogResult: TextView;
|
||||||
|
|
||||||
private lateinit var _uiChoiceTop: FrameLayout;
|
private lateinit var _uiChoiceTop: FrameLayout;
|
||||||
private lateinit var _uiProgressTop: FrameLayout;
|
private lateinit var _uiProgressTop: FrameLayout;
|
||||||
@@ -89,6 +91,7 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
|
|
||||||
_buttonCancel1 = findViewById(R.id.button_cancel_1);
|
_buttonCancel1 = findViewById(R.id.button_cancel_1);
|
||||||
_buttonCancel2 = findViewById(R.id.button_cancel_2);
|
_buttonCancel2 = findViewById(R.id.button_cancel_2);
|
||||||
|
_buttonAlways = findViewById(R.id.button_always);
|
||||||
_buttonUpdate = findViewById(R.id.button_update);
|
_buttonUpdate = findViewById(R.id.button_update);
|
||||||
|
|
||||||
_buttonOk = findViewById(R.id.button_ok);
|
_buttonOk = findViewById(R.id.button_ok);
|
||||||
@@ -99,6 +102,7 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
_textProgres = findViewById(R.id.text_progress);
|
_textProgres = findViewById(R.id.text_progress);
|
||||||
_textError = findViewById(R.id.text_error);
|
_textError = findViewById(R.id.text_error);
|
||||||
_textResult = findViewById(R.id.text_result);
|
_textResult = findViewById(R.id.text_result);
|
||||||
|
_textChangelogResult = findViewById(R.id.text_changelog_result);
|
||||||
|
|
||||||
_uiChoiceTop = findViewById(R.id.dialog_ui_choice_top);
|
_uiChoiceTop = findViewById(R.id.dialog_ui_choice_top);
|
||||||
_uiProgressTop = findViewById(R.id.dialog_ui_progress_top);
|
_uiProgressTop = findViewById(R.id.dialog_ui_progress_top);
|
||||||
@@ -119,17 +123,24 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
val changelog = _newConfig.changelog!![changelogVersion]!!;
|
val changelog = _newConfig.changelog!![changelogVersion]!!;
|
||||||
if(changelog.size > 1) {
|
if(changelog.size > 1) {
|
||||||
_textChangelog.text = "Changelog (${_newConfig.version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
|
_textChangelog.text = "Changelog (${_newConfig.version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
|
||||||
|
_textChangelogResult.text = _textChangelog.text;
|
||||||
}
|
}
|
||||||
else if(changelog.size == 1) {
|
else if(changelog.size == 1) {
|
||||||
_textChangelog.text = "Changelog (${_newConfig.version})\n" + changelog[0].trim();
|
_textChangelog.text = "Changelog (${_newConfig.version})\n" + changelog[0].trim();
|
||||||
|
_textChangelogResult.text = _textChangelog.text;
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
_textChangelog.visibility = View.GONE;
|
_textChangelog.visibility = View.GONE;
|
||||||
} else
|
_textChangelogResult.visibility = View.GONE;
|
||||||
_textChangelog.visibility = View.GONE;
|
}
|
||||||
|
} else {
|
||||||
|
_textChangelog.visibility = View.GONE;
|
||||||
|
_textChangelogResult.visibility = View.GONE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
_textChangelog.visibility = View.GONE;
|
_textChangelog.visibility = View.GONE;
|
||||||
|
_textChangelogResult.visibility = View.GONE;
|
||||||
Logger.e(TAG, "Invalid changelog? ", ex);
|
Logger.e(TAG, "Invalid changelog? ", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +156,18 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
_isUpdating = true;
|
_isUpdating = true;
|
||||||
update();
|
update();
|
||||||
};
|
};
|
||||||
|
_buttonAlways.setOnClickListener {
|
||||||
|
if (_isUpdating)
|
||||||
|
return@setOnClickListener;
|
||||||
|
val plugin = StatePlugins.instance.getPlugin(_oldConfig.id);
|
||||||
|
if(plugin != null) {
|
||||||
|
plugin.appSettings.automaticUpdate = true;
|
||||||
|
StatePlugins.instance.savePlugin(_oldConfig.id);
|
||||||
|
UIDialogs.appToast("Automatic update enabled, can be disabled in plugin settings");
|
||||||
|
}
|
||||||
|
_isUpdating = true;
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
|
||||||
Glide.with(_iconPlugin)
|
Glide.with(_iconPlugin)
|
||||||
.load(_oldConfig.absoluteIconUrl)
|
.load(_oldConfig.absoluteIconUrl)
|
||||||
@@ -158,7 +181,8 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
if (_isUpdating)
|
if (_isUpdating)
|
||||||
return;
|
return;
|
||||||
_isUpdating = true;
|
_isUpdating = true;
|
||||||
update();
|
|
||||||
|
update(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +191,7 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
super.dismiss();
|
super.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun update() {
|
private fun update(automatic: Boolean = false) {
|
||||||
_uiChoiceTop.visibility = View.GONE;
|
_uiChoiceTop.visibility = View.GONE;
|
||||||
_uiRiskTop.visibility = View.GONE;
|
_uiRiskTop.visibility = View.GONE;
|
||||||
_uiChoiceBot.visibility = View.GONE;
|
_uiChoiceBot.visibility = View.GONE;
|
||||||
@@ -187,9 +211,16 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
val scope = StateApp.instance.scopeOrNull;
|
val scope = StateApp.instance.scopeOrNull;
|
||||||
scope?.launch(Dispatchers.IO) {
|
scope?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_textProgres.setText("Loading current script file...");
|
||||||
|
}
|
||||||
val client = ManagedHttpClient();
|
val client = ManagedHttpClient();
|
||||||
|
client.setTimeout(10000);
|
||||||
val script = StatePlugins.instance.getScript(_oldConfig.id) ?: "";
|
val script = StatePlugins.instance.getScript(_oldConfig.id) ?: "";
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_textProgres.setText("Requesting new script file...");
|
||||||
|
}
|
||||||
val newScript = client.get(_newConfig.absoluteScriptUrl)?.body?.string();
|
val newScript = client.get(_newConfig.absoluteScriptUrl)?.body?.string();
|
||||||
if(newScript.isNullOrEmpty())
|
if(newScript.isNullOrEmpty())
|
||||||
throw IllegalStateException("No script found");
|
throw IllegalStateException("No script found");
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
package com.futo.platformplayer.downloads
|
package com.futo.platformplayer.downloads
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.media.MediaCodec
|
||||||
|
import android.media.MediaExtractor
|
||||||
|
import android.media.MediaMuxer
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.arthenica.ffmpegkit.FFmpegKit
|
import com.arthenica.ffmpegkit.FFmpegKit
|
||||||
import com.arthenica.ffmpegkit.ReturnCode
|
import com.arthenica.ffmpegkit.ReturnCode
|
||||||
import com.arthenica.ffmpegkit.StatisticsCallback
|
import com.arthenica.ffmpegkit.StatisticsCallback
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
@@ -36,10 +42,13 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManif
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
|
||||||
import com.futo.platformplayer.exceptions.DownloadException
|
import com.futo.platformplayer.exceptions.DownloadException
|
||||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.others.Language
|
||||||
import com.futo.platformplayer.parsers.HLS
|
import com.futo.platformplayer.parsers.HLS
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
@@ -82,6 +91,9 @@ import kotlin.time.times
|
|||||||
class VideoDownload {
|
class VideoDownload {
|
||||||
var state: State = State.QUEUED;
|
var state: State = State.QUEUED;
|
||||||
|
|
||||||
|
@Contextual
|
||||||
|
@Transient
|
||||||
|
var plugin: IPlatformClient? = null;
|
||||||
var video: SerializedPlatformVideo? = null;
|
var video: SerializedPlatformVideo? = null;
|
||||||
var videoDetails: SerializedPlatformVideoDetails? = null;
|
var videoDetails: SerializedPlatformVideoDetails? = null;
|
||||||
|
|
||||||
@@ -97,6 +109,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
var videoSource: VideoUrlSource?;
|
var videoSource: VideoUrlSource?;
|
||||||
var audioSource: AudioUrlSource?;
|
var audioSource: AudioUrlSource?;
|
||||||
|
var overrideResultAudioSource: IAudioSource? = null;
|
||||||
@Contextual
|
@Contextual
|
||||||
@Transient
|
@Transient
|
||||||
val videoSourceToUse: IVideoSource? get () = if(requiresLiveVideoSource) videoSourceLive as IVideoSource? else videoSource as IVideoSource?;
|
val videoSourceToUse: IVideoSource? get () = if(requiresLiveVideoSource) videoSourceLive as IVideoSource? else videoSource as IVideoSource?;
|
||||||
@@ -136,6 +149,8 @@ class VideoDownload {
|
|||||||
|
|
||||||
var hasVideoRequestExecutor: Boolean = false;
|
var hasVideoRequestExecutor: Boolean = false;
|
||||||
var hasAudioRequestExecutor: Boolean = false;
|
var hasAudioRequestExecutor: Boolean = false;
|
||||||
|
var hasVideoRequestModifier: Boolean = false;
|
||||||
|
var hasAudioRequestModifier: Boolean = false;
|
||||||
|
|
||||||
var progress: Double = 0.0;
|
var progress: Double = 0.0;
|
||||||
var isCancelled = false;
|
var isCancelled = false;
|
||||||
@@ -203,8 +218,10 @@ class VideoDownload {
|
|||||||
this.prepareTime = OffsetDateTime.now();
|
this.prepareTime = OffsetDateTime.now();
|
||||||
this.hasVideoRequestExecutor = videoSource is JSSource && videoSource.hasRequestExecutor;
|
this.hasVideoRequestExecutor = videoSource is JSSource && videoSource.hasRequestExecutor;
|
||||||
this.hasAudioRequestExecutor = audioSource is JSSource && audioSource.hasRequestExecutor;
|
this.hasAudioRequestExecutor = audioSource is JSSource && audioSource.hasRequestExecutor;
|
||||||
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (videoSource is JSDashManifestRawSource && videoSource.hasGenerate);
|
this.hasVideoRequestModifier = videoSource is JSSource && videoSource.hasRequestModifier;
|
||||||
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (audioSource is JSDashManifestRawAudioSource && audioSource.hasGenerate);
|
this.hasAudioRequestModifier = audioSource is JSSource && audioSource.hasRequestModifier;
|
||||||
|
this.requiresLiveVideoSource = this.hasVideoRequestModifier || this.hasVideoRequestExecutor || (videoSource is JSDashManifestRawSource && videoSource.hasGenerate);
|
||||||
|
this.requiresLiveAudioSource = this.hasAudioRequestModifier || this.hasAudioRequestExecutor || (audioSource is JSDashManifestRawAudioSource && audioSource.hasGenerate);
|
||||||
this.targetVideoName = videoSource?.name;
|
this.targetVideoName = videoSource?.name;
|
||||||
this.targetAudioName = audioSource?.name;
|
this.targetAudioName = audioSource?.name;
|
||||||
this.targetPixelCount = if(videoSource != null) (videoSource.width * videoSource.height).toLong() else null;
|
this.targetPixelCount = if(videoSource != null) (videoSource.width * videoSource.height).toLong() else null;
|
||||||
@@ -262,7 +279,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
//Fetch full video object and determine source
|
//Fetch full video object and determine source
|
||||||
if(video != null && videoDetails == null) {
|
if(video != null && videoDetails == null) {
|
||||||
val original = StatePlatform.instance.getContentDetails(video!!.url).await();
|
val original = if (plugin != null) plugin!!.getContentDetails(video!!.url) else StatePlatform.instance.getContentDetails(video!!.url)?.await();
|
||||||
if(original !is IPlatformVideoDetails)
|
if(original !is IPlatformVideoDetails)
|
||||||
throw IllegalStateException("Original content is not media?");
|
throw IllegalStateException("Original content is not media?");
|
||||||
|
|
||||||
@@ -429,6 +446,11 @@ class VideoDownload {
|
|||||||
videoFileNameBase = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}]".sanitizeFileName();
|
videoFileNameBase = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}]".sanitizeFileName();
|
||||||
videoFileNameExt = videoContainerToExtension(actualVideoSource!!.container);
|
videoFileNameExt = videoContainerToExtension(actualVideoSource!!.container);
|
||||||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||||
|
if(actualVideoSource is JSDashManifestRawSource && actualAudioSource == null) {
|
||||||
|
audioFileNameBase = "${videoDetails!!.id.value!!}-[unknown]".sanitizeFileName();
|
||||||
|
audioFileNameExt = videoAudioContainerToExtension(actualVideoSource!!.container);
|
||||||
|
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(actualAudioSource != null) {
|
if(actualAudioSource != null) {
|
||||||
audioFileNameBase = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}]".sanitizeFileName();
|
audioFileNameBase = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}]".sanitizeFileName();
|
||||||
@@ -478,11 +500,15 @@ class VideoDownload {
|
|||||||
|
|
||||||
if(actualVideoSource is IVideoUrlSource)
|
if(actualVideoSource is IVideoUrlSource)
|
||||||
videoFileSize = when (videoSource!!.container) {
|
videoFileSize = when (videoSource!!.container) {
|
||||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, if (actualVideoSource is JSSource) actualVideoSource else null, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||||
else -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
else -> downloadFileSource("Video", client, if (actualVideoSource is JSSource) actualVideoSource else null, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||||
}
|
}
|
||||||
else if(actualVideoSource is JSDashManifestRawSource) {
|
else if(actualVideoSource is JSDashManifestRawSource) {
|
||||||
videoFileSize = downloadDashFileSource("Video", client, actualVideoSource, File(downloadDir, videoFileName!!), progressCallback);
|
if(actualAudioSource == null)
|
||||||
|
videoFileSize = downloadDashFileSource("Video", client, actualVideoSource, File(downloadDir, videoFileName!!), progressCallback, 3,
|
||||||
|
File(downloadDir, audioFileName!!));
|
||||||
|
else
|
||||||
|
videoFileSize = downloadDashFileSource("Video", client, actualVideoSource, File(downloadDir, videoFileName!!), progressCallback, 1);
|
||||||
}
|
}
|
||||||
else throw NotImplementedError("NotImplemented video download: " + actualVideoSource.javaClass.name);
|
else throw NotImplementedError("NotImplemented video download: " + actualVideoSource.javaClass.name);
|
||||||
});
|
});
|
||||||
@@ -518,11 +544,11 @@ class VideoDownload {
|
|||||||
|
|
||||||
if(actualAudioSource is IAudioUrlSource)
|
if(actualAudioSource is IAudioUrlSource)
|
||||||
audioFileSize = when (audioSource!!.container) {
|
audioFileSize = when (audioSource!!.container) {
|
||||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, if (actualAudioSource is JSSource) actualAudioSource else null, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||||
else -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
else -> downloadFileSource("Audio", client, if (actualAudioSource is JSSource) actualAudioSource else null, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||||
}
|
}
|
||||||
else if(actualAudioSource is JSDashManifestRawAudioSource) {
|
else if(actualAudioSource is JSDashManifestRawAudioSource) {
|
||||||
audioFileSize = downloadDashFileSource("Audio", client, actualAudioSource, File(downloadDir, audioFileName!!), progressCallback);
|
audioFileSize = downloadDashFileSource("Audio", client, actualAudioSource, File(downloadDir, audioFileName!!), progressCallback, 2);
|
||||||
}
|
}
|
||||||
else throw NotImplementedError("NotImplemented audio download: " + actualAudioSource.javaClass.name);
|
else throw NotImplementedError("NotImplemented audio download: " + actualAudioSource.javaClass.name);
|
||||||
});
|
});
|
||||||
@@ -580,121 +606,305 @@ class VideoDownload {
|
|||||||
return cipher.doFinal(encryptedSegment)
|
return cipher.doFinal(encryptedSegment)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
|
||||||
if(targetFile.exists())
|
|
||||||
targetFile.delete();
|
|
||||||
|
|
||||||
var downloadedTotalLength = 0L
|
|
||||||
|
|
||||||
val segmentFiles = arrayListOf<File>()
|
|
||||||
try {
|
|
||||||
val response = client.get(hlsUrl)
|
|
||||||
check(response.isOk) { "Failed to get variant playlist: ${response.code}" }
|
|
||||||
|
|
||||||
val vpContent = response.body?.string()
|
|
||||||
?: throw Exception("Variant playlist content is empty")
|
|
||||||
|
|
||||||
val variantPlaylist = HLS.parseVariantPlaylist(vpContent, hlsUrl)
|
|
||||||
val decryptionInfo: DecryptionInfo? = if (variantPlaylist.decryptionInfo != null) {
|
|
||||||
val keyResponse = client.get(variantPlaylist.decryptionInfo.keyUrl)
|
|
||||||
check(keyResponse.isOk) { "HLS request failed for decryption key: ${keyResponse.code}" }
|
|
||||||
DecryptionInfo(keyResponse.body!!.bytes(), variantPlaylist.decryptionInfo.iv?.hexStringToByteArray())
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
variantPlaylist.segments.forEachIndexed { index, segment ->
|
|
||||||
if (segment !is HLS.MediaSegment) {
|
|
||||||
return@forEachIndexed
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Download '$name' segment $index Sequential");
|
|
||||||
val segmentFile = File(context.cacheDir, "segment-${UUID.randomUUID()}")
|
|
||||||
val outputStream = segmentFile.outputStream()
|
|
||||||
try {
|
|
||||||
segmentFiles.add(segmentFile)
|
|
||||||
|
|
||||||
val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri, if (index == 0) null else decryptionInfo, index) { segmentLength, totalRead, lastSpeed ->
|
|
||||||
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
|
|
||||||
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
|
|
||||||
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadedTotalLength += segmentLength
|
|
||||||
} finally {
|
|
||||||
outputStream.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Combining segments into $targetFile");
|
|
||||||
combineSegments(context, segmentFiles, targetFile)
|
|
||||||
|
|
||||||
Logger.i(TAG, "${name} downloadSource Finished");
|
|
||||||
}
|
|
||||||
catch(ioex: IOException) {
|
|
||||||
if(targetFile.exists())
|
|
||||||
targetFile.delete();
|
|
||||||
if(ioex.message?.contains("ENOSPC") ?: false)
|
|
||||||
throw Exception("Not enough space on device", ioex);
|
|
||||||
else
|
|
||||||
throw ioex;
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
if(targetFile.exists())
|
|
||||||
targetFile.delete();
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
for (segmentFile in segmentFiles) {
|
|
||||||
segmentFile.delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return downloadedTotalLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) = withContext(Dispatchers.IO) {
|
private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) = withContext(Dispatchers.IO) {
|
||||||
|
require(segmentFiles.isNotEmpty()) { "segmentFiles must not be empty" }
|
||||||
|
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
val cmd =
|
val concatInput = buildString {
|
||||||
"-i \"concat:${segmentFiles.joinToString("|")}\" -c copy \"${targetFile.absolutePath}\""
|
append("concat:")
|
||||||
|
append(
|
||||||
|
segmentFiles.joinToString("|") { file ->
|
||||||
|
file.absolutePath
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val cmd = "-i \"$concatInput\" -c copy \"${targetFile.absolutePath}\""
|
||||||
|
|
||||||
val statisticsCallback = StatisticsCallback { _ ->
|
val statisticsCallback = StatisticsCallback { _ ->
|
||||||
//TODO: Show progress?
|
//No callback
|
||||||
}
|
}
|
||||||
|
|
||||||
val executorService = Executors.newSingleThreadExecutor()
|
val executorService = Executors.newSingleThreadExecutor()
|
||||||
val session = FFmpegKit.executeAsync(cmd,
|
|
||||||
{ session ->
|
val session = FFmpegKit.executeAsync(
|
||||||
if (ReturnCode.isSuccess(session.returnCode)) {
|
cmd,
|
||||||
|
{ completedSession ->
|
||||||
|
executorService.shutdown()
|
||||||
|
|
||||||
|
if (ReturnCode.isSuccess(completedSession.returnCode)) {
|
||||||
continuation.resumeWith(Result.success(Unit))
|
continuation.resumeWith(Result.success(Unit))
|
||||||
} else {
|
} else {
|
||||||
val errorMessage = if (ReturnCode.isCancel(session.returnCode)) {
|
val errorMessage = if (ReturnCode.isCancel(completedSession.returnCode)) {
|
||||||
"Command cancelled"
|
"Command cancelled"
|
||||||
} else {
|
} else {
|
||||||
"Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}"
|
"Command failed with state '${completedSession.state}' " +
|
||||||
|
"and return code ${completedSession.returnCode}, " +
|
||||||
|
"stack trace ${completedSession.failStackTrace}"
|
||||||
}
|
}
|
||||||
continuation.resumeWithException(RuntimeException(errorMessage))
|
continuation.resumeWithException(RuntimeException(errorMessage))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ Logger.v(TAG, it.message) },
|
{ log ->
|
||||||
|
Logger.v(TAG, log.message)
|
||||||
|
},
|
||||||
statisticsCallback,
|
statisticsCallback,
|
||||||
executorService
|
executorService
|
||||||
)
|
)
|
||||||
|
|
||||||
continuation.invokeOnCancellation {
|
continuation.invokeOnCancellation {
|
||||||
session.cancel()
|
session.cancel()
|
||||||
|
executorService.shutdownNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadDashFileSource(name: String, client: ManagedHttpClient, source: IJSDashManifestRawSource, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, source: JSSource?, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
|
if (targetFile.exists())
|
||||||
|
targetFile.delete()
|
||||||
|
|
||||||
|
var downloadedTotalLength = 0L
|
||||||
|
val modifier = if (source is JSSource && source.hasRequestModifier)
|
||||||
|
source.getRequestModifier()
|
||||||
|
else
|
||||||
|
null
|
||||||
|
|
||||||
|
fun downloadBytes(url: String, rangeStart: Long? = null, rangeLength: Long? = null): ByteArray {
|
||||||
|
val headers = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
if (rangeStart != null) {
|
||||||
|
if (rangeLength != null && rangeLength > 0) {
|
||||||
|
val end = rangeStart + rangeLength - 1
|
||||||
|
headers["Range"] = "bytes=$rangeStart-$end"
|
||||||
|
} else {
|
||||||
|
headers["Range"] = "bytes=$rangeStart-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val modified = modifier?.modifyRequest(url, headers)
|
||||||
|
val finalUrl = modified?.url ?: url
|
||||||
|
val finalHeaders = modified?.headers?.toMutableMap() ?: headers
|
||||||
|
|
||||||
|
val resp = client.get(finalUrl, finalHeaders)
|
||||||
|
if (!resp.isOk) {
|
||||||
|
resp.body?.close()
|
||||||
|
throw IllegalStateException("Failed to download HLS resource ($finalUrl): HTTP ${resp.code}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = resp.body ?: throw IllegalStateException("Failed to download HLS resource ($finalUrl): Empty body")
|
||||||
|
val bytes = body.bytes()
|
||||||
|
body.close()
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildSequenceIv(sequenceNumber: Long): ByteArray {
|
||||||
|
return ByteBuffer.allocate(16)
|
||||||
|
.putLong(0L)
|
||||||
|
.putLong(sequenceNumber)
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
|
||||||
|
val segmentFiles = arrayListOf<File>()
|
||||||
|
try {
|
||||||
|
val playlistHeaders = mutableMapOf<String, String>()
|
||||||
|
val modifiedPlaylistReq = modifier?.modifyRequest(hlsUrl, playlistHeaders)
|
||||||
|
val playlistResp = client.get(
|
||||||
|
modifiedPlaylistReq?.url ?: hlsUrl,
|
||||||
|
modifiedPlaylistReq?.headers?.toMutableMap() ?: playlistHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
check(playlistResp.isOk) { "Failed to get variant playlist: ${playlistResp.code}" }
|
||||||
|
|
||||||
|
val vpContent = playlistResp.body?.string()
|
||||||
|
?: throw IllegalStateException("Variant playlist content is empty")
|
||||||
|
|
||||||
|
val variantPlaylist = HLS.parseVariantPlaylist(vpContent, hlsUrl)
|
||||||
|
val hlsDec = variantPlaylist.decryptionInfo
|
||||||
|
val useDecryption = hlsDec != null && !hlsDec.method.equals("NONE", ignoreCase = true)
|
||||||
|
var keyBytes: ByteArray? = null
|
||||||
|
var staticIvBytes: ByteArray? = null
|
||||||
|
|
||||||
|
if (useDecryption) {
|
||||||
|
if (!hlsDec.method.equals("AES-128", ignoreCase = true)) {
|
||||||
|
throw UnsupportedOperationException("HLS decryption method '${hlsDec.method}' is not supported.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyUrl = hlsDec.keyUrl ?: throw IllegalStateException("Encrypted HLS playlist without key URI is not supported.")
|
||||||
|
|
||||||
|
keyBytes = downloadBytes(keyUrl)
|
||||||
|
if (!hlsDec.iv.isNullOrEmpty()) {
|
||||||
|
staticIvBytes = hlsDec.iv.hexStringToByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mediaSequence = variantPlaylist.mediaSequence ?: 0L
|
||||||
|
val rangeOffsets = mutableMapOf<String, Long>()
|
||||||
|
|
||||||
|
if (!variantPlaylist.mapUrl.isNullOrEmpty()) {
|
||||||
|
if (isCancelled) throw CancellationException("Cancelled")
|
||||||
|
|
||||||
|
Logger.i(TAG, "Downloading HLS initialization map")
|
||||||
|
|
||||||
|
var mapRangeStart: Long? = null
|
||||||
|
var mapRangeLength: Long? = null
|
||||||
|
|
||||||
|
if (variantPlaylist.mapBytesLength > 0) {
|
||||||
|
mapRangeLength = variantPlaylist.mapBytesLength
|
||||||
|
|
||||||
|
val mapUrl = variantPlaylist.mapUrl
|
||||||
|
if (variantPlaylist.mapBytesStart >= 0) {
|
||||||
|
mapRangeStart = variantPlaylist.mapBytesStart
|
||||||
|
rangeOffsets[mapUrl] =
|
||||||
|
variantPlaylist.mapBytesStart + variantPlaylist.mapBytesLength
|
||||||
|
} else {
|
||||||
|
val offset = rangeOffsets[mapUrl] ?: 0L
|
||||||
|
mapRangeStart = offset
|
||||||
|
rangeOffsets[mapUrl] = offset + variantPlaylist.mapBytesLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapBytes = downloadBytes(variantPlaylist.mapUrl!!, mapRangeStart, mapRangeLength)
|
||||||
|
|
||||||
|
if (useDecryption) {
|
||||||
|
val kb = keyBytes ?: throw IllegalStateException("Decryption key bytes are missing.")
|
||||||
|
val iv = staticIvBytes
|
||||||
|
?: throw UnsupportedOperationException("Encrypted EXT-X-MAP without explicit IV is not supported.")
|
||||||
|
mapBytes = decryptSegment(mapBytes, kb, iv)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapBytes.size.toLong() > Int.MAX_VALUE) {
|
||||||
|
throw IllegalStateException("HLS MAP segment too large to handle.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val segmentFile = File(context.cacheDir, "segment-${UUID.randomUUID()}")
|
||||||
|
val outStr = segmentFile.outputStream()
|
||||||
|
try {
|
||||||
|
segmentFiles.add(segmentFile)
|
||||||
|
outStr.write(mapBytes)
|
||||||
|
outStr.flush()
|
||||||
|
} finally {
|
||||||
|
outStr.close()
|
||||||
|
}
|
||||||
|
downloadedTotalLength += mapBytes.size
|
||||||
|
}
|
||||||
|
|
||||||
|
val totalSegments = variantPlaylist.segments.size
|
||||||
|
var mediaSegmentIndex = 0
|
||||||
|
|
||||||
|
var bytesSinceLastSpeedUpdate = 0L
|
||||||
|
var lastSpeedUpdateTime = System.currentTimeMillis()
|
||||||
|
var lastSpeed = 0L
|
||||||
|
|
||||||
|
variantPlaylist.segments.forEachIndexed { index, segment ->
|
||||||
|
if (segment !is HLS.MediaSegment) return@forEachIndexed
|
||||||
|
if (isCancelled) throw CancellationException("Cancelled")
|
||||||
|
|
||||||
|
Logger.i(TAG, "Download '$name' segment $index sequential")
|
||||||
|
|
||||||
|
var rangeStart: Long? = null
|
||||||
|
var rangeLength: Long? = null
|
||||||
|
|
||||||
|
if (segment.bytesLength > 0) {
|
||||||
|
rangeLength = segment.bytesLength
|
||||||
|
|
||||||
|
val urlKey = segment.uri
|
||||||
|
if (segment.bytesStart >= 0) {
|
||||||
|
rangeStart = segment.bytesStart
|
||||||
|
rangeOffsets[urlKey] = segment.bytesStart + segment.bytesLength
|
||||||
|
} else {
|
||||||
|
val offset = rangeOffsets[urlKey] ?: 0L
|
||||||
|
rangeStart = offset
|
||||||
|
rangeOffsets[urlKey] = offset + segment.bytesLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var segmentBytes = downloadBytes(segment.uri, rangeStart, rangeLength)
|
||||||
|
|
||||||
|
if (useDecryption) {
|
||||||
|
val kb = keyBytes ?: throw IllegalStateException("Decryption key bytes are missing.")
|
||||||
|
val ivBytes = if (staticIvBytes != null) {
|
||||||
|
staticIvBytes
|
||||||
|
} else {
|
||||||
|
val sequenceNumber = mediaSequence + mediaSegmentIndex
|
||||||
|
buildSequenceIv(sequenceNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentBytes = decryptSegment(segmentBytes, kb, ivBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
val segmentLength = segmentBytes.size.toLong()
|
||||||
|
if (segmentLength > Int.MAX_VALUE) {
|
||||||
|
throw IllegalStateException("HLS media segment too large to handle.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val avgLen = if (index == 0) {
|
||||||
|
segmentLength
|
||||||
|
} else {
|
||||||
|
if (index > 0) downloadedTotalLength / index else segmentLength
|
||||||
|
}
|
||||||
|
val expectedTotal = avgLen * (totalSegments - 1) + segmentLength
|
||||||
|
|
||||||
|
val segmentFile = File(context.cacheDir, "segment-${UUID.randomUUID()}")
|
||||||
|
val outStr = segmentFile.outputStream()
|
||||||
|
try {
|
||||||
|
segmentFiles.add(segmentFile)
|
||||||
|
outStr.write(segmentBytes)
|
||||||
|
} finally {
|
||||||
|
outStr.close()
|
||||||
|
}
|
||||||
|
downloadedTotalLength += segmentLength
|
||||||
|
|
||||||
|
bytesSinceLastSpeedUpdate += segmentLength
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val elapsed = now - lastSpeedUpdateTime
|
||||||
|
if (elapsed >= 500 && bytesSinceLastSpeedUpdate > 0) {
|
||||||
|
lastSpeed = (bytesSinceLastSpeedUpdate * 1000L / elapsed)
|
||||||
|
bytesSinceLastSpeedUpdate = 0
|
||||||
|
lastSpeedUpdateTime = now
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress(expectedTotal, downloadedTotalLength, lastSpeed)
|
||||||
|
mediaSegmentIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
combineSegments(context, segmentFiles, targetFile)
|
||||||
|
Logger.i(TAG, "Finished HLS Source for $name")
|
||||||
|
} catch (ioex: IOException) {
|
||||||
|
if (targetFile.exists())
|
||||||
|
targetFile.delete()
|
||||||
|
if (ioex.message?.contains("ENOSPC") == true)
|
||||||
|
throw Exception("Not enough space on device", ioex)
|
||||||
|
else
|
||||||
|
throw ioex
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
if (targetFile.exists())
|
||||||
|
targetFile.delete()
|
||||||
|
throw ex
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
for (segmentFile in segmentFiles) {
|
||||||
|
segmentFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloadedTotalLength
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadDashFileSource(name: String, client: ManagedHttpClient, source: IJSDashManifestRawSource, targetFile: File, onProgress: (Long, Long, Long) -> Unit, downloadType: Int = 0, targetFileAudio: File? = null): Long {
|
||||||
if(targetFile.exists())
|
if(targetFile.exists())
|
||||||
targetFile.delete();
|
targetFile.delete();
|
||||||
|
if(targetFileAudio?.exists() ?: false)
|
||||||
|
targetFileAudio.delete();
|
||||||
|
|
||||||
targetFile.createNewFile();
|
targetFile.createNewFile();
|
||||||
|
targetFileAudio?.createNewFile();
|
||||||
|
|
||||||
val sourceLength: Long?;
|
val sourceLength: Long?;
|
||||||
|
val sourceLengthAudio: Long?;
|
||||||
val fileStream = FileOutputStream(targetFile);
|
val fileStream = FileOutputStream(targetFile);
|
||||||
|
val fileStream2 = if(targetFileAudio != null) FileOutputStream(targetFileAudio) else null;
|
||||||
|
|
||||||
|
var executor: JSRequestExecutor? = null;
|
||||||
try{
|
try{
|
||||||
var manifest = source.manifest;
|
var manifest = source.manifest;
|
||||||
if(source.hasGenerate)
|
if(source.hasGenerate)
|
||||||
@@ -703,35 +913,59 @@ class VideoDownload {
|
|||||||
throw IllegalStateException("No manifest after generation");
|
throw IllegalStateException("No manifest after generation");
|
||||||
|
|
||||||
//TODO: Temporary naive assume single-sourced dash
|
//TODO: Temporary naive assume single-sourced dash
|
||||||
val foundTemplate = REGEX_DASH_TEMPLATE.find(manifest);
|
val foundTemplates = REGEX_DASH_TEMPLATE_WITH_MIME.findAll(manifest);
|
||||||
if(foundTemplate == null || foundTemplate.groupValues.size != 3)
|
val foundTemplate = when(downloadType) {
|
||||||
|
1 -> foundTemplates.find({ it.groupValues[1].contains("video/") });
|
||||||
|
2 -> foundTemplates.find({ it.groupValues[1].contains("audio/") });
|
||||||
|
else -> foundTemplates.find({ it.groupValues[1].contains("video/") });
|
||||||
|
}
|
||||||
|
if(foundTemplate == null || foundTemplate.groupValues.size != 4)
|
||||||
throw IllegalStateException("No SegmentTemplate found in manifest (unsupported dash?)");
|
throw IllegalStateException("No SegmentTemplate found in manifest (unsupported dash?)");
|
||||||
val foundTemplateUrl = foundTemplate.groupValues[1];
|
val foundTemplateUrl = foundTemplate.groupValues[2];
|
||||||
val foundCues = REGEX_DASH_CUE.findAll(foundTemplate.groupValues[2]);
|
val foundCues = REGEX_DASH_CUE.findAll(foundTemplate.groupValues[3]).toList();
|
||||||
if(foundCues.count() <= 0)
|
if(foundCues.count() <= 0)
|
||||||
throw IllegalStateException("No Cues found in manifest (unsupported dash?)");
|
throw IllegalStateException("No Cues found in manifest (unsupported dash?)");
|
||||||
|
|
||||||
val executor = if(source is JSSource && source.hasRequestExecutor)
|
val foundTemplate2 = if(downloadType == 3) foundTemplates.find({ it.groupValues[1].contains("audio/") }); else null;
|
||||||
|
val foundTemplateUrl2 = if(foundTemplate2 != null) foundTemplate2.groupValues[2] else null;
|
||||||
|
val foundCues2 = if(foundTemplate2 != null) REGEX_DASH_CUE.findAll(foundTemplate2.groupValues[3]).toList() else null;
|
||||||
|
val foundCues2Downloaded = hashSetOf<MatchResult>();
|
||||||
|
|
||||||
|
if(foundTemplate2 != null)
|
||||||
|
overrideResultAudioSource = LocalAudioSource((videoSource?.name)?.let { it + " [audio]" } ?: "audio", "", 0, 0, foundTemplate2.groupValues[1], REGEX_CODECS.find(foundTemplate2.groupValues[0])?.groupValues?.get(1) ?: "", Language.UNKNOWN);
|
||||||
|
|
||||||
|
executor = if(source is JSSource && source.hasRequestExecutor)
|
||||||
source.getRequestExecutor();
|
source.getRequestExecutor();
|
||||||
else
|
else
|
||||||
null;
|
null;
|
||||||
|
|
||||||
|
val modifier = if (source is JSSource && source.hasRequestModifier)
|
||||||
|
source.getRequestModifier();
|
||||||
|
else
|
||||||
|
null;
|
||||||
val speedTracker = SpeedTracker(1000);
|
val speedTracker = SpeedTracker(1000);
|
||||||
|
|
||||||
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
|
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
|
||||||
|
|
||||||
var written: Long = 0;
|
var written: Long = 0;
|
||||||
|
var written2: Long = 0;
|
||||||
var indexCounter = 0;
|
var indexCounter = 0;
|
||||||
|
var indexCounter2 = 0;
|
||||||
onProgress(foundCues.count().toLong(), 0, 0);
|
onProgress(foundCues.count().toLong(), 0, 0);
|
||||||
|
val totalCues = foundCues.count().toLong() + (foundCues2?.count()?.toLong() ?: 0)
|
||||||
|
val lastCue = foundCues.lastOrNull();
|
||||||
for(cue in foundCues) {
|
for(cue in foundCues) {
|
||||||
val t = cue.groupValues[1];
|
val t = cue.groupValues[1];
|
||||||
val d = cue.groupValues[2];
|
val d = cue.groupValues[2];
|
||||||
|
|
||||||
|
Logger.i(TAG, "Downloading cue ${indexCounter}")
|
||||||
val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString());
|
val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString());
|
||||||
|
val modified = modifier?.modifyRequest(url, mapOf());
|
||||||
|
|
||||||
val data = if(executor != null)
|
val data = if(executor != null)
|
||||||
executor.executeRequest("GET", url, null, mapOf());
|
executor.executeRequest("GET", modified?.url ?: url, null, modified?.headers ?: mapOf());
|
||||||
else {
|
else {
|
||||||
val resp = client.get(url, mutableMapOf());
|
val resp = client.get(modified?.url ?: url, modified?.headers?.toMutableMap() ?: mutableMapOf());
|
||||||
if(!resp.isOk)
|
if(!resp.isOk)
|
||||||
throw IllegalStateException("Dash request failed for index " + indexCounter.toString() + ", with code: " + resp.code.toString());
|
throw IllegalStateException("Dash request failed for index " + indexCounter.toString() + ", with code: " + resp.code.toString());
|
||||||
resp.body!!.bytes()
|
resp.body!!.bytes()
|
||||||
@@ -740,17 +974,60 @@ class VideoDownload {
|
|||||||
speedTracker.addWork(data.size.toLong());
|
speedTracker.addWork(data.size.toLong());
|
||||||
written += data.size;
|
written += data.size;
|
||||||
|
|
||||||
onProgress(foundCues.count().toLong(), indexCounter.toLong(), speedTracker.lastSpeed);
|
onProgress(totalCues, indexCounter.toLong() + indexCounter2.toLong(), speedTracker.lastSpeed);
|
||||||
|
|
||||||
|
|
||||||
indexCounter++;
|
indexCounter++;
|
||||||
|
|
||||||
|
if(foundCues2 != null && foundTemplateUrl2 != null && fileStream2 != null) {
|
||||||
|
val toDownload = if(lastCue != null && cue == lastCue)
|
||||||
|
foundCues2.filter { !foundCues2Downloaded.contains(it) }.toList() else
|
||||||
|
foundCues2.filter { !foundCues2Downloaded.contains(it) && (it.groupValues[1].toLong()) < t.toLong() }.toList();
|
||||||
|
Logger.i(TAG, "Downloading audio cues (${toDownload.size})")
|
||||||
|
for(cue2 in toDownload) {
|
||||||
|
val index2 = foundCues2.indexOf(cue2);
|
||||||
|
val t2 = cue2.groupValues[1];
|
||||||
|
val d2 = cue2.groupValues[2];
|
||||||
|
val url2 = foundTemplateUrl2!!.replace("\$Number\$", (index2).toString());
|
||||||
|
val modified2 = modifier?.modifyRequest(url, mapOf());
|
||||||
|
|
||||||
|
val data = if(executor != null)
|
||||||
|
executor.executeRequest("GET", modified2?.url ?: url2, null, modified2?.headers ?: mapOf());
|
||||||
|
else {
|
||||||
|
val resp = client.get(modified2?.url ?: url, modified2?.headers?.toMutableMap() ?: mutableMapOf());
|
||||||
|
if(!resp.isOk)
|
||||||
|
throw IllegalStateException("Dash request2 failed for index " + indexCounter.toString() + ", with code: " + resp.code.toString());
|
||||||
|
resp.body!!.bytes()
|
||||||
|
}
|
||||||
|
fileStream2.write(data, 0, data.size);
|
||||||
|
speedTracker.addWork(data.size.toLong());
|
||||||
|
written2 += data.size;
|
||||||
|
indexCounter2++;
|
||||||
|
|
||||||
|
foundCues2Downloaded.add(cue2);
|
||||||
|
onProgress(totalCues, indexCounter.toLong() + indexCounter2.toLong(), speedTracker.lastSpeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sourceLength = written;
|
sourceLength = written;
|
||||||
|
sourceLengthAudio = written2;
|
||||||
|
|
||||||
Logger.i(TAG, "$name downloadSource Finished");
|
Logger.i(TAG, "$name downloadSource Finished");
|
||||||
}
|
}
|
||||||
|
catch(scriptEx: ScriptReloadRequiredException) {
|
||||||
|
if(targetFile.exists() ?: false)
|
||||||
|
targetFile.delete();
|
||||||
|
if(targetFileAudio?.exists() ?: false)
|
||||||
|
targetFileAudio.delete();
|
||||||
|
|
||||||
|
createNewPluginClient();
|
||||||
|
throw scriptEx;
|
||||||
|
}
|
||||||
catch(ioex: IOException) {
|
catch(ioex: IOException) {
|
||||||
if(targetFile.exists() ?: false)
|
if(targetFile.exists() ?: false)
|
||||||
targetFile.delete();
|
targetFile.delete();
|
||||||
|
if(targetFileAudio?.exists() ?: false)
|
||||||
|
targetFileAudio.delete();
|
||||||
if(ioex.message?.contains("ENOSPC") ?: false)
|
if(ioex.message?.contains("ENOSPC") ?: false)
|
||||||
throw Exception("Not enough space on device", ioex);
|
throw Exception("Not enough space on device", ioex);
|
||||||
else
|
else
|
||||||
@@ -759,14 +1036,38 @@ class VideoDownload {
|
|||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
if(targetFile.exists() ?: false)
|
if(targetFile.exists() ?: false)
|
||||||
targetFile.delete();
|
targetFile.delete();
|
||||||
|
if(targetFileAudio?.exists() ?: false)
|
||||||
|
targetFileAudio.delete();
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
fileStream.close();
|
fileStream.close();
|
||||||
|
fileStream2?.close();
|
||||||
|
executor?.closeAsync()
|
||||||
}
|
}
|
||||||
|
if(sourceLengthAudio != null && sourceLengthAudio > 0)
|
||||||
|
audioFileSize = sourceLengthAudio
|
||||||
return sourceLength!!;
|
return sourceLength!!;
|
||||||
}
|
}
|
||||||
private fun downloadFileSource(name: String, client: ManagedHttpClient, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
|
||||||
|
fun createNewPluginClient() {
|
||||||
|
UIDialogs.appToast("Download creating new client at request of plugin");
|
||||||
|
cleanupPluginClient();
|
||||||
|
plugin = (video?.url ?: videoDetails?.url)?.let { StatePlatform.instance.getContentClient(it) }?.let { if(it is JSClient) it else null }?.getCopy(false, true);
|
||||||
|
plugin?.initialize();
|
||||||
|
}
|
||||||
|
fun cleanupPluginClient() {
|
||||||
|
val oldPlugin = plugin;
|
||||||
|
plugin = null;
|
||||||
|
try {
|
||||||
|
oldPlugin?.disable();
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to dispose download client: ${ex.message}" , ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadFileSource(name: String, client: ManagedHttpClient, source: JSSource?, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
if(targetFile.exists())
|
if(targetFile.exists())
|
||||||
targetFile.delete();
|
targetFile.delete();
|
||||||
|
|
||||||
@@ -775,7 +1076,12 @@ class VideoDownload {
|
|||||||
val sourceLength: Long?;
|
val sourceLength: Long?;
|
||||||
val fileStream = FileOutputStream(targetFile);
|
val fileStream = FileOutputStream(targetFile);
|
||||||
|
|
||||||
try{
|
val modifier = if (source is JSSource && source.hasRequestModifier)
|
||||||
|
source.getRequestModifier();
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
|
||||||
|
try {
|
||||||
val head = client.tryHead(videoUrl);
|
val head = client.tryHead(videoUrl);
|
||||||
val relatedPlugin = (video?.url ?: videoDetails?.url)?.let { StatePlatform.instance.getContentClient(it) }?.let { if(it is JSClient) it else null };
|
val relatedPlugin = (video?.url ?: videoDetails?.url)?.let { StatePlatform.instance.getContentClient(it) }?.let { if(it is JSClient) it else null };
|
||||||
if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length"))
|
if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length"))
|
||||||
@@ -786,12 +1092,12 @@ class VideoDownload {
|
|||||||
Logger.i(TAG, "Download $name ByteRange Parallel (${concurrency}): " + videoUrl);
|
Logger.i(TAG, "Download $name ByteRange Parallel (${concurrency}): " + videoUrl);
|
||||||
sourceLength = head["content-length"]!!.toLong();
|
sourceLength = head["content-length"]!!.toLong();
|
||||||
onProgress(sourceLength, 0, 0);
|
onProgress(sourceLength, 0, 0);
|
||||||
downloadSource_Ranges(name, client, fileStream, videoUrl, sourceLength, 1024*512, concurrency, onProgress);
|
downloadSource_Ranges(name, client, modifier, fileStream, videoUrl, sourceLength, 1024*512, concurrency, onProgress);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Logger.i(TAG, "Download $name Sequential");
|
Logger.i(TAG, "Download $name Sequential");
|
||||||
try {
|
try {
|
||||||
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, null, 0, onProgress);
|
sourceLength = downloadSource_Sequential(client, modifier, fileStream, videoUrl, null, 0, onProgress);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to download sequentially (url = $videoUrl)")
|
Logger.w(TAG, "Failed to download sequentially (url = $videoUrl)")
|
||||||
throw e
|
throw e
|
||||||
@@ -842,7 +1148,7 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, decryptionInfo: DecryptionInfo?, index: Int, onProgress: (Long, Long, Long) -> Unit): Long {
|
private fun downloadSource_Sequential(client: ManagedHttpClient, modifier: IRequestModifier? = null, fileStream: FileOutputStream, url: String, decryptionInfo: DecryptionInfo?, index: Int, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
val progressRate: Int = 4096 * 5;
|
val progressRate: Int = 4096 * 5;
|
||||||
var lastProgressCount: Int = 0;
|
var lastProgressCount: Int = 0;
|
||||||
val speedRate: Int = 4096 * 5;
|
val speedRate: Int = 4096 * 5;
|
||||||
@@ -851,7 +1157,12 @@ class VideoDownload {
|
|||||||
|
|
||||||
var lastSpeed: Long = 0;
|
var lastSpeed: Long = 0;
|
||||||
|
|
||||||
val result = client.get(url);
|
val result = if (modifier != null) {
|
||||||
|
val modified = modifier.modifyRequest(url, mapOf())
|
||||||
|
client.get(modified.url!!, modified.headers.toMutableMap())
|
||||||
|
} else {
|
||||||
|
client.get(url)
|
||||||
|
}
|
||||||
if (!result.isOk) {
|
if (!result.isOk) {
|
||||||
result.body?.close()
|
result.body?.close()
|
||||||
throw IllegalStateException("Failed to download source. Web[${result.code}] Error");
|
throw IllegalStateException("Failed to download source. Web[${result.code}] Error");
|
||||||
@@ -988,7 +1299,7 @@ class VideoDownload {
|
|||||||
onProgress(sourceLength, totalRead, 0)
|
onProgress(sourceLength, totalRead, 0)
|
||||||
return sourceLength
|
return sourceLength
|
||||||
}*/
|
}*/
|
||||||
private fun downloadSource_Ranges(name: String, client: ManagedHttpClient, fileStream: FileOutputStream, url: String, sourceLength: Long, rangeSize: Int, concurrency: Int = 1, onProgress: (Long, Long, Long) -> Unit) {
|
private fun downloadSource_Ranges(name: String, client: ManagedHttpClient, modifier: IRequestModifier?, fileStream: FileOutputStream, url: String, sourceLength: Long, rangeSize: Int, concurrency: Int = 1, onProgress: (Long, Long, Long) -> Unit) {
|
||||||
val progressRate: Int = 4096 * 5;
|
val progressRate: Int = 4096 * 5;
|
||||||
var lastProgressCount: Int = 0;
|
var lastProgressCount: Int = 0;
|
||||||
val speedRate: Int = 4096 * 5;
|
val speedRate: Int = 4096 * 5;
|
||||||
@@ -1007,7 +1318,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
Logger.i(TAG, "Download ${name} Batch #${reqCount} [${concurrency}] (${lastSpeed.toHumanBytesSpeed()})");
|
Logger.i(TAG, "Download ${name} Batch #${reqCount} [${concurrency}] (${lastSpeed.toHumanBytesSpeed()})");
|
||||||
|
|
||||||
val byteRangeResults = requestByteRangeParallel(client, pool, url, sourceLength, concurrency, totalRead,
|
val byteRangeResults = requestByteRangeParallel(client, pool, modifier, url, sourceLength, concurrency, totalRead,
|
||||||
rangeSize, 1024 * 64);
|
rangeSize, 1024 * 64);
|
||||||
|
|
||||||
for(byteRange in byteRangeResults) {
|
for(byteRange in byteRangeResults) {
|
||||||
@@ -1038,7 +1349,7 @@ class VideoDownload {
|
|||||||
onProgress(sourceLength, totalRead, 0);
|
onProgress(sourceLength, totalRead, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestByteRangeParallel(client: ManagedHttpClient, pool: ForkJoinPool, url: String, totalLength: Long, concurrency: Int, rangePosition: Long, rangeSize: Int, rangeVariance: Int = -1): List<Triple<ByteArray, Long, Long>> {
|
private fun requestByteRangeParallel(client: ManagedHttpClient, pool: ForkJoinPool, modifier: IRequestModifier?, url: String, totalLength: Long, concurrency: Int, rangePosition: Long, rangeSize: Int, rangeVariance: Int = -1): List<Triple<ByteArray, Long, Long>> {
|
||||||
val tasks = mutableListOf<ForkJoinTask<Triple<ByteArray, Long, Long>>>();
|
val tasks = mutableListOf<ForkJoinTask<Triple<ByteArray, Long, Long>>>();
|
||||||
var readPosition = rangePosition;
|
var readPosition = rangePosition;
|
||||||
for(i in 0 until concurrency) {
|
for(i in 0 until concurrency) {
|
||||||
@@ -1052,21 +1363,25 @@ class VideoDownload {
|
|||||||
else readPosition + toRead;
|
else readPosition + toRead;
|
||||||
|
|
||||||
tasks.add(pool.submit<Triple<ByteArray, Long, Long>> {
|
tasks.add(pool.submit<Triple<ByteArray, Long, Long>> {
|
||||||
return@submit requestByteRange(client, url, rangeStart, rangeEnd);
|
return@submit requestByteRange(client, modifier, url, rangeStart, rangeEnd);
|
||||||
});
|
});
|
||||||
readPosition = rangeEnd + 1;
|
readPosition = rangeEnd + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tasks.map { it.get() };
|
return tasks.map { it.get() };
|
||||||
}
|
}
|
||||||
private fun requestByteRange(client: ManagedHttpClient, url: String, rangeStart: Long, rangeEnd: Long): Triple<ByteArray, Long, Long> {
|
private fun requestByteRange(client: ManagedHttpClient, modifier: IRequestModifier?, url: String, rangeStart: Long, rangeEnd: Long): Triple<ByteArray, Long, Long> {
|
||||||
var retryCount = 0
|
var retryCount = 0
|
||||||
var lastException: Throwable? = null
|
var lastException: Throwable? = null;
|
||||||
|
|
||||||
|
val headers = mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}"));
|
||||||
|
val modified = modifier?.modifyRequest(url, headers);
|
||||||
|
|
||||||
while (retryCount <= 3) {
|
while (retryCount <= 3) {
|
||||||
try {
|
try {
|
||||||
val toRead = rangeEnd - rangeStart;
|
val toRead = rangeEnd - rangeStart;
|
||||||
val req = client.get(url, mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}")));
|
|
||||||
|
val req = client.get(modified?.url ?: url, modified?.headers?.toMutableMap() ?: headers);
|
||||||
if (!req.isOk) {
|
if (!req.isOk) {
|
||||||
val bodyString = req.body?.string()
|
val bodyString = req.body?.string()
|
||||||
req.body?.close()
|
req.body?.close()
|
||||||
@@ -1111,7 +1426,7 @@ class VideoDownload {
|
|||||||
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
|
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(audioSourceToUse != null) {
|
if(audioSourceToUse != null || (videoSourceToUse is IJSDashManifestRawSource)) {
|
||||||
if(audioFilePath == null)
|
if(audioFilePath == null)
|
||||||
throw IllegalStateException("Missing audio file name after download");
|
throw IllegalStateException("Missing audio file name after download");
|
||||||
val expectedFile = File(audioFilePath!!);
|
val expectedFile = File(audioFilePath!!);
|
||||||
@@ -1134,7 +1449,7 @@ class VideoDownload {
|
|||||||
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
||||||
val existing = StateDownloads.instance.getCachedVideo(id);
|
val existing = StateDownloads.instance.getCachedVideo(id);
|
||||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0, videoOverrideContainer) };
|
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0, videoOverrideContainer) };
|
||||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0, audioOverrideContainer) };
|
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(overrideResultAudioSource ?: audioSourceToUse!!, it, audioFileSize ?: 0, audioOverrideContainer) };
|
||||||
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
||||||
|
|
||||||
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
||||||
@@ -1176,6 +1491,10 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cleanup(){
|
||||||
|
cleanupPluginClient()
|
||||||
|
}
|
||||||
|
|
||||||
enum class State {
|
enum class State {
|
||||||
QUEUED,
|
QUEUED,
|
||||||
PREPARING,
|
PREPARING,
|
||||||
@@ -1199,6 +1518,8 @@ class VideoDownload {
|
|||||||
const val GROUP_WATCHLATER= "WatchLater";
|
const val GROUP_WATCHLATER= "WatchLater";
|
||||||
|
|
||||||
val REGEX_DASH_TEMPLATE = Regex("<SegmentTemplate .*?media=\"(.*?)\".*?>(.*?)<\\/SegmentTemplate>", RegexOption.DOT_MATCHES_ALL);
|
val REGEX_DASH_TEMPLATE = Regex("<SegmentTemplate .*?media=\"(.*?)\".*?>(.*?)<\\/SegmentTemplate>", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
val REGEX_DASH_TEMPLATE_WITH_MIME = Regex("<Representation.*?mimeType=\\\"(.*?)\\\".*?>.*?<SegmentTemplate .*?media=\\\"(.*?)\\\".*?>(.*?)<\\/SegmentTemplate>", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
val REGEX_CODECS = Regex("codecs=\\\"(.*?)\\\"")
|
||||||
val REGEX_DASH_CUE = Regex("<S .*?t=\"([0-9]*?)\".*?d=\"([0-9]*?)\".*?\\/>", RegexOption.DOT_MATCHES_ALL);
|
val REGEX_DASH_CUE = Regex("<S .*?t=\"([0-9]*?)\".*?d=\"([0-9]*?)\".*?\\/>", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
|
||||||
fun videoContainerToExtension(container: String): String? {
|
fun videoContainerToExtension(container: String): String? {
|
||||||
@@ -1218,6 +1539,16 @@ class VideoDownload {
|
|||||||
return "video";//throw IllegalStateException("Unknown container: " + container)
|
return "video";//throw IllegalStateException("Unknown container: " + container)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Change usages of this to an accurate container instead of infering it.
|
||||||
|
fun videoAudioContainerToExtension(container: String): String? {
|
||||||
|
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
||||||
|
return "mp4a";
|
||||||
|
else if (container.contains("video/webm"))
|
||||||
|
return "webm";
|
||||||
|
else
|
||||||
|
return "mp4a";//throw IllegalStateException("Unknown container: " + container)
|
||||||
|
}
|
||||||
|
|
||||||
fun audioContainerToExtension(container: String): String {
|
fun audioContainerToExtension(container: String): String {
|
||||||
if (container.contains("audio/mp4"))
|
if (container.contains("audio/mp4"))
|
||||||
return "mp4a";
|
return "mp4a";
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import android.content.Context
|
|||||||
import com.caoccao.javet.exceptions.JavetCompilationException
|
import com.caoccao.javet.exceptions.JavetCompilationException
|
||||||
import com.caoccao.javet.exceptions.JavetException
|
import com.caoccao.javet.exceptions.JavetException
|
||||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||||
|
import com.caoccao.javet.interfaces.IJavetEntityError
|
||||||
|
import com.caoccao.javet.interfaces.IJavetEntityMap
|
||||||
import com.caoccao.javet.interop.V8Host
|
import com.caoccao.javet.interop.V8Host
|
||||||
import com.caoccao.javet.interop.V8Runtime
|
import com.caoccao.javet.interop.V8Runtime
|
||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
@@ -18,6 +20,7 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
|||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.engine.exceptions.NoInternetException
|
import com.futo.platformplayer.engine.exceptions.NoInternetException
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
|
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptAgeException
|
import com.futo.platformplayer.engine.exceptions.ScriptAgeException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCompilationException
|
import com.futo.platformplayer.engine.exceptions.ScriptCompilationException
|
||||||
@@ -33,9 +36,11 @@ import com.futo.platformplayer.engine.internal.V8Converter
|
|||||||
import com.futo.platformplayer.engine.packages.PackageBridge
|
import com.futo.platformplayer.engine.packages.PackageBridge
|
||||||
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
||||||
import com.futo.platformplayer.engine.packages.PackageHttp
|
import com.futo.platformplayer.engine.packages.PackageHttp
|
||||||
|
import com.futo.platformplayer.engine.packages.PackageHttpImp
|
||||||
import com.futo.platformplayer.engine.packages.PackageJSDOM
|
import com.futo.platformplayer.engine.packages.PackageJSDOM
|
||||||
import com.futo.platformplayer.engine.packages.PackageUtilities
|
import com.futo.platformplayer.engine.packages.PackageUtilities
|
||||||
import com.futo.platformplayer.engine.packages.V8Package
|
import com.futo.platformplayer.engine.packages.V8Package
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateAssets
|
import com.futo.platformplayer.states.StateAssets
|
||||||
@@ -379,6 +384,7 @@ class V8Plugin {
|
|||||||
return when(packageName) {
|
return when(packageName) {
|
||||||
"DOMParser" -> PackageDOMParser(this)
|
"DOMParser" -> PackageDOMParser(this)
|
||||||
"Http" -> PackageHttp(this, config)
|
"Http" -> PackageHttp(this, config)
|
||||||
|
"HttpImp" -> PackageHttpImp(this, config)
|
||||||
"Utilities" -> PackageUtilities(this, config)
|
"Utilities" -> PackageUtilities(this, config)
|
||||||
"JSDOM" -> PackageJSDOM(this, config)
|
"JSDOM" -> PackageJSDOM(this, config)
|
||||||
else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
||||||
@@ -407,6 +413,12 @@ class V8Plugin {
|
|||||||
return _runtimeMap.getOrDefault(runtime, null);
|
return _runtimeMap.getOrDefault(runtime, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ctxString(ctx: Any?, key: String): String? = when (ctx) {
|
||||||
|
is Map<*, *> -> ctx[key]?.toString()
|
||||||
|
is V8ValueObject -> if (ctx.has(key)) ctx.getString(key) else null
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
fun <T: Any?> catchScriptErrors(config: IV8PluginConfig, context: String, code: String? = null, handle: ()->T): T {
|
fun <T: Any?> catchScriptErrors(config: IV8PluginConfig, context: String, code: String? = null, handle: ()->T): T {
|
||||||
var codeStripped = code;
|
var codeStripped = code;
|
||||||
if(codeStripped != null) { //TODO: Improve code stripped
|
if(codeStripped != null) { //TODO: Improve code stripped
|
||||||
@@ -440,37 +452,6 @@ class V8Plugin {
|
|||||||
throw ScriptCompilationException(config, "Compilation: [${context}]: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped);
|
throw ScriptCompilationException(config, "Compilation: [${context}]: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped);
|
||||||
}
|
}
|
||||||
catch(executeEx: JavetExecutionException) {
|
catch(executeEx: JavetExecutionException) {
|
||||||
val obj = executeEx.scriptingError?.context
|
|
||||||
if(obj != null && obj.containsKey("plugin_type") == true) {
|
|
||||||
val pluginType = obj["plugin_type"].toString();
|
|
||||||
|
|
||||||
//Captcha
|
|
||||||
if (pluginType == "CaptchaRequiredException") {
|
|
||||||
throw ScriptCaptchaRequiredException(config,
|
|
||||||
obj["url"]?.toString(),
|
|
||||||
obj["body"]?.toString(),
|
|
||||||
executeEx, executeEx.scriptingError?.stack, codeStripped);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Reload Required
|
|
||||||
if (pluginType == "ReloadRequiredException") {
|
|
||||||
throw ScriptReloadRequiredException(config,
|
|
||||||
obj["msg"]?.toString(),
|
|
||||||
obj["reloadData"]?.toString(),
|
|
||||||
executeEx, executeEx.scriptingError?.stack, codeStripped);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Others
|
|
||||||
throwExceptionFromV8(
|
|
||||||
config,
|
|
||||||
pluginType,
|
|
||||||
(extractJSExceptionMessage(executeEx) ?: ""),
|
|
||||||
executeEx,
|
|
||||||
executeEx.scriptingError?.stack,
|
|
||||||
codeStripped
|
|
||||||
);
|
|
||||||
}
|
|
||||||
/* //Required for newer V8 versions
|
|
||||||
if(executeEx.scriptingError?.context is IJavetEntityError) {
|
if(executeEx.scriptingError?.context is IJavetEntityError) {
|
||||||
val obj = executeEx.scriptingError?.context as IJavetEntityError
|
val obj = executeEx.scriptingError?.context as IJavetEntityError
|
||||||
if(obj.context.containsKey("plugin_type") == true) {
|
if(obj.context.containsKey("plugin_type") == true) {
|
||||||
@@ -504,7 +485,6 @@ class V8Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped);
|
throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped);
|
||||||
}
|
}
|
||||||
catch(ex: Exception) {
|
catch(ex: Exception) {
|
||||||
@@ -513,18 +493,29 @@ class V8Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun throwExceptionFromV8(config: IV8PluginConfig, pluginType: String, msg: String, innerEx: Exception? = null, stack: String? = null, code: String? = null) {
|
private fun throwExceptionFromV8(config: IV8PluginConfig, pluginType: String, msg: String, innerEx: Exception? = null, stack: String? = null, code: String? = null) {
|
||||||
|
throw getExceptionFromPlugin(config, pluginType, msg, innerEx, stack, code);
|
||||||
|
}
|
||||||
|
fun getExceptionFromPlugin(config: IV8PluginConfig, obj: V8ValueObject, innerEx: Exception? = null, stack: String? = null, code: String? = null, prefix: String? = null): PluginException {
|
||||||
|
val pluginType = obj.getOrDefault(config, "plugin_type", "Exception Handling", "")?.let { if(!it.isNullOrBlank()) it + "" else "" } ?: "";
|
||||||
|
var msg = obj.getOrDefault<String?>(config, "msg", "Exception Handling", null)
|
||||||
|
?: obj.getOrDefault(config, "message", "Exception Handling", "");
|
||||||
|
if(!prefix.isNullOrBlank())
|
||||||
|
msg = prefix + msg;
|
||||||
|
return getExceptionFromPlugin(config, pluginType, msg ?: "Unknown exception", innerEx, stack, code);
|
||||||
|
}
|
||||||
|
fun getExceptionFromPlugin(config: IV8PluginConfig, pluginType: String, msg: String, innerEx: Exception? = null, stack: String? = null, code: String? = null): PluginException {
|
||||||
when(pluginType) {
|
when(pluginType) {
|
||||||
"ScriptException" -> throw ScriptException(config, msg, innerEx, stack, code);
|
"ScriptException" -> return ScriptException(config, msg, innerEx, stack, code);
|
||||||
"CriticalException" -> throw ScriptCriticalException(config, msg, innerEx, stack, code);
|
"CriticalException" -> return ScriptCriticalException(config, msg, innerEx, stack, code);
|
||||||
"AgeException" -> throw ScriptAgeException(config, msg, innerEx, stack, code);
|
"AgeException" -> return ScriptAgeException(config, msg, innerEx, stack, code);
|
||||||
"UnavailableException" -> throw ScriptUnavailableException(config, msg, innerEx, stack, code);
|
"UnavailableException" -> return ScriptUnavailableException(config, msg, innerEx, stack, code);
|
||||||
"ScriptLoginRequiredException" -> throw ScriptLoginRequiredException(config, msg, innerEx, stack, code);
|
"ScriptLoginRequiredException" -> return ScriptLoginRequiredException(config, msg, innerEx, stack, code);
|
||||||
"ScriptExecutionException" -> throw ScriptExecutionException(config, msg, innerEx, stack, code);
|
"ScriptExecutionException" -> return ScriptExecutionException(config, msg, innerEx, stack, code);
|
||||||
"ScriptCompilationException" -> throw ScriptCompilationException(config, msg, innerEx, code);
|
"ScriptCompilationException" -> return ScriptCompilationException(config, msg, innerEx, code);
|
||||||
"ScriptImplementationException" -> throw ScriptImplementationException(config, msg, innerEx, null, code);
|
"ScriptImplementationException" -> return ScriptImplementationException(config, msg, innerEx, null, code);
|
||||||
"ScriptTimeoutException" -> throw ScriptTimeoutException(config, msg, innerEx);
|
"ScriptTimeoutException" -> return ScriptTimeoutException(config, msg, innerEx);
|
||||||
"NoInternetException" -> throw NoInternetException(config, msg, innerEx, stack, code);
|
"NoInternetException" -> return NoInternetException(config, msg, innerEx, stack, code);
|
||||||
else -> throw ScriptExecutionException(config, msg, innerEx, stack, code);
|
else -> return ScriptExecutionException(config, msg, innerEx, stack, code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
package com.curlbind
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import kotlin.collections.iterator
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
object Libcurl {
|
||||||
|
init {
|
||||||
|
System.loadLibrary("curl-impersonate")
|
||||||
|
System.loadLibrary("curl-impersonate-jni")
|
||||||
|
// CURL_GLOBAL_ALL = 3
|
||||||
|
require(ce_global_init(3) == CURLcode.CURLE_OK) { "curl_global_init failed" }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
data class Request(
|
||||||
|
var url: String,
|
||||||
|
var method: String = "GET",
|
||||||
|
var headers: Map<String, String> = emptyMap(),
|
||||||
|
var body: ByteArray? = null,
|
||||||
|
var impersonateTarget: String = "chrome136",
|
||||||
|
var useBuiltInHeaders: Boolean = true,
|
||||||
|
var timeoutMs: Int = 30_000
|
||||||
|
)
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
data class Response(
|
||||||
|
val status: Int,
|
||||||
|
val effectiveUrl: String,
|
||||||
|
val bodyBytes: ByteArray,
|
||||||
|
val headers: Map<String, List<String>>
|
||||||
|
)
|
||||||
|
|
||||||
|
object CURLcode {
|
||||||
|
const val CURLE_OK = 0
|
||||||
|
const val CURLE_UNKNOWN_OPTION = 48
|
||||||
|
}
|
||||||
|
|
||||||
|
object CurlInfoConsts {
|
||||||
|
const val CURLINFO_STRING = 0x100000
|
||||||
|
const val CURLINFO_LONG = 0x200000
|
||||||
|
const val CURLINFO_DOUBLE = 0x300000
|
||||||
|
const val CURLINFO_SLIST = 0x400000
|
||||||
|
const val CURLINFO_PTR = 0x400000
|
||||||
|
const val CURLINFO_SOCKET = 0x500000
|
||||||
|
const val CURLINFO_OFF_T = 0x600000
|
||||||
|
const val CURLINFO_MASK = 0x0fffff
|
||||||
|
const val CURLINFO_TYPEMASK = 0xf00000
|
||||||
|
}
|
||||||
|
|
||||||
|
object CURLINFO {
|
||||||
|
const val NONE = 0
|
||||||
|
const val EFFECTIVE_URL = CurlInfoConsts.CURLINFO_STRING + 1
|
||||||
|
const val RESPONSE_CODE = CurlInfoConsts.CURLINFO_LONG + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
object CURLOPT {
|
||||||
|
const val URL = 10002
|
||||||
|
const val FOLLOWLOCATION = 52
|
||||||
|
const val MAXREDIRS = 68
|
||||||
|
const val CONNECTTIMEOUT_MS = 156
|
||||||
|
const val TIMEOUT_MS = 155
|
||||||
|
const val HTTP_VERSION = 84
|
||||||
|
const val ACCEPT_ENCODING = 10102
|
||||||
|
const val HTTPHEADER = 10023
|
||||||
|
const val COOKIEFILE = 10031
|
||||||
|
const val COOKIEJAR = 10082
|
||||||
|
const val CUSTOMREQUEST = 10036
|
||||||
|
const val IPRESOLVE = 113
|
||||||
|
const val POSTFIELDS = 10015
|
||||||
|
const val POSTFIELDSIZE = 60
|
||||||
|
const val WRITEFUNCTION = 20011
|
||||||
|
const val HEADERFUNCTION = 20079
|
||||||
|
const val WRITEDATA = 10001
|
||||||
|
const val HEADERDATA = 10029
|
||||||
|
const val COPYPOSTFIELDS = 10165
|
||||||
|
const val CURLOPT_DNS_SERVERS = 10211
|
||||||
|
const val CAPATH = 10097
|
||||||
|
const val CAINFO = 10065
|
||||||
|
}
|
||||||
|
|
||||||
|
object CURL_HTTP_VERSION { const val TWO_TLS = 4 }
|
||||||
|
object CURL_IPRESOLVE { const val WHATEVER = 0; const val V4 = 1; const val V6 = 2 }
|
||||||
|
|
||||||
|
@Keep interface WriteCallback { fun onWrite(chunk: ByteArray): Int }
|
||||||
|
@Keep interface HeaderCallback { fun onHeader(line: ByteArray): Int }
|
||||||
|
|
||||||
|
@Volatile private var defaultCAPath: String? = null
|
||||||
|
@Keep fun setDefaultCAPath(path: String) { defaultCAPath = path }
|
||||||
|
|
||||||
|
fun perform(req: Request): Response {
|
||||||
|
val easy = ce_easy_init()
|
||||||
|
require(easy != 0L) { "curl_easy_init failed" }
|
||||||
|
|
||||||
|
var slist: Long = 0L
|
||||||
|
val bodySink = ByteArrayOutputStream(64 * 1024)
|
||||||
|
val rawHeaderLines = ArrayList<String>(64)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val imp = ce_easy_impersonate(easy, req.impersonateTarget, req.useBuiltInHeaders)
|
||||||
|
if (imp != CURLcode.CURLE_OK && imp != CURLcode.CURLE_UNKNOWN_OPTION) {
|
||||||
|
error("curl_easy_impersonate failed: ${ce_easy_strerror(imp)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
checkOK(ce_setopt_str(easy, CURLOPT.URL, req.url))
|
||||||
|
checkOK(ce_setopt_long(easy, CURLOPT.FOLLOWLOCATION, 1))
|
||||||
|
checkOK(ce_setopt_long(easy, CURLOPT.MAXREDIRS, 10))
|
||||||
|
checkOK(ce_setopt_long(easy, CURLOPT.CONNECTTIMEOUT_MS, req.timeoutMs.toLong()))
|
||||||
|
checkOK(ce_setopt_long(easy, CURLOPT.TIMEOUT_MS, req.timeoutMs.toLong()))
|
||||||
|
checkOK(ce_setopt_long(easy, CURLOPT.HTTP_VERSION, CURL_HTTP_VERSION.TWO_TLS.toLong()))
|
||||||
|
checkOK(ce_setopt_str(easy, CURLOPT.ACCEPT_ENCODING, "")) // enable auto-decompress
|
||||||
|
|
||||||
|
if (req.headers.isNotEmpty()) {
|
||||||
|
for ((k, v) in req.headers) slist = ce_slist_append(slist, "$k: $v")
|
||||||
|
if (slist != 0L) checkOK(ce_setopt_ptr(easy, CURLOPT.HTTPHEADER, slist))
|
||||||
|
}
|
||||||
|
|
||||||
|
val method = req.method
|
||||||
|
if (!method.equals("GET", ignoreCase = true)) {
|
||||||
|
checkOK(ce_setopt_str(easy, CURLOPT.CUSTOMREQUEST, method))
|
||||||
|
val body = req.body
|
||||||
|
if (body != null && body.isNotEmpty()) {
|
||||||
|
checkOK(ce_set_postfields(easy, body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkOK(ce_set_write_callback(easy, object : WriteCallback {
|
||||||
|
override fun onWrite(chunk: ByteArray): Int {
|
||||||
|
bodySink.write(chunk)
|
||||||
|
return chunk.size
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
checkOK(ce_set_header_callback(easy, object : HeaderCallback {
|
||||||
|
override fun onHeader(line: ByteArray): Int {
|
||||||
|
// Keep raw but trim CRLF for convenience
|
||||||
|
val s = line.toString(Charset.forName("ISO-8859-1")).trimEnd('\r', '\n')
|
||||||
|
if (s.isNotBlank()) rawHeaderLines.add(s)
|
||||||
|
return line.size
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
checkOK(ce_setopt_str(easy, CURLOPT.CURLOPT_DNS_SERVERS, "1.1.1.1,8.8.8.8"));
|
||||||
|
defaultCAPath?.let { checkOK(ce_setopt_str(easy, CURLOPT.CAINFO, it)) }
|
||||||
|
|
||||||
|
val rc = ce_easy_perform(easy)
|
||||||
|
if (rc != CURLcode.CURLE_OK) error("curl_easy_perform failed: ${ce_easy_strerror(rc)}")
|
||||||
|
|
||||||
|
val codeArr = longArrayOf(0)
|
||||||
|
checkOK(ce_easy_getinfo_long(easy, CURLINFO.RESPONSE_CODE, codeArr))
|
||||||
|
val effective = ce_easy_getinfo_string(easy, CURLINFO.EFFECTIVE_URL) ?: req.url
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
status = codeArr[0].toInt(),
|
||||||
|
effectiveUrl = effective,
|
||||||
|
bodyBytes = bodySink.toByteArray(),
|
||||||
|
headers = parseHeaders(rawHeaderLines)
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
if (slist != 0L) ce_slist_free_all(slist)
|
||||||
|
ce_easy_cleanup(easy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun defaultCookieJarPath(): String {
|
||||||
|
val tmp = System.getProperty("java.io.tmpdir") ?: "/data/local/tmp"
|
||||||
|
return if (tmp.endsWith("/")) "${tmp}imphttp.cookies.txt" else "$tmp/imphttp.cookies.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkOK(code: Int) {
|
||||||
|
if (code != CURLcode.CURLE_OK) throw IllegalStateException("libcurl error: ${ce_easy_strerror(code)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseHeaders(lines: List<String>): Map<String, List<String>> {
|
||||||
|
val map = linkedMapOf<String, MutableList<String>>()
|
||||||
|
for (line in lines) {
|
||||||
|
val idx = line.indexOf(':')
|
||||||
|
if (idx <= 0) continue
|
||||||
|
val name = line.substring(0, idx).trim()
|
||||||
|
val value = line.substring(min(idx + 1, line.length)).trim()
|
||||||
|
map.getOrPut(name) { mutableListOf() }.add(value)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic external fun ce_set_write_callback(easy: Long, cb: WriteCallback?): Int
|
||||||
|
@JvmStatic external fun ce_set_header_callback(easy: Long, cb: HeaderCallback?): Int
|
||||||
|
|
||||||
|
@JvmStatic external fun ce_global_init(flags: Long): Int
|
||||||
|
@JvmStatic external fun ce_global_cleanup()
|
||||||
|
@JvmStatic external fun ce_easy_init(): Long
|
||||||
|
@JvmStatic external fun ce_easy_cleanup(easy: Long)
|
||||||
|
@JvmStatic external fun ce_easy_perform(easy: Long): Int
|
||||||
|
|
||||||
|
@JvmStatic external fun ce_easy_impersonate(easy: Long, target: String, defaultHeaders: Boolean): Int
|
||||||
|
@JvmStatic external fun ce_setopt_long(easy: Long, opt: Int, value: Long): Int
|
||||||
|
@JvmStatic external fun ce_setopt_str(easy: Long, opt: Int, value: String): Int
|
||||||
|
@JvmStatic external fun ce_setopt_ptr(easy: Long, opt: Int, ptr: Long): Int
|
||||||
|
@JvmStatic external fun ce_slist_append(list: Long, header: String): Long
|
||||||
|
@JvmStatic external fun ce_slist_free_all(list: Long)
|
||||||
|
@JvmStatic external fun ce_easy_getinfo_long(easy: Long, info: Int, outVal: LongArray): Int
|
||||||
|
@JvmStatic external fun ce_easy_getinfo_string(easy: Long, info: Int): String?
|
||||||
|
|
||||||
|
@JvmStatic external fun ce_set_postfields(easy: Long, body: ByteArray): Int
|
||||||
|
@JvmStatic external fun ce_easy_strerror(code: Int): String
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ class PackageDOMParser : V8Package {
|
|||||||
}
|
}
|
||||||
@V8Property
|
@V8Property
|
||||||
fun lastChild(): DOMNode? {
|
fun lastChild(): DOMNode? {
|
||||||
val result = _element.firstElementChild()?.let { DOMNode(_package, it) };
|
val result = _element.lastElementChild()?.let { DOMNode(_package, it) };
|
||||||
if(result != null)
|
if(result != null)
|
||||||
_children.add(result);
|
_children.add(result);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ class PackageHttp: V8Package {
|
|||||||
|
|
||||||
//TODO: This object is currently re-wrapped each modification, this is due to an issue passing the same object back and forth, should be fixed in future.
|
//TODO: This object is currently re-wrapped each modification, this is due to an issue passing the same object back and forth, should be fixed in future.
|
||||||
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
|
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
|
||||||
class BatchBuilder(private val _package: PackageHttp, existingRequests: MutableList<Pair<PackageHttpClient, RequestDescriptor>> = mutableListOf()): V8BindObject() {
|
class BatchBuilder(@Transient private val _package: PackageHttp, existingRequests: MutableList<Pair<PackageHttpClient, RequestDescriptor>> = mutableListOf()): V8BindObject() {
|
||||||
@Transient
|
@Transient
|
||||||
private val _reqs = existingRequests;
|
private val _reqs = existingRequests;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -6,7 +6,7 @@ import com.futo.platformplayer.activities.MainActivity
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||||
|
|
||||||
open class MainActivityFragment : Fragment() {
|
open class MainActivityFragment : Fragment() {
|
||||||
protected val currentMain : MainFragment
|
protected val currentMain : MainFragment?
|
||||||
get() {
|
get() {
|
||||||
isValidMainActivity();
|
isValidMainActivity();
|
||||||
return (activity as MainActivity).fragCurrent;
|
return (activity as MainActivity).fragCurrent;
|
||||||
|
|||||||
+239
-22
@@ -8,19 +8,25 @@ import android.app.Activity
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.animation.doOnEnd
|
import androidx.core.animation.doOnEnd
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fragment.mainactivity.MainActivityFragment
|
import com.futo.platformplayer.fragment.mainactivity.MainActivityFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.*
|
import com.futo.platformplayer.fragment.mainactivity.main.*
|
||||||
@@ -28,6 +34,10 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePayment
|
import com.futo.platformplayer.states.StatePayment
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.pills.RoundButton
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
@@ -70,9 +80,15 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
private val _inflater: LayoutInflater;
|
private val _inflater: LayoutInflater;
|
||||||
private val _subscribedActivity: MainActivity?;
|
private val _subscribedActivity: MainActivity?;
|
||||||
|
|
||||||
|
private val _containerMoreHeader: ConstraintLayout;
|
||||||
|
private val _toggleAirplaneMode: LinearLayout;
|
||||||
|
private val _togglePrivacy: LinearLayout;
|
||||||
|
|
||||||
private var _overlayMore: FrameLayout;
|
private var _overlayMore: FrameLayout;
|
||||||
private var _overlayMoreBackground: FrameLayout;
|
private var _overlayMoreBackground: FrameLayout;
|
||||||
private var _layoutMoreButtons: LinearLayout;
|
private var _layoutMoreButtons: RecyclerView;
|
||||||
|
private val _layoutMoreButtonItems = arrayListOf<MenuButtonItem>();
|
||||||
|
private var _layoutMoreButtonsAdapter: AnyAdapterView<MenuButtonItem, MenuButtonItemViewHolder>;
|
||||||
private var _layoutBottomBarButtons: LinearLayout;
|
private var _layoutBottomBarButtons: LinearLayout;
|
||||||
|
|
||||||
private var _moreVisible = false;
|
private var _moreVisible = false;
|
||||||
@@ -86,15 +102,90 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
|
|
||||||
private var currentButtonDefinitions: List<ButtonDefinition>? = null;
|
private var currentButtonDefinitions: List<ButtonDefinition>? = null;
|
||||||
|
|
||||||
|
private var moreColumns = 3;
|
||||||
|
|
||||||
constructor(fragment: MenuBottomBarFragment, inflater: LayoutInflater) : super(inflater.context) {
|
constructor(fragment: MenuBottomBarFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||||
_fragment = fragment;
|
_fragment = fragment;
|
||||||
_inflater = inflater;
|
_inflater = inflater;
|
||||||
inflater.inflate(R.layout.fragment_overview_bottom_bar, this);
|
inflater.inflate(R.layout.fragment_overview_bottom_bar, this);
|
||||||
|
|
||||||
|
_containerMoreHeader = findViewById(R.id.container_more_options);
|
||||||
|
_toggleAirplaneMode = findViewById(R.id.container_toggle_airplane);
|
||||||
|
_togglePrivacy = findViewById(R.id.container_toggle_privacy);
|
||||||
|
|
||||||
|
_toggleAirplaneMode.isVisible = false //TODO: Remove when airplane mode implemented
|
||||||
|
|
||||||
|
StateApp.instance.airplaneModeChanged.subscribe {
|
||||||
|
if(!StateApp.instance.airplaneMode)
|
||||||
|
_toggleAirplaneMode.setBackgroundResource(R.drawable.background_menu_toggle)
|
||||||
|
else
|
||||||
|
_toggleAirplaneMode.setBackgroundResource(R.drawable.background_menu_toggle_active)
|
||||||
|
}
|
||||||
|
if(!StateApp.instance.airplaneMode)
|
||||||
|
_toggleAirplaneMode.setBackgroundResource(R.drawable.background_menu_toggle)
|
||||||
|
else
|
||||||
|
_toggleAirplaneMode.setBackgroundResource(R.drawable.background_menu_toggle_active)
|
||||||
|
_toggleAirplaneMode.setOnClickListener {
|
||||||
|
if(StateApp.instance.airplaneMode) {
|
||||||
|
StateApp.instance.setAirMode(false);
|
||||||
|
UIDialogs.appToast("Airplane mode disabled");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
StateApp.instance.setAirMode(true);
|
||||||
|
UIDialogs.appToast("Airplane mode enabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StateApp.instance.privateModeChanged.subscribe {
|
||||||
|
if(!StateApp.instance.privateMode)
|
||||||
|
_togglePrivacy.setBackgroundResource(R.drawable.background_menu_toggle)
|
||||||
|
else
|
||||||
|
_togglePrivacy.setBackgroundResource(R.drawable.background_menu_toggle_active)
|
||||||
|
}
|
||||||
|
if(!StateApp.instance.privateMode)
|
||||||
|
_togglePrivacy.setBackgroundResource(R.drawable.background_menu_toggle)
|
||||||
|
else
|
||||||
|
_togglePrivacy.setBackgroundResource(R.drawable.background_menu_toggle_active)
|
||||||
|
_togglePrivacy.setOnClickListener {
|
||||||
|
if(StateApp.instance.privateMode) {
|
||||||
|
StateApp.instance.setPrivacyMode(false);
|
||||||
|
UIDialogs.appToast("Privacy mode disabled");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
StateApp.instance.setPrivacyMode(true);
|
||||||
|
UIDialogs.appToast("Privacy mode enabled");
|
||||||
|
|
||||||
|
if(Settings.instance.other.showPrivacyModeDialog)
|
||||||
|
UIDialogs.showDialog(it.context ?: return@setOnClickListener, R.drawable.incognito, "Privacy Mode",
|
||||||
|
"All requests will be processed anonymously (any logins will be disabled except for the personalized home page), local playback and history tracking will also be disabled.\n\nTap the icon to disable.", null, 0,
|
||||||
|
UIDialogs.Action("Don't show again", {
|
||||||
|
Settings.instance.other.showPrivacyModeDialog = false;
|
||||||
|
Settings.instance.save();
|
||||||
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
|
UIDialogs.Action("Understood", {
|
||||||
|
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_overlayMore = findViewById(R.id.more_overlay);
|
_overlayMore = findViewById(R.id.more_overlay);
|
||||||
_overlayMoreBackground = findViewById(R.id.more_overlay_background);
|
_overlayMoreBackground = findViewById(R.id.more_overlay_background);
|
||||||
_layoutMoreButtons = findViewById(R.id.more_menu_buttons);
|
_layoutMoreButtons = findViewById(R.id.more_menu_buttons);
|
||||||
_layoutBottomBarButtons = findViewById(R.id.bottom_bar_buttons)
|
_layoutBottomBarButtons = findViewById(R.id.bottom_bar_buttons);
|
||||||
|
|
||||||
|
val totalWidthDp = resources.displayMetrics.widthPixels / resources.displayMetrics.density;
|
||||||
|
val columns = MenuButtonItemViewHolder.getAutoSizeColumns(totalWidthDp);
|
||||||
|
_layoutMoreButtonsAdapter = _layoutMoreButtons.asAny<MenuButtonItem, MenuButtonItemViewHolder>(_layoutMoreButtonItems,
|
||||||
|
RecyclerView.VERTICAL, false, { button ->
|
||||||
|
button.setAutoSize(totalWidthDp);
|
||||||
|
button.parentFragment = this@MenuBottomBarView._fragment;
|
||||||
|
button.onClick.subscribe {
|
||||||
|
setMoreVisible(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
moreColumns = columns;
|
||||||
|
val layoutManager = GridLayoutManager(context, columns, GridLayoutManager.VERTICAL, true);
|
||||||
|
_layoutMoreButtons.layoutManager = layoutManager;
|
||||||
|
|
||||||
_overlayMoreBackground.setOnClickListener { setMoreVisible(false); };
|
_overlayMoreBackground.setOnClickListener { setMoreVisible(false); };
|
||||||
|
|
||||||
@@ -121,6 +212,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setMoreVisible(visible: Boolean) {
|
private fun setMoreVisible(visible: Boolean) {
|
||||||
|
|
||||||
|
//TODO: issues with these bools
|
||||||
if (_moreVisibleAnimating) {
|
if (_moreVisibleAnimating) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -129,9 +222,12 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
val height = _moreButtons.firstOrNull()?.let {
|
val height = _moreButtons.firstOrNull()?.let {
|
||||||
it.height.toFloat() + (it.layoutParams as MarginLayoutParams).bottomMargin
|
it.height.toFloat() + (it.layoutParams as MarginLayoutParams).bottomMargin
|
||||||
} ?: return
|
} ?: return
|
||||||
|
*/
|
||||||
|
|
||||||
_moreVisibleAnimating = true
|
_moreVisibleAnimating = true
|
||||||
val moreOverlayBackground = _overlayMoreBackground
|
val moreOverlayBackground = _overlayMoreBackground
|
||||||
@@ -143,10 +239,17 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
moreOverlay.visibility = VISIBLE
|
moreOverlay.visibility = VISIBLE
|
||||||
val animations = arrayListOf<Animator>()
|
val animations = arrayListOf<Animator>()
|
||||||
animations.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 0.0f, 1.0f).setDuration(duration))
|
animations.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 0.0f, 1.0f).setDuration(duration))
|
||||||
|
animations.add(ObjectAnimator.ofFloat(_layoutMoreButtons, "alpha", 0.0f, 1.0f).setDuration(duration))
|
||||||
|
animations.add(ObjectAnimator.ofFloat(_containerMoreHeader, "alpha", 0.0f, 1.0f).setDuration(duration))
|
||||||
|
_bottomButtons.find { it.definition.id == 99 }?.let {
|
||||||
|
animations.add(ObjectAnimator.ofFloat(it, "alpha", 0.5f, 1.0f)
|
||||||
|
.setDuration(duration));
|
||||||
|
}
|
||||||
|
|
||||||
|
animations.add(ObjectAnimator.ofFloat(_layoutMoreButtons, "translationY", resources.displayMetrics.heightPixels.toFloat(), 0.0f).setDuration(duration))
|
||||||
for ((index, button) in _moreButtons.withIndex()) {
|
for ((index, button) in _moreButtons.withIndex()) {
|
||||||
val i = _moreButtons.size - index
|
val i = _moreButtons.size - index
|
||||||
animations.add(ObjectAnimator.ofFloat(button, "translationY", height * staggerFactor * (i + 1), 0.0f).setDuration(duration))
|
//animations.add(ObjectAnimator.ofFloat(button, "translationY", height * staggerFactor * (i + 1), 0.0f).setDuration(duration))
|
||||||
}
|
}
|
||||||
|
|
||||||
val animatorSet = AnimatorSet()
|
val animatorSet = AnimatorSet()
|
||||||
@@ -158,11 +261,24 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
animatorSet.start()
|
animatorSet.start()
|
||||||
} else {
|
} else {
|
||||||
val animations = arrayListOf<Animator>()
|
val animations = arrayListOf<Animator>()
|
||||||
animations.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 1.0f, 0.0f).setDuration(duration))
|
animations
|
||||||
|
.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 1.0f, 0.0f)
|
||||||
|
.setDuration(duration))
|
||||||
|
animations
|
||||||
|
.add(ObjectAnimator.ofFloat(_layoutMoreButtons, "alpha", 1.0f, 0.0f)
|
||||||
|
.setDuration(duration))
|
||||||
|
animations
|
||||||
|
.add(ObjectAnimator.ofFloat(_containerMoreHeader, "alpha", 1.0f, 0.0f)
|
||||||
|
.setDuration(duration))
|
||||||
|
_bottomButtons.find { it.definition.id == 99 }?.let {
|
||||||
|
animations.add(ObjectAnimator.ofFloat(it, "alpha", 1.0f, 0.5f)
|
||||||
|
.setDuration(duration));
|
||||||
|
}
|
||||||
|
|
||||||
|
animations.add(ObjectAnimator.ofFloat(_layoutMoreButtons, "translationY", 0.0f, resources.displayMetrics.heightPixels.toFloat()).setDuration(duration))
|
||||||
for ((index, button) in _moreButtons.withIndex()) {
|
for ((index, button) in _moreButtons.withIndex()) {
|
||||||
val i = _moreButtons.size - index
|
val i = _moreButtons.size - index
|
||||||
animations.add(ObjectAnimator.ofFloat(button, "translationY", 0.0f, height * staggerFactor * (i + 1)).setDuration(duration))
|
//animations.add(ObjectAnimator.ofFloat(button, "translationY", 0.0f, height * staggerFactor * (i + 1)).setDuration(duration))
|
||||||
}
|
}
|
||||||
|
|
||||||
val animatorSet = AnimatorSet()
|
val animatorSet = AnimatorSet()
|
||||||
@@ -174,11 +290,12 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
animatorSet.playTogether(animations)
|
animatorSet.playTogether(animations)
|
||||||
animatorSet.start()
|
animatorSet.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateBottomMenuButtons(buttons: MutableList<ButtonDefinition>, hasMore: Boolean) {
|
private fun updateBottomMenuButtons(buttons: MutableList<ButtonDefinition>, hasMore: Boolean) {
|
||||||
if (hasMore) {
|
if (hasMore) {
|
||||||
buttons.add(ButtonDefinition(99, R.drawable.ic_more, R.drawable.ic_more, R.string.more, canToggle = false, { false }, { setMoreVisible(true) }))
|
buttons.add(ButtonDefinition(99, R.drawable.ic_more, R.drawable.ic_more, R.string.more, canToggle = false, { false }, { setMoreVisible(!_moreVisible) }))
|
||||||
}
|
}
|
||||||
|
|
||||||
_bottomButtons.clear();
|
_bottomButtons.clear();
|
||||||
@@ -218,32 +335,42 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
_layoutMoreButtons.removeAllViews();
|
_layoutMoreButtons.removeAllViews();
|
||||||
|
|
||||||
var insertedButtons = 0;
|
var insertedButtons = 0;
|
||||||
|
//Force settings to be first
|
||||||
|
val settingsIndex = buttons.indexOfFirst { b -> b.id == 7 };
|
||||||
|
if (settingsIndex != -1) {
|
||||||
|
val button = buttons[settingsIndex]
|
||||||
|
buttons.removeAt(settingsIndex)
|
||||||
|
buttons.add(0, button)
|
||||||
|
//insertedButtons++;
|
||||||
|
}
|
||||||
//Force buy to be on top for more buttons
|
//Force buy to be on top for more buttons
|
||||||
val buyIndex = buttons.indexOfFirst { b -> b.id == 98 };
|
val buyIndex = buttons.indexOfFirst { b -> b.id == 98 };
|
||||||
if (buyIndex != -1) {
|
if (buyIndex != -1) {
|
||||||
val button = buttons[buyIndex]
|
val button = buttons[buyIndex]
|
||||||
buttons.removeAt(buyIndex)
|
buttons.removeAt(buyIndex)
|
||||||
buttons.add(0, button)
|
buttons.add(button)
|
||||||
insertedButtons++;
|
//insertedButtons++;
|
||||||
}
|
}
|
||||||
//Force faq to be second
|
//Force faq to be second
|
||||||
val faqIndex = buttons.indexOfFirst { b -> b.id == 97 };
|
val faqIndex = buttons.indexOfFirst { b -> b.id == 97 };
|
||||||
if (faqIndex != -1) {
|
if (faqIndex != -1) {
|
||||||
val button = buttons[faqIndex]
|
val button = buttons[faqIndex]
|
||||||
buttons.removeAt(faqIndex)
|
buttons.removeAt(faqIndex)
|
||||||
buttons.add(if (insertedButtons == 1) 1 else 0, button)
|
buttons.add(button)
|
||||||
insertedButtons++;
|
//insertedButtons++;
|
||||||
}
|
}
|
||||||
//Force privacy to be third
|
//Force privacy to be third
|
||||||
val privacyIndex = buttons.indexOfFirst { b -> b.id == 96 };
|
val privacyIndex = buttons.indexOfFirst { b -> b.id == 96 };
|
||||||
if (privacyIndex != -1) {
|
if (privacyIndex != -1) {
|
||||||
val button = buttons[privacyIndex]
|
val button = buttons[privacyIndex]
|
||||||
buttons.removeAt(privacyIndex)
|
buttons.removeAt(privacyIndex)
|
||||||
buttons.add(if (insertedButtons == 2) 2 else (if(insertedButtons == 1) 1 else 0), button)
|
buttons.add(button)
|
||||||
insertedButtons++;
|
//insertedButtons++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val newButtons = mutableListOf<MenuButtonItem>();
|
||||||
for (data in buttons) {
|
for (data in buttons) {
|
||||||
|
/*
|
||||||
val button = MenuButton(context, data, _fragment, true);
|
val button = MenuButton(context, data, _fragment, true);
|
||||||
button.setOnClickListener {
|
button.setOnClickListener {
|
||||||
updateMenuIcons()
|
updateMenuIcons()
|
||||||
@@ -253,14 +380,19 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
|
|
||||||
_moreButtons.add(button);
|
_moreButtons.add(button);
|
||||||
_layoutMoreButtons.addView(button);
|
_layoutMoreButtons.addView(button);
|
||||||
|
*/
|
||||||
|
val buttonItem = MenuButtonItem(data);
|
||||||
|
newButtons.add(buttonItem);
|
||||||
}
|
}
|
||||||
|
_layoutMoreButtonsAdapter.setData(newButtons);
|
||||||
|
_layoutMoreButtonsAdapter.notifyContentChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMenuIcons() {
|
private fun updateMenuIcons() {
|
||||||
for(button in _bottomButtons.toList())
|
for(button in _bottomButtons.toList())
|
||||||
button.updateActive(_fragment);
|
button.updateActive(_fragment);
|
||||||
for(button in _moreButtons.toList())
|
for(button in _moreButtons.toList())
|
||||||
button.updateActive(_fragment);
|
button.updateActive(_fragment, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||||
@@ -341,6 +473,71 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MenuButtonItem(val def: ButtonDefinition);
|
||||||
|
class MenuButtonItemViewHolder(private val _viewGroup: ViewGroup): AnyAdapter.AnyViewHolder<MenuButtonItem>(
|
||||||
|
LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_menu_tile,
|
||||||
|
_viewGroup, false)) {
|
||||||
|
|
||||||
|
val onClick = Event1<MenuButtonItem>();
|
||||||
|
|
||||||
|
val root: ConstraintLayout;
|
||||||
|
val imageIcon: ImageView;
|
||||||
|
val textName: TextView;
|
||||||
|
|
||||||
|
|
||||||
|
var button: MenuButtonItem? = null;
|
||||||
|
|
||||||
|
var parentFragment: MenuBottomBarFragment? = null;
|
||||||
|
|
||||||
|
init {
|
||||||
|
root = _view.findViewById(R.id.root);
|
||||||
|
imageIcon = _view.findViewById(R.id.image_icon);
|
||||||
|
textName = _view.findViewById(R.id.text_name);
|
||||||
|
|
||||||
|
root.setOnClickListener {
|
||||||
|
button?.let {
|
||||||
|
it.def.action(parentFragment ?: return@let);
|
||||||
|
onClick.emit(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun bind(value: MenuButtonItem) {
|
||||||
|
button = value;
|
||||||
|
textName.text = _view.context.getString(value.def.string);
|
||||||
|
imageIcon.setImageResource(value.def.iconActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun setWidth(dp: Int) {
|
||||||
|
root.updateLayoutParams {
|
||||||
|
this.width = (dp - 6).dp(_viewGroup.context.resources);
|
||||||
|
this.height = (dp - 6).dp(_viewGroup.context.resources);
|
||||||
|
}
|
||||||
|
imageIcon.updateLayoutParams {
|
||||||
|
this.width = (dp - 54).dp(_viewGroup.context.resources);
|
||||||
|
this.height = (dp - 54).dp(_viewGroup.context.resources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAutoSize(totalWidth: Float) {
|
||||||
|
val dpWidth = totalWidth;
|
||||||
|
val columns = Math.max(((dpWidth) / viewWidthDp).toInt(), 1);
|
||||||
|
val remainder = dpWidth - columns * viewWidthDp;
|
||||||
|
val targetSize = viewWidthDp + (remainder / columns).toInt();
|
||||||
|
setWidth(targetSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val viewWidthDp = 90;
|
||||||
|
fun getAutoSizeColumns(totalWidth: Float): Int {
|
||||||
|
val dpWidth = totalWidth;
|
||||||
|
val columns = Math.max(((dpWidth) / viewWidthDp).toInt(), 1);
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
class MenuButton: LinearLayout {
|
class MenuButton: LinearLayout {
|
||||||
val definition: ButtonDefinition;
|
val definition: ButtonDefinition;
|
||||||
|
|
||||||
@@ -354,7 +551,14 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
this.definition = def;
|
this.definition = def;
|
||||||
|
|
||||||
_buttonImage = findViewById(R.id.image_button);
|
_buttonImage = findViewById(R.id.image_button);
|
||||||
_buttonImage.setImageResource(if (def.isActive(fragment)) def.iconActive else def.icon);
|
//_buttonImage.setImageResource(if (def.isActive(fragment)) def.iconActive else def.icon);
|
||||||
|
_buttonImage.setImageResource(definition.iconActive);
|
||||||
|
if(definition.isActive(fragment) || isMore) {
|
||||||
|
this.alpha = 1f;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.alpha = 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
_textButton = findViewById(R.id.text_button);
|
_textButton = findViewById(R.id.text_button);
|
||||||
_textButton.text = resources.getString(def.string);
|
_textButton.text = resources.getString(def.string);
|
||||||
@@ -365,8 +569,16 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateActive(fragment: MenuBottomBarFragment) {
|
fun updateActive(fragment: MenuBottomBarFragment, isMore: Boolean = false, overrideValue: Boolean? = null) {
|
||||||
_buttonImage.setImageResource(if (definition.isActive(fragment)) definition.iconActive else definition.icon);
|
//_buttonImage.setImageResource(if (definition.isActive(fragment)) definition.iconActive else definition.icon);
|
||||||
|
_buttonImage.setImageResource(definition.iconActive);
|
||||||
|
val isActive = overrideValue ?: definition.isActive(fragment) || isMore
|
||||||
|
if(isActive) {
|
||||||
|
this.alpha = 1f;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.alpha = 0.5f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -389,6 +601,9 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate<SubscriptionsFeedFragment>(withHistory = false) }),
|
ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate<SubscriptionsFeedFragment>(withHistory = false) }),
|
||||||
|
//if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P)
|
||||||
|
ButtonDefinition(12, R.drawable.ic_library, R.drawable.ic_library, R.string.library, canToggle = false, { it.currentMain is LibraryFragment }, { it.navigate<LibraryFragment>(withHistory = false) })
|
||||||
|
,//else null,
|
||||||
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>(withHistory = false) }),
|
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>(withHistory = false) }),
|
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate<PlaylistsFragment>(withHistory = false) }),
|
ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate<PlaylistsFragment>(withHistory = false) }),
|
||||||
@@ -398,7 +613,9 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>(withHistory = false) }),
|
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>(withHistory = false) }),
|
ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(10, R.drawable.ic_help_square, R.drawable.ic_help_square_fill, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>(withHistory = false) }),
|
ButtonDefinition(10, R.drawable.ic_help_square, R.drawable.ic_help_square_fill, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, {
|
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { it.currentMain is SettingsFragment }, {
|
||||||
|
it.navigate<SettingsFragment>();
|
||||||
|
/*
|
||||||
val c = it.context ?: return@ButtonDefinition;
|
val c = it.context ?: return@ButtonDefinition;
|
||||||
Logger.i(TAG, "settings preventPictureInPicture()");
|
Logger.i(TAG, "settings preventPictureInPicture()");
|
||||||
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
|
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
|
||||||
@@ -406,8 +623,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
c.startActivity(intent);
|
c.startActivity(intent);
|
||||||
if (c is Activity) {
|
if (c is Activity) {
|
||||||
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
|
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
|
||||||
}
|
}*/
|
||||||
}),
|
}),/*
|
||||||
ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = true, { false }, {
|
ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = true, { false }, {
|
||||||
UIDialogs.showDialog(it.context ?: return@ButtonDefinition, R.drawable.ic_disabled_visible_purple, "Privacy Mode",
|
UIDialogs.showDialog(it.context ?: return@ButtonDefinition, R.drawable.ic_disabled_visible_purple, "Privacy Mode",
|
||||||
"All requests will be processed anonymously (any logins will be disabled except for the personalized home page), local playback and history tracking will also be disabled.\n\nTap the icon to disable.", null, 0,
|
"All requests will be processed anonymously (any logins will be disabled except for the personalized home page), local playback and history tracking will also be disabled.\n\nTap the icon to disable.", null, 0,
|
||||||
@@ -417,14 +634,14 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
UIDialogs.Action("Enable", {
|
UIDialogs.Action("Enable", {
|
||||||
StateApp.instance.setPrivacyMode(true);
|
StateApp.instance.setPrivacyMode(true);
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
}),
|
}),*/
|
||||||
ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = true, { false }, {
|
ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = true, { false }, {
|
||||||
it.navigate<BrowserFragment>(Settings.URL_FAQ, withHistory = false);
|
it.navigate<BrowserFragment>(Settings.URL_FAQ, withHistory = false);
|
||||||
})
|
})
|
||||||
//96 is reserved for privacy button
|
//96 is reserved for privacy button
|
||||||
//98 is reserved for buy button
|
//98 is reserved for buy button
|
||||||
//99 is reserved for more button
|
//99 is reserved for more button
|
||||||
);
|
).filterNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ButtonDefinition(
|
data class ButtonDefinition(
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.Artist
|
||||||
|
import com.futo.platformplayer.states.ArtistOrdering
|
||||||
|
import com.futo.platformplayer.states.FileEntry
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
|
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||||
|
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
||||||
|
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithViews
|
||||||
|
import com.futo.platformplayer.views.LibrarySection
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.ArtistTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.FileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.LocalVideoTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
|
||||||
|
|
||||||
|
class BaseFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var view: FragView? = null;
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val newView = FragView(this);
|
||||||
|
view = newView;
|
||||||
|
return newView;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = BaseFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FragView: ConstraintLayout {
|
||||||
|
val fragment: BaseFragment;
|
||||||
|
|
||||||
|
constructor(fragment: BaseFragment) : super(fragment.requireContext()) {
|
||||||
|
inflate(context, R.layout.fragview_library, this);
|
||||||
|
this.fragment = fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-4
@@ -20,6 +20,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.UISlideOverlays
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
@@ -55,6 +56,7 @@ import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter
|
|||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||||
|
import com.futo.platformplayer.withTimestamp
|
||||||
import com.futo.polycentric.core.ApiMethods
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.PolycentricProfile
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
@@ -198,8 +200,12 @@ class ChannelFragment : MainFragment() {
|
|||||||
adapter.onContentClicked.subscribe { v, _ ->
|
adapter.onContentClicked.subscribe { v, _ ->
|
||||||
when (v) {
|
when (v) {
|
||||||
is IPlatformVideo -> {
|
is IPlatformVideo -> {
|
||||||
StatePlayer.instance.clearQueue()
|
//StatePlayer.instance.clearQueue()
|
||||||
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail()
|
if (StatePlayer.instance.hasQueue) {
|
||||||
|
StatePlayer.instance.insertToQueue(v, true);
|
||||||
|
} else {
|
||||||
|
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is IPlatformPlaylist -> {
|
is IPlatformPlaylist -> {
|
||||||
@@ -244,7 +250,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
adapter.onContentUrlClicked.subscribe { url, contentType ->
|
adapter.onContentUrlClicked.subscribe { url, contentType ->
|
||||||
when (contentType) {
|
when (contentType) {
|
||||||
ContentType.MEDIA -> {
|
ContentType.MEDIA -> {
|
||||||
StatePlayer.instance.clearQueue()
|
StatePlayer.instance.clearQueue();
|
||||||
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
|
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,7 +409,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
_fragment.topBar?.onShown(channel)
|
_fragment.topBar?.onShown(channel)
|
||||||
|
|
||||||
val buttons = arrayListOf(Pair(R.drawable.ic_playlist_add) {
|
val buttons = arrayListOf(Pair(R.drawable.ic_playlist_add) {
|
||||||
UIDialogs.showConfirmationDialog(context,
|
val dialog = UIDialogs.showConfirmationDialog(context,
|
||||||
context.getString(R.string.do_you_want_to_convert_channel_channelname_to_a_playlist)
|
context.getString(R.string.do_you_want_to_convert_channel_channelname_to_a_playlist)
|
||||||
.replace("{channelName}", channel.name),
|
.replace("{channelName}", channel.name),
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
player.modifyState("ThumbnailPlayer") { state -> state.muted = true };
|
player.modifyState("ThumbnailPlayer") { state -> state.muted = true };
|
||||||
_exoPlayer = player;
|
_exoPlayer = player;
|
||||||
|
|
||||||
return PreviewContentListAdapter(fragment.lifecycleScope, context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
|
return PreviewContentListAdapter(fragment.lifecycleScope, context, feedStyle ?: FeedStyle.THUMBNAIL, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
|
||||||
attachAdapterEvents(this);
|
attachAdapterEvents(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.SettingsDev
|
||||||
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
|
import com.futo.platformplayer.views.fields.IField
|
||||||
|
|
||||||
|
|
||||||
|
class DeveloperFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var view: FragView? = null;
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val newView = FragView(this);
|
||||||
|
view = newView;
|
||||||
|
_currentView = view;
|
||||||
|
return newView;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
_currentView = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = DeveloperFragment().apply {}
|
||||||
|
|
||||||
|
private var _currentView: FragView? = null;
|
||||||
|
val currentView: FragView?
|
||||||
|
get() = _currentView;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FragView: ConstraintLayout {
|
||||||
|
val fragment: DeveloperFragment;
|
||||||
|
|
||||||
|
private lateinit var _form: FieldForm;
|
||||||
|
private lateinit var _buttonBack: ImageButton;
|
||||||
|
|
||||||
|
private var _isFinished = false;
|
||||||
|
|
||||||
|
lateinit var overlay: FrameLayout;
|
||||||
|
|
||||||
|
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
||||||
|
|
||||||
|
constructor(fragment: DeveloperFragment) : super(fragment.requireContext()) {
|
||||||
|
inflate(context, R.layout.activity_dev, this);
|
||||||
|
this.fragment = fragment;
|
||||||
|
|
||||||
|
val activity = fragment.activity;
|
||||||
|
findViewById<LinearLayout>(R.id.container_topbar).isVisible = false;
|
||||||
|
|
||||||
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
|
_form = findViewById(R.id.settings_form);
|
||||||
|
|
||||||
|
_form.fromObject(SettingsDev.instance);
|
||||||
|
_form.onChanged.subscribe { _, _ ->
|
||||||
|
_form.setObjectValues();
|
||||||
|
SettingsDev.instance.save();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getField(id: String): IField? {
|
||||||
|
return _form.findField(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -39,6 +40,7 @@ import java.time.OffsetDateTime
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
|
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
|
||||||
|
protected val _feedRoot: ConstraintLayout;
|
||||||
protected val _recyclerResults: RecyclerView;
|
protected val _recyclerResults: RecyclerView;
|
||||||
protected val _overlayContainer: FrameLayout;
|
protected val _overlayContainer: FrameLayout;
|
||||||
protected val _swipeRefresh: SwipeRefreshLayout;
|
protected val _swipeRefresh: SwipeRefreshLayout;
|
||||||
@@ -51,8 +53,9 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
private val _emptyPagerContainer: FrameLayout;
|
private val _emptyPagerContainer: FrameLayout;
|
||||||
|
|
||||||
protected val _toolbarContentView: LinearLayout;
|
protected val _toolbarContentView: LinearLayout;
|
||||||
|
protected val _bottomContentView: LinearLayout;
|
||||||
|
|
||||||
private var _loading: Boolean = true;
|
private var _loading: Boolean = false;
|
||||||
|
|
||||||
private val _pagerLock = Object();
|
private val _pagerLock = Object();
|
||||||
private var _cache: ItemCache<TResult>? = null;
|
private var _cache: ItemCache<TResult>? = null;
|
||||||
@@ -67,7 +70,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
private var _sortByOptions: List<String>? = null;
|
private var _sortByOptions: List<String>? = null;
|
||||||
private var _activeTags: List<String>? = null;
|
private var _activeTags: List<String>? = null;
|
||||||
|
|
||||||
private var _nextPageHandler: TaskHandler<TPager, List<TResult>>;
|
private var _nextPageHandler: TaskHandler<TPager, Pair<TPager, List<TResult>>>;
|
||||||
val recyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>;
|
val recyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>;
|
||||||
|
|
||||||
val fragment: TFragment;
|
val fragment: TFragment;
|
||||||
@@ -80,6 +83,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
this.fragment = fragment;
|
this.fragment = fragment;
|
||||||
inflater.inflate(R.layout.fragment_feed, this);
|
inflater.inflate(R.layout.fragment_feed, this);
|
||||||
|
|
||||||
|
_feedRoot = findViewById(R.id.feed_root);
|
||||||
_textCentered = findViewById(R.id.text_centered);
|
_textCentered = findViewById(R.id.text_centered);
|
||||||
_emptyPagerContainer = findViewById(R.id.empty_pager_container);
|
_emptyPagerContainer = findViewById(R.id.empty_pager_container);
|
||||||
_progressBar = findViewById(R.id.progress_bar);
|
_progressBar = findViewById(R.id.progress_bar);
|
||||||
@@ -134,24 +138,29 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
setActiveTags(null);
|
setActiveTags(null);
|
||||||
|
|
||||||
_toolbarContentView = findViewById(R.id.container_toolbar_content);
|
_toolbarContentView = findViewById(R.id.container_toolbar_content);
|
||||||
|
_bottomContentView = findViewById(R.id.container_bottom);
|
||||||
|
|
||||||
_nextPageHandler = TaskHandler<TPager, List<TResult>>({fragment.lifecycleScope}, {
|
_nextPageHandler = TaskHandler<TPager, Pair<TPager, List<TResult>>>({fragment.lifecycleScope}, {
|
||||||
if (it is IAsyncPager<*>)
|
if (it is IAsyncPager<*>)
|
||||||
it.nextPageAsync();
|
it.nextPageAsync();
|
||||||
else
|
else
|
||||||
it.nextPage();
|
it.nextPage();
|
||||||
|
|
||||||
processPagerExceptions(it);
|
processPagerExceptions(it);
|
||||||
return@TaskHandler it.getResults();
|
return@TaskHandler Pair(it, it.getResults());
|
||||||
}).success {
|
}).success {
|
||||||
|
val pager = it.first;
|
||||||
|
val results = it.second
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
val posBefore = recyclerData.results.size;
|
val posBefore = recyclerData.results.size;
|
||||||
val filteredResults = filterResults(it);
|
val filteredResults = filterResults(results);
|
||||||
recyclerData.results.addAll(filteredResults);
|
recyclerData.results.addAll(filteredResults);
|
||||||
recyclerData.resultsUnfiltered.addAll(it);
|
recyclerData.resultsUnfiltered.addAll(results);
|
||||||
recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size);
|
recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size);
|
||||||
ensureEnoughContentVisible(filteredResults)
|
if(pager.hasMorePages())
|
||||||
|
ensureEnoughContentVisible(filteredResults)
|
||||||
}.exception<Throwable> {
|
}.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load next page.", it);
|
Logger.w(TAG, "Failed to load next page.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||||
@@ -171,10 +180,9 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
|
|
||||||
val visibleItemCount = _recyclerResults.childCount;
|
val visibleItemCount = _recyclerResults.childCount;
|
||||||
val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPosition()
|
val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPosition()
|
||||||
//Logger.i(TAG, "onScrolled loadNextPage visibleItemCount=$visibleItemCount firstVisibleItem=$visibleItemCount")
|
//Logger.i(TAG, "onScrolled loadNextPage(): firstVisibleItem=$firstVisibleItem visibleItemCount=$visibleItemCount visibleThreshold=$visibleThreshold recyclerData.results.size=${recyclerData.results.size}")
|
||||||
|
|
||||||
if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= recyclerData.results.size && firstVisibleItem > 0) {
|
if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= recyclerData.results.size) {
|
||||||
//Logger.i(TAG, "onScrolled loadNextPage(): firstVisibleItem=$firstVisibleItem visibleItemCount=$visibleItemCount visibleThreshold=$visibleThreshold recyclerData.results.size=${recyclerData.results.size}")
|
|
||||||
loadNextPage();
|
loadNextPage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,57 +196,44 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
|
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
|
||||||
val canScroll = if (recyclerData.results.isEmpty()) false else {
|
_recyclerResults.post {
|
||||||
val height = resources.displayMetrics.heightPixels;
|
val canScroll = _recyclerResults.canScrollVertically(1)
|
||||||
|
Logger.i(
|
||||||
|
TAG,
|
||||||
|
"ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter"
|
||||||
|
)
|
||||||
|
if (!canScroll || filteredResults.isEmpty()) {
|
||||||
|
_automaticNextPageCounter++
|
||||||
|
if (_automaticNextPageCounter < _automaticBackoff.size) {
|
||||||
|
if (_automaticNextPageCounter > 0) {
|
||||||
|
val automaticNextPageCounterSaved = _automaticNextPageCounter;
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Default) {
|
||||||
|
val backoff = _automaticBackoff[Math.min(
|
||||||
|
_automaticBackoff.size - 1,
|
||||||
|
_automaticNextPageCounter
|
||||||
|
)];
|
||||||
|
|
||||||
val layoutManager = recyclerData.layoutManager
|
|
||||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
|
||||||
val firstVisibleItemView = if(firstVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(firstVisibleItemPosition) else null;
|
|
||||||
val lastVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition();
|
|
||||||
val lastVisibleItemView = if(lastVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(lastVisibleItemPosition) else null;
|
|
||||||
val rows = if(recyclerData.layoutManager is GridLayoutManager) Math.max(1, recyclerData.results.size / recyclerData.layoutManager.spanCount) else 1;
|
|
||||||
val rowsHeight = (firstVisibleItemView?.height ?: 0) * rows;
|
|
||||||
if(lastVisibleItemView != null && lastVisibleItemPosition == (recyclerData.results.size - 1)) {
|
|
||||||
false;
|
|
||||||
}
|
|
||||||
else if (firstVisibleItemView != null && height != null && rowsHeight < height) {
|
|
||||||
false;
|
|
||||||
} else {
|
|
||||||
true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter")
|
|
||||||
if (!canScroll || filteredResults.isEmpty()) {
|
|
||||||
_automaticNextPageCounter++
|
|
||||||
if(_automaticNextPageCounter < _automaticBackoff.size) {
|
|
||||||
if(_automaticNextPageCounter > 0) {
|
|
||||||
val automaticNextPageCounterSaved = _automaticNextPageCounter;
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Default) {
|
|
||||||
val backoff = _automaticBackoff[Math.min(_automaticBackoff.size - 1, _automaticNextPageCounter)];
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
setLoading(true);
|
|
||||||
}
|
|
||||||
delay(backoff.toLong());
|
|
||||||
if(automaticNextPageCounterSaved == _automaticNextPageCounter) {
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
loadNextPage();
|
setLoading(true);
|
||||||
|
}
|
||||||
|
delay(backoff.toLong());
|
||||||
|
if (automaticNextPageCounterSaved == _automaticNextPageCounter) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
loadNextPage();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
} else
|
||||||
withContext(Dispatchers.Main) {
|
loadNextPage();
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
} else {
|
||||||
loadNextPage();
|
Logger.i(TAG, "ensureEnoughContentVisible automaticNextPageCounter reset");
|
||||||
|
_automaticNextPageCounter = 0;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Logger.i(TAG, "ensureEnoughContentVisible automaticNextPageCounter reset");
|
|
||||||
_automaticNextPageCounter = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun resetAutomaticNextPageCounter(){
|
fun resetAutomaticNextPageCounter(){
|
||||||
@@ -390,6 +385,9 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
protected fun finishRefreshLayoutLoader() {
|
protected fun finishRefreshLayoutLoader() {
|
||||||
_swipeRefresh.isRefreshing = false;
|
_swipeRefresh.isRefreshing = false;
|
||||||
}
|
}
|
||||||
|
protected fun disableRefreshLayout() {
|
||||||
|
_swipeRefresh.isEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
fun clearResults(){
|
fun clearResults(){
|
||||||
setPager(EmptyPager<TResult>() as TPager);
|
setPager(EmptyPager<TResult>() as TPager);
|
||||||
@@ -472,7 +470,9 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
recyclerData.resultsUnfiltered.addAll(toAdd);
|
recyclerData.resultsUnfiltered.addAll(toAdd);
|
||||||
recyclerData.adapter.notifyDataSetChanged();
|
recyclerData.adapter.notifyDataSetChanged();
|
||||||
recyclerData.loadedFeedStyle = feedStyle;
|
recyclerData.loadedFeedStyle = feedStyle;
|
||||||
ensureEnoughContentVisible(filteredResults)
|
setLoading(false)
|
||||||
|
if(pager.hasMorePages())
|
||||||
|
ensureEnoughContentVisible(filteredResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun detachPagerEvents() {
|
private fun detachPagerEvents() {
|
||||||
|
|||||||
+15
-3
@@ -26,6 +26,7 @@ import com.futo.platformplayer.models.HistoryVideo
|
|||||||
import com.futo.platformplayer.states.StateHistory
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.views.ToggleBar
|
import com.futo.platformplayer.views.ToggleBar
|
||||||
import com.futo.platformplayer.views.adapters.HistoryListViewHolder
|
import com.futo.platformplayer.views.adapters.HistoryListViewHolder
|
||||||
@@ -243,12 +244,23 @@ class HistoryFragment : MainFragment() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
|
||||||
val diff = v.video.duration - v.position;
|
val diff = v.video.duration - v.position;
|
||||||
val vid: Any = if (diff > 5) { v.video.withTimestamp(v.position) } else { v.video };
|
val vid: Any = if (diff > 5) { v.video.withTimestamp(v.position) } else { v.video };
|
||||||
StatePlayer.instance.clearQueue();
|
|
||||||
_fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail();
|
val playlistId = v.playlistId
|
||||||
|
val playlist = playlistId?.let { StatePlaylists.instance.getPlaylist(it) }
|
||||||
|
val playlistIndex = playlist?.videos?.indexOfFirst { it.url == v.video.url }
|
||||||
|
if (playlist != null && playlistIndex != null && playlistIndex >= 0) {
|
||||||
|
_fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail();
|
||||||
|
StatePlayer.instance.setPlaylist(playlist, playlistIndex)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
StatePlayer.instance.clearQueue();
|
||||||
|
_fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail();
|
||||||
|
}
|
||||||
|
|
||||||
_editSearch.clearFocus();
|
_editSearch.clearFocus();
|
||||||
|
val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
||||||
inputMethodManager.hideSoftInputFromWindow(_editSearch.windowToken, 0);
|
inputMethodManager.hideSoftInputFromWindow(_editSearch.windowToken, 0);
|
||||||
|
|
||||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
|||||||
+3
-1
@@ -365,8 +365,10 @@ class HomeFragment : MainFragment() {
|
|||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setPager(pager);
|
setPager(pager);
|
||||||
if(pager.getResults().isEmpty() && !pager.hasMorePages())
|
if(pager.getResults().isEmpty() && !pager.hasMorePages()) {
|
||||||
|
setLoading(false);
|
||||||
setEmptyPager(true);
|
setEmptyPager(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+159
@@ -0,0 +1,159 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.toHumanDuration
|
||||||
|
import com.futo.platformplayer.views.AlbumHeaderView
|
||||||
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.TrackViewHolder
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryAlbumFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var view: FragView? = null;
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): android.view.View {
|
||||||
|
val newView = FragView(this, inflater);
|
||||||
|
view = newView;
|
||||||
|
return newView;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown(parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibraryAlbumFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FragView : FeedView<LibraryAlbumFragment, IPlatformVideo, IPlatformVideo, IPager<IPlatformVideo>, TrackViewHolder> {
|
||||||
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
|
private val _header: AlbumHeaderView;
|
||||||
|
|
||||||
|
private var _album: Album? = null;
|
||||||
|
private var _tracks: List<IPlatformVideo>? = null;
|
||||||
|
private var _url: String? = null;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryAlbumFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||||
|
_header = AlbumHeaderView(context);
|
||||||
|
_toolbarContentView.addView(_header);
|
||||||
|
|
||||||
|
_header.onPlayAll.subscribe {
|
||||||
|
val playlist = _album?.toPlaylist(_tracks);
|
||||||
|
if (playlist != null) {
|
||||||
|
StatePlayer.instance.setPlaylist(playlist, focus = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_header.onShuffle.subscribe {
|
||||||
|
val playlist = _album?.toPlaylist(_tracks);
|
||||||
|
if (playlist != null) {
|
||||||
|
StatePlayer.instance.setPlaylist(playlist, focus = true, shuffle = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
_feedRoot.updateLayoutParams<LinearLayout.LayoutParams> {
|
||||||
|
this.setMargins(0,-50.dp(resources),0,0)
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown(parameter: Any?) {
|
||||||
|
val album = if(parameter is String)
|
||||||
|
StateLibrary.instance.getAlbum(parameter);
|
||||||
|
else if(parameter is Long)
|
||||||
|
StateLibrary.instance.getAlbum(parameter);
|
||||||
|
else if(parameter is Album)
|
||||||
|
parameter;
|
||||||
|
else null;
|
||||||
|
if(album == null) {
|
||||||
|
_album = null;
|
||||||
|
_tracks = null;
|
||||||
|
setPager(EmptyPager());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_header.setName(album.name);
|
||||||
|
_header.setThumbnail(album.thumbnail);
|
||||||
|
val tracks = album.getTracks();
|
||||||
|
_album = album;
|
||||||
|
_tracks = tracks;
|
||||||
|
_header.setMetadata("${tracks.size} tracks" + if(tracks.size > 0) (" • " + tracks.sumOf { it.duration }.toHumanDuration(false)) else "");
|
||||||
|
setPager(AdhocPager({listOf()}, tracks));
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<IPlatformVideo>): InsertedViewAdapterWithLoader<TrackViewHolder> {
|
||||||
|
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
|
||||||
|
childCountGetter = { dataset.size },
|
||||||
|
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
|
||||||
|
childViewHolderFactory = { viewGroup, _ ->
|
||||||
|
val holder = TrackViewHolder(viewGroup);
|
||||||
|
holder.onClick.subscribe { c ->
|
||||||
|
|
||||||
|
val playlist = _album?.toPlaylist(_tracks);
|
||||||
|
val index = playlist?.videos?.indexOfFirst { it.name == c.name } ?: -1;
|
||||||
|
if (playlist != null) {
|
||||||
|
if (index == -1)
|
||||||
|
return@subscribe;
|
||||||
|
|
||||||
|
StatePlayer.instance.setPlaylist(playlist, index, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
holder.onOptions.subscribe {
|
||||||
|
if(it is IPlatformVideo)
|
||||||
|
UISlideOverlays.showVideoOptionsOverlay(it, _overlayContainer);
|
||||||
|
}
|
||||||
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSpanCount(){ }
|
||||||
|
|
||||||
|
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
|
||||||
|
val glmResults = GridLayoutManager(context, 1)
|
||||||
|
|
||||||
|
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||||
|
rightMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8.0f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
return glmResults
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryArtistsFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout.GONE
|
||||||
|
import android.widget.LinearLayout.VISIBLE
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.LibraryTypeHeaderView
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
|
||||||
|
class LibraryAlbumsFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
|
||||||
|
var view: FragView? = null;
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val view = FragView(this, inflater);
|
||||||
|
this.view = view;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShown(parameter, isBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibraryAlbumsFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FragView : FeedView<LibraryAlbumsFragment, Album, Album, IPager<Album>, AlbumTileViewHolder> {
|
||||||
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
|
val libraryTypeHeader: LibraryTypeHeaderView;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryAlbumsFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||||
|
libraryTypeHeader = LibraryTypeHeaderView(context);
|
||||||
|
libraryTypeHeader.setSelectedType(LibraryTypeHeaderView.SelectedType.Albums);
|
||||||
|
libraryTypeHeader.setMetadata("");
|
||||||
|
|
||||||
|
libraryTypeHeader.onSelectedChanged.subscribe {
|
||||||
|
when(it) {
|
||||||
|
LibraryTypeHeaderView.SelectedType.Artists -> fragment.navigate<LibraryArtistsFragment>();
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_toolbarContentView.addView(libraryTypeHeader);
|
||||||
|
disableRefreshLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
val initialAlbums = StateLibrary.instance.getAlbums();
|
||||||
|
Logger.i(TAG, "Initial album count: " + initialAlbums.size);
|
||||||
|
|
||||||
|
libraryTypeHeader.setMetadata("${initialAlbums.size} albums");
|
||||||
|
setPager(AdhocPager<Album>({ listOf(); }, initialAlbums));
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reload() {
|
||||||
|
super.reload();
|
||||||
|
finishRefreshLayoutLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<Album>): InsertedViewAdapterWithLoader<AlbumTileViewHolder> {
|
||||||
|
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
|
||||||
|
childCountGetter = { dataset.size },
|
||||||
|
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
|
||||||
|
childViewHolderFactory = { viewGroup, _ ->
|
||||||
|
val holder = AlbumTileViewHolder(viewGroup);
|
||||||
|
holder.setAutoSize(resources.displayMetrics.widthPixels / resources.displayMetrics.density)
|
||||||
|
holder.onClick.subscribe { c -> fragment.navigate<LibraryAlbumFragment>(c) };
|
||||||
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSpanCount(){ }
|
||||||
|
|
||||||
|
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
|
||||||
|
val glmResults = GridLayoutManager(context, AlbumTileViewHolder.getAutoSizeColumns(resources.displayMetrics.widthPixels / resources.displayMetrics.density))
|
||||||
|
|
||||||
|
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||||
|
leftMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
3f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
return glmResults
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryAlbumsFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AlbumViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<Album>(
|
||||||
|
LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_album,
|
||||||
|
_viewGroup, false)) {
|
||||||
|
|
||||||
|
val onClick = Event1<Album?>();
|
||||||
|
|
||||||
|
protected var _album: Album? = null;
|
||||||
|
protected val _imageThumbnail: ImageView
|
||||||
|
protected val _textName: TextView
|
||||||
|
protected val _textMetadata: TextView
|
||||||
|
|
||||||
|
init {
|
||||||
|
_imageThumbnail = _view.findViewById(R.id.image_thumbnail);
|
||||||
|
_textName = _view.findViewById(R.id.text_name);
|
||||||
|
_textMetadata = _view.findViewById(R.id.text_metadata);
|
||||||
|
|
||||||
|
_view.setOnClickListener { onClick.emit(_album) };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun bind(album: Album) {
|
||||||
|
_album = album;
|
||||||
|
_imageThumbnail?.let {
|
||||||
|
if (album.thumbnail != null)
|
||||||
|
Glide.with(it)
|
||||||
|
.load(album.thumbnail)
|
||||||
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
|
.into(it)
|
||||||
|
else
|
||||||
|
Glide.with(it).load(R.drawable.placeholder_channel_thumbnail).into(it);
|
||||||
|
};
|
||||||
|
|
||||||
|
_textName.text = album.name;
|
||||||
|
_textMetadata.text = album.artist ?: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+634
@@ -0,0 +1,634 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.assume
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
import com.futo.platformplayer.constructs.Event3
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumsFragment.AlbumViewHolder
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.SearchType
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.Artist
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.TrackViewHolder
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
|
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class LibraryArtistFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var _textMeta: TextView? = null;
|
||||||
|
|
||||||
|
var view: FragView? = null;
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val view = FragView(this, inflater);
|
||||||
|
this.view = view;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShown(parameter, isBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown(parameter, isBack);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibraryArtistFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FragView(fragment: LibraryArtistFragment, inflater: LayoutInflater) : LinearLayout(inflater.context) {
|
||||||
|
private val _fragment: LibraryArtistFragment = fragment
|
||||||
|
|
||||||
|
private var _textChannel: TextView
|
||||||
|
private var _textChannelSub: TextView
|
||||||
|
private var _creatorThumbnail: CreatorThumbnail
|
||||||
|
private var _imageBanner: AppCompatImageView
|
||||||
|
|
||||||
|
private var _tabs: TabLayout
|
||||||
|
private var _viewPager: ViewPager2
|
||||||
|
|
||||||
|
// private var _adapter: ChannelViewPagerAdapter;
|
||||||
|
private var _tabLayoutMediator: TabLayoutMediator
|
||||||
|
private var _buttonSubscribe: SubscribeButton
|
||||||
|
private var _buttonSubscriptionSettings: ImageButton
|
||||||
|
|
||||||
|
private var _overlayContainer: FrameLayout
|
||||||
|
private var _overlayLoading: LinearLayout
|
||||||
|
private var _overlayLoadingSpinner: ImageView
|
||||||
|
|
||||||
|
private var _slideUpOverlay: SlideUpMenuOverlay? = null
|
||||||
|
|
||||||
|
private var _isLoading: Boolean = false
|
||||||
|
private var _selectedTabIndex: Int = -1
|
||||||
|
var channel: Artist? = null
|
||||||
|
private set
|
||||||
|
private var _url: String? = null
|
||||||
|
|
||||||
|
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
|
||||||
|
|
||||||
|
init {
|
||||||
|
inflater.inflate(R.layout.fragment_artist, this)
|
||||||
|
|
||||||
|
val tabs: TabLayout = findViewById(R.id.tabs)
|
||||||
|
val viewPager: ViewPager2 = findViewById(R.id.view_pager)
|
||||||
|
_textChannel = findViewById(R.id.text_channel_name)
|
||||||
|
_textChannelSub = findViewById(R.id.text_metadata)
|
||||||
|
_creatorThumbnail = findViewById(R.id.creator_thumbnail)
|
||||||
|
_imageBanner = findViewById(R.id.image_channel_banner)
|
||||||
|
_buttonSubscribe = findViewById(R.id.button_subscribe)
|
||||||
|
_buttonSubscriptionSettings = findViewById(R.id.button_sub_settings)
|
||||||
|
_overlayLoading = findViewById(R.id.channel_loading_overlay)
|
||||||
|
_overlayLoadingSpinner = findViewById(R.id.channel_loader_frag)
|
||||||
|
_overlayContainer = findViewById(R.id.overlay_container)
|
||||||
|
_buttonSubscribe.onSubscribed.subscribe {
|
||||||
|
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer)
|
||||||
|
_buttonSubscriptionSettings.visibility =
|
||||||
|
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
_buttonSubscribe.onUnSubscribed.subscribe {
|
||||||
|
_buttonSubscriptionSettings.visibility =
|
||||||
|
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
_buttonSubscriptionSettings.setOnClickListener {
|
||||||
|
val url = channel?.contentUrl ?: _url ?: return@setOnClickListener
|
||||||
|
val sub =
|
||||||
|
StateSubscriptions.instance.getSubscription(url) ?: return@setOnClickListener
|
||||||
|
UISlideOverlays.showSubscriptionOptionsOverlay(sub, _overlayContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Determine if this is really the only solution (isSaveEnabled=false)
|
||||||
|
viewPager.isSaveEnabled = false
|
||||||
|
viewPager.registerOnPageChangeCallback(_onPageChangeCallback)
|
||||||
|
val adapter = ArtistViewPagerAdapter(fragment, fragment.childFragmentManager, fragment.lifecycle)
|
||||||
|
adapter.onChannelClicked.subscribe { c -> fragment.navigate<ChannelFragment>(c) }
|
||||||
|
adapter.onContentClicked.subscribe { v, _ ->
|
||||||
|
when (v) {
|
||||||
|
is IPlatformVideo -> {
|
||||||
|
StatePlayer.instance.clearQueue()
|
||||||
|
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail()
|
||||||
|
}
|
||||||
|
|
||||||
|
is IPlatformPlaylist -> {
|
||||||
|
fragment.navigate<RemotePlaylistFragment>(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
is IPlatformPost -> {
|
||||||
|
fragment.navigate<PostDetailFragment>(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.onShortClicked.subscribe { v, _, pagerPair ->
|
||||||
|
when (v) {
|
||||||
|
is IPlatformVideo -> {
|
||||||
|
StatePlayer.instance.clearQueue()
|
||||||
|
fragment.navigate<ShortsFragment>(Triple(v, pagerPair!!.first, pagerPair.second))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.onAddToClicked.subscribe { content ->
|
||||||
|
_overlayContainer.let {
|
||||||
|
if (content is IPlatformVideo) _slideUpOverlay =
|
||||||
|
UISlideOverlays.showVideoOptionsOverlay(content, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.onAddToQueueClicked.subscribe { content ->
|
||||||
|
if (content is IPlatformVideo) {
|
||||||
|
StatePlayer.instance.addToQueue(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||||
|
if (content is IPlatformVideo) {
|
||||||
|
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
|
||||||
|
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||||
|
else
|
||||||
|
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.onUrlClicked.subscribe { url ->
|
||||||
|
fragment.navigate<BrowserFragment>(url)
|
||||||
|
}
|
||||||
|
adapter.onContentUrlClicked.subscribe { url, contentType ->
|
||||||
|
when (contentType) {
|
||||||
|
ContentType.MEDIA -> {
|
||||||
|
StatePlayer.instance.clearQueue()
|
||||||
|
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentType.URL -> fragment.navigate<BrowserFragment>(url)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.onLongPress.subscribe { content ->
|
||||||
|
_overlayContainer.let {
|
||||||
|
if (content is IPlatformVideo) _slideUpOverlay =
|
||||||
|
UISlideOverlays.showVideoOptionsOverlay(content, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewPager.adapter = adapter
|
||||||
|
val tabLayoutMediator = TabLayoutMediator(
|
||||||
|
tabs, viewPager, (viewPager.adapter as ArtistViewPagerAdapter)::getTabNames
|
||||||
|
)
|
||||||
|
tabLayoutMediator.attach()
|
||||||
|
|
||||||
|
_tabLayoutMediator = tabLayoutMediator
|
||||||
|
_tabs = tabs
|
||||||
|
_viewPager = viewPager
|
||||||
|
if (_selectedTabIndex != -1) {
|
||||||
|
selectTab(_selectedTabIndex)
|
||||||
|
}
|
||||||
|
setLoading(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectTab(tab: ArtistTab) {
|
||||||
|
(_viewPager.adapter as ArtistViewPagerAdapter).getTabPosition(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cleanup() {
|
||||||
|
_tabLayoutMediator.detach()
|
||||||
|
_viewPager.unregisterOnPageChangeCallback(_onPageChangeCallback)
|
||||||
|
hideSlideUpOverlay()
|
||||||
|
(_overlayLoadingSpinner.drawable as Animatable?)?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
|
hideSlideUpOverlay()
|
||||||
|
_selectedTabIndex = -1
|
||||||
|
|
||||||
|
if (!isBack || _url == null) {
|
||||||
|
_imageBanner.setImageDrawable(null)
|
||||||
|
|
||||||
|
when (parameter) {
|
||||||
|
is String -> {
|
||||||
|
_buttonSubscribe.setSubscribeChannel(parameter)
|
||||||
|
_buttonSubscriptionSettings.visibility =
|
||||||
|
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
_url = parameter
|
||||||
|
|
||||||
|
val parsed = Uri.parse(parameter);
|
||||||
|
val idLong = parsed.lastPathSegment?.toLongOrNull();
|
||||||
|
if(idLong != null) {
|
||||||
|
val artist = StateLibrary.instance.getArtist(idLong) ?: return;
|
||||||
|
showArtist(artist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is Artist -> {
|
||||||
|
showArtist(parameter)
|
||||||
|
_url = parameter.contentUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectTab(selectedTabIndex: Int) {
|
||||||
|
_selectedTabIndex = selectedTabIndex
|
||||||
|
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(isLoading: Boolean) {
|
||||||
|
if (_isLoading == isLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoading = isLoading
|
||||||
|
if (isLoading) {
|
||||||
|
_overlayLoading.visibility = View.VISIBLE
|
||||||
|
(_overlayLoadingSpinner.drawable as Animatable?)?.start()
|
||||||
|
} else {
|
||||||
|
(_overlayLoadingSpinner.drawable as Animatable?)?.stop()
|
||||||
|
_overlayLoading.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBackPressed(): Boolean {
|
||||||
|
if (_slideUpOverlay != null) {
|
||||||
|
hideSlideUpOverlay()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideSlideUpOverlay() {
|
||||||
|
_slideUpOverlay?.hide(false)
|
||||||
|
_slideUpOverlay = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showArtist(channel: Artist) {
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
_fragment.topBar?.onShown(channel)
|
||||||
|
|
||||||
|
val buttons = arrayListOf<Pair<Int, ()->Unit>>();
|
||||||
|
|
||||||
|
_fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val plugin = StatePlatform.instance.getChannelClientOrNull(channel.contentUrl ?: return@launch)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
buttons.add(Pair(R.drawable.ic_search) {
|
||||||
|
_fragment.navigate<SuggestionsFragment>(
|
||||||
|
SuggestionsFragmentData(
|
||||||
|
"", SearchType.VIDEO
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_buttonSubscribe.visibility = GONE;
|
||||||
|
_buttonSubscriptionSettings.visibility = View.GONE
|
||||||
|
_textChannel.text = channel.name
|
||||||
|
_textChannelSub.text = "${channel.countTracks} songs, ${channel.countAlbums} albums";
|
||||||
|
|
||||||
|
var supportsPlaylists = false;
|
||||||
|
val playlistPosition = 1
|
||||||
|
// keep the current tab selected
|
||||||
|
if (_viewPager.currentItem >= playlistPosition) {
|
||||||
|
_viewPager.setCurrentItem(_viewPager.currentItem + 1, false)
|
||||||
|
}
|
||||||
|
(_viewPager.adapter as ArtistViewPagerAdapter).insert(
|
||||||
|
playlistPosition,
|
||||||
|
ArtistTab.ALBUMS
|
||||||
|
)
|
||||||
|
|
||||||
|
// sets the channel for each tab
|
||||||
|
for (fragment in _fragment.childFragmentManager.fragments) {
|
||||||
|
(fragment as IArtistTabFragment).setArtist(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
(_viewPager.adapter as ArtistViewPagerAdapter).artist = channel
|
||||||
|
|
||||||
|
|
||||||
|
_viewPager.adapter!!.notifyDataSetChanged();
|
||||||
|
|
||||||
|
val artistThumbnail = channel.getThumbnailOrAlbum();
|
||||||
|
if(artistThumbnail != null) {
|
||||||
|
_creatorThumbnail.isVisible = true;
|
||||||
|
_creatorThumbnail.setThumbnail(channel.getThumbnailOrAlbum(), true, true);
|
||||||
|
Glide.with(_imageBanner)
|
||||||
|
.load(artistThumbnail)
|
||||||
|
.into(_imageBanner);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_creatorThumbnail.isVisible = false;
|
||||||
|
Glide.with(_imageBanner).clear(_imageBanner);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.channel = channel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryArtistFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enum class ArtistTab {
|
||||||
|
SONGS, ALBUMS
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtistViewPagerAdapter(private val fragment: LibraryArtistFragment, fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||||
|
FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||||
|
private val _supportedFragments = mutableMapOf(
|
||||||
|
ArtistTab.SONGS.ordinal to ArtistTab.SONGS
|
||||||
|
)
|
||||||
|
private val _tabs = arrayListOf(ArtistTab.SONGS, ArtistTab.ALBUMS)
|
||||||
|
|
||||||
|
var artist: Artist? = null
|
||||||
|
|
||||||
|
val onContentUrlClicked = Event2<String, ContentType>()
|
||||||
|
val onUrlClicked = Event1<String>()
|
||||||
|
val onContentClicked = Event2<IPlatformContent, Long>()
|
||||||
|
val onShortClicked = Event3<IPlatformContent, Long, Pair<IPager<IPlatformContent>, ArrayList<IPlatformContent>>?>()
|
||||||
|
val onChannelClicked = Event1<PlatformAuthorLink>()
|
||||||
|
val onAddToClicked = Event1<IPlatformContent>()
|
||||||
|
val onAddToQueueClicked = Event1<IPlatformContent>()
|
||||||
|
val onAddToWatchLaterClicked = Event1<IPlatformContent>()
|
||||||
|
val onLongPress = Event1<IPlatformContent>()
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long {
|
||||||
|
return _tabs[position].ordinal.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun containsItem(itemId: Long): Boolean {
|
||||||
|
return _supportedFragments.containsKey(itemId.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return _supportedFragments.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTabPosition(tab: ArtistTab): Int {
|
||||||
|
return _tabs.indexOf(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTabNames(tab: TabLayout.Tab, position: Int) {
|
||||||
|
tab.text = _tabs[position].name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun insert(position: Int, tab: ArtistTab) {
|
||||||
|
_supportedFragments[tab.ordinal] = tab
|
||||||
|
_tabs.add(position, tab)
|
||||||
|
notifyItemInserted(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(position: Int) {
|
||||||
|
_supportedFragments.remove(_tabs[position].ordinal)
|
||||||
|
_tabs.removeAt(position)
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
val fragment: Fragment
|
||||||
|
when (_tabs[position]) {
|
||||||
|
ArtistTab.SONGS -> {
|
||||||
|
fragment = ChannelContentsFragment(this.fragment).apply {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistTab.ALBUMS -> {
|
||||||
|
fragment = ArtistAlbumsFragment(this.fragment).apply {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
artist?.let { (fragment as IArtistTabFragment).setArtist(it) }
|
||||||
|
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IArtistTabFragment {
|
||||||
|
fun setArtist(artist: Artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChannelContentsFragment(private val frag: LibraryArtistFragment) : Fragment(), IArtistTabFragment {
|
||||||
|
|
||||||
|
var view: ArtistContentView? = null;
|
||||||
|
|
||||||
|
private var _lastArtist: Artist? = null;
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
view = ArtistContentView(frag, inflater);
|
||||||
|
_lastArtist?.let {
|
||||||
|
view?.setArtist(it);
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setArtist(artist: Artist) {
|
||||||
|
view?.setArtist(artist);
|
||||||
|
_lastArtist = artist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ArtistContentView : FeedView<LibraryArtistFragment, IPlatformContent, IPlatformVideo, IPager<IPlatformContent>, TrackViewHolder> {
|
||||||
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
|
protected var _artist: Artist? = null;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryArtistFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setArtist(artist: Artist) {
|
||||||
|
this._artist = artist;
|
||||||
|
val tracks = artist.getAudioTracks();
|
||||||
|
if(tracks.getResults().isEmpty())
|
||||||
|
UIDialogs.appToast("No tracks found");
|
||||||
|
setPager(tracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformVideo> {
|
||||||
|
return results.filter { it is IPlatformVideo }.map { it as IPlatformVideo };
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<IPlatformVideo>): InsertedViewAdapterWithLoader<TrackViewHolder> {
|
||||||
|
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
|
||||||
|
childCountGetter = { dataset.size },
|
||||||
|
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
|
||||||
|
childViewHolderFactory = { viewGroup, _ ->
|
||||||
|
val holder = TrackViewHolder(viewGroup);
|
||||||
|
holder.onClick.subscribe { c ->
|
||||||
|
|
||||||
|
val playlist = _artist?.toPlaylist();
|
||||||
|
if (playlist != null) {
|
||||||
|
val sameVideo = playlist.videos.find { it.name == c.name };
|
||||||
|
val index = sameVideo?.let {
|
||||||
|
playlist.videos.indexOf(sameVideo)
|
||||||
|
} ?: -1;
|
||||||
|
if (index == -1)
|
||||||
|
return@subscribe;
|
||||||
|
|
||||||
|
StatePlayer.instance.setPlaylist(playlist, index, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
holder.onOptions.subscribe {
|
||||||
|
if(it is IPlatformVideo)
|
||||||
|
UISlideOverlays.showVideoOptionsOverlay(it, _overlayContainer);
|
||||||
|
}
|
||||||
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
|
||||||
|
val glmResults = GridLayoutManager(context, 1)
|
||||||
|
|
||||||
|
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||||
|
rightMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8.0f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
return glmResults
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSpanCount(){ }
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryAlbumsFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ArtistAlbumsFragment(private val frag: LibraryArtistFragment) : Fragment(), IArtistTabFragment {
|
||||||
|
|
||||||
|
var view: ArtistAlbumsView? = null;
|
||||||
|
|
||||||
|
private var _lastArtist: Artist? = null;
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
view = ArtistAlbumsView(frag, inflater);
|
||||||
|
_lastArtist?.let {
|
||||||
|
view?.setArtist(it);
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setArtist(artist: Artist) {
|
||||||
|
view?.setArtist(artist);
|
||||||
|
_lastArtist = artist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ArtistAlbumsView : FeedView<LibraryArtistFragment, Album, Album, IPager<Album>, AlbumTileViewHolder> {
|
||||||
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryArtistFragment, inflater: LayoutInflater) : super(fragment, inflater)
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setArtist(artist: Artist) {
|
||||||
|
val initialAlbums = artist.getAlbums();
|
||||||
|
Logger.i(TAG, "Initial album count: " + initialAlbums.size);
|
||||||
|
|
||||||
|
setPager(AdhocPager({ listOf() }, initialAlbums));
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<Album>): InsertedViewAdapterWithLoader<AlbumTileViewHolder> {
|
||||||
|
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
|
||||||
|
childCountGetter = { dataset.size },
|
||||||
|
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
|
||||||
|
childViewHolderFactory = { viewGroup, _ ->
|
||||||
|
val holder = AlbumTileViewHolder(viewGroup);
|
||||||
|
holder.setAutoSize(resources.displayMetrics.widthPixels / resources.displayMetrics.density)
|
||||||
|
holder.onClick.subscribe { c -> fragment.navigate<LibraryAlbumFragment>(c) };
|
||||||
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSpanCount(){ }
|
||||||
|
|
||||||
|
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
|
||||||
|
val glmResults = GridLayoutManager(context, AlbumTileViewHolder.getAutoSizeColumns(resources.displayMetrics.widthPixels / resources.displayMetrics.density))
|
||||||
|
|
||||||
|
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||||
|
rightMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8.0f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
return glmResults
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryAlbumsFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+200
@@ -0,0 +1,200 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.LinearLayout.GONE
|
||||||
|
import android.widget.LinearLayout.VISIBLE
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.Artist
|
||||||
|
import com.futo.platformplayer.states.ArtistOrdering
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.LibraryTypeHeaderView
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
|
||||||
|
class LibraryArtistsFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var _textMeta: TextView? = null;
|
||||||
|
|
||||||
|
var view: FragView? = null;
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val view = FragView(this, inflater);
|
||||||
|
this.view = view;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShown(parameter, isBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibraryArtistsFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FragView : FeedView<LibraryArtistsFragment, Artist, Artist, IPager<Artist>, ArtistViewHolder> {
|
||||||
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
|
val libraryTypeHeader: LibraryTypeHeaderView;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryArtistsFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||||
|
libraryTypeHeader = LibraryTypeHeaderView(context);
|
||||||
|
libraryTypeHeader.setSelectedType(LibraryTypeHeaderView.SelectedType.Artists);
|
||||||
|
libraryTypeHeader.setMetadata("");
|
||||||
|
|
||||||
|
libraryTypeHeader.onSelectedChanged.subscribe {
|
||||||
|
when(it) {
|
||||||
|
LibraryTypeHeaderView.SelectedType.Albums -> fragment.navigate<LibraryAlbumsFragment>();
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_toolbarContentView.addView(libraryTypeHeader);
|
||||||
|
disableRefreshLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun reload() {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
val intialArtists = StateLibrary.instance.getArtists(ArtistOrdering.Alphabethic);
|
||||||
|
Logger.i(TAG, "Initial album count: " + intialArtists.size);
|
||||||
|
|
||||||
|
libraryTypeHeader.setMetadata("${intialArtists.size} artists");
|
||||||
|
setPager(AdhocPager<Artist>({ listOf(); }, intialArtists));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<Artist>): InsertedViewAdapterWithLoader<ArtistViewHolder> {
|
||||||
|
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
|
||||||
|
childCountGetter = { dataset.size },
|
||||||
|
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
|
||||||
|
childViewHolderFactory = { viewGroup, _ ->
|
||||||
|
val holder = ArtistViewHolder(viewGroup);
|
||||||
|
holder.onClick.subscribe { c ->
|
||||||
|
fragment.navigate<LibraryArtistFragment>(c)
|
||||||
|
};
|
||||||
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSpanCount(){ }
|
||||||
|
|
||||||
|
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
|
||||||
|
val glmResults = GridLayoutManager(context, 1)
|
||||||
|
|
||||||
|
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||||
|
rightMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8.0f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
return glmResults
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryArtistsFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtistViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<Artist>(
|
||||||
|
LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_artist,
|
||||||
|
_viewGroup, false)) {
|
||||||
|
|
||||||
|
val onClick = Event1<Artist>();
|
||||||
|
|
||||||
|
protected var _artist: Artist? = null;
|
||||||
|
//protected val _imageThumbnail: ImageView
|
||||||
|
protected val _textName: TextView
|
||||||
|
protected val _textMetadata: TextView
|
||||||
|
|
||||||
|
init {
|
||||||
|
//_imageThumbnail = _view.findViewById(R.id.image_thumbnail);
|
||||||
|
_textName = _view.findViewById(R.id.text_name);
|
||||||
|
_textMetadata = _view.findViewById(R.id.text_metadata);
|
||||||
|
|
||||||
|
_view.setOnClickListener { _artist?.let { onClick.emit(it) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(artist: Artist) {
|
||||||
|
_artist = artist;
|
||||||
|
/*
|
||||||
|
_imageThumbnail?.let {
|
||||||
|
if (artist.thumbnail != null)
|
||||||
|
Glide.with(it)
|
||||||
|
.load(artist.thumbnail)
|
||||||
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
|
.into(it)
|
||||||
|
else
|
||||||
|
Glide.with(it).load(R.drawable.placeholder_channel_thumbnail).into(it);
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
_textName.text = artist.name;
|
||||||
|
|
||||||
|
val metaComps = listOf(
|
||||||
|
artist.countTracks?.let { "${it} tracks" },
|
||||||
|
artist.countAlbums?.let { "${it} albums" }
|
||||||
|
).filterNotNull();
|
||||||
|
|
||||||
|
_textMetadata.text = metaComps.joinToString(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+264
@@ -0,0 +1,264 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.api.media.models.video.LocalVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.topbar.FilesTopBarFragment
|
||||||
|
import com.futo.platformplayer.models.Playlist
|
||||||
|
import com.futo.platformplayer.states.FileEntry
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.NoResultsView
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.FileViewHolder
|
||||||
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import com.futo.platformplayer.views.buttons.ButtonsContainer
|
||||||
|
|
||||||
|
class LibraryFilesFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
|
||||||
|
var view: FragView? = null;
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val view = FragView(this, inflater);
|
||||||
|
this.view = view;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShown(parameter, isBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown(parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibraryFilesFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FragView : FeedView<LibraryFilesFragment, FileEntry, FileEntry, IPager<FileEntry>, FileViewHolder> {
|
||||||
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
|
val navStack = mutableListOf<FileStack>()
|
||||||
|
var buttonUp: BigButton? = null;
|
||||||
|
var buttonAdd: BigButton? = null;
|
||||||
|
|
||||||
|
private var root: FileEntry? = null;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryFilesFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||||
|
disableRefreshLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown(parameter: Any? = null) {
|
||||||
|
this.root = if(parameter is FileEntry) parameter else null;
|
||||||
|
loadTop();
|
||||||
|
}
|
||||||
|
fun loadTop() {
|
||||||
|
var initialDirectories = listOf<FileEntry>();
|
||||||
|
var path = "";
|
||||||
|
if(root == null) {
|
||||||
|
initialDirectories = StateLibrary.instance.getFileDirectories();
|
||||||
|
if (initialDirectories.size == 0) {
|
||||||
|
setEmptyPager(true);
|
||||||
|
setPager(EmptyPager());
|
||||||
|
buttonAdd?.let {
|
||||||
|
it.isVisible = false;
|
||||||
|
}
|
||||||
|
buttonUp?.let {
|
||||||
|
it.isVisible = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else
|
||||||
|
setEmptyPager(false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buttonAdd?.let {
|
||||||
|
it.isVisible = false;
|
||||||
|
}
|
||||||
|
buttonUp?.let {
|
||||||
|
it.isVisible = false;
|
||||||
|
}
|
||||||
|
initialDirectories = root?.getSubFiles() ?: listOf();
|
||||||
|
path = root?.path ?: "";
|
||||||
|
}
|
||||||
|
navStack.clear();
|
||||||
|
val entry = FileStack(path, initialDirectories);
|
||||||
|
navStack.add(entry);
|
||||||
|
openDirectory(navStack.last());
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is FilesTopBarFragment) {
|
||||||
|
it.setUpNavigate(null);
|
||||||
|
it.setTitle(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun leaveDirectory() {
|
||||||
|
if (navStack.size > 1) {
|
||||||
|
navStack.removeAt(navStack.size - 1)
|
||||||
|
openDirectory(navStack.last())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun openDirectory(stack: FileStack, addToStack: Boolean = false) {
|
||||||
|
if(addToStack)
|
||||||
|
navStack.add(stack);
|
||||||
|
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is FilesTopBarFragment) {
|
||||||
|
it.setTitle(stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonAdd?.let {
|
||||||
|
it.isVisible = navStack.size < 2
|
||||||
|
}
|
||||||
|
buttonUp?.let {
|
||||||
|
it.isVisible = navStack.size > 1;
|
||||||
|
}
|
||||||
|
setPager(AdhocPager<FileEntry>({ listOf(); }, stack.files));
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
val allSongs = stack.files.filter { !it.isDirectory };
|
||||||
|
if(allSongs.any()) {
|
||||||
|
_bottomContentView.addView(ButtonsContainer(context,
|
||||||
|
listOf(
|
||||||
|
ButtonsContainer.Button("Play All", R.drawable.background_button_primary) {
|
||||||
|
StatePlayer.instance.setPlaylist(Playlist(stack.path.toUri().lastPathSegment ?: "", allSongs.map {
|
||||||
|
SerializedPlatformVideo.fromVideo(LocalVideoDetails.fromContent(it.path))
|
||||||
|
}), focus = true, shuffle = false)
|
||||||
|
},
|
||||||
|
ButtonsContainer.Button("Shuffle", R.drawable.background_button_accent) {
|
||||||
|
StatePlayer.instance.setPlaylist(Playlist(stack.path.toUri().lastPathSegment ?: "", allSongs.map {
|
||||||
|
SerializedPlatformVideo.fromVideo(LocalVideoDetails.fromContent(it.path))
|
||||||
|
}), focus = true, shuffle = true)
|
||||||
|
}
|
||||||
|
)).apply {
|
||||||
|
this.layoutParams = LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_bottomContentView.removeAllViews();
|
||||||
|
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is FilesTopBarFragment) {
|
||||||
|
if(navStack.size > 1)
|
||||||
|
it.setUpNavigate{
|
||||||
|
leaveDirectory();
|
||||||
|
};
|
||||||
|
else it.setUpNavigate(null);
|
||||||
|
it.setTitle(stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBack() {
|
||||||
|
fragment.topBar?.view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEmptyPagerView(): View? {
|
||||||
|
return NoResultsView(context, "No Directories Added",
|
||||||
|
"To see files in Grayjay you have to add directories to view",
|
||||||
|
R.drawable.ic_library, listOf(
|
||||||
|
BigButton(context, "Add Directory", "Select a directory to add", R.drawable.ic_add, {
|
||||||
|
StateLibrary.instance.addFileDirectory({
|
||||||
|
loadTop();
|
||||||
|
}, true);
|
||||||
|
})
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<FileEntry>): InsertedViewAdapterWithLoader<FileViewHolder> {
|
||||||
|
/*
|
||||||
|
val buttonUp = BigButton(fragment.requireContext(), "Go up", "Go up a directory", R.drawable.ic_move_up) {
|
||||||
|
if(navStack.size > 1)
|
||||||
|
leaveDirectory();
|
||||||
|
}
|
||||||
|
val buttonAdd = BigButton(fragment.requireContext(), "Add Directory", "Select a directory to add", R.drawable.ic_add) {
|
||||||
|
StateLibrary.instance.addFileDirectory {
|
||||||
|
loadTop();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
//this.buttonUp = buttonUp;
|
||||||
|
//this.buttonAdd = buttonAdd;
|
||||||
|
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
|
||||||
|
childCountGetter = { dataset.size },
|
||||||
|
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
|
||||||
|
childViewHolderFactory = { viewGroup, _ ->
|
||||||
|
val holder = FileViewHolder(viewGroup);
|
||||||
|
holder.onClick.subscribe { c ->
|
||||||
|
if (c != null) {
|
||||||
|
if(c.isDirectory) {
|
||||||
|
openDirectory(FileStack(c.path, c.getSubFiles()), true);
|
||||||
|
} else {
|
||||||
|
fragment.navigate<VideoDetailFragment>(c.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
holder.onDelete.subscribe { c ->
|
||||||
|
if(c != null) {
|
||||||
|
StateLibrary.instance.deleteFileDirectory(c.path);
|
||||||
|
loadTop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSpanCount(){ }
|
||||||
|
|
||||||
|
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
|
||||||
|
val glmResults = GridLayoutManager(context, 1)
|
||||||
|
|
||||||
|
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||||
|
rightMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8.0f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
return glmResults
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryAlbumsFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class FileStack(
|
||||||
|
val path: String,
|
||||||
|
val files: List<FileEntry>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
+393
@@ -0,0 +1,393 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.collection.emptyLongSet
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.Artist
|
||||||
|
import com.futo.platformplayer.states.ArtistOrdering
|
||||||
|
import com.futo.platformplayer.states.FileEntry
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
|
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||||
|
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
||||||
|
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithViews
|
||||||
|
import com.futo.platformplayer.views.LibrarySection
|
||||||
|
import com.futo.platformplayer.views.NoResultsView
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.ArtistTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.FileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.LocalVideoTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.Dispatcher
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var view: FragView? = null;
|
||||||
|
|
||||||
|
private var allowedMusic = false;
|
||||||
|
private var allowedVideo = false;
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): android.view.View {
|
||||||
|
val newView = FragView(this, allowedMusic, allowedVideo);
|
||||||
|
view = newView;
|
||||||
|
return newView;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown();
|
||||||
|
|
||||||
|
requestPermissionMusic();
|
||||||
|
requestPermissionVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPermissionResultAudio(access: Boolean) {
|
||||||
|
allowedMusic = access;
|
||||||
|
view?.setMusicPermissions(access);
|
||||||
|
StateApp.instance.hasMediaStoreAudioPermission = (access);
|
||||||
|
}
|
||||||
|
fun setPermissionResultVideo(access: Boolean) {
|
||||||
|
allowedVideo = access;
|
||||||
|
view?.setVideoPermissions(access);
|
||||||
|
StateApp.instance.hasMediaStoreVideoPermission = (access);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestPermissionMusic() {
|
||||||
|
when {
|
||||||
|
ContextCompat.checkSelfPermission(requireContext(), android.Manifest.permission.READ_MEDIA_AUDIO) == PackageManager.PERMISSION_GRANTED -> {
|
||||||
|
setPermissionResultAudio(true);
|
||||||
|
}
|
||||||
|
ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), android.Manifest.permission.READ_MEDIA_AUDIO) -> {
|
||||||
|
UIDialogs.showDialog(requireContext(), R.drawable.ic_library,
|
||||||
|
"Music permissions", "We require permissions to see your on-device music, denying this will hide the option to see local music.", null, 1,
|
||||||
|
UIDialogs.Action("Ok", {
|
||||||
|
StateApp?.instance?.activity?.requestPermissionAudio {
|
||||||
|
setPermissionResultAudio(it);
|
||||||
|
}
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY),
|
||||||
|
UIDialogs.Action("Cancel", {
|
||||||
|
|
||||||
|
}, UIDialogs.ActionStyle.NONE));
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
StateApp?.instance?.activity?.requestPermissionAudio {
|
||||||
|
setPermissionResultAudio(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun requestPermissionVideo() {
|
||||||
|
when {
|
||||||
|
ContextCompat.checkSelfPermission(requireContext(), android.Manifest.permission.READ_MEDIA_VIDEO) == PackageManager.PERMISSION_GRANTED -> {
|
||||||
|
setPermissionResultVideo(true);
|
||||||
|
}
|
||||||
|
ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), android.Manifest.permission.READ_MEDIA_VIDEO) -> {
|
||||||
|
UIDialogs.showDialog(requireContext(), R.drawable.ic_library, false,
|
||||||
|
"Videos permissions", "We require permissions to see your on-device videos, denying this will hide the option to see local videos.", null, 1,
|
||||||
|
UIDialogs.Action("Ok", {
|
||||||
|
StateApp?.instance?.activity?.requestPermissionVideo {
|
||||||
|
setPermissionResultVideo(it);
|
||||||
|
}
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY),
|
||||||
|
UIDialogs.Action("Cancel", {
|
||||||
|
|
||||||
|
}, UIDialogs.ActionStyle.NONE));
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
StateApp?.instance?.activity?.requestPermissionVideo {
|
||||||
|
setPermissionResultVideo(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibraryFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FragView: ConstraintLayout {
|
||||||
|
val fragment: LibraryFragment;
|
||||||
|
|
||||||
|
var sectionArtists: LibrarySection;
|
||||||
|
var sectionAlbums: LibrarySection;
|
||||||
|
var sectionVideos: LibrarySection;
|
||||||
|
var sectionFiles: LibrarySection;
|
||||||
|
var noContent: NoResultsView;
|
||||||
|
//var buttonFiles: BigButton;
|
||||||
|
|
||||||
|
val recycler: RecyclerView;
|
||||||
|
|
||||||
|
var adapterFiles: AnyInsertedAdapterView<FileEntry, FileViewHolder>? = null;
|
||||||
|
|
||||||
|
//var metaInfo: TextView;
|
||||||
|
|
||||||
|
var allowMusic: Boolean = false;
|
||||||
|
var allowVideo: Boolean = false;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryFragment, allowMusic: Boolean?, allowVideo: Boolean?) : super(fragment.requireContext()) {
|
||||||
|
inflate(context, R.layout.fragview_library, this);
|
||||||
|
this.fragment = fragment;
|
||||||
|
recycler = findViewById(R.id.recycler);
|
||||||
|
sectionArtists = LibrarySection(context)//findViewById<LibrarySection>(R.id.section_artists);
|
||||||
|
sectionArtists.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 140.dp(resources)).apply {
|
||||||
|
this.setMargins(0,10.dp(resources), 0, 0);
|
||||||
|
}
|
||||||
|
sectionAlbums = LibrarySection(context)//findViewById(R.id.section_albums);
|
||||||
|
sectionAlbums.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 185.dp(resources)).apply {
|
||||||
|
this.setMargins(0,0, 0, 0);
|
||||||
|
}
|
||||||
|
sectionVideos = LibrarySection(context)//findViewById(R.id.section_videos);
|
||||||
|
sectionVideos.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 170.dp(resources)).apply {
|
||||||
|
this.setMargins(0,0, 0, 0);
|
||||||
|
}
|
||||||
|
sectionFiles = LibrarySection(context)//findViewById(R.id.section_videos);
|
||||||
|
sectionFiles.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 40.dp(resources)).apply {
|
||||||
|
this.setMargins(0,0, 0, 0);
|
||||||
|
}
|
||||||
|
sectionFiles.setSection("Directories") {
|
||||||
|
StateLibrary.instance.addFileDirectory({
|
||||||
|
reloadFiles();
|
||||||
|
}, true)
|
||||||
|
}
|
||||||
|
sectionFiles.setNavIcon(R.drawable.ic_add);
|
||||||
|
//buttonFiles = findViewById<BigButton>(R.id.button_files);
|
||||||
|
//metaInfo = findViewById(R.id.meta_info);
|
||||||
|
|
||||||
|
noContent = NoResultsView(context, "No directories", "No directories have been added.\nAdd them using the (+) icon.", -1, listOf());
|
||||||
|
noContent.isVisible = false;
|
||||||
|
|
||||||
|
this.allowMusic = allowMusic ?: false;
|
||||||
|
this.allowVideo = allowVideo ?: false;
|
||||||
|
|
||||||
|
sectionArtists.setSection("Artists", {
|
||||||
|
if(this.allowMusic)
|
||||||
|
fragment.navigate<LibraryArtistsFragment>();
|
||||||
|
else
|
||||||
|
fragment.requestPermissionMusic();
|
||||||
|
});
|
||||||
|
|
||||||
|
sectionAlbums.setSection("Albums", {
|
||||||
|
if(this.allowMusic)
|
||||||
|
fragment.navigate<LibraryAlbumsFragment>();
|
||||||
|
else
|
||||||
|
fragment.requestPermissionMusic();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
sectionVideos.setSection("Videos", {
|
||||||
|
if(this.allowVideo)
|
||||||
|
fragment.navigate<LibraryVideosFragment>();
|
||||||
|
else
|
||||||
|
fragment.requestPermissionVideo();
|
||||||
|
});
|
||||||
|
|
||||||
|
reloadLibraryUI();
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
buttonFiles.onClick.subscribe {
|
||||||
|
fragment.navigate<LibraryFilesFragment>()
|
||||||
|
} */
|
||||||
|
//buttonFiles.setButtonEnabled(false);
|
||||||
|
setMusicPermissions(allowMusic ?: false);
|
||||||
|
setVideoPermissions(allowVideo ?: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reloadFiles() {
|
||||||
|
val files = StateLibrary.instance.getFileDirectories();
|
||||||
|
adapterFiles?.setData(files);
|
||||||
|
if(files.size == 0) {
|
||||||
|
noContent.isVisible = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
noContent.isVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reloadLibraryUI() {
|
||||||
|
|
||||||
|
val adapterAlbums = sectionAlbums.getAnyAdapter<Album, AlbumTileViewHolder>({
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it != null)
|
||||||
|
fragment.navigate<LibraryAlbumFragment>(it);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
val adapterArtists = sectionArtists.getAnyAdapter<Artist, ArtistTileViewHolder>({
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it != null)
|
||||||
|
fragment.navigate<LibraryArtistFragment>(it);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
val adapterVideos = sectionVideos.getAnyAdapter<IPlatformVideo, LocalVideoTileViewHolder>({
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it != null)
|
||||||
|
fragment.navigate<VideoDetailFragment>(it);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(this.allowMusic) {
|
||||||
|
val artists = StateLibrary.instance.getArtists(ArtistOrdering.TrackCount);
|
||||||
|
adapterArtists.setData(artists);
|
||||||
|
if (artists.size == 0)
|
||||||
|
sectionArtists.setEmpty(
|
||||||
|
"No artists",
|
||||||
|
"No artists were found on your device",
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
else
|
||||||
|
sectionArtists.clearEmpty();
|
||||||
|
}
|
||||||
|
else if(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
sectionAlbums.isVisible = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sectionArtists.setEmpty(
|
||||||
|
"No Music Permissions",
|
||||||
|
"You have not granted music access permissions to Grayjay",
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.allowMusic) {
|
||||||
|
val albums = StateLibrary.instance.getAlbums();
|
||||||
|
adapterAlbums.setData(albums);
|
||||||
|
if (albums.size == 0)
|
||||||
|
sectionAlbums.setEmpty("No albums", "No albums were found on your device", -1);
|
||||||
|
else
|
||||||
|
sectionAlbums.clearEmpty();
|
||||||
|
}
|
||||||
|
else if(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
sectionArtists.isVisible = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sectionAlbums.setEmpty(
|
||||||
|
"No Music Permissions",
|
||||||
|
"You have not granted music access permissions to Grayjay",
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.allowVideo) {
|
||||||
|
val videos = StateLibrary.instance.getRecentVideos(null, 20);
|
||||||
|
adapterVideos.setData(videos);
|
||||||
|
if (videos.size == 0)
|
||||||
|
sectionVideos.setEmpty("No videos", "No videos were found on your device", -1);
|
||||||
|
else
|
||||||
|
sectionVideos.clearEmpty();
|
||||||
|
}
|
||||||
|
else if(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
sectionVideos.isVisible = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sectionVideos.setEmpty(
|
||||||
|
"No Video Permissions",
|
||||||
|
"You have not granted video access permissions to Grayjay",
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterFiles = recycler.asAnyWithViews<FileEntry, FileViewHolder>(
|
||||||
|
arrayListOf(
|
||||||
|
sectionArtists,
|
||||||
|
sectionAlbums,
|
||||||
|
sectionVideos,
|
||||||
|
sectionFiles,
|
||||||
|
noContent
|
||||||
|
),
|
||||||
|
arrayListOf(View(context).apply { this.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 20.dp(resources)) }),
|
||||||
|
RecyclerView.VERTICAL, false, {
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it != null)
|
||||||
|
fragment.navigate<LibraryFilesFragment>(it);
|
||||||
|
}
|
||||||
|
it.onDelete.subscribe {
|
||||||
|
if(it != null) {
|
||||||
|
StateLibrary.instance.deleteFileDirectory(it.path);
|
||||||
|
reloadFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
reloadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMusicPermissions(access: Boolean) {
|
||||||
|
allowMusic = access;
|
||||||
|
sectionAlbums.setContentEmptyMessage(R.drawable.ic_library, "No mediastore permissions");
|
||||||
|
sectionArtists.setContentEmptyMessage(R.drawable.ic_library, "No mediastore permissions");
|
||||||
|
//buttonArtists.setButtonEnabled(access);
|
||||||
|
//metaInfo.text = listOf(
|
||||||
|
// if(!allowMusic) "You did not give access to local music, so these options are disabled" else null,
|
||||||
|
// if(!allowVideo) "You did not give access to local videos, so these options are disabled" else null
|
||||||
|
//).filterNotNull().joinToString("\n");
|
||||||
|
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
reloadLibraryUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun setVideoPermissions(access: Boolean) {
|
||||||
|
allowVideo = access;
|
||||||
|
sectionVideos.setContentEmptyMessage(R.drawable.ic_library, "No video permissions");
|
||||||
|
//metaInfo.text = listOf(
|
||||||
|
// if(!allowMusic) "You did not give access to local music, so these options are disabled" else null,
|
||||||
|
// if(!allowVideo) "You did not give access to local videos, so these options are disabled" else null
|
||||||
|
//).filterNotNull().joinToString("\n");
|
||||||
|
// }
|
||||||
|
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
reloadLibraryUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
if(didShowAlpha)
|
||||||
|
return;
|
||||||
|
didShowAlpha = true;
|
||||||
|
UIDialogs.appToast("Library is in alpha\nImprovements are coming to local media playback.")
|
||||||
|
}
|
||||||
|
companion object {
|
||||||
|
var didShowAlpha: Boolean = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+233
@@ -0,0 +1,233 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.LinearLayout.GONE
|
||||||
|
import android.widget.LinearLayout.VISIBLE
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.Artist
|
||||||
|
import com.futo.platformplayer.states.ArtistOrdering
|
||||||
|
import com.futo.platformplayer.states.FileEntry
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
|
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||||
|
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithViews
|
||||||
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.LibrarySection
|
||||||
|
import com.futo.platformplayer.views.LibraryTypeHeaderView
|
||||||
|
import com.futo.platformplayer.views.LibraryTypeHeaderView.SelectedType
|
||||||
|
import com.futo.platformplayer.views.PillV2
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.ArtistTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.FileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.LocalVideoTileViewHolder
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.TrackViewHolder
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
|
||||||
|
class LibrarySearchFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
|
||||||
|
var view: FragView? = null;
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val view = FragView(this);
|
||||||
|
this.view = view;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShown(parameter, isBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibrarySearchFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FragView: ConstraintLayout {
|
||||||
|
val fragment: LibrarySearchFragment;
|
||||||
|
|
||||||
|
val pillArtist: PillV2;
|
||||||
|
val pillAlbums: PillV2;
|
||||||
|
val pillSongs: PillV2;
|
||||||
|
val pills: List<PillV2>;
|
||||||
|
|
||||||
|
val textMetadata: TextView;
|
||||||
|
|
||||||
|
val recycler: RecyclerView;
|
||||||
|
|
||||||
|
val adapterArtists: AnyAdapterView<Artist, LibraryArtistsFragment.ArtistViewHolder>;
|
||||||
|
val adapterSongs: AnyAdapterView<IPlatformContent, TrackViewHolder>;
|
||||||
|
val adapterAlbums: AnyAdapterView<Album, LibraryAlbumsFragment.AlbumViewHolder>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
constructor(fragment: LibrarySearchFragment) : super(fragment.requireContext()) {
|
||||||
|
inflate(context, R.layout.fragview_library_search, this);
|
||||||
|
this.fragment = fragment;
|
||||||
|
|
||||||
|
pillArtist = findViewById(R.id.pill_artist);
|
||||||
|
pillAlbums = findViewById(R.id.pill_albums);
|
||||||
|
pillSongs = findViewById(R.id.pill_songs);
|
||||||
|
pills = listOf(pillArtist, pillAlbums, pillSongs);
|
||||||
|
|
||||||
|
textMetadata = findViewById(R.id.text_metadata);
|
||||||
|
|
||||||
|
pillArtist.onClick.subscribe {
|
||||||
|
pills.forEach { it.setIsEnabled(false) };
|
||||||
|
pillArtist.setIsEnabled(true);
|
||||||
|
loadArtists();
|
||||||
|
}
|
||||||
|
pillAlbums.onClick.subscribe {
|
||||||
|
pills.forEach { it.setIsEnabled(false) };
|
||||||
|
pillAlbums.setIsEnabled(true);
|
||||||
|
loadAlbums();
|
||||||
|
}
|
||||||
|
pillSongs.onClick.subscribe {
|
||||||
|
pills.forEach { it.setIsEnabled(false) };
|
||||||
|
pillSongs.setIsEnabled(true);
|
||||||
|
loadSongs();
|
||||||
|
}
|
||||||
|
|
||||||
|
recycler = findViewById(R.id.recycler);
|
||||||
|
adapterArtists = recycler.asAny<Artist, LibraryArtistsFragment.ArtistViewHolder>(RecyclerView.VERTICAL, false, {
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it != null)
|
||||||
|
fragment.navigate<LibraryArtistFragment>(it);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
adapterAlbums = recycler.asAny<Album, LibraryAlbumsFragment.AlbumViewHolder>(RecyclerView.VERTICAL, false, {
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it != null)
|
||||||
|
fragment.navigate<LibraryAlbumFragment>(it);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
adapterSongs = recycler.asAny<IPlatformContent, TrackViewHolder>(RecyclerView.VERTICAL, false, {
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it != null && it is IPlatformVideo)
|
||||||
|
fragment.navigate<VideoDetailFragment>(it);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is SearchTopBarFragment) {
|
||||||
|
it.onSearch.subscribe {
|
||||||
|
search(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pillArtist.setIsEnabled(true);
|
||||||
|
loadArtists();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadArtists(){
|
||||||
|
recycler.adapter = adapterArtists.adapter.adapter;
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is SearchTopBarFragment)
|
||||||
|
search(it.getSearchText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun loadAlbums() {
|
||||||
|
recycler.adapter = adapterAlbums.adapter.adapter;
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is SearchTopBarFragment)
|
||||||
|
search(it.getSearchText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun loadSongs() {
|
||||||
|
recycler.adapter = adapterSongs.adapter.adapter;
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is SearchTopBarFragment)
|
||||||
|
search(it.getSearchText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(str: String) {
|
||||||
|
if(recycler.adapter == adapterArtists.adapter.adapter) {
|
||||||
|
val data = if(!str.isNullOrBlank())
|
||||||
|
StateLibrary.instance.searchArtists(str)
|
||||||
|
else listOf();
|
||||||
|
adapterArtists.setData(data);
|
||||||
|
textMetadata.text = "${data.size} artists";
|
||||||
|
}
|
||||||
|
else if(recycler.adapter == adapterAlbums.adapter.adapter) {
|
||||||
|
val data = if(!str.isNullOrBlank())
|
||||||
|
StateLibrary.instance.searchAlbums(str)
|
||||||
|
else listOf();
|
||||||
|
adapterAlbums.setData(data);
|
||||||
|
textMetadata.text = "${data.size} albums";
|
||||||
|
}
|
||||||
|
else if(recycler.adapter == adapterSongs.adapter.adapter) {
|
||||||
|
val data = if(!str.isNullOrBlank())
|
||||||
|
StateLibrary.instance.searchTracks(str)
|
||||||
|
else listOf();
|
||||||
|
|
||||||
|
adapterSongs.setData(data);
|
||||||
|
textMetadata.text = "${data.size} songs";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
fragment.topBar?.let {
|
||||||
|
if(it is SearchTopBarFragment)
|
||||||
|
it.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout.GONE
|
||||||
|
import android.widget.LinearLayout.VISIBLE
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.allViews
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
|
import com.futo.platformplayer.UISlideOverlays.Companion.showOrderOverlay
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.Album
|
||||||
|
import com.futo.platformplayer.states.StateLibrary
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.ToggleBar
|
||||||
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
|
||||||
|
class LibraryVideosFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var _toggleBuckets = StateLibrary.instance.getVideoBucketNames().map { it.name }.toMutableList();
|
||||||
|
|
||||||
|
var view: FragView? = null;
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val view = FragView(this, inflater);
|
||||||
|
this.view = view;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShown(parameter, isBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
view?.onShown();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LibraryVideosFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FragView : ContentFeedView<LibraryVideosFragment> {
|
||||||
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
|
private var _toggleBar: ToggleBar? = null;
|
||||||
|
|
||||||
|
constructor(fragment: LibraryVideosFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||||
|
initializeToolbarContent();
|
||||||
|
disableRefreshLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown() {
|
||||||
|
val initialAlbums = StateLibrary.instance.getAlbums();
|
||||||
|
Logger.i(TAG, "Initial album count: " + initialAlbums.size);
|
||||||
|
setPager(StateLibrary.instance.getVideos(fragment._toggleBuckets));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val _filterLock = Object();
|
||||||
|
fun initializeToolbarContent() {
|
||||||
|
if(_toolbarContentView.allViews.any { it is ToggleBar })
|
||||||
|
_toolbarContentView.removeView(_toolbarContentView.allViews.find { it is ToggleBar });
|
||||||
|
_toggleBar = ToggleBar(context).apply {
|
||||||
|
layoutParams =
|
||||||
|
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(_filterLock) {
|
||||||
|
var buttonsPlugins: List<ToggleBar.Toggle> = listOf()
|
||||||
|
buttonsPlugins =
|
||||||
|
(StateLibrary.instance.getVideoBucketNames()
|
||||||
|
.map { bucket ->
|
||||||
|
ToggleBar.Toggle(bucket.name, null, fragment._toggleBuckets.contains(bucket.name), { view, active ->
|
||||||
|
var dontSwap = false;
|
||||||
|
if (!active) {
|
||||||
|
if (fragment._toggleBuckets.contains(bucket.name))
|
||||||
|
fragment._toggleBuckets.remove(bucket.name);
|
||||||
|
} else {
|
||||||
|
if (!fragment._toggleBuckets.contains(bucket.name)) {
|
||||||
|
val enabledClients = StatePlatform.instance.getEnabledClients();
|
||||||
|
val availableAfterDisable = enabledClients.count { !fragment._toggleBuckets.contains(it.id) && it.id != bucket.name };
|
||||||
|
if(availableAfterDisable > 0)
|
||||||
|
fragment._toggleBuckets.add(bucket.name);
|
||||||
|
else {
|
||||||
|
UIDialogs.appToast("Select atleast 1 bucket");
|
||||||
|
dontSwap = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!dontSwap)
|
||||||
|
reloadForFilters();
|
||||||
|
else {
|
||||||
|
view.setToggle(active);
|
||||||
|
}
|
||||||
|
}, { view, views, enabled ->
|
||||||
|
val toDisable = views.filter { it != view && it.tag == "plugins" };
|
||||||
|
if(!view.isActive)
|
||||||
|
view.handleClick();
|
||||||
|
for(tag in toDisable) {
|
||||||
|
if(tag.isActive)
|
||||||
|
tag.handleClick();
|
||||||
|
}
|
||||||
|
}).withTag("plugins")
|
||||||
|
})
|
||||||
|
val buttons = (buttonsPlugins)
|
||||||
|
.sortedBy { it.name }.toTypedArray()
|
||||||
|
|
||||||
|
_toggleBar?.setToggles(*buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
_toolbarContentView.addView(_toggleBar, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reloadForFilters() {
|
||||||
|
setPager(StateLibrary.instance.getVideos(fragment._toggleBuckets));
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateSpanCount(){ }
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LibraryAlbumsFragmentsView";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user