mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-29 11:03:01 +02:00
Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 678305e366 | |||
| 9f07673d85 | |||
| 19429263a9 | |||
| 986652adab | |||
| 4d93a58d5d | |||
| 817c90f3af | |||
| 77348b3787 | |||
| 31e26d03c6 | |||
| 1ef566ab16 | |||
| 7597f5136c | |||
| 9a2a70622f | |||
| 4fc33411fd | |||
| a9bb900994 | |||
| 8c1a18d8b4 | |||
| 14ae5f1572 | |||
| ed40994600 | |||
| 90e8c35b19 | |||
| 4d017ad357 | |||
| 2ca2a9db23 | |||
| 713d46c781 | |||
| 0429665173 | |||
| ac05edca77 | |||
| ad3dacf68f | |||
| 91a8996c11 | |||
| 40c4a51a2b | |||
| f8e0aaf4d2 | |||
| ad97b5a406 | |||
| b0e0c1b75f | |||
| b1fce443e9 | |||
| 7c70e58129 | |||
| 940bed2cee | |||
| 4eb20a1843 | |||
| 98c6378148 | |||
| bb066a7a31 | |||
| b5d3261f03 | |||
| 755bebaecb | |||
| 004e4be4d3 |
@@ -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
|
||||||
|
|||||||
+50
-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'
|
||||||
@@ -39,7 +39,7 @@ protobuf {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.futo.platformplayer'
|
namespace 'com.futo.platformplayer'
|
||||||
compileSdk 34
|
compileSdk 36
|
||||||
flavorDimensions "buildType"
|
flavorDimensions "buildType"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
stable {
|
stable {
|
||||||
@@ -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,80 +156,84 @@ 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:5.0.1'
|
||||||
//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
|
||||||
|
implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.3.1') {
|
||||||
|
// Polycentricandroid includes this
|
||||||
|
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"
|
||||||
@@ -153,30 +156,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 +192,62 @@
|
|||||||
<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" />
|
||||||
</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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class ScriptException extends Error {
|
|||||||
super(arguments[0]);
|
super(arguments[0]);
|
||||||
this.plugin_type = "ScriptException";
|
this.plugin_type = "ScriptException";
|
||||||
this.message = arguments[0];
|
this.message = arguments[0];
|
||||||
|
this.msg = arguments[0];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
super(msg);
|
super(msg);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -194,7 +196,6 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
|
fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
|
||||||
val latch = CountDownLatch(1);
|
val latch = CountDownLatch(1);
|
||||||
var promiseResult: T? = null;
|
var promiseResult: T? = null;
|
||||||
@@ -204,16 +205,19 @@ fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
|
|||||||
override fun onFulfilled(p0: V8Value?) {
|
override fun onFulfilled(p0: V8Value?) {
|
||||||
if(p0 is V8ValueError)
|
if(p0 is V8ValueError)
|
||||||
promiseException = ScriptExecutionException(plugin.config, p0.message);
|
promiseException = ScriptExecutionException(plugin.config, p0.message);
|
||||||
else
|
else {
|
||||||
|
if(p0 is V8ValueObject)
|
||||||
|
p0.setWeak();
|
||||||
promiseResult = p0 as T;
|
promiseResult = p0 as T;
|
||||||
|
}
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
override fun onRejected(p0: V8Value?) {
|
override fun onRejected(p0: V8Value?) {
|
||||||
promiseException = (NotImplementedError("onRejected promise not implemented.."));
|
promiseException = p0?.toException(plugin.config);
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
override fun onCatch(p0: V8Value?) {
|
override fun onCatch(p0: V8Value?) {
|
||||||
promiseException = (NotImplementedError("onCatch promise not implemented.."));
|
promiseException = p0?.toException(plugin.config);
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -223,8 +227,25 @@ fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
|
|||||||
promiseException = CancellationException("Cancelled by system");
|
promiseException = CancellationException("Cancelled by system");
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
plugin.unbusy {
|
//Logger.i("V8", "V8ValueBlocking started (Busy) [" + blockCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString());
|
||||||
latch.await();
|
|
||||||
|
|
||||||
|
if(!promise.isPending) {
|
||||||
|
try {
|
||||||
|
Logger.i("V8", "V8Promise resolved synchronously");
|
||||||
|
if(promise.isFulfilled)
|
||||||
|
promiseResult = promise.getResult<T>();
|
||||||
|
else
|
||||||
|
promiseException = promise.getResult<V8Value>().toException(plugin.config);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
promiseException = ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
plugin.unbusy {
|
||||||
|
latch.await();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(promiseException != null)
|
if(promiseException != null)
|
||||||
throw promiseException!!;
|
throw promiseException!!;
|
||||||
@@ -249,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(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(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -265,6 +299,23 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
|
|||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun V8Value.toException(config: IV8PluginConfig): Throwable {
|
||||||
|
val p0 = this;
|
||||||
|
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 msg = p0.getOrDefault<String?>(config, "msg", "Promise Exception", null)
|
||||||
|
?: p0.getOrDefault(config, "message", "Promise Exception", "");
|
||||||
|
return Throwable("Promise Failed: " + pluginType + msg);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
else if(p0 is V8ValueString)
|
||||||
|
return Throwable("Promise Failed:" + p0.value);
|
||||||
|
else
|
||||||
|
return NotImplementedError("onCatch promise not implemented..");
|
||||||
|
}
|
||||||
|
|
||||||
class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Deferred<T> by deferred {
|
class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Deferred<T> by deferred {
|
||||||
|
|
||||||
fun <R> convert(conversion: (result: T)->R): V8Deferred<R>{
|
fun <R> convert(conversion: (result: T)->R): V8Deferred<R>{
|
||||||
@@ -325,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
|
||||||
@@ -25,6 +25,7 @@ import com.futo.platformplayer.states.StateCache
|
|||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePayment
|
import com.futo.platformplayer.states.StatePayment
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.states.StateSync
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||||
@@ -34,13 +35,13 @@ import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
|
|||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
import com.futo.platformplayer.views.fields.FormField
|
||||||
import com.futo.platformplayer.views.fields.FormFieldButton
|
import com.futo.platformplayer.views.fields.FormFieldButton
|
||||||
|
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
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
|
||||||
@@ -62,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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,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));
|
||||||
@@ -89,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
|
||||||
}
|
}
|
||||||
@@ -99,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
|
||||||
}
|
}
|
||||||
@@ -130,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) {
|
||||||
@@ -143,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);
|
||||||
}
|
}
|
||||||
@@ -152,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)
|
||||||
}
|
}
|
||||||
@@ -161,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;
|
||||||
@@ -201,6 +202,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
8 -> "zh";
|
8 -> "zh";
|
||||||
9 -> "ru";
|
9 -> "ru";
|
||||||
10 -> "ar";
|
10 -> "ar";
|
||||||
|
11 -> "it";
|
||||||
|
12 -> "tr";
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,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;
|
||||||
|
|
||||||
@@ -422,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -608,6 +618,11 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@AdvancedField
|
@AdvancedField
|
||||||
@FormField(R.string.shorts_pregenerate, FieldForm.TOGGLE, R.string.shorts_pregenerate_description, 28)
|
@FormField(R.string.shorts_pregenerate, FieldForm.TOGGLE, R.string.shorts_pregenerate_description, 28)
|
||||||
var shortsPregenerate: Boolean = false;
|
var shortsPregenerate: Boolean = false;
|
||||||
|
|
||||||
|
@AdvancedField
|
||||||
|
@FormField(R.string.shorts_fit_video, FieldForm.TOGGLE, R.string.shorts_fit_video_description, 29)
|
||||||
|
@FormFieldWarning(R.string.shorts_fit_video_warning)
|
||||||
|
var shortsFitVideo: Boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||||
@@ -710,6 +725,11 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var allowLinkLocalIpv4: Boolean = false;
|
var allowLinkLocalIpv4: Boolean = false;
|
||||||
|
|
||||||
|
@AdvancedField
|
||||||
|
@FormField(R.string.experimental_cast, FieldForm.TOGGLE, R.string.experimental_cast_description, 6)
|
||||||
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
|
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)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
@@ -742,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) {
|
||||||
@@ -759,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)); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -827,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -842,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") };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -879,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) {
|
||||||
@@ -897,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) {
|
||||||
@@ -937,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;
|
||||||
|
|
||||||
@@ -946,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);
|
||||||
@@ -963,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();
|
||||||
}),
|
}),
|
||||||
@@ -980,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()
|
||||||
@@ -1000,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();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1092,6 +1113,39 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
|
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
|
||||||
var localConnections: Boolean = true;
|
var localConnections: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var syncServerUrl: String? = null;
|
||||||
|
@FormField(R.string.relay_server, FieldForm.READONLYTEXT, -1, 6)
|
||||||
|
val syncServer: String get() = if(syncServerUrl?.isBlank() == true) StateSync.RELAY_SERVER else syncServerUrl ?: StateSync.RELAY_SERVER;
|
||||||
|
|
||||||
|
@AdvancedField
|
||||||
|
@FormField(R.string.configure_sync_server, FieldForm.BUTTON, R.string.configure_sync_server_description, 7)
|
||||||
|
fun configureSyncServer() {
|
||||||
|
StateApp.instance.activity?.let { context ->
|
||||||
|
UIDialogs.showDialog(context, R.drawable.device_sync, false,
|
||||||
|
"Enter the url to your relay server",
|
||||||
|
"Using your own relay server requires a proper setup with portforwarding.\nUse at your own risk.",
|
||||||
|
null,
|
||||||
|
syncServerUrl ?: "",
|
||||||
|
"YourRelayServerDomain.com", 0,
|
||||||
|
UIDialogs.Action("Cancel", {}),
|
||||||
|
UIDialogs.Action("Reset", {
|
||||||
|
syncServerUrl = null;
|
||||||
|
instance.save();
|
||||||
|
SettingsFragment.currentView?.reloadSettings();
|
||||||
|
UIDialogs.toast("Sync server changes require a restart");
|
||||||
|
}, UIDialogs.ActionStyle.ACCENT),
|
||||||
|
UIDialogs.Action.withInput("Configure", {
|
||||||
|
syncServerUrl = it?.text
|
||||||
|
instance.save();
|
||||||
|
SettingsFragment.currentView?.reloadSettings();
|
||||||
|
UIDialogs.toast("Sync server changes require a restart");
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ class UIDialogs {
|
|||||||
currentDialog.code,
|
currentDialog.code,
|
||||||
currentDialog.defaultCloseAction,
|
currentDialog.defaultCloseAction,
|
||||||
*currentDialog.actions.map {
|
*currentDialog.actions.map {
|
||||||
return@map Action(it.text, {
|
return@map Action.withInput(it.text, { str ->
|
||||||
it.action();
|
it.invokeAction(str);
|
||||||
multiShowDialog(context, dialogDescriptor.drop(1), finally);
|
multiShowDialog(context, dialogDescriptor.drop(1), finally);
|
||||||
}, it.style);
|
}, it.style);
|
||||||
}.toTypedArray());
|
}.toTypedArray());
|
||||||
@@ -203,7 +203,9 @@ class UIDialogs {
|
|||||||
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
||||||
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
|
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
|
||||||
}
|
}
|
||||||
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog
|
||||||
|
= showDialog(context, icon, animated, text, textDetails, code, null, null, defaultCloseAction, *actions);
|
||||||
|
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, input: String?, placeholder: String?, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
||||||
val builder = AlertDialog.Builder(context);
|
val builder = AlertDialog.Builder(context);
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
||||||
builder.setView(view);
|
builder.setView(view);
|
||||||
@@ -226,6 +228,16 @@ class UIDialogs {
|
|||||||
this.text = textDetails;
|
this.text = textDetails;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
var inputView = view.findViewById<TextView>(R.id.dialog_text_input);
|
||||||
|
inputView.apply {
|
||||||
|
if (input == null && placeholder == null) this.visibility = View.GONE;
|
||||||
|
else {
|
||||||
|
this.text = input ?: "";
|
||||||
|
this.hint = placeholder ?: "";
|
||||||
|
this.visibility = View.VISIBLE;
|
||||||
|
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
|
||||||
|
}
|
||||||
|
};
|
||||||
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
||||||
if (code == null) this.visibility = View.GONE;
|
if (code == null) this.visibility = View.GONE;
|
||||||
else {
|
else {
|
||||||
@@ -250,7 +262,7 @@ class UIDialogs {
|
|||||||
buttonView.textSize = 14f;
|
buttonView.textSize = 14f;
|
||||||
buttonView.typeface = resources.getFont(R.font.inter_regular);
|
buttonView.typeface = resources.getFont(R.font.inter_regular);
|
||||||
buttonView.text = act.text;
|
buttonView.text = act.text;
|
||||||
buttonView.setOnClickListener { act.action(); dialog.dismiss(); };
|
buttonView.setOnClickListener { act.invokeAction(DialogResult(inputView?.text?.toString())); dialog.dismiss(); };
|
||||||
when(act.style) {
|
when(act.style) {
|
||||||
ActionStyle.PRIMARY -> buttonView.setBackgroundResource(R.drawable.background_button_primary);
|
ActionStyle.PRIMARY -> buttonView.setBackgroundResource(R.drawable.background_button_primary);
|
||||||
ActionStyle.ACCENT -> buttonView.setBackgroundResource(R.drawable.background_button_accent);
|
ActionStyle.ACCENT -> buttonView.setBackgroundResource(R.drawable.background_button_accent);
|
||||||
@@ -275,7 +287,7 @@ class UIDialogs {
|
|||||||
};
|
};
|
||||||
dialog.setOnCancelListener {
|
dialog.setOnCancelListener {
|
||||||
if(defaultCloseAction >= 0 && defaultCloseAction < actions.size)
|
if(defaultCloseAction >= 0 && defaultCloseAction < actions.size)
|
||||||
actions[defaultCloseAction].action();
|
actions[defaultCloseAction].invokeAction(DialogResult(inputView?.text?.toString()));
|
||||||
}
|
}
|
||||||
dialog.setOnDismissListener {
|
dialog.setOnDismissListener {
|
||||||
registerDialogClosed(dialog);
|
registerDialogClosed(dialog);
|
||||||
@@ -535,17 +547,36 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
class Action {
|
class Action {
|
||||||
val text: String;
|
val text: String;
|
||||||
val action: ()->Unit;
|
val action: ((DialogResult?)->Unit);
|
||||||
val style: ActionStyle;
|
val style: ActionStyle;
|
||||||
var center: Boolean;
|
var center: Boolean;
|
||||||
|
|
||||||
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
||||||
|
this.text = text;
|
||||||
|
this.action = { action() };
|
||||||
|
this.style = style;
|
||||||
|
this.center = center;
|
||||||
|
}
|
||||||
|
protected constructor(text: String, action: (DialogResult?)->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.action = action;
|
this.action = action;
|
||||||
this.style = style;
|
this.style = style;
|
||||||
this.center = center;
|
this.center = center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun invokeAction(input: DialogResult? = null) {
|
||||||
|
this.action(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun withInput(text: String, action: (DialogResult?)->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false): Action {
|
||||||
|
return Action(text, action, style, center);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
class DialogResult(
|
||||||
|
val text: String?
|
||||||
|
);
|
||||||
enum class ActionStyle {
|
enum class ActionStyle {
|
||||||
NONE,
|
NONE,
|
||||||
PRIMARY,
|
PRIMARY,
|
||||||
|
|||||||
@@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,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);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,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,9 +35,11 @@ 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
|
||||||
|
import com.futo.platformplayer.api.media.models.video.LocalVideoDetails
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
@@ -51,17 +52,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
|
||||||
@@ -75,6 +87,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
|
||||||
@@ -146,6 +159,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;
|
||||||
@@ -178,6 +192,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;
|
||||||
|
|
||||||
@@ -186,7 +211,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;
|
||||||
@@ -198,6 +223,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)
|
||||||
@@ -218,6 +244,19 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private val _notifPermission = "android.permission.POST_NOTIFICATIONS";
|
||||||
|
private val _notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||||
|
if (isGranted)
|
||||||
|
UIDialogs.toast(this, "Notification permission granted");
|
||||||
|
else
|
||||||
|
UIDialogs.toast(this, "Notification permission denied");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fun requestNotificationPermissions() {
|
||||||
|
_notificationPermissionLauncher?.launch(_notifPermission);
|
||||||
|
}
|
||||||
|
|
||||||
val mainId = UUID.randomUUID().toString().substring(0, 5)
|
val mainId = UUID.randomUUID().toString().substring(0, 5)
|
||||||
|
|
||||||
@@ -273,6 +312,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);
|
||||||
|
|
||||||
@@ -283,9 +323,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 {
|
||||||
@@ -300,6 +337,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
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);
|
||||||
@@ -316,6 +356,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();
|
||||||
@@ -348,6 +389,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();
|
||||||
|
|
||||||
@@ -410,6 +462,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 {
|
||||||
@@ -474,6 +531,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;
|
||||||
|
|
||||||
@@ -499,7 +566,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;
|
||||||
@@ -638,6 +705,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,
|
||||||
@@ -696,17 +768,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) {
|
||||||
@@ -768,7 +836,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (targetData != null) {
|
if (targetData != null) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
handleUrlAll(targetData)
|
handleUrlAll(targetData, intent)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Unhandled exception in handleUrlAll", e)
|
Logger.e(TAG, "Unhandled exception in handleUrlAll", e)
|
||||||
}
|
}
|
||||||
@@ -779,8 +847,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun handleUrlAll(url: String) {
|
suspend fun handleUrlAll(url: String, openIntent: Intent? = null) {
|
||||||
val uri = Uri.parse(url)
|
val uri = Uri.parse(url)
|
||||||
|
val intent = openIntent ?: this.intent;
|
||||||
when (uri.scheme) {
|
when (uri.scheme) {
|
||||||
"grayjay" -> {
|
"grayjay" -> {
|
||||||
if (url.startsWith("grayjay://license/")) {
|
if (url.startsWith("grayjay://license/")) {
|
||||||
@@ -807,11 +876,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
"content" -> {
|
"content" -> {
|
||||||
if (!handleContent(url, intent.type)) {
|
if (!handleContent(url, intent?.type)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
|
getString(R.string.unknown_content_format) + " [${url}]\n[${intent?.type}]",
|
||||||
"Ok",
|
"Ok",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -932,6 +1001,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
} else if (file.lowercase().endsWith(".txt") || mime == "text/plain") {
|
} else if (file.lowercase().endsWith(".txt") || mime == "text/plain") {
|
||||||
return handleUnknownText(String(data));
|
return handleUnknownText(String(data));
|
||||||
}
|
}
|
||||||
|
else if (mime?.let { it.startsWith("video/") || it.startsWith("audio/") } ?: false) {
|
||||||
|
val mediaItem = LocalVideoDetails.fromContent(file, mime);
|
||||||
|
navigateWhenReady(_fragVideoDetail, mediaItem);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1046,7 +1121,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.i(TAG, "handleFCast");
|
Logger.i(TAG, "handleFCast");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
StateCasting.instance.handleUrl(this, url)
|
StateCasting.instance.handleUrl(url)
|
||||||
return true;
|
return true;
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
||||||
@@ -1078,7 +1153,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1129,6 +1204,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
|
||||||
@@ -1151,27 +1231,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();
|
||||||
@@ -1184,10 +1264,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;
|
||||||
@@ -1241,6 +1321,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;
|
||||||
@@ -1265,6 +1346,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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1272,7 +1364,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(
|
||||||
@@ -1289,6 +1381,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) {
|
||||||
|
|||||||
+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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+17
-3
@@ -2,10 +2,24 @@ package com.futo.platformplayer.api.media.models.streams
|
|||||||
|
|
||||||
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.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
|
|
||||||
class LocalVideoUnMuxedSourceDescriptor(private val video: VideoLocal) : VideoUnMuxedSourceDescriptor() {
|
class LocalVideoUnMuxedSourceDescriptor : VideoUnMuxedSourceDescriptor {
|
||||||
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
override val videoSources: Array<IVideoSource>;
|
||||||
override val audioSources: Array<IAudioSource> get() = video.audioSource.toTypedArray();
|
override val audioSources: Array<IAudioSource>;
|
||||||
|
|
||||||
|
constructor(video: VideoLocal) {
|
||||||
|
videoSources = video.videoSource.toTypedArray();
|
||||||
|
audioSources = video.audioSource.toTypedArray();
|
||||||
|
}
|
||||||
|
constructor(audio: LocalAudioContentSource) {
|
||||||
|
videoSources = arrayOf()
|
||||||
|
audioSources = arrayOf(audio);
|
||||||
|
}
|
||||||
|
constructor(videoSources: Array<IVideoSource>, audioSources: Array<IAudioSource>) {
|
||||||
|
this.videoSources = videoSources;
|
||||||
|
this.audioSources = audioSources;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -14,7 +14,8 @@ class AudioUrlSource(
|
|||||||
override val language: String = Language.UNKNOWN,
|
override val language: String = Language.UNKNOWN,
|
||||||
override val duration: Long? = null,
|
override val duration: Long? = null,
|
||||||
override var priority: Boolean = false,
|
override var priority: Boolean = false,
|
||||||
override var original: Boolean = false
|
override var original: Boolean = false,
|
||||||
|
var isLocal: Boolean = false
|
||||||
) : IAudioUrlSource, IStreamMetaDataSource{
|
) : IAudioUrlSource, IStreamMetaDataSource{
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
|
|||||||
+1
@@ -41,6 +41,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
|
||||||
|
|||||||
+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
|
||||||
);
|
);
|
||||||
|
|||||||
+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,
|
||||||
|
|||||||
+2
-1
@@ -14,7 +14,8 @@ open class VideoUrlSource(
|
|||||||
override val codec : String = "",
|
override val codec : String = "",
|
||||||
override val bitrate : Int? = 0,
|
override val bitrate : Int? = 0,
|
||||||
|
|
||||||
override var priority: Boolean = false
|
override var priority: Boolean = false,
|
||||||
|
var isLocal: Boolean = false
|
||||||
) : IVideoUrlSource, IStreamMetaDataSource {
|
) : IVideoUrlSource, IStreamMetaDataSource {
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
|
|||||||
+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?;
|
||||||
|
|
||||||
|
|||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.video
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
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.playback.IPlaybackTracker
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||||
|
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.IDashManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.SubtitleRawSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.LocalVideoMuxedSourceDescriptor
|
||||||
|
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.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.others.Language
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
open class LocalVideoDetails(
|
||||||
|
override val id: PlatformID,
|
||||||
|
override val name: String,
|
||||||
|
override val thumbnails: Thumbnails,
|
||||||
|
override val author: PlatformAuthorLink,
|
||||||
|
override val url: String,
|
||||||
|
override val duration: Long,
|
||||||
|
|
||||||
|
val mimeType: String? = null,
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
override val datetime: OffsetDateTime?
|
||||||
|
) : IPlatformVideo, IPlatformVideoDetails {
|
||||||
|
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||||
|
|
||||||
|
override var playbackTime: Long = -1;
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
override var playbackDate: OffsetDateTime? = null;
|
||||||
|
|
||||||
|
override val isLive: Boolean get() = false;
|
||||||
|
|
||||||
|
override val dash: IDashManifestSource? get() = null;
|
||||||
|
override val hls: IHLSManifestSource? get() = null;
|
||||||
|
override val live: IVideoSource? get() = null;
|
||||||
|
|
||||||
|
|
||||||
|
override val shareUrl: String = ""
|
||||||
|
override val viewCount: Long = -1
|
||||||
|
override val rating: IRating = RatingLikes(0)
|
||||||
|
override val description: String = "";
|
||||||
|
override val video: IVideoSourceDescriptor = (if(mimeType?.startsWith("audio/") ?: false)
|
||||||
|
(LocalVideoUnMuxedSourceDescriptor(
|
||||||
|
arrayOf(),
|
||||||
|
arrayOf(LocalAudioContentSource(url, mimeType ?: "", name))
|
||||||
|
))
|
||||||
|
else (LocalVideoMuxedSourceDescriptor(
|
||||||
|
LocalVideoContentSource(url, mimeType ?: "", name)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
override val preview: ISerializedVideoSourceDescriptor? = null;
|
||||||
|
|
||||||
|
override val subtitles: List<SubtitleRawSource> = listOf()
|
||||||
|
override val isShort: Boolean = false
|
||||||
|
|
||||||
|
fun toJson() : String {
|
||||||
|
return Json.encodeToString(this);
|
||||||
|
}
|
||||||
|
fun fromJson(str : String) : SerializedPlatformVideoDetails {
|
||||||
|
return Serializer.json.decodeFromString<SerializedPlatformVideoDetails>(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||||
|
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||||
|
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromFile(name: String, filePath: String, mimeType: String? = null) : LocalVideoDetails {
|
||||||
|
if(filePath.startsWith("content://"))
|
||||||
|
return fromContent(filePath, mimeType);
|
||||||
|
|
||||||
|
return LocalVideoDetails(PlatformID("FILE", filePath, null, 0, -1),
|
||||||
|
name, Thumbnails(), PlatformAuthorLink.UNKNOWN, filePath, -1, mimeType, null);
|
||||||
|
}
|
||||||
|
fun fromContent(contentUrl: String, mimeType: String? = null) : LocalVideoDetails {
|
||||||
|
var nameToUse = getFileNameFromContentUrl(contentUrl) ?: "File";
|
||||||
|
|
||||||
|
return LocalVideoDetails(PlatformID("FILE", contentUrl, null, 0, -1),
|
||||||
|
nameToUse, Thumbnails(), PlatformAuthorLink.UNKNOWN, contentUrl, -1, mimeType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("Range")
|
||||||
|
private fun getFileNameFromContentUrl(url: String): String? {
|
||||||
|
val cursor = StateApp.instance.context.contentResolver.query(url.toUri(), null, null, null, null);
|
||||||
|
cursor?.moveToFirst();
|
||||||
|
val fileName = cursor?.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
||||||
|
cursor?.close();
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
+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)"
|
||||||
|
}
|
||||||
|
|||||||
+41
-31
@@ -31,42 +31,52 @@ 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 container: String =
|
||||||
val canMerge: Boolean;
|
_obj.getOrDefault<String>(cfg, "container", ctx, null) ?: "application/dash+xml"
|
||||||
|
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override val name: String =
|
||||||
|
_obj.getOrThrow<String>(cfg, "name", ctx)
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
override val width: Int =
|
||||||
val contextName = "DashRawSource";
|
_obj.getOrDefault<Int>(cfg, "width", ctx, null)?.toInt() ?: 0
|
||||||
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 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;
|
||||||
|
|||||||
+35
-30
@@ -5,42 +5,47 @@ 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 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;
|
||||||
}
|
}
|
||||||
+14
-4
@@ -1,13 +1,23 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.local.models
|
package com.futo.platformplayer.api.media.platforms.local.models
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
|
||||||
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
class LocalVideoMuxedSourceDescriptor(
|
class LocalVideoMuxedSourceDescriptor: VideoMuxedSourceDescriptor {
|
||||||
private val video: LocalVideoFileSource
|
override val videoSources: Array<IVideoSource>;
|
||||||
) : VideoMuxedSourceDescriptor() {
|
|
||||||
override val videoSources: Array<IVideoSource> get() = arrayOf(video);
|
constructor(video: LocalVideoFileSource) {
|
||||||
|
videoSources = arrayOf(video);
|
||||||
|
}
|
||||||
|
constructor(video: LocalVideoContentSource) {
|
||||||
|
videoSources = arrayOf(video);
|
||||||
|
}
|
||||||
|
constructor(videoSources: Array<IVideoSource>) {
|
||||||
|
this.videoSources = videoSources;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models.sources
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import com.futo.platformplayer.others.Language
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class LocalAudioContentSource : IAudioSource {
|
||||||
|
|
||||||
|
override val name: String;
|
||||||
|
override val container: String;
|
||||||
|
override val codec: String = ""
|
||||||
|
override val bitrate: Int = 0
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean = false;
|
||||||
|
override val language: String = Language.UNKNOWN
|
||||||
|
override val original: Boolean = false;
|
||||||
|
|
||||||
|
var contentUrl: String;
|
||||||
|
|
||||||
|
constructor(contentUrl: String, mime: String, name: String? = null) {
|
||||||
|
this.name = name ?: "File";
|
||||||
|
container = mime;
|
||||||
|
duration = 0;
|
||||||
|
|
||||||
|
this.contentUrl = contentUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models.sources
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import com.futo.platformplayer.others.Language
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class LocalAudioFileSource: IAudioSource {
|
||||||
|
|
||||||
|
|
||||||
|
override val name: String;
|
||||||
|
override val container: String;
|
||||||
|
override val codec: String = ""
|
||||||
|
override val bitrate: Int = 0
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean = false;
|
||||||
|
override val language: String = Language.UNKNOWN;
|
||||||
|
override val original: Boolean = false;
|
||||||
|
|
||||||
|
var file: File;
|
||||||
|
|
||||||
|
constructor(file: File) {
|
||||||
|
this.file = file;
|
||||||
|
name = file.name;
|
||||||
|
container = VideoHelper.videoExtensionToMimetype(file.extension) ?: "";
|
||||||
|
duration = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models.sources
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class LocalVideoContentSource: IVideoSource {
|
||||||
|
|
||||||
|
|
||||||
|
override val name: String;
|
||||||
|
override val width: Int;
|
||||||
|
override val height: Int;
|
||||||
|
override val container: String;
|
||||||
|
override val codec: String = ""
|
||||||
|
override val bitrate: Int = 0
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean = false;
|
||||||
|
|
||||||
|
var contentUrl: String;
|
||||||
|
|
||||||
|
constructor(contentUrl: String, mime: String, name: String? = null) {
|
||||||
|
this.name = name ?: "File";
|
||||||
|
width = 0;
|
||||||
|
height = 0;
|
||||||
|
container = mime;
|
||||||
|
duration = 0;
|
||||||
|
this.contentUrl = contentUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -20,7 +20,10 @@ class LocalVideoFileSource: IVideoSource {
|
|||||||
override val duration: Long;
|
override val duration: Long;
|
||||||
override val priority: Boolean = false;
|
override val priority: Boolean = false;
|
||||||
|
|
||||||
|
var file: File;
|
||||||
|
|
||||||
constructor(file: File) {
|
constructor(file: File) {
|
||||||
|
this.file = file;
|
||||||
name = file.name;
|
name = file.name;
|
||||||
width = 0;
|
width = 0;
|
||||||
height = 0;
|
height = 0;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import kotlinx.coroutines.launch
|
|||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class AirPlayCastingDevice : CastingDevice {
|
class AirPlayCastingDevice : CastingDeviceLegacy {
|
||||||
//See for more info: https://nto.github.io/AirPlay
|
//See for more info: https://nto.github.io/AirPlay
|
||||||
|
|
||||||
override val protocol: CastProtocolType get() = CastProtocolType.AIRPLAY;
|
override val protocol: CastProtocolType get() = CastProtocolType.AIRPLAY;
|
||||||
|
|||||||
@@ -2,147 +2,78 @@ package com.futo.platformplayer.casting
|
|||||||
|
|
||||||
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 org.fcast.sender_sdk.Metadata
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
|
||||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
|
||||||
enum class CastConnectionState {
|
|
||||||
DISCONNECTED,
|
|
||||||
CONNECTING,
|
|
||||||
CONNECTED
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable(with = CastProtocolType.CastProtocolTypeSerializer::class)
|
|
||||||
enum class CastProtocolType {
|
|
||||||
CHROMECAST,
|
|
||||||
AIRPLAY,
|
|
||||||
FCAST;
|
|
||||||
|
|
||||||
object CastProtocolTypeSerializer : KSerializer<CastProtocolType> {
|
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CastProtocolType", PrimitiveKind.STRING)
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: CastProtocolType) {
|
|
||||||
encoder.encodeString(value.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): CastProtocolType {
|
|
||||||
val name = decoder.decodeString()
|
|
||||||
return when (name) {
|
|
||||||
"FASTCAST" -> FCAST // Handle the renamed case
|
|
||||||
else -> CastProtocolType.valueOf(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class CastingDevice {
|
abstract class CastingDevice {
|
||||||
abstract val protocol: CastProtocolType;
|
abstract val isReady: Boolean
|
||||||
abstract val isReady: Boolean;
|
abstract val usedRemoteAddress: InetAddress?
|
||||||
abstract var usedRemoteAddress: InetAddress?;
|
abstract val localAddress: InetAddress?
|
||||||
abstract var localAddress: InetAddress?;
|
abstract val name: String?
|
||||||
abstract val canSetVolume: Boolean;
|
abstract val onConnectionStateChanged: Event1<CastConnectionState>
|
||||||
abstract val canSetSpeed: Boolean;
|
abstract val onPlayChanged: Event1<Boolean>
|
||||||
|
abstract val onTimeChanged: Event1<Double>
|
||||||
|
abstract val onDurationChanged: Event1<Double>
|
||||||
|
abstract val onVolumeChanged: Event1<Double>
|
||||||
|
abstract val onSpeedChanged: Event1<Double>
|
||||||
|
abstract var connectionState: CastConnectionState
|
||||||
|
abstract val protocolType: CastProtocolType
|
||||||
|
abstract var isPlaying: Boolean
|
||||||
|
abstract val expectedCurrentTime: Double
|
||||||
|
abstract var speed: Double
|
||||||
|
abstract var time: Double
|
||||||
|
abstract var duration: Double
|
||||||
|
abstract var volume: Double
|
||||||
|
abstract fun canSetVolume(): Boolean
|
||||||
|
abstract fun canSetSpeed(): Boolean
|
||||||
|
|
||||||
var name: String? = null;
|
@Throws
|
||||||
var isPlaying: Boolean = false
|
abstract fun resumePlayback()
|
||||||
set(value) {
|
|
||||||
val changed = value != field;
|
|
||||||
field = value;
|
|
||||||
if (changed) {
|
|
||||||
onPlayChanged.emit(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private var lastTimeChangeTime_ms: Long = 0
|
@Throws
|
||||||
var time: Double = 0.0
|
abstract fun pausePlayback()
|
||||||
private set
|
|
||||||
|
|
||||||
protected fun setTime(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastTimeChangeTime_ms && value != time) {
|
abstract fun stopPlayback()
|
||||||
time = value
|
|
||||||
lastTimeChangeTime_ms = changeTime_ms
|
|
||||||
onTimeChanged.emit(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var lastDurationChangeTime_ms: Long = 0
|
@Throws
|
||||||
var duration: Double = 0.0
|
abstract fun seekTo(timeSeconds: Double)
|
||||||
private set
|
|
||||||
|
|
||||||
protected fun setDuration(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastDurationChangeTime_ms && value != duration) {
|
abstract fun changeVolume(timeSeconds: Double)
|
||||||
duration = value
|
|
||||||
lastDurationChangeTime_ms = changeTime_ms
|
|
||||||
onDurationChanged.emit(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var lastVolumeChangeTime_ms: Long = 0
|
@Throws
|
||||||
var volume: Double = 1.0
|
abstract fun changeSpeed(speed: Double)
|
||||||
private set
|
|
||||||
|
|
||||||
protected fun setVolume(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastVolumeChangeTime_ms && value != volume) {
|
abstract fun connect()
|
||||||
volume = value
|
|
||||||
lastVolumeChangeTime_ms = changeTime_ms
|
|
||||||
onVolumeChanged.emit(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var lastSpeedChangeTime_ms: Long = 0
|
@Throws
|
||||||
var speed: Double = 1.0
|
abstract fun disconnect()
|
||||||
private set
|
abstract fun getDeviceInfo(): CastingDeviceInfo
|
||||||
|
abstract fun getAddresses(): List<InetAddress>
|
||||||
|
|
||||||
protected fun setSpeed(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastSpeedChangeTime_ms && value != speed) {
|
abstract fun loadVideo(
|
||||||
speed = value
|
streamType: String,
|
||||||
lastSpeedChangeTime_ms = changeTime_ms
|
contentType: String,
|
||||||
onSpeedChanged.emit(value)
|
contentId: String,
|
||||||
}
|
resumePosition: Double,
|
||||||
}
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
)
|
||||||
|
|
||||||
val expectedCurrentTime: Double
|
@Throws
|
||||||
get() {
|
abstract fun loadContent(
|
||||||
val diff = if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0;
|
contentType: String,
|
||||||
return time + diff;
|
content: String,
|
||||||
};
|
resumePosition: Double,
|
||||||
var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED
|
duration: Double,
|
||||||
set(value) {
|
speed: Double?,
|
||||||
val changed = value != field;
|
metadata: Metadata?
|
||||||
field = value;
|
)
|
||||||
|
|
||||||
if (changed) {
|
abstract fun ensureThreadStarted()
|
||||||
onConnectionStateChanged.emit(value);
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var onConnectionStateChanged = Event1<CastConnectionState>();
|
|
||||||
var onPlayChanged = Event1<Boolean>();
|
|
||||||
var onTimeChanged = Event1<Double>();
|
|
||||||
var onDurationChanged = Event1<Double>();
|
|
||||||
var onVolumeChanged = Event1<Double>();
|
|
||||||
var onSpeedChanged = Event1<Double>();
|
|
||||||
|
|
||||||
abstract fun stopCasting();
|
|
||||||
|
|
||||||
abstract fun seekVideo(timeSeconds: Double);
|
|
||||||
abstract fun stopVideo();
|
|
||||||
abstract fun pauseVideo();
|
|
||||||
abstract fun resumeVideo();
|
|
||||||
abstract fun loadVideo(streamType: String, contentType: String, contentId: String, resumePosition: Double, duration: Double, speed: Double?);
|
|
||||||
abstract fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?);
|
|
||||||
open fun changeVolume(volume: Double) { throw NotImplementedError() }
|
|
||||||
open fun changeSpeed(speed: Double) { throw NotImplementedError() }
|
|
||||||
|
|
||||||
abstract fun start();
|
|
||||||
abstract fun stop();
|
|
||||||
|
|
||||||
abstract fun getDeviceInfo(): CastingDeviceInfo;
|
|
||||||
|
|
||||||
abstract fun getAddresses(): List<InetAddress>;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import com.futo.platformplayer.BuildConfig
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
|
import org.fcast.sender_sdk.ApplicationInfo
|
||||||
|
import org.fcast.sender_sdk.GenericKeyEvent
|
||||||
|
import org.fcast.sender_sdk.GenericMediaEvent
|
||||||
|
import org.fcast.sender_sdk.PlaybackState
|
||||||
|
import org.fcast.sender_sdk.Source
|
||||||
|
import java.net.InetAddress
|
||||||
|
import org.fcast.sender_sdk.CastingDevice as RsCastingDevice;
|
||||||
|
import org.fcast.sender_sdk.DeviceEventHandler as RsDeviceEventHandler;
|
||||||
|
import org.fcast.sender_sdk.DeviceConnectionState
|
||||||
|
import org.fcast.sender_sdk.DeviceFeature
|
||||||
|
import org.fcast.sender_sdk.IpAddr
|
||||||
|
import org.fcast.sender_sdk.LoadRequest
|
||||||
|
import org.fcast.sender_sdk.Metadata
|
||||||
|
import org.fcast.sender_sdk.ProtocolType
|
||||||
|
import org.fcast.sender_sdk.urlFormatIpAddr
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
|
|
||||||
|
private fun ipAddrToInetAddress(addr: IpAddr): InetAddress = when (addr) {
|
||||||
|
is IpAddr.V4 -> Inet4Address.getByAddress(
|
||||||
|
byteArrayOf(
|
||||||
|
addr.o1.toByte(),
|
||||||
|
addr.o2.toByte(),
|
||||||
|
addr.o3.toByte(),
|
||||||
|
addr.o4.toByte()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
is IpAddr.V6 -> Inet6Address.getByAddress(
|
||||||
|
byteArrayOf(
|
||||||
|
addr.o1.toByte(),
|
||||||
|
addr.o2.toByte(),
|
||||||
|
addr.o3.toByte(),
|
||||||
|
addr.o4.toByte(),
|
||||||
|
addr.o5.toByte(),
|
||||||
|
addr.o6.toByte(),
|
||||||
|
addr.o7.toByte(),
|
||||||
|
addr.o8.toByte(),
|
||||||
|
addr.o9.toByte(),
|
||||||
|
addr.o10.toByte(),
|
||||||
|
addr.o11.toByte(),
|
||||||
|
addr.o12.toByte(),
|
||||||
|
addr.o13.toByte(),
|
||||||
|
addr.o14.toByte(),
|
||||||
|
addr.o15.toByte(),
|
||||||
|
addr.o16.toByte()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
||||||
|
class EventHandler : RsDeviceEventHandler {
|
||||||
|
var onConnectionStateChanged = Event1<DeviceConnectionState>();
|
||||||
|
var onPlayChanged = Event1<Boolean>()
|
||||||
|
var onTimeChanged = Event1<Double>()
|
||||||
|
var onDurationChanged = Event1<Double>()
|
||||||
|
var onVolumeChanged = Event1<Double>()
|
||||||
|
var onSpeedChanged = Event1<Double>()
|
||||||
|
|
||||||
|
override fun connectionStateChanged(state: DeviceConnectionState) {
|
||||||
|
onConnectionStateChanged.emit(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun volumeChanged(volume: Double) {
|
||||||
|
onVolumeChanged.emit(volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun timeChanged(time: Double) {
|
||||||
|
onTimeChanged.emit(time)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun playbackStateChanged(state: PlaybackState) {
|
||||||
|
onPlayChanged.emit(state == PlaybackState.PLAYING)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun durationChanged(duration: Double) {
|
||||||
|
onDurationChanged.emit(duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun speedChanged(speed: Double) {
|
||||||
|
onSpeedChanged.emit(speed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sourceChanged(source: Source) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun keyEvent(event: GenericKeyEvent) {
|
||||||
|
// Unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mediaEvent(event: GenericMediaEvent) {
|
||||||
|
// Unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun playbackError(message: String) {
|
||||||
|
Logger.e(TAG, "Playback error: $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventHandler = EventHandler()
|
||||||
|
override val isReady: Boolean
|
||||||
|
get() = device.isReady()
|
||||||
|
override val name: String
|
||||||
|
get() = device.name()
|
||||||
|
override var usedRemoteAddress: InetAddress? = null
|
||||||
|
override var localAddress: InetAddress? = null
|
||||||
|
override fun canSetVolume(): Boolean = device.supportsFeature(DeviceFeature.SET_VOLUME)
|
||||||
|
override fun canSetSpeed(): Boolean = device.supportsFeature(DeviceFeature.SET_SPEED)
|
||||||
|
|
||||||
|
override val onConnectionStateChanged =
|
||||||
|
Event1<CastConnectionState>()
|
||||||
|
override val onPlayChanged: Event1<Boolean>
|
||||||
|
get() = eventHandler.onPlayChanged
|
||||||
|
override val onTimeChanged: Event1<Double>
|
||||||
|
get() = eventHandler.onTimeChanged
|
||||||
|
override val onDurationChanged: Event1<Double>
|
||||||
|
get() = eventHandler.onDurationChanged
|
||||||
|
override val onVolumeChanged: Event1<Double>
|
||||||
|
get() = eventHandler.onVolumeChanged
|
||||||
|
override val onSpeedChanged: Event1<Double>
|
||||||
|
get() = eventHandler.onSpeedChanged
|
||||||
|
|
||||||
|
override fun resumePlayback() = device.resumePlayback()
|
||||||
|
override fun pausePlayback() = device.pausePlayback()
|
||||||
|
override fun stopPlayback() = device.stopPlayback()
|
||||||
|
override fun seekTo(timeSeconds: Double) = device.seek(timeSeconds)
|
||||||
|
override fun changeVolume(newVolume: Double) {
|
||||||
|
device.changeVolume(newVolume)
|
||||||
|
volume = newVolume
|
||||||
|
}
|
||||||
|
override fun changeSpeed(speed: Double) = device.changeSpeed(speed)
|
||||||
|
override fun connect() = device.connect(
|
||||||
|
ApplicationInfo(
|
||||||
|
"Grayjay Android",
|
||||||
|
"${BuildConfig.VERSION_NAME}-${BuildConfig.FLAVOR}",
|
||||||
|
"${Build.MANUFACTURER} ${Build.MODEL}"
|
||||||
|
),
|
||||||
|
eventHandler,
|
||||||
|
1000.toULong()
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun disconnect() = device.disconnect()
|
||||||
|
|
||||||
|
override fun getDeviceInfo(): CastingDeviceInfo {
|
||||||
|
val info = device.getDeviceInfo()
|
||||||
|
return CastingDeviceInfo(
|
||||||
|
info.name,
|
||||||
|
when (info.protocol) {
|
||||||
|
ProtocolType.CHROMECAST -> CastProtocolType.CHROMECAST
|
||||||
|
ProtocolType.F_CAST -> CastProtocolType.FCAST
|
||||||
|
},
|
||||||
|
addresses = info.addresses.map { urlFormatIpAddr(it) }.toTypedArray(),
|
||||||
|
port = info.port.toInt(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAddresses(): List<InetAddress> = device.getAddresses().map {
|
||||||
|
ipAddrToInetAddress(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadVideo(
|
||||||
|
streamType: String,
|
||||||
|
contentType: String,
|
||||||
|
contentId: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
) = device.load(
|
||||||
|
LoadRequest.Video(
|
||||||
|
contentType = contentType,
|
||||||
|
url = contentId,
|
||||||
|
resumePosition = resumePosition,
|
||||||
|
speed = speed,
|
||||||
|
volume = volume,
|
||||||
|
metadata = metadata
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun loadContent(
|
||||||
|
contentType: String,
|
||||||
|
content: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
) = device.load(
|
||||||
|
LoadRequest.Content(
|
||||||
|
contentType = contentType,
|
||||||
|
content = content,
|
||||||
|
resumePosition = resumePosition,
|
||||||
|
speed = speed,
|
||||||
|
volume = volume,
|
||||||
|
metadata = metadata,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override var connectionState = CastConnectionState.DISCONNECTED
|
||||||
|
override val protocolType: CastProtocolType
|
||||||
|
get() = when (device.castingProtocol()) {
|
||||||
|
ProtocolType.CHROMECAST -> CastProtocolType.CHROMECAST
|
||||||
|
ProtocolType.F_CAST -> CastProtocolType.FCAST
|
||||||
|
}
|
||||||
|
override var volume: Double = 1.0
|
||||||
|
override var duration: Double = 0.0
|
||||||
|
private var lastTimeChangeTime_ms: Long = 0
|
||||||
|
override var time: Double = 0.0
|
||||||
|
override var speed: Double = 0.0
|
||||||
|
override var isPlaying: Boolean = false
|
||||||
|
|
||||||
|
override val expectedCurrentTime: Double
|
||||||
|
get() {
|
||||||
|
val diff =
|
||||||
|
if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0;
|
||||||
|
return time + diff
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
eventHandler.onConnectionStateChanged.subscribe { newState ->
|
||||||
|
when (newState) {
|
||||||
|
is DeviceConnectionState.Connected -> {
|
||||||
|
usedRemoteAddress = ipAddrToInetAddress(newState.usedRemoteAddr)
|
||||||
|
localAddress = ipAddrToInetAddress(newState.localAddr)
|
||||||
|
connectionState = CastConnectionState.CONNECTED
|
||||||
|
onConnectionStateChanged.emit(CastConnectionState.CONNECTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceConnectionState.Connecting, DeviceConnectionState.Reconnecting -> {
|
||||||
|
connectionState = CastConnectionState.CONNECTING
|
||||||
|
onConnectionStateChanged.emit(CastConnectionState.CONNECTING)
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceConnectionState.Disconnected -> {
|
||||||
|
connectionState = CastConnectionState.CONNECTING
|
||||||
|
onConnectionStateChanged.emit(CastConnectionState.DISCONNECTED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newState == DeviceConnectionState.Disconnected) {
|
||||||
|
try {
|
||||||
|
Logger.i(TAG, "Stopping device")
|
||||||
|
device.disconnect()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to stop device: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eventHandler.onPlayChanged.subscribe { isPlaying = it }
|
||||||
|
eventHandler.onTimeChanged.subscribe {
|
||||||
|
lastTimeChangeTime_ms = System.currentTimeMillis()
|
||||||
|
time = it
|
||||||
|
}
|
||||||
|
eventHandler.onDurationChanged.subscribe { duration = it }
|
||||||
|
eventHandler.onVolumeChanged.subscribe { volume = it }
|
||||||
|
eventHandler.onSpeedChanged.subscribe { speed = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ensureThreadStarted() {}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "CastingDeviceExp"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import org.fcast.sender_sdk.Metadata
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
enum class CastConnectionState {
|
||||||
|
DISCONNECTED,
|
||||||
|
CONNECTING,
|
||||||
|
CONNECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable(with = CastProtocolType.CastProtocolTypeSerializer::class)
|
||||||
|
enum class CastProtocolType {
|
||||||
|
CHROMECAST,
|
||||||
|
AIRPLAY,
|
||||||
|
FCAST;
|
||||||
|
|
||||||
|
object CastProtocolTypeSerializer : KSerializer<CastProtocolType> {
|
||||||
|
override val descriptor: SerialDescriptor =
|
||||||
|
PrimitiveSerialDescriptor("CastProtocolType", PrimitiveKind.STRING)
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: CastProtocolType) {
|
||||||
|
encoder.encodeString(value.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): CastProtocolType {
|
||||||
|
val name = decoder.decodeString()
|
||||||
|
return when (name) {
|
||||||
|
"FASTCAST" -> FCAST // Handle the renamed case
|
||||||
|
else -> CastProtocolType.valueOf(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class CastingDeviceLegacy {
|
||||||
|
abstract val protocol: CastProtocolType;
|
||||||
|
abstract val isReady: Boolean;
|
||||||
|
abstract var usedRemoteAddress: InetAddress?;
|
||||||
|
abstract var localAddress: InetAddress?;
|
||||||
|
abstract val canSetVolume: Boolean;
|
||||||
|
abstract val canSetSpeed: Boolean;
|
||||||
|
|
||||||
|
var name: String? = null;
|
||||||
|
var isPlaying: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
val changed = value != field;
|
||||||
|
field = value;
|
||||||
|
if (changed) {
|
||||||
|
onPlayChanged.emit(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private var lastTimeChangeTime_ms: Long = 0
|
||||||
|
var time: Double = 0.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
protected fun setTime(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
||||||
|
if (changeTime_ms > lastTimeChangeTime_ms && value != time) {
|
||||||
|
time = value
|
||||||
|
lastTimeChangeTime_ms = changeTime_ms
|
||||||
|
onTimeChanged.emit(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastDurationChangeTime_ms: Long = 0
|
||||||
|
var duration: Double = 0.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
protected fun setDuration(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
||||||
|
if (changeTime_ms > lastDurationChangeTime_ms && value != duration) {
|
||||||
|
duration = value
|
||||||
|
lastDurationChangeTime_ms = changeTime_ms
|
||||||
|
onDurationChanged.emit(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastVolumeChangeTime_ms: Long = 0
|
||||||
|
var volume: Double = 1.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
protected fun setVolume(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
||||||
|
if (changeTime_ms > lastVolumeChangeTime_ms && value != volume) {
|
||||||
|
volume = value
|
||||||
|
lastVolumeChangeTime_ms = changeTime_ms
|
||||||
|
onVolumeChanged.emit(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastSpeedChangeTime_ms: Long = 0
|
||||||
|
var speed: Double = 1.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
protected fun setSpeed(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
||||||
|
if (changeTime_ms > lastSpeedChangeTime_ms && value != speed) {
|
||||||
|
speed = value
|
||||||
|
lastSpeedChangeTime_ms = changeTime_ms
|
||||||
|
onSpeedChanged.emit(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val expectedCurrentTime: Double
|
||||||
|
get() {
|
||||||
|
val diff =
|
||||||
|
if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0;
|
||||||
|
return time + diff;
|
||||||
|
};
|
||||||
|
var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED
|
||||||
|
set(value) {
|
||||||
|
val changed = value != field;
|
||||||
|
field = value;
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
onConnectionStateChanged.emit(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var onConnectionStateChanged = Event1<CastConnectionState>();
|
||||||
|
var onPlayChanged = Event1<Boolean>();
|
||||||
|
var onTimeChanged = Event1<Double>();
|
||||||
|
var onDurationChanged = Event1<Double>();
|
||||||
|
var onVolumeChanged = Event1<Double>();
|
||||||
|
var onSpeedChanged = Event1<Double>();
|
||||||
|
|
||||||
|
abstract fun stopCasting();
|
||||||
|
|
||||||
|
abstract fun seekVideo(timeSeconds: Double);
|
||||||
|
abstract fun stopVideo();
|
||||||
|
abstract fun pauseVideo();
|
||||||
|
abstract fun resumeVideo();
|
||||||
|
abstract fun loadVideo(
|
||||||
|
streamType: String,
|
||||||
|
contentType: String,
|
||||||
|
contentId: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?
|
||||||
|
);
|
||||||
|
|
||||||
|
abstract fun loadContent(
|
||||||
|
contentType: String,
|
||||||
|
content: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?
|
||||||
|
);
|
||||||
|
|
||||||
|
open fun changeVolume(volume: Double) {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun changeSpeed(speed: Double) {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun start();
|
||||||
|
abstract fun stop();
|
||||||
|
|
||||||
|
abstract fun getDeviceInfo(): CastingDeviceInfo;
|
||||||
|
|
||||||
|
abstract fun getAddresses(): List<InetAddress>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CastingDeviceLegacyWrapper(val inner: CastingDeviceLegacy) : CastingDevice() {
|
||||||
|
override val isReady: Boolean get() = inner.isReady
|
||||||
|
override val usedRemoteAddress: InetAddress? get() = inner.usedRemoteAddress
|
||||||
|
override val localAddress: InetAddress? get() = inner.localAddress
|
||||||
|
override val name: String? get() = inner.name
|
||||||
|
override val onConnectionStateChanged: Event1<CastConnectionState> get() = inner.onConnectionStateChanged
|
||||||
|
override val onPlayChanged: Event1<Boolean> get() = inner.onPlayChanged
|
||||||
|
override val onTimeChanged: Event1<Double> get() = inner.onTimeChanged
|
||||||
|
override val onDurationChanged: Event1<Double> get() = inner.onDurationChanged
|
||||||
|
override val onVolumeChanged: Event1<Double> get() = inner.onVolumeChanged
|
||||||
|
override val onSpeedChanged: Event1<Double> get() = inner.onSpeedChanged
|
||||||
|
override var connectionState: CastConnectionState
|
||||||
|
get() = inner.connectionState
|
||||||
|
set(_) = Unit
|
||||||
|
override val protocolType: CastProtocolType get() = inner.protocol
|
||||||
|
override var isPlaying: Boolean
|
||||||
|
get() = inner.isPlaying
|
||||||
|
set(_) = Unit
|
||||||
|
override val expectedCurrentTime: Double
|
||||||
|
get() = inner.expectedCurrentTime
|
||||||
|
override var speed: Double
|
||||||
|
get() = inner.speed
|
||||||
|
set(_) = Unit
|
||||||
|
override var time: Double
|
||||||
|
get() = inner.time
|
||||||
|
set(_) = Unit
|
||||||
|
override var duration: Double
|
||||||
|
get() = inner.duration
|
||||||
|
set(_) = Unit
|
||||||
|
override var volume: Double
|
||||||
|
get() = inner.volume
|
||||||
|
set(_) = Unit
|
||||||
|
|
||||||
|
override fun canSetVolume(): Boolean = inner.canSetVolume
|
||||||
|
override fun canSetSpeed(): Boolean = inner.canSetSpeed
|
||||||
|
override fun resumePlayback() = inner.resumeVideo()
|
||||||
|
override fun pausePlayback() = inner.pauseVideo()
|
||||||
|
override fun stopPlayback() = inner.stopVideo()
|
||||||
|
override fun seekTo(timeSeconds: Double) = inner.seekVideo(timeSeconds)
|
||||||
|
override fun changeVolume(timeSeconds: Double) = inner.changeVolume(timeSeconds)
|
||||||
|
override fun changeSpeed(speed: Double) = inner.changeSpeed(speed)
|
||||||
|
override fun connect() = inner.start()
|
||||||
|
override fun disconnect() = inner.stop()
|
||||||
|
override fun getDeviceInfo(): CastingDeviceInfo = inner.getDeviceInfo()
|
||||||
|
override fun getAddresses(): List<InetAddress> = inner.getAddresses()
|
||||||
|
override fun loadVideo(
|
||||||
|
streamType: String,
|
||||||
|
contentType: String,
|
||||||
|
contentId: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
) = inner.loadVideo(streamType, contentType, contentId, resumePosition, duration, speed)
|
||||||
|
|
||||||
|
override fun loadContent(
|
||||||
|
contentType: String,
|
||||||
|
content: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
) = inner.loadContent(contentType, content, resumePosition, duration, speed)
|
||||||
|
|
||||||
|
override fun ensureThreadStarted() = when (inner) {
|
||||||
|
is FCastCastingDevice -> inner.ensureThreadStarted()
|
||||||
|
is ChromecastCastingDevice -> inner.ensureThreadsStarted()
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ import javax.net.ssl.SSLSocket
|
|||||||
import javax.net.ssl.TrustManager
|
import javax.net.ssl.TrustManager
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
class ChromecastCastingDevice : CastingDevice {
|
class ChromecastCastingDevice : CastingDeviceLegacy {
|
||||||
//See for more info: https://developers.google.com/cast/docs/media/messages
|
//See for more info: https://developers.google.com/cast/docs/media/messages
|
||||||
|
|
||||||
override val protocol: CastProtocolType get() = CastProtocolType.CHROMECAST;
|
override val protocol: CastProtocolType get() = CastProtocolType.CHROMECAST;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.futo.platformplayer.casting
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.futo.platformplayer.Settings
|
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
||||||
@@ -25,7 +24,6 @@ import com.futo.platformplayer.toInetAddress
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@@ -34,7 +32,6 @@ import java.io.IOException
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.net.Inet4Address
|
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
@@ -72,7 +69,7 @@ enum class Opcode(val value: Byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FCastCastingDevice : CastingDevice {
|
class FCastCastingDevice : CastingDeviceLegacy {
|
||||||
//See for more info: TODO
|
//See for more info: TODO
|
||||||
|
|
||||||
override val protocol: CastProtocolType get() = CastProtocolType.FCAST;
|
override val protocol: CastProtocolType get() = CastProtocolType.FCAST;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,178 @@
|
|||||||
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.futo.platformplayer.BuildConfig
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import org.fcast.sender_sdk.DeviceInfo as RsDeviceInfo
|
||||||
|
import org.fcast.sender_sdk.ProtocolType
|
||||||
|
import org.fcast.sender_sdk.CastContext
|
||||||
|
import org.fcast.sender_sdk.NsdDeviceDiscoverer
|
||||||
|
|
||||||
|
class StateCastingExp : StateCasting() {
|
||||||
|
private val _context = CastContext()
|
||||||
|
var _deviceDiscoverer: NsdDeviceDiscoverer? = null
|
||||||
|
|
||||||
|
class DiscoveryEventHandler(
|
||||||
|
private val onDeviceAdded: (RsDeviceInfo) -> Unit,
|
||||||
|
private val onDeviceRemoved: (String) -> Unit,
|
||||||
|
private val onDeviceUpdated: (RsDeviceInfo) -> Unit,
|
||||||
|
) : org.fcast.sender_sdk.DeviceDiscovererEventHandler {
|
||||||
|
override fun deviceAvailable(deviceInfo: RsDeviceInfo) {
|
||||||
|
onDeviceAdded(deviceInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deviceChanged(deviceInfo: RsDeviceInfo) {
|
||||||
|
onDeviceUpdated(deviceInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deviceRemoved(deviceName: String) {
|
||||||
|
onDeviceRemoved(deviceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
org.fcast.sender_sdk.initLogger(org.fcast.sender_sdk.LogLevelFilter.DEBUG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleUrl(url: String) {
|
||||||
|
try {
|
||||||
|
val foundDeviceInfo = org.fcast.sender_sdk.deviceInfoFromUrl(url)!!
|
||||||
|
val foundDevice = _context.createDeviceFromInfo(foundDeviceInfo)
|
||||||
|
connectDevice(CastingDeviceExp(foundDevice))
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to handle URL: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
val ad = activeDevice ?: return
|
||||||
|
_resumeCastingDevice = ad.getDeviceInfo()
|
||||||
|
Log.i(TAG, "_resumeCastingDevice set to '${ad.name}'")
|
||||||
|
Logger.i(TAG, "Stopping active device because of onStop.")
|
||||||
|
try {
|
||||||
|
ad.disconnect()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to disconnect from device: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun start(context: Context) {
|
||||||
|
if (_started)
|
||||||
|
return
|
||||||
|
_started = true
|
||||||
|
|
||||||
|
Log.i(TAG, "_resumeCastingDevice set null start")
|
||||||
|
_resumeCastingDevice = null
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService starting...")
|
||||||
|
|
||||||
|
_castServer.start()
|
||||||
|
enableDeveloper(true)
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService started.")
|
||||||
|
|
||||||
|
_deviceDiscoverer = NsdDeviceDiscoverer(
|
||||||
|
context,
|
||||||
|
DiscoveryEventHandler(
|
||||||
|
{ deviceInfo -> // Added
|
||||||
|
Logger.i(TAG, "Device added: ${deviceInfo.name}")
|
||||||
|
val device = _context.createDeviceFromInfo(deviceInfo)
|
||||||
|
val deviceHandle = CastingDeviceExp(device)
|
||||||
|
devices[deviceHandle.device.name()] = deviceHandle
|
||||||
|
invokeInMainScopeIfRequired {
|
||||||
|
onDeviceAdded.emit(deviceHandle)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deviceName -> // Removed
|
||||||
|
invokeInMainScopeIfRequired {
|
||||||
|
if (devices.containsKey(deviceName)) {
|
||||||
|
val device = devices.remove(deviceName)
|
||||||
|
if (device != null) {
|
||||||
|
onDeviceRemoved.emit(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deviceInfo -> // Updated
|
||||||
|
Logger.i(TAG, "Device updated: $deviceInfo")
|
||||||
|
val handle = devices[deviceInfo.name]
|
||||||
|
if (handle != null && handle is CastingDeviceExp) {
|
||||||
|
handle.device.setPort(deviceInfo.port)
|
||||||
|
handle.device.setAddresses(deviceInfo.addresses)
|
||||||
|
invokeInMainScopeIfRequired {
|
||||||
|
onDeviceChanged.emit(handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun stop() {
|
||||||
|
if (!_started) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_started = false
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService stopping.")
|
||||||
|
|
||||||
|
_scopeIO.cancel()
|
||||||
|
_scopeMain.cancel()
|
||||||
|
|
||||||
|
Logger.i(TAG, "Stopping active device because StateCasting is being stopped.")
|
||||||
|
val d = activeDevice
|
||||||
|
activeDevice = null
|
||||||
|
try {
|
||||||
|
d?.disconnect()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to disconnect device: $e")
|
||||||
|
}
|
||||||
|
|
||||||
|
_castServer.stop()
|
||||||
|
_castServer.removeAllHandlers()
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService stopped.")
|
||||||
|
|
||||||
|
_deviceDiscoverer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startUpdateTimeJob(
|
||||||
|
onTimeJobTimeChanged_s: Event1<Long>,
|
||||||
|
setTime: (Long) -> Unit
|
||||||
|
): Job? = null
|
||||||
|
|
||||||
|
override fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDeviceExp? {
|
||||||
|
try {
|
||||||
|
val rsAddrs =
|
||||||
|
deviceInfo.addresses.map { org.fcast.sender_sdk.tryIpAddrFromStr(it) }
|
||||||
|
val rsDeviceInfo = RsDeviceInfo(
|
||||||
|
name = deviceInfo.name,
|
||||||
|
protocol = when (deviceInfo.type) {
|
||||||
|
com.futo.platformplayer.casting.CastProtocolType.CHROMECAST -> ProtocolType.CHROMECAST
|
||||||
|
com.futo.platformplayer.casting.CastProtocolType.FCAST -> ProtocolType.F_CAST
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
},
|
||||||
|
addresses = rsAddrs,
|
||||||
|
port = deviceInfo.port.toUShort(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return CastingDeviceExp(_context.createDeviceFromInfo(rsDeviceInfo))
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "StateCastingExp"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.net.nsd.NsdManager
|
||||||
|
import android.net.nsd.NsdServiceInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.net.InetAddress
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
class StateCastingLegacy : StateCasting() {
|
||||||
|
private var _nsdManager: NsdManager? = null
|
||||||
|
|
||||||
|
private val _discoveryListeners = mapOf(
|
||||||
|
"_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice),
|
||||||
|
"_airplay._tcp" to createDiscoveryListener(::addOrUpdateAirPlayDevice),
|
||||||
|
"_fastcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice),
|
||||||
|
"_fcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun handleUrl(url: String) {
|
||||||
|
val uri = Uri.parse(url)
|
||||||
|
if (uri.scheme != "fcast") {
|
||||||
|
throw Exception("Expected scheme to be FCast")
|
||||||
|
}
|
||||||
|
|
||||||
|
val type = uri.host
|
||||||
|
if (type != "r") {
|
||||||
|
throw Exception("Expected type r")
|
||||||
|
}
|
||||||
|
|
||||||
|
val connectionInfo = uri.pathSegments[0]
|
||||||
|
val json =
|
||||||
|
Base64.decode(connectionInfo, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
||||||
|
.toString(Charsets.UTF_8)
|
||||||
|
val networkConfig = Json.decodeFromString<FCastNetworkConfig>(json)
|
||||||
|
val tcpService = networkConfig.services.first { v -> v.type == 0 }
|
||||||
|
|
||||||
|
val foundInfo = addRememberedDevice(
|
||||||
|
CastingDeviceInfo(
|
||||||
|
name = networkConfig.name,
|
||||||
|
type = CastProtocolType.FCAST,
|
||||||
|
addresses = networkConfig.addresses.toTypedArray(),
|
||||||
|
port = tcpService.port
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (foundInfo != null) {
|
||||||
|
connectDevice(deviceFromInfo(foundInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
val ad = activeDevice ?: return;
|
||||||
|
_resumeCastingDevice = ad.getDeviceInfo()
|
||||||
|
Log.i(TAG, "_resumeCastingDevice set to '${ad.name}'")
|
||||||
|
Logger.i(TAG, "Stopping active device because of onStop.");
|
||||||
|
ad.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun start(context: Context) {
|
||||||
|
if (_started)
|
||||||
|
return;
|
||||||
|
_started = true;
|
||||||
|
|
||||||
|
Log.i(TAG, "_resumeCastingDevice set null start")
|
||||||
|
_resumeCastingDevice = null;
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService starting...");
|
||||||
|
|
||||||
|
_castServer.start();
|
||||||
|
enableDeveloper(true);
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService started.");
|
||||||
|
|
||||||
|
_nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||||
|
startDiscovering()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun startDiscovering() {
|
||||||
|
_nsdManager?.apply {
|
||||||
|
_discoveryListeners.forEach {
|
||||||
|
discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun stopDiscovering() {
|
||||||
|
_nsdManager?.apply {
|
||||||
|
_discoveryListeners.forEach {
|
||||||
|
try {
|
||||||
|
stopServiceDiscovery(it.value)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to stop service discovery", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun stop() {
|
||||||
|
if (!_started)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_started = false;
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService stopping.")
|
||||||
|
|
||||||
|
stopDiscovering()
|
||||||
|
_scopeIO.cancel();
|
||||||
|
_scopeMain.cancel();
|
||||||
|
|
||||||
|
Logger.i(TAG, "Stopping active device because StateCasting is being stopped.")
|
||||||
|
val d = activeDevice;
|
||||||
|
activeDevice = null;
|
||||||
|
d?.disconnect();
|
||||||
|
|
||||||
|
_castServer.stop();
|
||||||
|
_castServer.removeAllHandlers();
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService stopped.")
|
||||||
|
|
||||||
|
_nsdManager = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDiscoveryListener(addOrUpdate: (String, Array<InetAddress>, Int) -> Unit): NsdManager.DiscoveryListener {
|
||||||
|
return object : NsdManager.DiscoveryListener {
|
||||||
|
override fun onDiscoveryStarted(regType: String) {
|
||||||
|
Log.d(TAG, "Service discovery started for $regType")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDiscoveryStopped(serviceType: String) {
|
||||||
|
Log.i(TAG, "Discovery stopped: $serviceType")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceLost(service: NsdServiceInfo) {
|
||||||
|
Log.e(TAG, "service lost: $service")
|
||||||
|
// TODO: Handle service lost, e.g., remove device
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||||
|
Log.e(TAG, "Discovery failed for $serviceType: Error code:$errorCode")
|
||||||
|
try {
|
||||||
|
_nsdManager?.stopServiceDiscovery(this)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to stop service discovery", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||||
|
Log.e(TAG, "Stop discovery failed for $serviceType: Error code:$errorCode")
|
||||||
|
try {
|
||||||
|
_nsdManager?.stopServiceDiscovery(this)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to stop service discovery", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceFound(service: NsdServiceInfo) {
|
||||||
|
Log.v(TAG, "Service discovery success for ${service.serviceType}: $service")
|
||||||
|
val addresses = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
service.hostAddresses.toTypedArray()
|
||||||
|
} else {
|
||||||
|
arrayOf(service.host)
|
||||||
|
}
|
||||||
|
addOrUpdate(service.serviceName, addresses, service.port)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
_nsdManager?.registerServiceInfoCallback(
|
||||||
|
service,
|
||||||
|
{ it.run() },
|
||||||
|
object : NsdManager.ServiceInfoCallback {
|
||||||
|
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
|
||||||
|
Log.v(TAG, "onServiceUpdated: $serviceInfo")
|
||||||
|
addOrUpdate(
|
||||||
|
serviceInfo.serviceName,
|
||||||
|
serviceInfo.hostAddresses.toTypedArray(),
|
||||||
|
serviceInfo.port
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceLost() {
|
||||||
|
Log.v(TAG, "onServiceLost: $service")
|
||||||
|
// TODO: Handle service lost
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
|
||||||
|
Log.v(TAG, "onServiceInfoCallbackRegistrationFailed: $errorCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceInfoCallbackUnregistered() {
|
||||||
|
Log.v(TAG, "onServiceInfoCallbackUnregistered")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
_nsdManager?.resolveService(service, object : NsdManager.ResolveListener {
|
||||||
|
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||||
|
Log.v(TAG, "Resolve failed: $errorCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
||||||
|
Log.v(TAG, "Resolve Succeeded: $serviceInfo")
|
||||||
|
addOrUpdate(
|
||||||
|
serviceInfo.serviceName,
|
||||||
|
arrayOf(serviceInfo.host),
|
||||||
|
serviceInfo.port
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startUpdateTimeJob(
|
||||||
|
onTimeJobTimeChanged_s: Event1<Long>,
|
||||||
|
setTime: (Long) -> Unit
|
||||||
|
): Job? {
|
||||||
|
val d = activeDevice;
|
||||||
|
if (d is CastingDeviceLegacyWrapper && (d.inner is AirPlayCastingDevice || d.inner is ChromecastCastingDevice)) {
|
||||||
|
return _scopeMain.launch {
|
||||||
|
while (true) {
|
||||||
|
val device = instance.activeDevice
|
||||||
|
if (device == null || !device.isPlaying) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(1000)
|
||||||
|
val time_ms = (device.expectedCurrentTime * 1000.0).toLong()
|
||||||
|
setTime(time_ms)
|
||||||
|
onTimeJobTimeChanged_s.emit(device.expectedCurrentTime.toLong())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDevice {
|
||||||
|
return CastingDeviceLegacyWrapper(
|
||||||
|
when (deviceInfo.type) {
|
||||||
|
CastProtocolType.CHROMECAST -> {
|
||||||
|
ChromecastCastingDevice(deviceInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
CastProtocolType.AIRPLAY -> {
|
||||||
|
AirPlayCastingDevice(deviceInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
CastProtocolType.FCAST -> {
|
||||||
|
FCastCastingDevice(deviceInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addOrUpdateChromeCastDevice(
|
||||||
|
name: String,
|
||||||
|
addresses: Array<InetAddress>,
|
||||||
|
port: Int
|
||||||
|
) {
|
||||||
|
return addOrUpdateCastDevice(
|
||||||
|
name,
|
||||||
|
deviceFactory = {
|
||||||
|
CastingDeviceLegacyWrapper(
|
||||||
|
ChromecastCastingDevice(
|
||||||
|
name,
|
||||||
|
addresses,
|
||||||
|
port
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
deviceUpdater = { d ->
|
||||||
|
if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is ChromecastCastingDevice) {
|
||||||
|
return@addOrUpdateCastDevice false;
|
||||||
|
}
|
||||||
|
|
||||||
|
val changed =
|
||||||
|
addresses.contentEquals(d.inner.addresses) || d.name != name || d.inner.port != port;
|
||||||
|
if (changed) {
|
||||||
|
d.inner.name = name;
|
||||||
|
d.inner.addresses = addresses;
|
||||||
|
d.inner.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
return@addOrUpdateCastDevice changed;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addOrUpdateAirPlayDevice(name: String, addresses: Array<InetAddress>, port: Int) {
|
||||||
|
return addOrUpdateCastDevice(
|
||||||
|
name,
|
||||||
|
deviceFactory = {
|
||||||
|
CastingDeviceLegacyWrapper(
|
||||||
|
AirPlayCastingDevice(
|
||||||
|
name,
|
||||||
|
addresses,
|
||||||
|
port
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
deviceUpdater = { d ->
|
||||||
|
if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is AirPlayCastingDevice) {
|
||||||
|
return@addOrUpdateCastDevice false;
|
||||||
|
}
|
||||||
|
|
||||||
|
val changed =
|
||||||
|
addresses.contentEquals(addresses) || d.name != name || d.inner.port != port;
|
||||||
|
if (changed) {
|
||||||
|
d.inner.name = name;
|
||||||
|
d.inner.port = port;
|
||||||
|
d.inner.addresses = addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
return@addOrUpdateCastDevice changed;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addOrUpdateFastCastDevice(name: String, addresses: Array<InetAddress>, port: Int) {
|
||||||
|
return addOrUpdateCastDevice(
|
||||||
|
name,
|
||||||
|
deviceFactory = { CastingDeviceLegacyWrapper(FCastCastingDevice(name, addresses, port)) },
|
||||||
|
deviceUpdater = { d ->
|
||||||
|
if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is FCastCastingDevice) {
|
||||||
|
return@addOrUpdateCastDevice false;
|
||||||
|
}
|
||||||
|
|
||||||
|
val changed =
|
||||||
|
addresses.contentEquals(addresses) || d.name != name || d.inner.port != port;
|
||||||
|
if (changed) {
|
||||||
|
d.inner.name = name;
|
||||||
|
d.inner.port = port;
|
||||||
|
d.inner.addresses = addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
return@addOrUpdateCastDevice changed;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun addOrUpdateCastDevice(
|
||||||
|
name: String,
|
||||||
|
deviceFactory: () -> CastingDevice,
|
||||||
|
deviceUpdater: (device: CastingDevice) -> Boolean
|
||||||
|
) {
|
||||||
|
var invokeEvents: (() -> Unit)? = null;
|
||||||
|
|
||||||
|
synchronized(devices) {
|
||||||
|
val device = devices[name];
|
||||||
|
if (device != null) {
|
||||||
|
val changed = deviceUpdater(device);
|
||||||
|
if (changed) {
|
||||||
|
invokeEvents = {
|
||||||
|
onDeviceChanged.emit(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val newDevice = deviceFactory();
|
||||||
|
this.devices[name] = newDevice
|
||||||
|
|
||||||
|
invokeEvents = {
|
||||||
|
onDeviceAdded.emit(newDevice);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invokeEvents?.let { _scopeMain.launch { it(); }; };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class FCastNetworkConfig(
|
||||||
|
val name: String,
|
||||||
|
val addresses: List<String>,
|
||||||
|
val services: List<FCastService>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class FCastService(
|
||||||
|
val port: Int,
|
||||||
|
val type: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "StateCastingLegacy"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -268,11 +269,15 @@ 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;
|
||||||
}
|
}
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import android.view.View
|
|||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
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.casting.CastProtocolType
|
import com.futo.platformplayer.casting.CastProtocolType
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.toInetAddress
|
import com.futo.platformplayer.toInetAddress
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
|
||||||
|
|
||||||
class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
||||||
@@ -38,7 +40,13 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
|||||||
_buttonConfirm = findViewById(R.id.button_confirm);
|
_buttonConfirm = findViewById(R.id.button_confirm);
|
||||||
_buttonTutorial = findViewById(R.id.button_tutorial)
|
_buttonTutorial = findViewById(R.id.button_tutorial)
|
||||||
|
|
||||||
ArrayAdapter.createFromResource(context, R.array.casting_device_type_array, R.layout.spinner_item_simple).also { adapter ->
|
val deviceTypeArray = if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
R.array.exp_casting_device_type_array
|
||||||
|
} else {
|
||||||
|
R.array.casting_device_type_array
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayAdapter.createFromResource(context, deviceTypeArray, R.layout.spinner_item_simple).also { adapter ->
|
||||||
adapter.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
adapter.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
||||||
_spinnerType.adapter = adapter;
|
_spinnerType.adapter = adapter;
|
||||||
};
|
};
|
||||||
@@ -101,7 +109,11 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
_textError.visibility = View.GONE;
|
_textError.visibility = View.GONE;
|
||||||
val castingDeviceInfo = CastingDeviceInfo(name, castProtocolType, arrayOf(ip), port.toInt());
|
val castingDeviceInfo = CastingDeviceInfo(name, castProtocolType, arrayOf(ip), port.toInt());
|
||||||
StateCasting.instance.addRememberedDevice(castingDeviceInfo);
|
try {
|
||||||
|
StateCasting.instance.addRememberedDevice(castingDeviceInfo)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to add remembered device: $e")
|
||||||
|
}
|
||||||
performDismiss();
|
performDismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
@@ -18,7 +17,6 @@ import com.futo.platformplayer.R
|
|||||||
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.casting.CastConnectionState
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
import com.futo.platformplayer.casting.CastingDevice
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
@@ -108,15 +106,16 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
synchronized(StateCasting.instance.devices) {
|
synchronized(StateCasting.instance.devices) {
|
||||||
_devices.addAll(StateCasting.instance.devices.values.mapNotNull { it.name })
|
_devices.addAll(StateCasting.instance.devices.values.mapNotNull { it.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
|
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
|
||||||
|
|
||||||
updateUnifiedList()
|
updateUnifiedList()
|
||||||
|
|
||||||
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
||||||
val name = d.name
|
val name = d.name
|
||||||
if (name != null)
|
if (name != null) {
|
||||||
_devices.add(name)
|
_devices.add(name)
|
||||||
updateUnifiedList()
|
updateUnifiedList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ import android.widget.ImageView
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.casting.AirPlayCastingDevice
|
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
|
import com.futo.platformplayer.casting.CastProtocolType
|
||||||
import com.futo.platformplayer.casting.CastingDevice
|
import com.futo.platformplayer.casting.CastingDevice
|
||||||
import com.futo.platformplayer.casting.ChromecastCastingDevice
|
|
||||||
import com.futo.platformplayer.casting.FCastCastingDevice
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -69,18 +68,18 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
_buttonPlay = findViewById(R.id.button_play);
|
_buttonPlay = findViewById(R.id.button_play);
|
||||||
_buttonPlay.setOnClickListener {
|
_buttonPlay.setOnClickListener {
|
||||||
StateCasting.instance.activeDevice?.resumeVideo()
|
StateCasting.instance.resumeVideo()
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonPause = findViewById(R.id.button_pause);
|
_buttonPause = findViewById(R.id.button_pause);
|
||||||
_buttonPause.setOnClickListener {
|
_buttonPause.setOnClickListener {
|
||||||
StateCasting.instance.activeDevice?.pauseVideo()
|
StateCasting.instance.pauseVideo()
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonStop = findViewById(R.id.button_stop);
|
_buttonStop = findViewById(R.id.button_stop);
|
||||||
_buttonStop.setOnClickListener {
|
_buttonStop.setOnClickListener {
|
||||||
(ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
|
(ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
|
||||||
StateCasting.instance.activeDevice?.stopVideo()
|
StateCasting.instance.stopVideo()
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonNext = findViewById(R.id.button_next);
|
_buttonNext = findViewById(R.id.button_next);
|
||||||
@@ -90,7 +89,12 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
_buttonClose.setOnClickListener { dismiss(); };
|
_buttonClose.setOnClickListener { dismiss(); };
|
||||||
_buttonDisconnect.setOnClickListener {
|
_buttonDisconnect.setOnClickListener {
|
||||||
StateCasting.instance.activeDevice?.stopCasting();
|
try {
|
||||||
|
StateCasting.instance.stopVideo()
|
||||||
|
StateCasting.instance.activeDevice?.disconnect()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Active device failed to disconnect: $e")
|
||||||
|
}
|
||||||
dismiss();
|
dismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,12 +103,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
return@OnChangeListener
|
return@OnChangeListener
|
||||||
}
|
}
|
||||||
|
|
||||||
val activeDevice = StateCasting.instance.activeDevice ?: return@OnChangeListener;
|
StateCasting.instance.videoSeekTo(value.toDouble())
|
||||||
try {
|
|
||||||
activeDevice.seekVideo(value.toDouble());
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to change volume.", e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//TODO: Check if volume slider is properly hidden in all cases
|
//TODO: Check if volume slider is properly hidden in all cases
|
||||||
@@ -113,14 +112,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
return@OnChangeListener
|
return@OnChangeListener
|
||||||
}
|
}
|
||||||
|
|
||||||
val activeDevice = StateCasting.instance.activeDevice ?: return@OnChangeListener;
|
StateCasting.instance.changeVolume(value.toDouble())
|
||||||
if (activeDevice.canSetVolume) {
|
|
||||||
try {
|
|
||||||
activeDevice.changeVolume(value.toDouble());
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to change volume.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -172,15 +164,25 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
private fun updateDevice() {
|
private fun updateDevice() {
|
||||||
val d = StateCasting.instance.activeDevice ?: return;
|
val d = StateCasting.instance.activeDevice ?: return;
|
||||||
|
|
||||||
if (d is ChromecastCastingDevice) {
|
when (d.protocolType) {
|
||||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
CastProtocolType.CHROMECAST -> {
|
||||||
_textType.text = "Chromecast";
|
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
||||||
} else if (d is AirPlayCastingDevice) {
|
_textType.text = "Chromecast";
|
||||||
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
}
|
||||||
_textType.text = "AirPlay";
|
CastProtocolType.AIRPLAY -> {
|
||||||
} else if (d is FCastCastingDevice) {
|
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
||||||
_imageDevice.setImageResource(R.drawable.ic_fc);
|
_textType.text = "AirPlay";
|
||||||
_textType.text = "FastCast";
|
}
|
||||||
|
CastProtocolType.FCAST -> {
|
||||||
|
_imageDevice.setImageResource(
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
R.drawable.ic_exp_fc
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_fc
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_textType.text = "FCast";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_textName.text = d.name;
|
_textName.text = d.name;
|
||||||
@@ -192,7 +194,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
||||||
_sliderPosition.valueTo = dur
|
_sliderPosition.valueTo = dur
|
||||||
|
|
||||||
if (d.canSetVolume) {
|
if (d.canSetVolume()) {
|
||||||
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
||||||
_layoutVolumeFixed.visibility = View.GONE;
|
_layoutVolumeFixed.visibility = View.GONE;
|
||||||
} else {
|
} else {
|
||||||
@@ -214,8 +216,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
CastConnectionState.CONNECTED -> {
|
CastConnectionState.CONNECTED -> {
|
||||||
enableControls(interactiveControls)
|
enableControls(interactiveControls)
|
||||||
}
|
}
|
||||||
CastConnectionState.CONNECTING,
|
CastConnectionState.CONNECTING, CastConnectionState.DISCONNECTED -> {
|
||||||
CastConnectionState.DISCONNECTED -> {
|
|
||||||
disableControls(interactiveControls)
|
disableControls(interactiveControls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.arthenica.ffmpegkit.StatisticsCallback
|
|||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
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
|
||||||
@@ -136,6 +137,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 +206,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;
|
||||||
@@ -478,8 +483,8 @@ 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);
|
videoFileSize = downloadDashFileSource("Video", client, actualVideoSource, File(downloadDir, videoFileName!!), progressCallback);
|
||||||
@@ -518,8 +523,8 @@ 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);
|
||||||
@@ -580,110 +585,266 @@ 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 {
|
private fun remuxWithFfmpegInPlace(inputFile: File): Boolean {
|
||||||
if(targetFile.exists())
|
val inputPath = inputFile.absolutePath
|
||||||
targetFile.delete();
|
if (!inputFile.exists()) {
|
||||||
|
Logger.w(TAG, "remuxWithFfmpegInPlace: input does not exist: $inputPath")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
var downloadedTotalLength = 0L
|
val parent = inputFile.parentFile
|
||||||
|
if (parent == null) {
|
||||||
|
Logger.w(TAG, "remuxWithFfmpegInPlace: input has no parent: $inputPath")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
val segmentFiles = arrayListOf<File>()
|
val tmpFile = File(parent, inputFile.nameWithoutExtension + "_fixed." + inputFile.extension)
|
||||||
try {
|
val cmd = buildString {
|
||||||
val response = client.get(hlsUrl)
|
append("-y ")
|
||||||
check(response.isOk) { "Failed to get variant playlist: ${response.code}" }
|
append("-i \"").append(inputFile.absolutePath).append("\" ")
|
||||||
|
append("-c copy ")
|
||||||
|
append("-movflags +faststart ")
|
||||||
|
append("\"").append(tmpFile.absolutePath).append("\"")
|
||||||
|
}
|
||||||
|
|
||||||
val vpContent = response.body?.string()
|
Logger.i(TAG, "FFmpeg remux command: $cmd")
|
||||||
?: throw Exception("Variant playlist content is empty")
|
|
||||||
|
|
||||||
val variantPlaylist = HLS.parseVariantPlaylist(vpContent, hlsUrl)
|
val session = FFmpegKit.execute(cmd)
|
||||||
val decryptionInfo: DecryptionInfo? = if (variantPlaylist.decryptionInfo != null) {
|
val returnCode = session.returnCode
|
||||||
val keyResponse = client.get(variantPlaylist.decryptionInfo.keyUrl)
|
|
||||||
check(keyResponse.isOk) { "HLS request failed for decryption key: ${keyResponse.code}" }
|
if (ReturnCode.isSuccess(returnCode)) {
|
||||||
DecryptionInfo(keyResponse.body!!.bytes(), variantPlaylist.decryptionInfo.iv?.hexStringToByteArray())
|
val newLen = tmpFile.length()
|
||||||
|
|
||||||
|
if (!inputFile.delete()) {
|
||||||
|
Logger.w(TAG, "remuxWithFfmpegInPlace: failed to delete original: ${inputFile.absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tmpFile.renameTo(inputFile)) {
|
||||||
|
Logger.w(TAG, "remuxWithFfmpegInPlace: failed to move tmp: ${tmpFile.absolutePath}")
|
||||||
} else {
|
} else {
|
||||||
null
|
Logger.i(TAG, "remuxWithFfmpegInPlace: success for $inputPath (size=$newLen bytes)")
|
||||||
}
|
}
|
||||||
|
|
||||||
variantPlaylist.segments.forEachIndexed { index, segment ->
|
return true
|
||||||
if (segment !is HLS.MediaSegment) {
|
} else {
|
||||||
return@forEachIndexed
|
Logger.e(TAG, "FFmpeg remux failed for $inputPath. rc=$returnCode, logs=${session.allLogsAsString}")
|
||||||
}
|
tmpFile.delete()
|
||||||
|
return false
|
||||||
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 fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, source: JSSource?, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
suspendCancellableCoroutine { continuation ->
|
if (targetFile.exists())
|
||||||
val cmd =
|
targetFile.delete()
|
||||||
"-i \"concat:${segmentFiles.joinToString("|")}\" -c copy \"${targetFile.absolutePath}\""
|
|
||||||
|
|
||||||
val statisticsCallback = StatisticsCallback { _ ->
|
var downloadedTotalLength = 0L
|
||||||
//TODO: Show progress?
|
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 executorService = Executors.newSingleThreadExecutor()
|
val modified = modifier?.modifyRequest(url, headers)
|
||||||
val session = FFmpegKit.executeAsync(cmd,
|
val finalUrl = modified?.url ?: url
|
||||||
{ session ->
|
val finalHeaders = modified?.headers?.toMutableMap() ?: headers
|
||||||
if (ReturnCode.isSuccess(session.returnCode)) {
|
|
||||||
continuation.resumeWith(Result.success(Unit))
|
val resp = client.get(finalUrl, finalHeaders)
|
||||||
} else {
|
if (!resp.isOk) {
|
||||||
val errorMessage = if (ReturnCode.isCancel(session.returnCode)) {
|
resp.body?.close()
|
||||||
"Command cancelled"
|
throw IllegalStateException("Failed to download HLS resource ($finalUrl): HTTP ${resp.code}")
|
||||||
} else {
|
}
|
||||||
"Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}"
|
|
||||||
}
|
val body = resp.body ?: throw IllegalStateException("Failed to download HLS resource ($finalUrl): Empty body")
|
||||||
continuation.resumeWithException(RuntimeException(errorMessage))
|
val bytes = body.bytes()
|
||||||
}
|
body.close()
|
||||||
},
|
return bytes
|
||||||
{ Logger.v(TAG, it.message) },
|
}
|
||||||
statisticsCallback,
|
|
||||||
executorService
|
fun buildSequenceIv(sequenceNumber: Long): ByteArray {
|
||||||
|
return ByteBuffer.allocate(16)
|
||||||
|
.putLong(0L)
|
||||||
|
.putLong(sequenceNumber)
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val playlistHeaders = mutableMapOf<String, String>()
|
||||||
|
val modifiedPlaylistReq = modifier?.modifyRequest(hlsUrl, playlistHeaders)
|
||||||
|
val playlistResp = client.get(
|
||||||
|
modifiedPlaylistReq?.url ?: hlsUrl,
|
||||||
|
modifiedPlaylistReq?.headers?.toMutableMap() ?: playlistHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
continuation.invokeOnCancellation {
|
check(playlistResp.isOk) { "Failed to get variant playlist: ${playlistResp.code}" }
|
||||||
session.cancel()
|
|
||||||
|
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>()
|
||||||
|
|
||||||
|
targetFile.outputStream().use { outStr ->
|
||||||
|
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.")
|
||||||
|
}
|
||||||
|
|
||||||
|
outStr.write(mapBytes)
|
||||||
|
outStr.flush()
|
||||||
|
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
|
||||||
|
|
||||||
|
outStr.write(segmentBytes)
|
||||||
|
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++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remuxWithFfmpegInPlace(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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return downloadedTotalLength
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadDashFileSource(name: String, client: ManagedHttpClient, source: IJSDashManifestRawSource, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
private fun downloadDashFileSource(name: String, client: ManagedHttpClient, source: IJSDashManifestRawSource, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
@@ -715,6 +876,11 @@ class VideoDownload {
|
|||||||
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());
|
||||||
@@ -726,12 +892,14 @@ class VideoDownload {
|
|||||||
val t = cue.groupValues[1];
|
val t = cue.groupValues[1];
|
||||||
val d = cue.groupValues[2];
|
val d = cue.groupValues[2];
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
@@ -766,7 +934,7 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
return sourceLength!!;
|
return sourceLength!!;
|
||||||
}
|
}
|
||||||
private fun downloadFileSource(name: String, client: ManagedHttpClient, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
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 +943,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 +959,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 +1015,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 +1024,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 +1166,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 +1185,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 +1216,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 +1230,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()
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -242,10 +247,12 @@ class V8Plugin {
|
|||||||
}
|
}
|
||||||
fun <T> busy(handle: ()->T): T {
|
fun <T> busy(handle: ()->T): T {
|
||||||
_busyLock.lock();
|
_busyLock.lock();
|
||||||
|
//Logger.i(TAG, "Busy Enter [" + _busyLock.holdCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString())
|
||||||
try {
|
try {
|
||||||
return handle();
|
return handle();
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
//Logger.i(TAG, "Busy Leave [" + _busyLock.holdCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString())
|
||||||
_busyLock.unlock();
|
_busyLock.unlock();
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
@@ -377,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}");
|
||||||
@@ -405,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
|
||||||
@@ -438,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) {
|
||||||
@@ -502,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) {
|
||||||
@@ -511,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;
|
||||||
|
|||||||
+208
-13
@@ -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;
|
||||||
@@ -91,10 +107,71 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
_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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
val layoutManager = GridLayoutManager(context, columns, GridLayoutManager.VERTICAL, true);
|
||||||
|
_layoutMoreButtons.layoutManager = layoutManager;
|
||||||
|
|
||||||
_overlayMoreBackground.setOnClickListener { setMoreVisible(false); };
|
_overlayMoreBackground.setOnClickListener { setMoreVisible(false); };
|
||||||
|
|
||||||
@@ -121,6 +198,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 +208,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 +225,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 +247,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 +276,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();
|
||||||
@@ -243,7 +346,9 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
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 +358,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 +451,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 +529,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 +547,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 +579,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) }),
|
||||||
@@ -399,6 +592,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
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, { false }, {
|
||||||
|
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,7 +601,7 @@ 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",
|
||||||
@@ -424,7 +619,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
//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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+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,6 +53,7 @@ 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 = true;
|
||||||
|
|
||||||
@@ -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, {
|
||||||
@@ -390,6 +399,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 +484,8 @@ 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)
|
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) {
|
||||||
|
|||||||
+11
-1
@@ -279,6 +279,14 @@ class HomeFragment : MainFragment() {
|
|||||||
else {
|
else {
|
||||||
view.setToggle(!active);
|
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")
|
}).withTag("plugins")
|
||||||
})
|
})
|
||||||
else listOf())
|
else listOf())
|
||||||
@@ -357,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(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+265
@@ -0,0 +1,265 @@
|
|||||||
|
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.removeLast();
|
||||||
|
openDirectory(navStack.last());
|
||||||
|
}
|
||||||
|
else {}
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
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);
|
||||||
|
val buckets = StateLibrary.instance.getVideoBucketNames();
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.others.LoginWebViewClient
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.text.matches
|
||||||
|
|
||||||
|
|
||||||
|
class LoginFragment : 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(parameter ?: throw IllegalArgumentException("No parameter for login"));
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = LoginFragment().apply {}
|
||||||
|
|
||||||
|
private var _callback: ((SourceAuth?) -> Unit)? = null;
|
||||||
|
fun showLogin(config: SourcePluginConfig, callback: ((SourceAuth?) -> Unit)? = null) {
|
||||||
|
if(_callback != null) _callback?.invoke(null);
|
||||||
|
_callback = callback;
|
||||||
|
StateApp.instance.activity?.navigate<LoginFragment>(config, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FragView: ConstraintLayout {
|
||||||
|
val fragment: LoginFragment;
|
||||||
|
|
||||||
|
private val _webView: WebView;
|
||||||
|
private val _textUrl: TextView;
|
||||||
|
private val _buttonClose: ImageButton;
|
||||||
|
|
||||||
|
constructor(fragment: LoginFragment) : super(fragment.requireContext()) {
|
||||||
|
inflate(context, R.layout.activity_login, this);
|
||||||
|
this.fragment = fragment;
|
||||||
|
|
||||||
|
_textUrl = findViewById(R.id.text_url);
|
||||||
|
_buttonClose = findViewById(R.id.button_close);
|
||||||
|
_buttonClose.setOnClickListener {
|
||||||
|
UIDialogs.toast("Login cancelled", false);
|
||||||
|
fragment.close(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_webView = findViewById(R.id.web_view);
|
||||||
|
_webView.settings.javaScriptEnabled = true;
|
||||||
|
CookieManager.getInstance().setAcceptCookie(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onShown(parameter: Any) {
|
||||||
|
|
||||||
|
|
||||||
|
val config = parameter as? SourcePluginConfig;
|
||||||
|
|
||||||
|
val authConfig = if(config != null)
|
||||||
|
config.authentication ?: throw IllegalStateException("Plugin has no authentication support");
|
||||||
|
else if(parameter is SourcePluginAuthConfig)
|
||||||
|
parameter
|
||||||
|
else throw IllegalStateException("No valid configuration?");
|
||||||
|
//TODO: Backwards compat removal?
|
||||||
|
|
||||||
|
_webView.settings.userAgentString = authConfig.userAgent ?: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36";
|
||||||
|
_webView.settings.useWideViewPort = true;
|
||||||
|
_webView.settings.loadWithOverviewMode = true;
|
||||||
|
|
||||||
|
val webViewClient = if(config != null) LoginWebViewClient(config) else LoginWebViewClient(authConfig);
|
||||||
|
|
||||||
|
webViewClient.onLogin.subscribe { auth ->
|
||||||
|
_callback?.let {
|
||||||
|
_callback = null;
|
||||||
|
it.invoke(auth);
|
||||||
|
}
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
fragment.close(true);
|
||||||
|
}catch (ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to close login", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var isFirstLoad = true;
|
||||||
|
val loginWarnings = authConfig.loginWarnings?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.Warning>();
|
||||||
|
val uiMods = authConfig.uiMods?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.UIMod>();
|
||||||
|
var currentScale = 100;
|
||||||
|
var currentDesktop = false;
|
||||||
|
webViewClient.onPageLoaded.subscribe { view, url ->
|
||||||
|
_textUrl.setText(url ?: "");
|
||||||
|
|
||||||
|
if(loginWarnings.size > 0 && url != null) {
|
||||||
|
synchronized(loginWarnings) {
|
||||||
|
val warning = loginWarnings.find { url.matches(it.getRegex()) };
|
||||||
|
if(warning != null) {
|
||||||
|
if(warning.once == true)
|
||||||
|
loginWarnings.remove(warning);
|
||||||
|
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow, warning.text ?: "", warning.details ?: "", null, 0,
|
||||||
|
UIDialogs.Action("Understood", {
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isFirstLoad)
|
||||||
|
return@subscribe;
|
||||||
|
isFirstLoad = false;
|
||||||
|
|
||||||
|
if(!authConfig.loginButton.isNullOrEmpty() && authConfig.loginButton.matches(REGEX_LOGIN_BUTTON)) {
|
||||||
|
Logger.i(TAG, "Clicking login button [${authConfig.loginButton}]");
|
||||||
|
//TODO: Find most reliable way to wait for page js to finish
|
||||||
|
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_webView.settings.domStorageEnabled = true;
|
||||||
|
|
||||||
|
_webView.webViewClient = webViewClient;
|
||||||
|
_webView.loadUrl(authConfig.loginUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "LoginFragment";
|
||||||
|
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#:_ ]*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
|
|
||||||
|
class RecyclerFragment : MainFragment(){
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var view: View? = null;
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): android.view.View {
|
||||||
|
val newView = RecyclerFragment.View(inflater.context);
|
||||||
|
view = newView;
|
||||||
|
return newView;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = RecyclerFragment().apply {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class View: ConstraintLayout {
|
||||||
|
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
|
||||||
|
inflate(context, R.layout.fragview_filter_recycler, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+184
@@ -0,0 +1,184 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
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 androidx.lifecycle.lifecycleScope
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.SettingsDev
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.assume
|
||||||
|
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 SettingsFragment : 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);
|
||||||
|
view = newView;
|
||||||
|
return newView;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
_currentView = view;
|
||||||
|
view?.onShown(parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHide() {
|
||||||
|
super.onHide();
|
||||||
|
onClosed.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
view = null;
|
||||||
|
_currentView = null;
|
||||||
|
super.onDestroyMainView();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = SettingsFragment().apply {}
|
||||||
|
|
||||||
|
val onClosed = Event0();
|
||||||
|
|
||||||
|
private var _currentView: FragView? = null;
|
||||||
|
val currentView: FragView?
|
||||||
|
get() = _currentView;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FragView: ConstraintLayout {
|
||||||
|
val fragment: SettingsFragment;
|
||||||
|
|
||||||
|
private val _form: FieldForm;
|
||||||
|
private val _buttonBack: ImageButton;
|
||||||
|
private val _loaderView: LoaderView;
|
||||||
|
|
||||||
|
private val _devSets: LinearLayout;
|
||||||
|
private val _buttonDev: MaterialButton;
|
||||||
|
|
||||||
|
private var _isFinished = false;
|
||||||
|
|
||||||
|
lateinit var overlay: FrameLayout;
|
||||||
|
|
||||||
|
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
||||||
|
|
||||||
|
constructor(fragment: SettingsFragment) : super(fragment.requireContext()) {
|
||||||
|
inflate(context, R.layout.activity_settings, this);
|
||||||
|
this.fragment = fragment;
|
||||||
|
|
||||||
|
val activity = fragment.activity;
|
||||||
|
|
||||||
|
findViewById<LinearLayout>(R.id.container_topbar).isVisible = false;
|
||||||
|
_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(context, Settings.instance.language.getAppLanguageLocaleString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(field.descriptor?.id == "background_update" && activity is MainActivity) {
|
||||||
|
Logger.i("SettingsActivity", "Detected change in background work ${field.value}");
|
||||||
|
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval > 0) {
|
||||||
|
val notifManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||||
|
if(!notifManager.areNotificationsEnabled()) {
|
||||||
|
UIDialogs.toast(context, "Notifications aren't enabled");
|
||||||
|
activity.requestNotificationPermissions("Notifications need to be enabled for background updating to function")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_buttonBack.setOnClickListener {
|
||||||
|
//finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
_buttonDev.setOnClickListener {
|
||||||
|
//startActivity(Intent(this, DeveloperActivity::class.java));
|
||||||
|
fragment.navigate<DeveloperFragment>(null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//_lastActivity = this;
|
||||||
|
|
||||||
|
reloadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
var isFirstLoad = true;
|
||||||
|
fun reloadSettings() {
|
||||||
|
val firstLoad = isFirstLoad;
|
||||||
|
isFirstLoad = false;
|
||||||
|
_form.setSearchVisible(false);
|
||||||
|
_loaderView.start();
|
||||||
|
_form.fromObject(fragment.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(context, fragment.getString(R.string.you_are_now_in_developer_mode));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
if(firstLoad) {
|
||||||
|
val query = intent.getStringExtra("query");
|
||||||
|
if(!query.isNullOrEmpty()) {
|
||||||
|
_form.setSearchQuery(query);
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun onShown(str: Any? = null) {
|
||||||
|
updateDevMode();
|
||||||
|
if(str is String)
|
||||||
|
_form.setSearchQuery(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDevMode() {
|
||||||
|
if(SettingsDev.instance.developerMode)
|
||||||
|
_devSets.visibility = View.VISIBLE;
|
||||||
|
else
|
||||||
|
_devSets.visibility = View.GONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import android.content.Intent
|
|||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.WindowManager
|
||||||
import android.view.animation.AccelerateInterpolator
|
import android.view.animation.AccelerateInterpolator
|
||||||
import android.view.animation.OvershootInterpolator
|
import android.view.animation.OvershootInterpolator
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
@@ -214,6 +215,16 @@ class ShortView : FrameLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
player.onPlayChanged.subscribe {
|
||||||
|
if (it) {
|
||||||
|
Logger.i(TAG, "Keep screen on set because isPlaying")
|
||||||
|
fragment.activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
} else {
|
||||||
|
Logger.i(TAG, "Keep screen on cleared because not isPlaying")
|
||||||
|
fragment.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onPlayingToggled.subscribe { playing ->
|
onPlayingToggled.subscribe { playing ->
|
||||||
if (playing) {
|
if (playing) {
|
||||||
playPauseIcon.setImageResource(R.drawable.ic_play)
|
playPauseIcon.setImageResource(R.drawable.ic_play)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.SoundEffectConstants
|
import android.view.SoundEffectConstants
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.WindowManager
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
@@ -309,6 +310,12 @@ class ShortsFragment : MainFragment() {
|
|||||||
customViewAdapter?.previousShownView?.stop()
|
customViewAdapter?.previousShownView?.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
super.onDestroyMainView()
|
||||||
|
Logger.i(TAG, "Keep screen on cleared because onDestroyMainView fragment")
|
||||||
|
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ShortsFragment"
|
private const val TAG = "ShortsFragment"
|
||||||
|
|
||||||
|
|||||||
+60
-5
@@ -1,5 +1,8 @@
|
|||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -32,9 +35,11 @@ import com.futo.platformplayer.views.buttons.BigButton
|
|||||||
import com.futo.platformplayer.views.buttons.BigButtonGroup
|
import com.futo.platformplayer.views.buttons.BigButtonGroup
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.sources.SourceHeaderView
|
import com.futo.platformplayer.views.sources.SourceHeaderView
|
||||||
|
import com.google.gson.Gson
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class SourceDetailFragment : MainFragment() {
|
class SourceDetailFragment : MainFragment() {
|
||||||
override val isMainView : Boolean = true;
|
override val isMainView : Boolean = true;
|
||||||
@@ -415,12 +420,40 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val advancedButtons = BigButtonGroup(c, "Advanced",
|
val advancedButtons = BigButtonGroup(c, "Advanced",
|
||||||
|
BigButton(c, "Reset Settings", "Resets the settings to their default (deleting existing settings)", R.drawable.ic_refresh) {
|
||||||
|
_config?.let {
|
||||||
|
StatePlugins.instance.setPluginSettings(it.id, hashMapOf());
|
||||||
|
loadConfig(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
BigButton(c, "Share Settings", "Shares the settings of this plugin as json, mostly used for bug reporting", R.drawable.ic_code) {
|
||||||
|
|
||||||
|
val structure = Json { this.prettyPrint = true; this.prettyPrintIndent = " " }
|
||||||
|
.encodeToString(_settings);
|
||||||
|
fragment.startActivity(Intent.createChooser(Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND;
|
||||||
|
putExtra(Intent.EXTRA_TEXT, structure);
|
||||||
|
type = "text/plain";
|
||||||
|
}, null));
|
||||||
|
/*
|
||||||
|
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText("Settings Json", structure)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
UIDialogs.toast(context, "Copied", false);
|
||||||
|
*/
|
||||||
|
}.apply {
|
||||||
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
};
|
||||||
|
} ,
|
||||||
|
/*
|
||||||
BigButton(c, "Edit Code", "Modify the source of this plugin", R.drawable.ic_code) {
|
BigButton(c, "Edit Code", "Modify the source of this plugin", R.drawable.ic_code) {
|
||||||
|
|
||||||
}.apply {
|
}.apply {
|
||||||
this.alpha = 0.5f;
|
this.alpha = 0.5f;
|
||||||
},
|
},*/
|
||||||
if(isEmbedded) BigButton(c, "Reinstall", "Modify the source of this plugin", R.drawable.ic_refresh) {
|
if(isEmbedded) BigButton(c, "Reinstall", "Reinstall the original version that was embedded with this version of Grayjay", R.drawable.ic_refresh) {
|
||||||
val embeddedConfig = StatePlugins.instance.getEmbeddedPluginConfigFromID(context, config.id);
|
val embeddedConfig = StatePlugins.instance.getEmbeddedPluginConfigFromID(context, config.id);
|
||||||
|
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow, "Are you sure you want to downgrade (${config.version}=>${embeddedConfig?.version})?",
|
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow, "Are you sure you want to downgrade (${config.version}=>${embeddedConfig?.version})?",
|
||||||
@@ -434,7 +467,29 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
};
|
};
|
||||||
} else null
|
} else
|
||||||
|
BigButton(c, "Reinstall", "Reinstall the current version from the remote repository", R.drawable.ic_refresh) {
|
||||||
|
var newConfig: SourcePluginConfig? = null;
|
||||||
|
try {
|
||||||
|
newConfig = StatePlugins.instance.requestConfig(config?.sourceUrl ?: throw IllegalArgumentException("No config"));
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to fetch new plugin config", ex);
|
||||||
|
}
|
||||||
|
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow, "Are you sure you want to downgrade (${config.version}=>)?",
|
||||||
|
"This will revert the plugin back to the originally embedded version.\nVersion change: ${config.version}=>${newConfig?.version}", null,
|
||||||
|
0, UIDialogs.Action("Cancel", {}), UIDialogs.Action("Reinstall", {
|
||||||
|
val url = config.sourceUrl ?: return@Action;
|
||||||
|
StatePlugins.instance.installPlugin(context, fragment.lifecycleScope, url) {
|
||||||
|
reloadSource(config.id);
|
||||||
|
UIDialogs.toast(context, "Plugin reinstalled, may require refresh");
|
||||||
|
}
|
||||||
|
}, UIDialogs.ActionStyle.DANGEROUS));
|
||||||
|
}.apply {
|
||||||
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
};
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
_sourceAdvancedButtons.removeAllViews();
|
_sourceAdvancedButtons.removeAllViews();
|
||||||
@@ -453,7 +508,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
config.authentication.loginWarning, null, 0,
|
config.authentication.loginWarning, null, 0,
|
||||||
UIDialogs.Action("Cancel", {}, UIDialogs.ActionStyle.NONE),
|
UIDialogs.Action("Cancel", {}, UIDialogs.ActionStyle.NONE),
|
||||||
UIDialogs.Action("Login", {
|
UIDialogs.Action("Login", {
|
||||||
LoginActivity.showLogin(StateApp.instance.context, config) {
|
LoginFragment.showLogin(config) {//LoginActivity.showLogin(StateApp.instance.context, config) {
|
||||||
try {
|
try {
|
||||||
StatePlugins.instance.setPluginAuth(config.id, it);
|
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||||
reloadSource(config.id);
|
reloadSource(config.id);
|
||||||
@@ -467,7 +522,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
}, UIDialogs.ActionStyle.PRIMARY))
|
}, UIDialogs.ActionStyle.PRIMARY))
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
LoginActivity.showLogin(StateApp.instance.context, config) {
|
LoginFragment.showLogin(config) {//LoginActivity.showLogin(StateApp.instance.context, config) {
|
||||||
try {
|
try {
|
||||||
StatePlugins.instance.setPluginAuth(config.id, it);
|
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||||
reloadSource(config.id);
|
reloadSource(config.id);
|
||||||
|
|||||||
+3
-3
@@ -24,7 +24,6 @@ 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
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
@@ -401,9 +400,10 @@ class VideoDetailFragment() : MainFragment() {
|
|||||||
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
|
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
|
||||||
maximizeVideoDetail();
|
maximizeVideoDetail();
|
||||||
|
|
||||||
|
/*
|
||||||
SettingsActivity.settingsActivityClosed.subscribe(this) {
|
SettingsActivity.settingsActivityClosed.subscribe(this) {
|
||||||
updateOrientation()
|
updateOrientation()
|
||||||
}
|
} */
|
||||||
|
|
||||||
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
|
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
|
||||||
updateOrientation()
|
updateOrientation()
|
||||||
@@ -547,7 +547,7 @@ class VideoDetailFragment() : MainFragment() {
|
|||||||
super.onDestroyMainView();
|
super.onDestroyMainView();
|
||||||
Logger.v(TAG, "onDestroyMainView");
|
Logger.v(TAG, "onDestroyMainView");
|
||||||
|
|
||||||
SettingsActivity.settingsActivityClosed.remove(this)
|
//SettingsActivity.settingsActivityClosed.remove(this)
|
||||||
StatePlayer.instance.onRotationLockChanged.remove(this)
|
StatePlayer.instance.onRotationLockChanged.remove(this)
|
||||||
|
|
||||||
_landscapeOrientationListener?.disableListener()
|
_landscapeOrientationListener?.disableListener()
|
||||||
|
|||||||
+135
-59
@@ -55,6 +55,7 @@ import com.futo.platformplayer.api.media.LiveChatManager
|
|||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
||||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
|
||||||
import com.futo.platformplayer.api.media.models.chapters.ChapterType
|
import com.futo.platformplayer.api.media.models.chapters.ChapterType
|
||||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
@@ -77,6 +78,7 @@ import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
|||||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.video.LocalVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
@@ -175,6 +177,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -244,6 +247,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private val _buttonSubscribe: SubscribeButton;
|
private val _buttonSubscribe: SubscribeButton;
|
||||||
|
|
||||||
private val _buttonPins: RoundButtonGroup;
|
private val _buttonPins: RoundButtonGroup;
|
||||||
|
private var _loaderGameVisible = false
|
||||||
//private val _buttonMore: RoundButton;
|
//private val _buttonMore: RoundButton;
|
||||||
|
|
||||||
var preventPictureInPicture: Boolean = false
|
var preventPictureInPicture: Boolean = false
|
||||||
@@ -261,7 +265,6 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private val _textSkip: TextView;
|
private val _textSkip: TextView;
|
||||||
private val _textResume: TextView;
|
private val _textResume: TextView;
|
||||||
private val _layoutResume: LinearLayout;
|
private val _layoutResume: LinearLayout;
|
||||||
private var _jobHideResume: Job? = null;
|
|
||||||
private val _layoutPlayerContainer: TouchInterceptFrameLayout;
|
private val _layoutPlayerContainer: TouchInterceptFrameLayout;
|
||||||
private val _layoutChangeBottomSection: LinearLayout;
|
private val _layoutChangeBottomSection: LinearLayout;
|
||||||
|
|
||||||
@@ -336,7 +339,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
!StateCasting.instance.isCasting &&
|
!StateCasting.instance.isCasting &&
|
||||||
Settings.instance.playback.isBackgroundPictureInPicture() &&
|
Settings.instance.playback.isBackgroundPictureInPicture() &&
|
||||||
!isAudioOnlyUserAction &&
|
!isAudioOnlyUserAction &&
|
||||||
isPlaying
|
(isPlaying || _loaderGameVisible)
|
||||||
|
|
||||||
val onShouldEnterPictureInPictureChanged = Event0();
|
val onShouldEnterPictureInPictureChanged = Event0();
|
||||||
|
|
||||||
@@ -357,6 +360,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
Pair(-5 * 60, 30), //around 5 minutes, try every 30 seconds
|
Pair(-5 * 60, 30), //around 5 minutes, try every 30 seconds
|
||||||
Pair(0, 10) //around live, try every 10 seconds
|
Pair(0, 10) //around live, try every 10 seconds
|
||||||
);
|
);
|
||||||
|
private var _subtitleLanguage: String? = null
|
||||||
|
|
||||||
@androidx.annotation.OptIn(UnstableApi::class)
|
@androidx.annotation.OptIn(UnstableApi::class)
|
||||||
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
|
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
|
||||||
@@ -548,10 +552,32 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_buttonMore = buttonMore;
|
_buttonMore = buttonMore;
|
||||||
updateMoreButtons();
|
updateMoreButtons();
|
||||||
|
|
||||||
|
val handleLoaderGameVisibilityChanged = { b: Boolean ->
|
||||||
|
_loaderGameVisible = b
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
onShouldEnterPictureInPictureChanged.emit()
|
||||||
|
}
|
||||||
|
updateResumeVisibilityFor(lastPositionMilliseconds)
|
||||||
|
}
|
||||||
|
_player.loaderGameVisibilityChanged.subscribe(handleLoaderGameVisibilityChanged)
|
||||||
|
_cast.loaderGameVisibilityChanged.subscribe(handleLoaderGameVisibilityChanged)
|
||||||
|
|
||||||
_channelButton.setOnClickListener {
|
_channelButton.setOnClickListener {
|
||||||
if (video is TutorialFragment.TutorialVideo) {
|
if (video is TutorialFragment.TutorialVideo) {
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
|
if(video is LocalVideoDetails) {
|
||||||
|
video?.author?.let {
|
||||||
|
if(it.url.startsWith("content://media/external/audio/artists")) {
|
||||||
|
fragment.navigate<LibraryArtistFragment>(it.url);
|
||||||
|
fragment.lifecycleScope.launch {
|
||||||
|
delay(100);
|
||||||
|
fragment.minimizeVideoDetail();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@setOnClickListener;
|
||||||
|
}
|
||||||
|
|
||||||
(video?.author ?: _searchVideo?.author)?.let {
|
(video?.author ?: _searchVideo?.author)?.let {
|
||||||
fragment.navigate<ChannelFragment>(it);
|
fragment.navigate<ChannelFragment>(it);
|
||||||
@@ -576,9 +602,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if(chapter?.type == ChapterType.SKIPPABLE) {
|
if(chapter?.type == ChapterType.SKIPPABLE) {
|
||||||
_layoutSkip.visibility = VISIBLE;
|
_layoutSkip.visibility = VISIBLE;
|
||||||
} else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) {
|
} else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) {
|
||||||
val ad = StateCasting.instance.activeDevice
|
if (StateCasting.instance.activeDevice != null) {
|
||||||
if (ad != null) {
|
StateCasting.instance.videoSeekTo(chapter.timeEnd)
|
||||||
ad.seekVideo(chapter.timeEnd)
|
|
||||||
} else {
|
} else {
|
||||||
_player.seekTo((chapter.timeEnd * 1000).toLong());
|
_player.seekTo((chapter.timeEnd * 1000).toLong());
|
||||||
}
|
}
|
||||||
@@ -615,6 +640,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
_player.onSourceChanged.subscribe(::onSourceChanged);
|
_player.onSourceChanged.subscribe(::onSourceChanged);
|
||||||
_player.onSourceEnded.subscribe {
|
_player.onSourceEnded.subscribe {
|
||||||
|
if (_isCasting) {
|
||||||
|
Logger.i(TAG, "Ignoring onSourceEnded because casting is active")
|
||||||
|
return@subscribe
|
||||||
|
}
|
||||||
|
|
||||||
if (!fragment.isInPictureInPicture) {
|
if (!fragment.isInPictureInPicture) {
|
||||||
_player.gestureControl.showControls(false);
|
_player.gestureControl.showControls(false);
|
||||||
}
|
}
|
||||||
@@ -694,6 +724,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
val v = video;
|
val v = video;
|
||||||
if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) {
|
if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) {
|
||||||
|
Log.i(TAG, "Next video (loop?)")
|
||||||
nextVideo();
|
nextVideo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -873,11 +904,6 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_layoutResume.setOnClickListener {
|
_layoutResume.setOnClickListener {
|
||||||
handleSeek(_historicalPosition * 1000);
|
handleSeek(_historicalPosition * 1000);
|
||||||
|
|
||||||
val job = _jobHideResume;
|
|
||||||
_jobHideResume = null;
|
|
||||||
job?.cancel();
|
|
||||||
|
|
||||||
_layoutResume.visibility = View.GONE;
|
_layoutResume.visibility = View.GONE;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -886,7 +912,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if (ad != null) {
|
if (ad != null) {
|
||||||
val currentChapter = _cast.getCurrentChapter((ad.time * 1000).toLong());
|
val currentChapter = _cast.getCurrentChapter((ad.time * 1000).toLong());
|
||||||
if(currentChapter?.type == ChapterType.SKIPPABLE) {
|
if(currentChapter?.type == ChapterType.SKIPPABLE) {
|
||||||
ad.seekVideo(currentChapter.timeEnd);
|
StateCasting.instance.videoSeekTo(currentChapter.timeEnd);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val currentChapter = _player.getCurrentChapter(_player.position);
|
val currentChapter = _player.getCurrentChapter(_player.position);
|
||||||
@@ -1030,7 +1056,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
}
|
}
|
||||||
else null,
|
else null,
|
||||||
if(!isLimitedVersion && !(video?.isLive ?: false))
|
if(!isLimitedVersion && !(video?.isLive ?: false) && !(video is LocalVideoDetails))
|
||||||
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
||||||
video?.let {
|
video?.let {
|
||||||
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
||||||
@@ -1053,15 +1079,16 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
}
|
}
|
||||||
else null,
|
else null,
|
||||||
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
|
if(!(video is LocalVideoDetails))
|
||||||
|
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
|
||||||
video?.let {
|
video?.let {
|
||||||
val url = video?.shareUrl ?: _searchVideo?.shareUrl ?: _url;
|
val url = video?.shareUrl ?: _searchVideo?.shareUrl ?: _url;
|
||||||
fragment.navigate<BrowserFragment>(url);
|
fragment.navigate<BrowserFragment>(url);
|
||||||
fragment.minimizeVideoDetail();
|
fragment.minimizeVideoDetail();
|
||||||
};
|
};
|
||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
},
|
} else null,
|
||||||
if (StateSync.instance.hasAuthorizedDevice()) {
|
if (StateSync.instance.hasAuthorizedDevice() && !(video is LocalVideoDetails)) {
|
||||||
RoundButton(context, R.drawable.ic_device, context.getString(R.string.send_to_device), TAG_SEND_TO_DEVICE) {
|
RoundButton(context, R.drawable.ic_device, context.getString(R.string.send_to_device), TAG_SEND_TO_DEVICE) {
|
||||||
val devices = StateSync.instance.getAuthorizedSessions();
|
val devices = StateSync.instance.getAuthorizedSessions();
|
||||||
val videoToSend = video ?: return@RoundButton;
|
val videoToSend = video ?: return@RoundButton;
|
||||||
@@ -1084,10 +1111,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}} else null,
|
}} else null,
|
||||||
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") {
|
if(!(video is LocalVideoDetails))
|
||||||
|
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") {
|
||||||
reloadVideo();
|
reloadVideo();
|
||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
}).filterNotNull();
|
} else null).filterNotNull();
|
||||||
if(!_buttonPinStore.getAllValues().any())
|
if(!_buttonPinStore.getAllValues().any())
|
||||||
_buttonPins.setButtons(*(buttons + listOf(_buttonMore)).toTypedArray());
|
_buttonPins.setButtons(*(buttons + listOf(_buttonMore)).toTypedArray());
|
||||||
else {
|
else {
|
||||||
@@ -1154,7 +1182,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
//Recover cancelled loads
|
//Recover cancelled loads
|
||||||
if(video == null) {
|
if(video == null) {
|
||||||
val t = (lastPositionMilliseconds / 1000.0f).roundToLong();
|
val t = (lastPositionMilliseconds / 1000.0f).roundToLong();
|
||||||
if(_searchVideo != null)
|
if(_searchVideo != null && !wasLoginCall)
|
||||||
setVideoOverview(_searchVideo!!, true, t);
|
setVideoOverview(_searchVideo!!, true, t);
|
||||||
else if(_url != null && !wasLoginCall)
|
else if(_url != null && !wasLoginCall)
|
||||||
setVideo(_url!!, t, _playWhenReady);
|
setVideo(_url!!, t, _playWhenReady);
|
||||||
@@ -1256,10 +1284,6 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
MediaControlReceiver.onCloseReceived.remove(this);
|
MediaControlReceiver.onCloseReceived.remove(this);
|
||||||
MediaControlReceiver.onBackgroundReceived.remove(this);
|
MediaControlReceiver.onBackgroundReceived.remove(this);
|
||||||
MediaControlReceiver.onSeekToReceived.remove(this);
|
MediaControlReceiver.onSeekToReceived.remove(this);
|
||||||
|
|
||||||
val job = _jobHideResume;
|
|
||||||
_jobHideResume = null;
|
|
||||||
job?.cancel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Video Setters
|
//Video Setters
|
||||||
@@ -1326,7 +1350,22 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
return;
|
return;
|
||||||
//Loop workaround
|
//Loop workaround
|
||||||
if(bypassSameVideoCheck && this.video?.url == video.url && StatePlayer.instance.loopVideo) {
|
if(bypassSameVideoCheck && this.video?.url == video.url && StatePlayer.instance.loopVideo) {
|
||||||
_player.seekTo(0);
|
Log.i(TAG, "Loop")
|
||||||
|
if (_isCasting) {
|
||||||
|
Log.i(TAG, "Loop casting")
|
||||||
|
StateCasting.instance.activeDevice?.seekTo(0.0)
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
delay(300)
|
||||||
|
StateCasting.instance.activeDevice?.resumePlayback()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "Failed to resume", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Loop player")
|
||||||
|
_player.seekTo(0);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1353,6 +1392,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_minimize_title.text = video.name;
|
_minimize_title.text = video.name;
|
||||||
_minimize_meta.text = video.author.name;
|
_minimize_meta.text = video.author.name;
|
||||||
|
StatePlayer.instance.setCurrentlyPlaying(video);
|
||||||
|
Log.i(TAG, "setCurrentlyPlaying (setVideoOverview) ${video.url} (${video.name})")
|
||||||
|
|
||||||
val subTitleSegments : ArrayList<String> = ArrayList();
|
val subTitleSegments : ArrayList<String> = ArrayList();
|
||||||
if(video.viewCount > 0)
|
if(video.viewCount > 0)
|
||||||
@@ -1622,7 +1663,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_buttonSubscribe.setSubscribeChannel(video.author.url);
|
_buttonSubscribe.setSubscribeChannel(video.author.url);
|
||||||
setDescription(video.description.fixHtmlLinks());
|
setDescription(video.description.fixHtmlLinks());
|
||||||
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
_creatorThumbnail.setThumbnail(video.author.thumbnail, false,
|
||||||
|
video is LocalVideoDetails
|
||||||
|
);
|
||||||
setPolycentricProfile(null, animate = false);
|
setPolycentricProfile(null, animate = false);
|
||||||
_taskLoadPolycentricProfile.run(video.author.id);
|
_taskLoadPolycentricProfile.run(video.author.id);
|
||||||
|
|
||||||
@@ -1650,7 +1693,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_rating.visibility = View.GONE;
|
_rating.visibility = View.GONE;
|
||||||
|
|
||||||
if (StatePolycentric.instance.enabled) {
|
if (StatePolycentric.instance.enabled && !(video is LocalVideoDetails)) {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val queryReferencesResponse = ApiMethods.getQueryReferences(
|
val queryReferencesResponse = ApiMethods.getQueryReferences(
|
||||||
@@ -1710,7 +1753,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to get polycentric likes/dislikes.", e);
|
Logger.e(TAG, "Failed to get polycentric likes/dislikes.", e);
|
||||||
_rating.visibility = View.GONE;
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
_rating.visibility = View.GONE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1775,37 +1820,20 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
false,
|
false,
|
||||||
(toResume.toFloat() / 1000.0f).toLong(),
|
(toResume.toFloat() / 1000.0f).toLong(),
|
||||||
null,
|
null,
|
||||||
true
|
true,
|
||||||
|
StatePlayer.instance.playlistId
|
||||||
);
|
);
|
||||||
Logger.i(
|
Logger.i(
|
||||||
TAG,
|
TAG,
|
||||||
"Historical position: $_historicalPosition, last position: $lastPositionMilliseconds"
|
"Historical position: $_historicalPosition, last position: $lastPositionMilliseconds"
|
||||||
);
|
);
|
||||||
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(
|
updateResumeVisibilityFor(lastPositionMilliseconds)
|
||||||
_historicalPosition - lastPositionMilliseconds / 1000
|
|
||||||
) > 5.0
|
|
||||||
) {
|
|
||||||
_layoutResume.visibility = View.VISIBLE;
|
|
||||||
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
|
|
||||||
|
|
||||||
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
try {
|
|
||||||
delay(8000);
|
|
||||||
_layoutResume.visibility = View.GONE;
|
|
||||||
_textResume.text = "";
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to set resume changes.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_layoutResume.visibility = View.GONE;
|
|
||||||
_textResume.text = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||||
|
Log.i(TAG, "setCurrentlyPlaying (nextVideo) ${video.url} (${video.name})")
|
||||||
StatePlayer.instance.setCurrentlyPlaying(video);
|
StatePlayer.instance.setCurrentlyPlaying(video);
|
||||||
|
|
||||||
_liveChat?.stop();
|
_liveChat?.stop();
|
||||||
@@ -1827,17 +1855,19 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_player.updateNextPrevious();
|
_player.updateNextPrevious();
|
||||||
updateMoreButtons();
|
updateMoreButtons();
|
||||||
|
|
||||||
if (videoDetail is TutorialFragment.TutorialVideo) {
|
if (videoDetail is TutorialFragment.TutorialVideo || videoDetail is LocalVideoDetails) {
|
||||||
_buttonSubscribe.visibility = View.GONE
|
_buttonSubscribe.visibility = View.GONE
|
||||||
_buttonMore.visibility = View.GONE
|
_buttonMore.visibility = if(videoDetail is LocalVideoDetails) View.VISIBLE else View.GONE;
|
||||||
_buttonPins.visibility = View.GONE
|
_buttonPins.visibility = if(videoDetail is LocalVideoDetails) View.VISIBLE else View.GONE;
|
||||||
_layoutRating.visibility = View.GONE
|
_layoutRating.visibility = View.GONE
|
||||||
|
_rating.visibility = View.GONE;
|
||||||
_layoutChangeBottomSection.visibility = View.GONE
|
_layoutChangeBottomSection.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
_buttonSubscribe.visibility = View.VISIBLE
|
_buttonSubscribe.visibility = View.VISIBLE
|
||||||
_buttonMore.visibility = View.VISIBLE
|
_buttonMore.visibility = View.VISIBLE
|
||||||
_buttonPins.visibility = View.VISIBLE
|
_buttonPins.visibility = View.VISIBLE
|
||||||
_layoutRating.visibility = View.VISIBLE
|
_layoutRating.visibility = View.VISIBLE
|
||||||
|
_rating.visibility = View.VISIBLE;
|
||||||
_layoutChangeBottomSection.visibility = View.VISIBLE
|
_layoutChangeBottomSection.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1846,6 +1876,35 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_taskLoadRecommendations.run(videoDetail.url)
|
_taskLoadRecommendations.run(videoDetail.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun shouldShowResume(positionMs: Long): Boolean {
|
||||||
|
if (_loaderGameVisible) return false
|
||||||
|
val v = video ?: return false
|
||||||
|
val resumeS = _historicalPosition
|
||||||
|
val durS = v.duration
|
||||||
|
|
||||||
|
if (_overlay_loading.visibility == View.VISIBLE) return false
|
||||||
|
if (resumeS <= 60) return false
|
||||||
|
if (durS - resumeS <= 5) return false
|
||||||
|
|
||||||
|
val posMs = positionMs
|
||||||
|
val resumeMs = resumeS * 1000
|
||||||
|
val durMs = durS * 1000L
|
||||||
|
val inFirstFewSeconds = posMs < 8_000
|
||||||
|
val notYetReachedResume = (resumeMs - posMs) > 5_000
|
||||||
|
return inFirstFewSeconds && notYetReachedResume && durMs > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateResumeVisibilityFor(positionMs: Long) {
|
||||||
|
val visible = shouldShowResume(positionMs)
|
||||||
|
if (visible) {
|
||||||
|
_layoutResume.visibility = View.VISIBLE
|
||||||
|
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}"
|
||||||
|
} else {
|
||||||
|
_layoutResume.visibility = View.GONE
|
||||||
|
_textResume.text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
fun loadVODChat(video: IPlatformVideoDetails) {
|
fun loadVODChat(video: IPlatformVideoDetails) {
|
||||||
_liveChat?.stop();
|
_liveChat?.stop();
|
||||||
_container_content_liveChat.cancel();
|
_container_content_liveChat.cancel();
|
||||||
@@ -1965,7 +2024,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
try {
|
try {
|
||||||
val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount());
|
val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount());
|
||||||
val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context));
|
val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context));
|
||||||
val subtitleSource = _lastSubtitleSource ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null);
|
val subtitleSource = _lastSubtitleSource ?: (if (Settings.instance.playback.stickySubtitles) _player.getPreferredSubtitleSource(video, _subtitleLanguage) else null) ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null);
|
||||||
Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)")
|
Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)")
|
||||||
|
|
||||||
if(videoSource == null && audioSource == null) {
|
if(videoSource == null && audioSource == null) {
|
||||||
@@ -2275,6 +2334,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
checkAndRemoveWatchLater();
|
checkAndRemoveWatchLater();
|
||||||
|
|
||||||
var next = StatePlayer.instance.nextQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9, bypassVideoLoop);
|
var next = StatePlayer.instance.nextQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9, bypassVideoLoop);
|
||||||
|
Log.i(TAG, "next queue item ${next?.url} (${next?.name})")
|
||||||
|
|
||||||
val autoplayVideo = _autoplayVideo
|
val autoplayVideo = _autoplayVideo
|
||||||
if (next == null && autoplayVideo != null && StatePlayer.instance.autoplay) {
|
if (next == null && autoplayVideo != null && StatePlayer.instance.autoplay) {
|
||||||
Logger.i(TAG, "Found autoplay video!")
|
Logger.i(TAG, "Found autoplay video!")
|
||||||
@@ -2287,11 +2348,14 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if(next == null && forceLoop)
|
if(next == null && forceLoop)
|
||||||
next = StatePlayer.instance.restartQueue();
|
next = StatePlayer.instance.restartQueue();
|
||||||
if(next != null) {
|
if(next != null) {
|
||||||
|
Logger.i(TAG, "Set video overview (next = ${next.url} (${next.name}))")
|
||||||
setVideoOverview(next, true, 0, true);
|
setVideoOverview(next, true, 0, true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
|
Log.i(TAG, "setCurrentlyPlaying (nextVideo) null")
|
||||||
StatePlayer.instance.setCurrentlyPlaying(null);
|
StatePlayer.instance.setCurrentlyPlaying(null);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2368,11 +2432,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
?.distinct()
|
?.distinct()
|
||||||
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
|
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
|
||||||
|
|
||||||
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
|
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed() == true
|
||||||
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
|
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
|
||||||
val qualityPlaybackSpeedTitle = if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", currentPlaybackRate)})"); } else null;
|
val qualityPlaybackSpeedTitle = if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", currentPlaybackRate)})"); } else null;
|
||||||
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
|
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
|
||||||
R.string.quality), null, true,
|
R.string.quality), null, true,
|
||||||
qualityPlaybackSpeedTitle,
|
qualityPlaybackSpeedTitle,
|
||||||
if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply {
|
if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply {
|
||||||
val playbackSpeeds = Settings.instance.playback.getPlaybackSpeeds();
|
val playbackSpeeds = Settings.instance.playback.getPlaybackSpeeds();
|
||||||
@@ -2393,7 +2457,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
val newPlaybackSpeed = playbackSpeedString.toDouble();
|
val newPlaybackSpeed = playbackSpeedString.toDouble();
|
||||||
if (_isCasting) {
|
if (_isCasting) {
|
||||||
val ad = StateCasting.instance.activeDevice ?: return@subscribe
|
val ad = StateCasting.instance.activeDevice ?: return@subscribe
|
||||||
if (!ad.canSetSpeed) {
|
if (!ad.canSetSpeed()) {
|
||||||
return@subscribe
|
return@subscribe
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2517,6 +2581,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if (!StateCasting.instance.resumeVideo()) {
|
if (!StateCasting.instance.resumeVideo()) {
|
||||||
_player.play();
|
_player.play();
|
||||||
}
|
}
|
||||||
|
onShouldEnterPictureInPictureChanged.emit()
|
||||||
|
|
||||||
//TODO: This was needed because handleLowerVolume was done.
|
//TODO: This was needed because handleLowerVolume was done.
|
||||||
//_player.setVolume(1.0f);
|
//_player.setVolume(1.0f);
|
||||||
@@ -2533,6 +2598,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if (!StateCasting.instance.pauseVideo()) {
|
if (!StateCasting.instance.pauseVideo()) {
|
||||||
_player.pause();
|
_player.pause();
|
||||||
}
|
}
|
||||||
|
onShouldEnterPictureInPictureChanged.emit()
|
||||||
}
|
}
|
||||||
private fun handleSeek(ms: Long) {
|
private fun handleSeek(ms: Long) {
|
||||||
Logger.i(TAG, "handleSeek(ms=$ms)")
|
Logger.i(TAG, "handleSeek(ms=$ms)")
|
||||||
@@ -2647,6 +2713,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_lastSubtitleSource = toSet;
|
_lastSubtitleSource = toSet;
|
||||||
|
_subtitleLanguage = toSet?.language
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUnavailableVideo(msg: String? = null) {
|
private fun handleUnavailableVideo(msg: String? = null) {
|
||||||
@@ -2669,7 +2736,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private fun fetchComments() {
|
private fun fetchComments() {
|
||||||
Logger.i(TAG, "fetchComments")
|
Logger.i(TAG, "fetchComments")
|
||||||
video?.let {
|
video?.let {
|
||||||
_commentsList.load(true) { StatePlatform.instance.getComments(it); };
|
if(video is LocalVideoDetails) {
|
||||||
|
_commentsList.clearComments();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_commentsList.load(true) { StatePlatform.instance.getComments(it); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun fetchPolycentricComments() {
|
private fun fetchPolycentricComments() {
|
||||||
@@ -2804,6 +2875,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_overlay_loading.visibility = View.GONE;
|
_overlay_loading.visibility = View.GONE;
|
||||||
(_overlay_loading_spinner.drawable as Animatable?)?.stop()
|
(_overlay_loading_spinner.drawable as Animatable?)?.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateResumeVisibilityFor(lastPositionMilliseconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
//UI Actions
|
//UI Actions
|
||||||
@@ -2954,6 +3027,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onChannelClicked.subscribe {
|
onChannelClicked.subscribe {
|
||||||
|
Logger.i(TAG, "Opening channel url: ${it.url}");
|
||||||
if(it.url.isNotBlank()) {
|
if(it.url.isNotBlank()) {
|
||||||
fragment.minimizeVideoDetail()
|
fragment.minimizeVideoDetail()
|
||||||
fragment.navigate<ChannelFragment>(it)
|
fragment.navigate<ChannelFragment>(it)
|
||||||
@@ -3037,9 +3111,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val playpauseAction = if(_player.playing)
|
val playpauseAction = if(_player.playing)
|
||||||
RemoteAction(Icon.createWithResource(context, R.drawable.ic_pause_notif), context.getString(R.string.pause), context.getString(R.string.pauses_the_video), MediaControlReceiver.getPauseIntent(context, 5));
|
RemoteAction(Icon.createWithResource(context, R.drawable.ic_pause_notif), context.getString(R.string.pause), context.getString(R.string.pauses_the_video), MediaControlReceiver.getPauseIntent(context, 2));
|
||||||
else
|
else
|
||||||
RemoteAction(Icon.createWithResource(context, R.drawable.ic_play_notif), context.getString(R.string.play), context.getString(R.string.resumes_the_video), MediaControlReceiver.getPlayIntent(context, 6));
|
RemoteAction(Icon.createWithResource(context, R.drawable.ic_play_notif), context.getString(R.string.play), context.getString(R.string.resumes_the_video), MediaControlReceiver.getPlayIntent(context, 1));
|
||||||
|
|
||||||
val toBackgroundAction = RemoteAction(Icon.createWithResource(context, R.drawable.ic_screen_share), context.getString(R.string.background), context.getString(R.string.background_switch_audio), MediaControlReceiver.getToBackgroundIntent(context, 7));
|
val toBackgroundAction = RemoteAction(Icon.createWithResource(context, R.drawable.ic_screen_share), context.getString(R.string.background), context.getString(R.string.background_switch_audio), MediaControlReceiver.getToBackgroundIntent(context, 7));
|
||||||
|
|
||||||
@@ -3078,7 +3152,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if (v !is TutorialFragment.TutorialVideo) {
|
if (v !is TutorialFragment.TutorialVideo) {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val history = getHistoryIndex(v) ?: return@launch;
|
val history = getHistoryIndex(v) ?: return@launch;
|
||||||
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong(), null, true);
|
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong(), null, true, StatePlayer.instance.playlistId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_lastPositionSaveTime = currentTime;
|
_lastPositionSaveTime = currentTime;
|
||||||
@@ -3094,6 +3168,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
handleSeek(55000);
|
handleSeek(55000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateResumeVisibilityFor(positionMilliseconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTracker(positionMs: Long, isPlaying: Boolean, forceUpdate: Boolean = false) {
|
private fun updateTracker(positionMs: Long, isPlaying: Boolean, forceUpdate: Boolean = false) {
|
||||||
|
|||||||
+24
-10
@@ -20,6 +20,7 @@ import com.futo.platformplayer.UISlideOverlays
|
|||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.assume
|
import com.futo.platformplayer.assume
|
||||||
import com.futo.platformplayer.downloads.VideoDownload
|
import com.futo.platformplayer.downloads.VideoDownload
|
||||||
|
import com.futo.platformplayer.images.GlideHelper
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
@@ -194,22 +195,35 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
_textMetadata.text = parts.joinToString(" • ");
|
_textMetadata.text = parts.joinToString(" • ");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun setVideos(videos: List<IPlatformVideo>?, canEdit: Boolean) {
|
protected fun setVideos(videos: List<IPlatformVideo>?, canEdit: Boolean, thumbnail: String? = null) {
|
||||||
if (videos != null && videos.isNotEmpty()) {
|
if(thumbnail != null) {
|
||||||
val video = videos.first();
|
|
||||||
_imagePlaylistThumbnail.let {
|
_imagePlaylistThumbnail.let {
|
||||||
Glide.with(it)
|
Glide.with(it)
|
||||||
.load(video.thumbnails.getHQThumbnail())
|
.load(thumbnail)
|
||||||
.placeholder(R.drawable.placeholder_video_thumbnail)
|
.placeholder(R.drawable.placeholder_video_thumbnail)
|
||||||
.crossfade()
|
.crossfade()
|
||||||
.into(it);
|
.into(it);
|
||||||
};
|
}
|
||||||
} else {
|
|
||||||
_textMetadata.text = "0 " + context.getString(R.string.videos);
|
|
||||||
Glide.with(_imagePlaylistThumbnail)
|
|
||||||
.load(R.drawable.placeholder_video_thumbnail)
|
|
||||||
.into(_imagePlaylistThumbnail)
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
if (videos != null && videos.isNotEmpty()) {
|
||||||
|
val video = videos.first();
|
||||||
|
_imagePlaylistThumbnail.let {
|
||||||
|
Glide.with(it)
|
||||||
|
.load(video.thumbnails.getHQThumbnail())
|
||||||
|
.placeholder(R.drawable.placeholder_video_thumbnail)
|
||||||
|
.crossfade()
|
||||||
|
.into(it);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
Glide.with(_imagePlaylistThumbnail)
|
||||||
|
.load(R.drawable.placeholder_video_thumbnail)
|
||||||
|
.into(_imagePlaylistThumbnail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(videos == null || videos.isEmpty())
|
||||||
|
_textMetadata.text = "0 " + context.getString(R.string.videos);
|
||||||
|
|
||||||
_loadedVideos = videos;
|
_loadedVideos = videos;
|
||||||
_loadedVideosCanEdit = canEdit;
|
_loadedVideosCanEdit = canEdit;
|
||||||
_videoListEditorView.setVideos(videos, canEdit);
|
_videoListEditorView.setVideos(videos, canEdit);
|
||||||
|
|||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.topbar
|
||||||
|
|
||||||
|
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.appcompat.widget.AppCompatImageView
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFilesFragment
|
||||||
|
import com.futo.platformplayer.models.Playlist
|
||||||
|
import com.futo.platformplayer.states.FileEntry
|
||||||
|
import com.futo.platformplayer.views.casting.CastButton
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
|
class FilesTopBarFragment : TopFragment() {
|
||||||
|
private var _buttonBack: ImageButton? = null;
|
||||||
|
private var _buttonCast: CastButton? = null;
|
||||||
|
private var _textTitle: TextView? = null;
|
||||||
|
private var _menuItems: LinearLayout? = null;
|
||||||
|
|
||||||
|
private var _upHandle: (()->Unit)? = null;
|
||||||
|
|
||||||
|
override fun onShown(parameter: Any?) {
|
||||||
|
setTitle(parameter);
|
||||||
|
setMenuItems(listOf());
|
||||||
|
}
|
||||||
|
override fun onHide() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTitle(parameter: Any? = null) {
|
||||||
|
if(parameter is IPlatformChannel) {
|
||||||
|
_textTitle?.text = parameter.name;
|
||||||
|
} else if(parameter is PlatformAuthorLink) {
|
||||||
|
_textTitle?.text = parameter.name;
|
||||||
|
} else if (parameter is Playlist) {
|
||||||
|
_textTitle?.text = parameter.name;
|
||||||
|
} else if (parameter is String) {
|
||||||
|
_textTitle?.text = parameter;
|
||||||
|
} else if (parameter is IPlatformClient) {
|
||||||
|
_textTitle?.text = parameter.name;
|
||||||
|
} else if (parameter is PolycentricProfile) {
|
||||||
|
_textTitle?.text = parameter.systemState.username;
|
||||||
|
} else if(parameter is FileEntry) {
|
||||||
|
val treePrefix = "content://com.android.externalstorage.documents/tree/";
|
||||||
|
if(parameter.path.startsWith(treePrefix)) {
|
||||||
|
_textTitle?.text = parameter.path.substring(treePrefix.length - 1).replace("%3A", " ").replace("%2F", "/");
|
||||||
|
}
|
||||||
|
else if(parameter.path.isNullOrBlank())
|
||||||
|
_textTitle?.text = parameter.name;
|
||||||
|
else
|
||||||
|
_textTitle?.text = parameter.path;
|
||||||
|
}
|
||||||
|
else if(parameter is LibraryFilesFragment.FileStack) {
|
||||||
|
val treePrefix = "content://com.android.externalstorage.documents/tree/";
|
||||||
|
if(parameter.path.startsWith(treePrefix)) {
|
||||||
|
_textTitle?.text = parameter.path.substring(treePrefix.length - 1).replace("%3A", " ").replace("%2F", "/");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_textTitle?.text = parameter.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_files_top_bar, container, false);
|
||||||
|
|
||||||
|
val buttonBack: ImageButton = view.findViewById(R.id.button_back);
|
||||||
|
_textTitle = view.findViewById(R.id.text_title);
|
||||||
|
_menuItems = view.findViewById(R.id.menu_buttons)
|
||||||
|
|
||||||
|
buttonBack.setOnClickListener {
|
||||||
|
if(_upHandle != null)
|
||||||
|
_upHandle?.invoke();
|
||||||
|
else
|
||||||
|
closeSegment();
|
||||||
|
};
|
||||||
|
|
||||||
|
_buttonBack = buttonBack;
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUpNavigate(handle: (()->Unit)? = null) {
|
||||||
|
_upHandle = handle;
|
||||||
|
_buttonBack?.setImageResource(if(handle == null) R.drawable.ic_back_nav else R.drawable.ic_arrow_up);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
|
||||||
|
_buttonBack?.setOnClickListener(null);
|
||||||
|
_buttonBack = null;
|
||||||
|
_buttonCast?.cleanup();
|
||||||
|
_buttonCast = null;
|
||||||
|
_textTitle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMenuItems(items: List<Pair<Int, ()->Unit>>) {
|
||||||
|
_menuItems?.removeAllViews();
|
||||||
|
|
||||||
|
val dp4 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4f, resources.displayMetrics).toInt();
|
||||||
|
val dp9 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 9f, resources.displayMetrics).toInt();
|
||||||
|
|
||||||
|
for(item in items) {
|
||||||
|
val compatImageItem = AppCompatImageView(requireContext());
|
||||||
|
compatImageItem.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT);
|
||||||
|
compatImageItem.setImageResource(item.first);
|
||||||
|
compatImageItem.setPadding(dp4, dp9, dp4, dp9);
|
||||||
|
compatImageItem.scaleType = ImageView.ScaleType.FIT_CENTER;
|
||||||
|
compatImageItem.setOnClickListener {
|
||||||
|
item.second.invoke();
|
||||||
|
};
|
||||||
|
|
||||||
|
_menuItems?.addView(compatImageItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = FilesTopBarFragment().apply { }
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user