mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-17 05:22:40 +02:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4723a0b29a | |||
| adbe0357ba | |||
| b0a35bcf3f | |||
| 0e7482321c | |||
| e50d195b85 | |||
| 33780f1046 | |||
| 8b20b4909f | |||
| 71a3828fe4 | |||
| d713f2bd55 | |||
| 069a615193 | |||
| f7d2cb4055 | |||
| f109d82537 | |||
| ab49d4749b | |||
| 507eed4f53 | |||
| 23ca4addf9 | |||
| 331ed09775 | |||
| 85303b54bc | |||
| f224cd1ca5 | |||
| d433d6e774 | |||
| 90de54ac5c | |||
| 5ff8f1ba6d | |||
| bc00b12b8c | |||
| 1c0cfa89a3 | |||
| efa1361fbe | |||
| 73918a8d76 | |||
| a3c8bbb21f | |||
| 53525cb365 | |||
| e4d39cbec4 | |||
| a15e4beafb | |||
| d47298102e | |||
| 280feea06e | |||
| f649d62e38 |
+43
-36
@@ -1,11 +1,11 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
||||
id 'org.ajoberstar.grgit' version '1.7.2'
|
||||
id 'com.google.protobuf'
|
||||
id 'kotlin-parcelize'
|
||||
id 'kotlin-kapt'
|
||||
id 'com.google.devtools.ksp'
|
||||
}
|
||||
|
||||
ext {
|
||||
@@ -24,7 +24,7 @@ if (keystorePropertiesFile.exists()) {
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.22.3'
|
||||
artifact = 'com.google.protobuf:protoc:3.25.1'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
@@ -97,11 +97,15 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
minSdk 28
|
||||
targetSdk 33
|
||||
targetSdk 34
|
||||
versionCode gitVersionCode
|
||||
versionName gitVersionName
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -137,43 +141,46 @@ android {
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
//Core
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
|
||||
//Images
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1'
|
||||
implementation 'com.github.bumptech.glide:glide:4.15.1'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.16.0'
|
||||
|
||||
//Async
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
||||
|
||||
//HTTP
|
||||
implementation "com.squareup.okhttp3:okhttp:4.10.0"
|
||||
implementation "com.squareup.okhttp3:okhttp:4.11.0"
|
||||
|
||||
//JSON
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" //Used for structured json
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" //Used for structured json
|
||||
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
||||
|
||||
//JS
|
||||
implementation("com.caoccao.javet:javet-android:2.2.1")
|
||||
|
||||
//Exoplayer
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.18.7'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.18.7'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.18.7'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.18.7'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-rtsp:2.18.7'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.18.7'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-transformer:2.18.7'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.19.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-rtsp:2.19.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.19.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-transformer:2.19.1'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'
|
||||
|
||||
//Other
|
||||
implementation 'org.jmdns:jmdns:3.5.1'
|
||||
@@ -181,34 +188,34 @@ dependencies {
|
||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||
implementation 'com.google.zxing:core:3.4.1'
|
||||
implementation 'com.journeyapps:zxing-android-embedded:4.2.0'
|
||||
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
|
||||
//Protobuf
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.22.3'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
|
||||
|
||||
implementation 'com.polycentric.core:app:1.0'
|
||||
implementation 'com.futo.futopay:app:1.0'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
||||
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
|
||||
|
||||
//Database
|
||||
implementation("androidx.room:room-runtime:2.6.0")
|
||||
annotationProcessor("androidx.room:room-compiler:2.6.0")
|
||||
kapt("androidx.room:room-compiler:2.6.0")
|
||||
implementation("androidx.room:room-ktx:2.6.0")
|
||||
implementation("androidx.room:room-runtime:2.6.1")
|
||||
annotationProcessor("androidx.room:room-compiler:2.6.1")
|
||||
ksp("androidx.room:room-compiler:2.6.1")
|
||||
implementation("androidx.room:room-ktx:2.6.1")
|
||||
|
||||
//Payment
|
||||
implementation 'com.stripe:stripe-android:20.28.3'
|
||||
implementation 'com.stripe:stripe-android:20.35.1'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2'
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.20"
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.22"
|
||||
testImplementation "org.xmlunit:xmlunit-core:2.9.1"
|
||||
testImplementation "org.mockito:mockito-core:5.4.0"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
}
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 3,
|
||||
"identityHash": "ffba56c2f572c25080ce8596e8bb8945",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "history",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `url` TEXT NOT NULL, `position` INTEGER NOT NULL, `datetime` INTEGER NOT NULL, `name` TEXT NOT NULL, `serialized` BLOB)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "position",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "datetime",
|
||||
"columnName": "datetime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serialized",
|
||||
"columnName": "serialized",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_history_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_url` ON `${TABLE_NAME}` (`url`)"
|
||||
},
|
||||
{
|
||||
"name": "index_history_name",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_name` ON `${TABLE_NAME}` (`name`)"
|
||||
},
|
||||
{
|
||||
"name": "index_history_datetime",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"datetime"
|
||||
],
|
||||
"orders": [
|
||||
"DESC"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_datetime` ON `${TABLE_NAME}` (`datetime` DESC)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ffba56c2f572c25080ce8596e8bb8945')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 5,
|
||||
"identityHash": "eb813d54b9c44d29f1d7bb198a16d4d1",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "subscription_cache",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `url` TEXT, `channelUrl` TEXT, `datetime` INTEGER, `serialized` BLOB)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "channelUrl",
|
||||
"columnName": "channelUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "datetime",
|
||||
"columnName": "datetime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "serialized",
|
||||
"columnName": "serialized",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_subscription_cache_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_url` ON `${TABLE_NAME}` (`url`)"
|
||||
},
|
||||
{
|
||||
"name": "index_subscription_cache_channelUrl",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"channelUrl"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_channelUrl` ON `${TABLE_NAME}` (`channelUrl`)"
|
||||
},
|
||||
{
|
||||
"name": "index_subscription_cache_datetime",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"datetime"
|
||||
],
|
||||
"orders": [
|
||||
"DESC"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_datetime` ON `${TABLE_NAME}` (`datetime` DESC)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eb813d54b9c44d29f1d7bb198a16d4d1')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 3,
|
||||
"identityHash": "6e3b2d286325c4ea8a7a4c94c290daec",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "testing",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`someString` TEXT NOT NULL, `someNum` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `serialized` BLOB)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "someString",
|
||||
"columnName": "someString",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "someNum",
|
||||
"columnName": "someNum",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "serialized",
|
||||
"columnName": "serialized",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6e3b2d286325c4ea8a7a4c94c290daec')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,11 @@ class ScriptException extends Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
class ScriptLoginRequiredException extends ScriptException {
|
||||
constructor(msg) {
|
||||
super("ScriptLoginRequiredException", msg);
|
||||
}
|
||||
}
|
||||
class CaptchaRequiredException extends Error {
|
||||
constructor(url, body) {
|
||||
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
|
||||
|
||||
@@ -109,6 +109,10 @@ class UISlideOverlays {
|
||||
menu.onOK.subscribe {
|
||||
subscription.save();
|
||||
menu.hide(true);
|
||||
|
||||
if(subscription.doNotifications && !originalNotif && Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
|
||||
UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work");
|
||||
}
|
||||
};
|
||||
menu.onCancel.subscribe {
|
||||
subscription.doNotifications = originalNotif;
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
||||
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
|
||||
@@ -146,7 +146,7 @@ class DashBuilder : XMLBuilder {
|
||||
dashBuilder.withAdaptationSet(
|
||||
mapOf(
|
||||
Pair("mimeType", subtitleSource.format ?: "text/vtt"),
|
||||
Pair("lang", "en"),
|
||||
Pair("lang", "df"),
|
||||
Pair("default", "true")
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -18,6 +18,7 @@ class AirPlayCastingDevice : CastingDevice {
|
||||
override var usedRemoteAddress: InetAddress? = null;
|
||||
override var localAddress: InetAddress? = null;
|
||||
override val canSetVolume: Boolean get() = false;
|
||||
override val canSetSpeed: Boolean get() = false; //TODO: Implement playback speed for AirPlay
|
||||
|
||||
var addresses: Array<InetAddress>? = null;
|
||||
var port: Int = 0;
|
||||
@@ -43,12 +44,12 @@ class AirPlayCastingDevice : CastingDevice {
|
||||
return addresses?.toList() ?: listOf();
|
||||
}
|
||||
|
||||
override fun loadVideo(streamType: String, contentType: String, contentId: String, resumePosition: Double, duration: Double) {
|
||||
if (invokeInIOScopeIfRequired({ loadVideo(streamType, contentType, contentId, resumePosition, duration) })) {
|
||||
override fun loadVideo(streamType: String, contentType: String, contentId: String, resumePosition: Double, duration: Double, speed: Double?) {
|
||||
if (invokeInIOScopeIfRequired({ loadVideo(streamType, contentType, contentId, resumePosition, duration, speed) })) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.i(FCastCastingDevice.TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration)");
|
||||
Logger.i(FCastCastingDevice.TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration, speed: $speed)");
|
||||
|
||||
time = resumePosition;
|
||||
if (resumePosition > 0.0) {
|
||||
@@ -60,7 +61,7 @@ class AirPlayCastingDevice : CastingDevice {
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double) {
|
||||
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
|
||||
throw NotImplementedError();
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ abstract class CastingDevice {
|
||||
abstract var usedRemoteAddress: InetAddress?;
|
||||
abstract var localAddress: InetAddress?;
|
||||
abstract val canSetVolume: Boolean;
|
||||
abstract val canSetSpeed: Boolean;
|
||||
|
||||
var name: String? = null;
|
||||
var isPlaying: Boolean = false
|
||||
@@ -77,6 +78,14 @@ abstract class CastingDevice {
|
||||
onVolumeChanged.emit(value);
|
||||
}
|
||||
};
|
||||
var speed: Double = 1.0
|
||||
set(value) {
|
||||
val changed = value != field;
|
||||
speed = value;
|
||||
if (changed) {
|
||||
onSpeedChanged.emit(value);
|
||||
}
|
||||
};
|
||||
val expectedCurrentTime: Double
|
||||
get() {
|
||||
val diff = timeReceivedAt.getNowDiffMiliseconds().toDouble() / 1000.0;
|
||||
@@ -96,6 +105,7 @@ abstract class CastingDevice {
|
||||
var onPlayChanged = Event1<Boolean>();
|
||||
var onTimeChanged = Event1<Double>();
|
||||
var onVolumeChanged = Event1<Double>();
|
||||
var onSpeedChanged = Event1<Double>();
|
||||
|
||||
abstract fun stopCasting();
|
||||
|
||||
@@ -103,9 +113,10 @@ abstract class CastingDevice {
|
||||
abstract fun stopVideo();
|
||||
abstract fun pauseVideo();
|
||||
abstract fun resumeVideo();
|
||||
abstract fun loadVideo(streamType: String, contentType: String, contentId: String, resumePosition: Double, duration: Double);
|
||||
abstract fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double);
|
||||
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();
|
||||
|
||||
@@ -27,6 +27,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
override var usedRemoteAddress: InetAddress? = null;
|
||||
override var localAddress: InetAddress? = null;
|
||||
override val canSetVolume: Boolean get() = true;
|
||||
override val canSetSpeed: Boolean get() = false; //TODO: Implement
|
||||
|
||||
var addresses: Array<InetAddress>? = null;
|
||||
var port: Int = 0;
|
||||
@@ -62,12 +63,12 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
return addresses?.toList() ?: listOf();
|
||||
}
|
||||
|
||||
override fun loadVideo(streamType: String, contentType: String, contentId: String, resumePosition: Double, duration: Double) {
|
||||
if (invokeInIOScopeIfRequired({ loadVideo(streamType, contentType, contentId, resumePosition, duration) })) {
|
||||
override fun loadVideo(streamType: String, contentType: String, contentId: String, resumePosition: Double, duration: Double, speed: Double?) {
|
||||
if (invokeInIOScopeIfRequired({ loadVideo(streamType, contentType, contentId, resumePosition, duration, speed) })) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration)");
|
||||
Logger.i(TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration, speed: $speed)");
|
||||
|
||||
time = resumePosition;
|
||||
_streamType = streamType;
|
||||
@@ -77,7 +78,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
playVideo();
|
||||
}
|
||||
|
||||
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double) {
|
||||
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
|
||||
//TODO: Can maybe be implemented by sending data:contentType,base64...
|
||||
throw NotImplementedError();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.casting
|
||||
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.casting.models.*
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.getConnectedSocket
|
||||
@@ -27,7 +28,10 @@ enum class Opcode(val value: Byte) {
|
||||
SEEK(5),
|
||||
PLAYBACK_UPDATE(6),
|
||||
VOLUME_UPDATE(7),
|
||||
SET_VOLUME(8)
|
||||
SET_VOLUME(8),
|
||||
PLAYBACK_ERROR(9),
|
||||
SET_SPEED(10),
|
||||
VERSION(11)
|
||||
}
|
||||
|
||||
class FCastCastingDevice : CastingDevice {
|
||||
@@ -38,6 +42,7 @@ class FCastCastingDevice : CastingDevice {
|
||||
override var usedRemoteAddress: InetAddress? = null;
|
||||
override var localAddress: InetAddress? = null;
|
||||
override val canSetVolume: Boolean get() = true;
|
||||
override val canSetSpeed: Boolean get() = true;
|
||||
|
||||
var addresses: Array<InetAddress>? = null;
|
||||
var port: Int = 0;
|
||||
@@ -47,6 +52,7 @@ class FCastCastingDevice : CastingDevice {
|
||||
private var _inputStream: DataInputStream? = null;
|
||||
private var _scopeIO: CoroutineScope? = null;
|
||||
private var _started: Boolean = false;
|
||||
private var _version: Long = 1;
|
||||
|
||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||
this.name = name;
|
||||
@@ -64,33 +70,45 @@ class FCastCastingDevice : CastingDevice {
|
||||
return addresses?.toList() ?: listOf();
|
||||
}
|
||||
|
||||
override fun loadVideo(streamType: String, contentType: String, contentId: String, resumePosition: Double, duration: Double) {
|
||||
if (invokeInIOScopeIfRequired({ loadVideo(streamType, contentType, contentId, resumePosition, duration) })) {
|
||||
override fun loadVideo(streamType: String, contentType: String, contentId: String, resumePosition: Double, duration: Double, speed: Double?) {
|
||||
if (invokeInIOScopeIfRequired({ loadVideo(streamType, contentType, contentId, resumePosition, duration, speed) })) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration)");
|
||||
//TODO: Remove this later, temporary for the transition
|
||||
if (_version <= 1L) {
|
||||
UIDialogs.toast("Version not received, if you are experiencing issues, try updating FCast")
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration, speed: $speed)");
|
||||
|
||||
time = resumePosition;
|
||||
sendMessage(Opcode.PLAY, FCastPlayMessage(
|
||||
container = contentType,
|
||||
url = contentId,
|
||||
time = resumePosition.toInt()
|
||||
time = resumePosition,
|
||||
speed = speed
|
||||
));
|
||||
}
|
||||
|
||||
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double) {
|
||||
if (invokeInIOScopeIfRequired({ loadContent(contentType, content, resumePosition, duration) })) {
|
||||
override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) {
|
||||
if (invokeInIOScopeIfRequired({ loadContent(contentType, content, resumePosition, duration, speed) })) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Start streaming content (contentType: $contentType, resumePosition: $resumePosition, duration: $duration)");
|
||||
//TODO: Remove this later, temporary for the transition
|
||||
if (_version <= 1L) {
|
||||
UIDialogs.toast("Version not received, if you are experiencing issues, try updating FCast")
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Start streaming content (contentType: $contentType, resumePosition: $resumePosition, duration: $duration, speed: $speed)");
|
||||
|
||||
time = resumePosition;
|
||||
sendMessage(Opcode.PLAY, FCastPlayMessage(
|
||||
container = contentType,
|
||||
content = content,
|
||||
time = resumePosition.toInt()
|
||||
time = resumePosition,
|
||||
speed = speed
|
||||
));
|
||||
}
|
||||
|
||||
@@ -103,13 +121,22 @@ class FCastCastingDevice : CastingDevice {
|
||||
sendMessage(Opcode.SET_VOLUME, FCastSetVolumeMessage(volume))
|
||||
}
|
||||
|
||||
override fun changeSpeed(speed: Double) {
|
||||
if (invokeInIOScopeIfRequired({ changeSpeed(volume) })) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.speed = speed
|
||||
sendMessage(Opcode.SET_SPEED, FCastSetSpeedMessage(volume))
|
||||
}
|
||||
|
||||
override fun seekVideo(timeSeconds: Double) {
|
||||
if (invokeInIOScopeIfRequired({ seekVideo(timeSeconds) })) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendMessage(Opcode.SEEK, FCastSeekMessage(
|
||||
time = timeSeconds.toInt()
|
||||
time = timeSeconds
|
||||
));
|
||||
}
|
||||
|
||||
@@ -282,8 +309,8 @@ class FCastCastingDevice : CastingDevice {
|
||||
return;
|
||||
}
|
||||
|
||||
val playbackUpdate = Json.decodeFromString<FCastPlaybackUpdateMessage>(json);
|
||||
time = playbackUpdate.time.toDouble();
|
||||
val playbackUpdate = FCastCastingDevice.json.decodeFromString<FCastPlaybackUpdateMessage>(json);
|
||||
time = playbackUpdate.time;
|
||||
isPlaying = when (playbackUpdate.state) {
|
||||
1 -> true
|
||||
else -> false
|
||||
@@ -295,9 +322,28 @@ class FCastCastingDevice : CastingDevice {
|
||||
return;
|
||||
}
|
||||
|
||||
val volumeUpdate = Json.decodeFromString<FCastVolumeUpdateMessage>(json);
|
||||
val volumeUpdate = FCastCastingDevice.json.decodeFromString<FCastVolumeUpdateMessage>(json);
|
||||
volume = volumeUpdate.volume;
|
||||
}
|
||||
Opcode.PLAYBACK_ERROR -> {
|
||||
if (json == null) {
|
||||
Logger.w(TAG, "Got playback error without JSON, ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
val playbackError = FCastCastingDevice.json.decodeFromString<FCastPlaybackErrorMessage>(json);
|
||||
Logger.e(TAG, "Remote casting playback error received: $playbackError")
|
||||
}
|
||||
Opcode.VERSION -> {
|
||||
if (json == null) {
|
||||
Logger.w(TAG, "Got version without JSON, ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
val version = FCastCastingDevice.json.decodeFromString<FCastVersionMessage>(json);
|
||||
_version = version.version;
|
||||
Logger.i(TAG, "Remote version received: $version")
|
||||
}
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
@@ -333,7 +379,7 @@ class FCastCastingDevice : CastingDevice {
|
||||
val data: ByteArray;
|
||||
var jsonString: String? = null;
|
||||
if (message != null) {
|
||||
jsonString = Json.encodeToString(message);
|
||||
jsonString = json.encodeToString(message);
|
||||
data = jsonString.encodeToByteArray();
|
||||
} else {
|
||||
data = ByteArray(0);
|
||||
@@ -403,5 +449,6 @@ class FCastCastingDevice : CastingDevice {
|
||||
|
||||
companion object {
|
||||
val TAG = "FastCastCastingDevice";
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
}
|
||||
}
|
||||
@@ -395,17 +395,17 @@ class StateCasting {
|
||||
} else {
|
||||
if (videoSource is IVideoUrlSource) {
|
||||
Logger.i(TAG, "Casting as singular video");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble());
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble(), null);
|
||||
} else if (audioSource is IAudioUrlSource) {
|
||||
Logger.i(TAG, "Casting as singular audio");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble());
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble(), null);
|
||||
} else if(videoSource is IHLSManifestSource) {
|
||||
if (ad is ChromecastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as proxied HLS");
|
||||
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition);
|
||||
} else {
|
||||
Logger.i(TAG, "Casting as non-proxied HLS");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble());
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), null);
|
||||
}
|
||||
} else if(audioSource is IHLSManifestAudioSource) {
|
||||
if (ad is ChromecastCastingDevice) {
|
||||
@@ -413,7 +413,7 @@ class StateCasting {
|
||||
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition);
|
||||
} else {
|
||||
Logger.i(TAG, "Casting as non-proxied audio HLS");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble());
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), null);
|
||||
}
|
||||
} else if (videoSource is LocalVideoSource) {
|
||||
Logger.i(TAG, "Casting as local video");
|
||||
@@ -480,7 +480,7 @@ class StateCasting {
|
||||
).withTag("cast");
|
||||
|
||||
Logger.i(TAG, "Casting local video (videoUrl: $videoUrl).");
|
||||
ad.loadVideo("BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble());
|
||||
ad.loadVideo("BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), null);
|
||||
|
||||
return listOf(videoUrl);
|
||||
}
|
||||
@@ -499,7 +499,7 @@ class StateCasting {
|
||||
).withTag("cast");
|
||||
|
||||
Logger.i(TAG, "Casting local audio (audioUrl: $audioUrl).");
|
||||
ad.loadVideo("BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble());
|
||||
ad.loadVideo("BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), null);
|
||||
|
||||
return listOf(audioUrl);
|
||||
}
|
||||
@@ -563,7 +563,7 @@ class StateCasting {
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castLocalHls")
|
||||
|
||||
mediaRenditions.add(HLS.MediaRendition("AUDIO", audioVariantPlaylistUrl, "audio", "en", "english", true, true, true))
|
||||
mediaRenditions.add(HLS.MediaRendition("AUDIO", audioVariantPlaylistUrl, "audio", "df", "default", true, true, true))
|
||||
}
|
||||
|
||||
if (subtitleSource != null) {
|
||||
@@ -584,7 +584,7 @@ class StateCasting {
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castLocalHls")
|
||||
|
||||
mediaRenditions.add(HLS.MediaRendition("SUBTITLES", subtitleVariantPlaylistUrl, "subtitles", "en", "english", true, true, true))
|
||||
mediaRenditions.add(HLS.MediaRendition("SUBTITLES", subtitleVariantPlaylistUrl, "subtitles", "df", "default", true, true, true))
|
||||
}
|
||||
|
||||
val masterPlaylist = HLS.MasterPlaylist(variantPlaylistReferences, mediaRenditions, listOf(), true)
|
||||
@@ -595,7 +595,7 @@ class StateCasting {
|
||||
).withTag("castLocalHls")
|
||||
|
||||
Logger.i(TAG, "added new castLocalHls handlers (hlsPath: $hlsPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath).")
|
||||
ad.loadVideo("BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble())
|
||||
ad.loadVideo("BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble(), null)
|
||||
|
||||
return listOf(hlsUrl, videoUrl, audioUrl, subtitleUrl)
|
||||
}
|
||||
@@ -641,7 +641,7 @@ class StateCasting {
|
||||
}
|
||||
|
||||
Logger.i(TAG, "added new castLocalDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath).");
|
||||
ad.loadVideo("BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble());
|
||||
ad.loadVideo("BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), null);
|
||||
|
||||
return listOf(dashUrl, videoUrl, audioUrl, subtitleUrl);
|
||||
}
|
||||
@@ -686,7 +686,7 @@ class StateCasting {
|
||||
val content = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
|
||||
|
||||
Logger.i(TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl).");
|
||||
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble());
|
||||
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), null);
|
||||
|
||||
return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "");
|
||||
}
|
||||
@@ -812,7 +812,7 @@ class StateCasting {
|
||||
|
||||
//ChromeCast is sometimes funky with resume position 0
|
||||
val hackfixResumePosition = if (ad is ChromecastCastingDevice && !video.isLive && resumePosition == 0.0) 0.1 else resumePosition;
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, hackfixResumePosition, video.duration.toDouble());
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, hackfixResumePosition, video.duration.toDouble(), null);
|
||||
|
||||
return listOf(hlsUrl);
|
||||
}
|
||||
@@ -892,7 +892,7 @@ class StateCasting {
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castHlsIndirectVariant");
|
||||
|
||||
mediaRenditions.add(HLS.MediaRendition("AUDIO", audioVariantPlaylistUrl, "audio", "en", "english", true, true, true))
|
||||
mediaRenditions.add(HLS.MediaRendition("AUDIO", audioVariantPlaylistUrl, "audio", "df", "default", true, true, true))
|
||||
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
||||
@@ -942,7 +942,7 @@ class StateCasting {
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("castHlsIndirectVariant");
|
||||
|
||||
mediaRenditions.add(HLS.MediaRendition("SUBTITLES", subtitleVariantPlaylistUrl, "subtitles", "en", "english", true, true, true))
|
||||
mediaRenditions.add(HLS.MediaRendition("SUBTITLES", subtitleVariantPlaylistUrl, "subtitles", "df", "default", true, true, true))
|
||||
}
|
||||
|
||||
if (videoSource != null) {
|
||||
@@ -986,7 +986,7 @@ class StateCasting {
|
||||
).withTag("castHlsIndirectMaster")
|
||||
|
||||
Logger.i(TAG, "added new castHls handlers (hlsPath: $hlsPath).");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble());
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble(), null);
|
||||
|
||||
return listOf(hlsUrl, videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||
}
|
||||
@@ -1061,7 +1061,7 @@ class StateCasting {
|
||||
}
|
||||
|
||||
Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath).");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble());
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), null);
|
||||
|
||||
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||
}
|
||||
|
||||
@@ -2,32 +2,52 @@ package com.futo.platformplayer.casting.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class FCastPlayMessage(
|
||||
val container: String,
|
||||
val url: String? = null,
|
||||
val content: String? = null,
|
||||
val time: Int? = null
|
||||
val time: Double? = null,
|
||||
val speed: Double? = null
|
||||
) { }
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class FCastSeekMessage(
|
||||
val time: Int
|
||||
val time: Double
|
||||
) { }
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class FCastPlaybackUpdateMessage(
|
||||
val time: Int,
|
||||
val state: Int
|
||||
val generationTime: Long,
|
||||
val time: Double,
|
||||
val duration: Double,
|
||||
val state: Int,
|
||||
val speed: Double
|
||||
) { }
|
||||
|
||||
|
||||
@Serializable
|
||||
data class FCastVolumeUpdateMessage(
|
||||
val generationTime: Long,
|
||||
val volume: Double
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FCastSetVolumeMessage(
|
||||
val volume: Double
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FCastSetSpeedMessage(
|
||||
val speed: Double
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FCastPlaybackErrorMessage(
|
||||
val message: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FCastVersionMessage(
|
||||
val version: Long
|
||||
)
|
||||
@@ -7,6 +7,7 @@ import android.view.LayoutInflater
|
||||
import android.widget.Button
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
|
||||
import com.futo.platformplayer.readBytes
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
@@ -19,6 +20,7 @@ class ImportOptionsDialog: AlertDialog {
|
||||
private lateinit var _button_import_ezip: BigButton;
|
||||
private lateinit var _button_import_txt: BigButton;
|
||||
private lateinit var _button_import_newpipe_subs: BigButton;
|
||||
private lateinit var _button_import_platform: BigButton;
|
||||
private lateinit var _button_close: Button;
|
||||
|
||||
|
||||
@@ -33,6 +35,7 @@ class ImportOptionsDialog: AlertDialog {
|
||||
_button_import_ezip = findViewById(R.id.button_import_ezip);
|
||||
_button_import_txt = findViewById(R.id.button_import_txt);
|
||||
_button_import_newpipe_subs = findViewById(R.id.button_import_newpipe_subs);
|
||||
_button_import_platform = findViewById(R.id.button_import_platform);
|
||||
_button_close = findViewById(R.id.button_cancel);
|
||||
|
||||
_button_import_zip.onClick.subscribe {
|
||||
@@ -61,6 +64,10 @@ class ImportOptionsDialog: AlertDialog {
|
||||
StateBackup.importNewPipeSubs(_context, json);
|
||||
};
|
||||
};
|
||||
_button_import_platform.onClick.subscribe {
|
||||
dismiss();
|
||||
_context.navigate(_context.getFragment<SourcesFragment>());
|
||||
};
|
||||
_button_close.setOnClickListener {
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@@ -301,6 +301,7 @@ class V8Plugin {
|
||||
"CriticalException" -> throw ScriptCriticalException(config, msg, innerEx, stack, code);
|
||||
"AgeException" -> throw ScriptAgeException(config, msg, innerEx, stack, code);
|
||||
"UnavailableException" -> throw ScriptUnavailableException(config, msg, innerEx, stack, code);
|
||||
"ScriptLoginRequiredException" -> throw ScriptLoginRequiredException(config, msg, innerEx, stack, code);
|
||||
"ScriptExecutionException" -> throw ScriptExecutionException(config, msg, innerEx, stack, code);
|
||||
"ScriptCompilationException" -> throw ScriptCompilationException(config, msg, innerEx, code);
|
||||
"ScriptImplementationException" -> throw ScriptImplementationException(config, msg, innerEx, null, code);
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.futo.platformplayer.engine.exceptions
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class ScriptLoginRequiredException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
|
||||
return ScriptLoginRequiredException(config, obj.getOrThrow(config, "message", "ScriptLoginRequiredException"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -78,7 +78,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
private var _moreButtons = arrayListOf<MenuButton>();
|
||||
|
||||
private var _buttonsVisible = 0;
|
||||
private var _subscriptionsVisible = false;
|
||||
private var _subscriptionsVisible = true;
|
||||
|
||||
var currentButtonDefinitions: List<ButtonDefinition>? = null;
|
||||
|
||||
@@ -261,11 +261,12 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
}
|
||||
|
||||
private fun registerUpdateButtonEvents() {
|
||||
/*
|
||||
_subscriptionsVisible = StateSubscriptions.instance.getSubscriptionCount() > 0;
|
||||
StateSubscriptions.instance.onSubscriptionsChanged.subscribe(this) { subs, _ ->
|
||||
_subscriptionsVisible = subs.isNotEmpty();
|
||||
updateButtonDefinitions()
|
||||
}
|
||||
}*/
|
||||
|
||||
StatePayment.instance.hasPaidChanged.subscribe(this) {
|
||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
|
||||
-2
@@ -207,8 +207,6 @@ class ChannelFragment : MainFragment() {
|
||||
adapter.onAddToQueueClicked.subscribe { content ->
|
||||
if(content is IPlatformVideo) {
|
||||
StatePlayer.instance.addToQueue(content);
|
||||
val name = if (content.name.length > 20) (content.name.subSequence(0, 20).toString() + "...") else content.name;
|
||||
UIDialogs.toast(context, "Queued [$name]", false);
|
||||
}
|
||||
}
|
||||
adapter.onUrlClicked.subscribe { url ->
|
||||
|
||||
-2
@@ -79,8 +79,6 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
adapter.onAddToQueueClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlayer.instance.addToQueue(it);
|
||||
val name = if (it.name.length > 20) (it.name.subSequence(0, 20).toString() + "...") else it.name;
|
||||
UIDialogs.toast(context, context.getString(R.string.queued) + " [$name]", false);
|
||||
}
|
||||
};
|
||||
adapter.onLongPress.subscribe(this) {
|
||||
|
||||
@@ -38,6 +38,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
private val _containerSortBy: LinearLayout;
|
||||
private val _tagsView: TagsView;
|
||||
private val _textCentered: TextView;
|
||||
private val _emptyPagerContainer: FrameLayout;
|
||||
|
||||
protected val _toolbarContentView: LinearLayout;
|
||||
|
||||
@@ -69,6 +70,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
inflater.inflate(R.layout.fragment_feed, this);
|
||||
|
||||
_textCentered = findViewById(R.id.text_centered);
|
||||
_emptyPagerContainer = findViewById(R.id.empty_pager_container);
|
||||
_progress_bar = findViewById(R.id.progress_bar);
|
||||
_progress_bar.inactiveColor = Color.TRANSPARENT;
|
||||
|
||||
@@ -199,6 +201,30 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
protected fun setTextCentered(text: String?) {
|
||||
_textCentered.text = text;
|
||||
}
|
||||
protected open fun getEmptyPagerView(): View? {
|
||||
return null;
|
||||
}
|
||||
|
||||
fun setEmptyPager(enable: Boolean) {
|
||||
if(enable) {
|
||||
val viewToShow = getEmptyPagerView();
|
||||
if(viewToShow != null) {
|
||||
_emptyPagerContainer.removeAllViews();
|
||||
_emptyPagerContainer.addView(viewToShow);
|
||||
_emptyPagerContainer.visibility = VISIBLE;
|
||||
setTextCentered(null);
|
||||
}
|
||||
else {
|
||||
setTextCentered(context.getString(R.string.no_results_found_swipe_down_to_refresh));
|
||||
_emptyPagerContainer.visibility = GONE;
|
||||
}
|
||||
}
|
||||
else {
|
||||
setTextCentered(null);
|
||||
_emptyPagerContainer.removeAllViews();
|
||||
_emptyPagerContainer.visibility = GONE;
|
||||
}
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
//Reload the pager if the plugin was killed
|
||||
@@ -420,7 +446,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
synchronized(_pager_lock) {
|
||||
val pager: TPager = recyclerData.pager ?: return;
|
||||
val hasMorePages = pager.hasMorePages();
|
||||
Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages");
|
||||
Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages, page size=${pager.getResults().size}");
|
||||
|
||||
//loadCachedPage();
|
||||
if (pager.hasMorePages()) {
|
||||
@@ -429,7 +455,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
_nextPageHandler.run(pager);
|
||||
}
|
||||
else if(_lastNextPage) {
|
||||
Logger.i(TAG, "End of page reached");
|
||||
Logger.i(TAG, "End of page reached (Last page size: ${pager.getResults().size})");
|
||||
_lastNextPage = false;
|
||||
}
|
||||
}
|
||||
|
||||
+50
-12
@@ -22,6 +22,10 @@ import com.futo.platformplayer.views.adapters.viewholders.SelectableIPlatformCha
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ImportSubscriptionsFragment : MainFragment() {
|
||||
override val isMainView : Boolean = true;
|
||||
@@ -59,11 +63,15 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
class ImportSubscriptionsView : LinearLayout {
|
||||
private val _fragment: ImportSubscriptionsFragment;
|
||||
|
||||
private val SLOWDOWN_COUNT = 100;
|
||||
private val SLOWDOWN_MS: Long = 1000;
|
||||
|
||||
private var _spinner: ImageView;
|
||||
private var _textSelectDeselectAll: TextView;
|
||||
private var _textNothingToImport: TextView;
|
||||
private var _textCounter: TextView;
|
||||
private var _textLoadMore: TextView;
|
||||
private var _loadProgress: TextView;
|
||||
//private var _textLoadMore: TextView;
|
||||
private var _adapterView: AnyAdapterView<SelectableIPlatformChannel, ImportSubscriptionViewHolder>;
|
||||
private var _links: List<String> = listOf();
|
||||
private val _items: ArrayList<SelectableIPlatformChannel> = arrayListOf();
|
||||
@@ -80,8 +88,9 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
_textNothingToImport = findViewById(R.id.nothing_to_import);
|
||||
_textSelectDeselectAll = findViewById(R.id.text_select_deselect_all);
|
||||
_textCounter = findViewById(R.id.text_select_counter);
|
||||
_textLoadMore = findViewById(R.id.text_load_more);
|
||||
//_textLoadMore = findViewById(R.id.text_load_more);
|
||||
_spinner = findViewById(R.id.channel_loader);
|
||||
_loadProgress = findViewById(R.id.text_load_progress);
|
||||
|
||||
_adapterView = findViewById<RecyclerView>(R.id.recycler_import).asAny( _items) {
|
||||
it.onSelectedChange.subscribe { c ->
|
||||
@@ -113,6 +122,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
return@TaskHandler channel;
|
||||
}).success {
|
||||
_items.add(SelectableIPlatformChannel(it));
|
||||
_loadProgress.text = "(${_items.size}/${_links.size})";
|
||||
_adapterView.adapter.notifyItemInserted(_items.size - 1);
|
||||
loadNext();
|
||||
}.exceptionWithParameter<Throwable> { ex, para ->
|
||||
@@ -123,6 +133,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
loadNext();
|
||||
};
|
||||
|
||||
/*
|
||||
_textLoadMore.setOnClickListener {
|
||||
if (!_limitToastShown) {
|
||||
return@setOnClickListener;
|
||||
@@ -134,7 +145,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
load();
|
||||
};
|
||||
|
||||
_textLoadMore.visibility = View.GONE;
|
||||
_textLoadMore.visibility = View.GONE;*/
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
@@ -165,12 +176,23 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
it.title = context.getString(R.string.import_subscriptions);
|
||||
it.onImport.subscribe(this) {
|
||||
val subscriptionsToImport = _items.filter { i -> i.selected }.toList();
|
||||
for (subscriptionToImport in subscriptionsToImport) {
|
||||
StateSubscriptions.instance.addSubscription(subscriptionToImport.channel);
|
||||
UIDialogs.showDialogProgress(context) {
|
||||
it.setText("Importing subscriptions..");
|
||||
_fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
for ((i, subscriptionToImport) in subscriptionsToImport.withIndex()) {
|
||||
StateSubscriptions.instance.addSubscription(subscriptionToImport.channel);
|
||||
withContext(Dispatchers.Main) {
|
||||
it.setProgress(i.toDouble() / subscriptionsToImport.size);
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast("${subscriptionsToImport.size} " + context.getString(R.string.subscriptions_imported));
|
||||
_fragment.closeSegment();
|
||||
it.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UIDialogs.toast("${subscriptionsToImport.size} " + context.getString(R.string.subscriptions_imported));
|
||||
_fragment.closeSegment();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -180,7 +202,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
if (_counter >= MAXIMUM_BATCH_SIZE) {
|
||||
if (!_limitToastShown) {
|
||||
_limitToastShown = true;
|
||||
_textLoadMore.visibility = View.VISIBLE;
|
||||
// _textLoadMore.visibility = View.VISIBLE;
|
||||
UIDialogs.toast(context, context.getString(R.string.stopped_after_requestcount_to_avoid_rate_limit_click_load_more_to_load_more).replace("{requestCount}", MAXIMUM_BATCH_SIZE.toString()));
|
||||
}
|
||||
|
||||
@@ -192,11 +214,25 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
|
||||
private fun loadNext() {
|
||||
_currentLoadIndex++;
|
||||
|
||||
if (_currentLoadIndex < _links.size) {
|
||||
load();
|
||||
} else {
|
||||
setLoading(false);
|
||||
if(_currentLoadIndex >= SLOWDOWN_COUNT) {
|
||||
if(_currentLoadIndex % 10 == 0) {
|
||||
val estTime = (SLOWDOWN_MS * (_links.size - _currentLoadIndex)) / 1000;
|
||||
UIDialogs.toast(context, "Import slowed down to prevent rate limit (Estimate ${estTime.toInt().toHumanTimeIndicator()})");
|
||||
}
|
||||
_fragment.lifecycleScope.launch(Dispatchers.Default) {
|
||||
delay(SLOWDOWN_MS);
|
||||
withContext(Dispatchers.Main) {
|
||||
load();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
load();
|
||||
}
|
||||
else
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
private fun updateSelected() {
|
||||
@@ -216,17 +252,19 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
if(isLoading){
|
||||
(_spinner.drawable as Animatable?)?.start();
|
||||
_spinner.visibility = View.VISIBLE;
|
||||
_loadProgress.visibility = View.VISIBLE;
|
||||
}
|
||||
else {
|
||||
_spinner.visibility = View.GONE;
|
||||
(_spinner.drawable as Animatable?)?.stop();
|
||||
_loadProgress.visibility = View.GONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = "ImportSubscriptionsFragment";
|
||||
private const val MAXIMUM_BATCH_SIZE = 100;
|
||||
private const val MAXIMUM_BATCH_SIZE = 2000;
|
||||
fun newInstance() = ImportSubscriptionsFragment().apply {}
|
||||
}
|
||||
}
|
||||
+27
@@ -264,6 +264,31 @@ class SourceDetailFragment : MainFragment() {
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
val migrationButtons = mutableListOf<BigButton>();
|
||||
if (isEnabled && source.capabilities.hasGetUserSubscriptions) {
|
||||
migrationButtons.add(
|
||||
BigButton(c, context.getString(R.string.import_subscriptions), context.getString(R.string.login_required), R.drawable.ic_subscriptions) {
|
||||
|
||||
}.apply { this.alpha = 0.5f }
|
||||
);
|
||||
}
|
||||
|
||||
if (isEnabled && source.capabilities.hasGetUserPlaylists && source.capabilities.hasGetPlaylist) {
|
||||
val bigButton = BigButton(c, context.getString(R.string.import_playlists), context.getString(R.string.login_required), R.drawable.ic_playlist) {
|
||||
|
||||
}.apply { this.alpha = 0.5f };
|
||||
|
||||
bigButton.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);
|
||||
};
|
||||
|
||||
migrationButtons.add(bigButton);
|
||||
}
|
||||
|
||||
if (migrationButtons.size > 0) {
|
||||
groups.add(BigButtonGroup(c, context.getString(R.string.migration), *migrationButtons.toTypedArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +305,8 @@ class SourceDetailFragment : MainFragment() {
|
||||
if(clientIfExists?.captchaEncrypted != null)
|
||||
BigButton(c, context.getString(R.string.delete_captcha), context.getString(R.string.deletes_stored_captcha_answer_for_this_plugin), R.drawable.ic_block) {
|
||||
clientIfExists.updateCaptcha(null);
|
||||
updateButtons();
|
||||
UIDialogs.toast(context, "Captcha data deleted");
|
||||
}.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);
|
||||
|
||||
+25
-8
@@ -9,17 +9,18 @@ import android.widget.LinearLayout
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
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.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.exceptions.ChannelException
|
||||
import com.futo.platformplayer.exceptions.RateLimitException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.SearchType
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateCache
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
@@ -28,9 +29,11 @@ import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||
import com.futo.platformplayer.views.announcements.AnnouncementView
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.NoResultsView
|
||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.futo.platformplayer.views.subscriptions.SubscriptionBar
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -41,7 +44,6 @@ import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.system.measureTimeMillis
|
||||
import kotlin.time.measureTime
|
||||
|
||||
class SubscriptionsFeedFragment : MainFragment() {
|
||||
override val isMainView : Boolean = true;
|
||||
@@ -321,7 +323,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
withContext(Dispatchers.Main) {
|
||||
val results = cachePager.getResults();
|
||||
Logger.i(TAG, "Subscriptions show cache (${results.size})");
|
||||
setTextCentered(if (results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
|
||||
setEmptyPager(results.isEmpty());
|
||||
setPager(cachePager);
|
||||
}
|
||||
}
|
||||
@@ -331,15 +333,14 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
Logger.i(TAG, "Subscriptions load");
|
||||
if(recyclerData.results.size == 0) {
|
||||
loadCache();
|
||||
} else {
|
||||
setTextCentered(null);
|
||||
}
|
||||
} else
|
||||
setEmptyPager(false);
|
||||
_taskGetPager.run(withRefetch);
|
||||
}
|
||||
|
||||
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
|
||||
super.onRestoreCachedData(cachedData);
|
||||
setTextCentered(if (cachedData.results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
|
||||
setEmptyPager(cachedData.results.isEmpty());
|
||||
}
|
||||
private fun loadedResult(pager: IPager<IPlatformContent>) {
|
||||
Logger.i(TAG, "Subscriptions new pager loaded (${pager.getResults().size})");
|
||||
@@ -349,7 +350,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
finishRefreshLayoutLoader();
|
||||
setLoading(false);
|
||||
setPager(pager);
|
||||
setTextCentered(if (pager.getResults().isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
|
||||
setEmptyPager(pager.getResults().isEmpty());
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to finish loading", e)
|
||||
}
|
||||
@@ -359,6 +360,22 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
}
|
||||
}*/
|
||||
}
|
||||
override fun getEmptyPagerView(): View? {
|
||||
val dp10 = 10.dp(resources);
|
||||
val dp30 = 30.dp(resources);
|
||||
if(StateSubscriptions.instance.getSubscriptions().isEmpty())
|
||||
return NoResultsView(context, "You have no subscriptions", "Subscribe to some creators or import them from elsewhere.", R.drawable.ic_explore, listOf(
|
||||
BigButton(context, "Search", "Search for creators in your enabled plugins", R.drawable.ic_creators) {
|
||||
fragment.navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.CREATOR));
|
||||
}.withMargin(dp10, dp30),
|
||||
BigButton(context, "Import", "Import your subscriptions from another format", R.drawable.ic_move_up) {
|
||||
val activity = StateApp.instance.context;
|
||||
if(activity is MainActivity)
|
||||
UIDialogs.showImportOptionsDialog(activity);
|
||||
}.withMargin(dp10, dp30)
|
||||
));
|
||||
return null;
|
||||
}
|
||||
|
||||
private fun handleExceptions(exs: List<Throwable>) {
|
||||
context?.let {
|
||||
|
||||
+1
@@ -216,6 +216,7 @@ class VideoDetailFragment : MainFragment {
|
||||
}
|
||||
_view!!.setTransitionListener(object : MotionLayout.TransitionListener {
|
||||
override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float) {
|
||||
_viewDetail?.stopAllGestures()
|
||||
|
||||
if (state != State.MINIMIZED && progress < 0.1) {
|
||||
state = State.MINIMIZED;
|
||||
|
||||
+48
-57
@@ -51,6 +51,8 @@ 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.IPlatformVideoDetails
|
||||
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.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
@@ -62,6 +64,7 @@ import com.futo.platformplayer.downloads.VideoLocal
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptAgeException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
@@ -465,8 +468,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
nextVideo();
|
||||
};
|
||||
_player.onDatasourceError.subscribe(::onDataSourceError);
|
||||
_player.onNext.subscribe { nextVideo(true) };
|
||||
_player.onPrevious.subscribe { previousVideo(true) };
|
||||
_player.onNext.subscribe { nextVideo(true, true, true) };
|
||||
_player.onPrevious.subscribe { prevVideo(true) };
|
||||
|
||||
_minimize_controls_play.setOnClickListener { handlePlay(); };
|
||||
_minimize_controls_pause.setOnClickListener { handlePause(); };
|
||||
@@ -535,6 +538,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
if(!_destroyed) {
|
||||
updateQueueState();
|
||||
StatePlayer.instance.updateMediaSession(null);
|
||||
_cast.setLoopVisible(!StatePlayer.instance.hasQueue);
|
||||
}
|
||||
};
|
||||
StatePlayer.instance.onVideoChanging.subscribe(this) {
|
||||
@@ -543,8 +547,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
MediaControlReceiver.onLowerVolumeReceived.subscribe(this) { handleLowerVolume() };
|
||||
MediaControlReceiver.onPlayReceived.subscribe(this) { handlePlay() };
|
||||
MediaControlReceiver.onPauseReceived.subscribe(this) { handlePause() };
|
||||
MediaControlReceiver.onNextReceived.subscribe(this) { nextVideo(true) };
|
||||
MediaControlReceiver.onPreviousReceived.subscribe(this) { previousVideo(true) };
|
||||
MediaControlReceiver.onNextReceived.subscribe(this) { nextVideo(true, true, true) };
|
||||
MediaControlReceiver.onPreviousReceived.subscribe(this) { prevVideo(true) };
|
||||
MediaControlReceiver.onCloseReceived.subscribe(this) {
|
||||
Logger.i(TAG, "MediaControlReceiver.onCloseReceived")
|
||||
onClose.emit()
|
||||
@@ -562,9 +566,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
};
|
||||
|
||||
_upNext.onNextItem.subscribe {
|
||||
val item = StatePlayer.instance.nextQueueItem();
|
||||
if(item != null)
|
||||
setVideoOverview(item, true);
|
||||
nextVideo(true, true, true);
|
||||
};
|
||||
_upNext.onOpenQueueClick.subscribe {
|
||||
_container_content_queue.updateQueue();
|
||||
@@ -683,6 +685,11 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fun stopAllGestures() {
|
||||
_player.stopAllGestures();
|
||||
_cast.stopAllGestures();
|
||||
}
|
||||
|
||||
fun updateMoreButtons() {
|
||||
val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) {
|
||||
(video ?: _searchVideo)?.let {
|
||||
@@ -1324,6 +1331,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
if(video.isLive && video.live == null && !video.video.videoSources.any())
|
||||
startLiveTry(video);
|
||||
|
||||
_player.updateNextPrevious();
|
||||
updateMoreButtons();
|
||||
}
|
||||
fun loadLiveChat(video: IPlatformVideoDetails) {
|
||||
@@ -1534,63 +1542,26 @@ class VideoDetailView : ConstraintLayout {
|
||||
_slideUpOverlay = _overlay_quality_selector;
|
||||
}
|
||||
|
||||
private fun getPreviousVideo(withoutRemoval: Boolean, forceLoop: Boolean = false): IPlatformVideo? {
|
||||
if (!StatePlayer.instance.hasQueue) {
|
||||
if (forceLoop) {
|
||||
return StatePlayer.instance.currentVideo
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
val shouldNotRemove = _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9
|
||||
var previous = StatePlayer.instance.prevQueueItem(withoutRemoval || shouldNotRemove);
|
||||
if(previous == null && forceLoop)
|
||||
previous = StatePlayer.instance.getQueue().last();
|
||||
return previous;
|
||||
fun prevVideo(withoutRemoval: Boolean = false) {
|
||||
Logger.i(TAG, "prevVideo")
|
||||
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
|
||||
if(next != null) {
|
||||
setVideoOverview(next);
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNextVideo(withoutRemoval: Boolean, forceLoop: Boolean = false): IPlatformVideo? {
|
||||
if (!StatePlayer.instance.hasQueue) {
|
||||
if (forceLoop) {
|
||||
return StatePlayer.instance.currentVideo
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
val shouldNotRemove = _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9
|
||||
var next = StatePlayer.instance.nextQueueItem(withoutRemoval || shouldNotRemove);
|
||||
fun nextVideo(forceLoop: Boolean = false, withoutRemoval: Boolean = false, bypassVideoLoop: Boolean = false): Boolean {
|
||||
Logger.i(TAG, "nextVideo")
|
||||
var next = StatePlayer.instance.nextQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9, bypassVideoLoop);
|
||||
if(next == null && forceLoop)
|
||||
next = StatePlayer.instance.restartQueue();
|
||||
return next;
|
||||
}
|
||||
|
||||
fun previousVideo(forceLoop: Boolean = false): Boolean {
|
||||
Logger.i(TAG, "previousVideo")
|
||||
|
||||
val previous = getPreviousVideo(false, forceLoop);
|
||||
if(previous != null) {
|
||||
setVideoOverview(previous);
|
||||
return true;
|
||||
} else {
|
||||
StatePlayer.instance.setCurrentlyPlaying(null);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fun nextVideo(forceLoop: Boolean = false): Boolean {
|
||||
Logger.i(TAG, "nextVideo")
|
||||
|
||||
val next = getNextVideo(false, forceLoop);
|
||||
if(next != null) {
|
||||
setVideoOverview(next);
|
||||
return true;
|
||||
} else {
|
||||
StatePlayer.instance.setCurrentlyPlaying(null);
|
||||
}
|
||||
|
||||
else
|
||||
StatePlayer.instance.setCurrentlyPlaying(null);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1941,6 +1912,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
video?.let { updateQualitySourcesOverlay(it, videoLocal); };
|
||||
|
||||
val changed = _isCasting != isCasting;
|
||||
_isCasting = isCasting;
|
||||
|
||||
if(isCasting) {
|
||||
@@ -1948,8 +1920,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_player.stop();
|
||||
_player.hideControls(false);
|
||||
_cast.visibility = View.VISIBLE;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
StateCasting.instance.stopVideo();
|
||||
_cast.stopTimeJob();
|
||||
_cast.visibility = View.GONE;
|
||||
@@ -1958,6 +1929,10 @@ class VideoDetailView : ConstraintLayout {
|
||||
_player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed());
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
stopAllGestures();
|
||||
}
|
||||
}
|
||||
|
||||
fun setFullscreen(fullscreen : Boolean) {
|
||||
@@ -2317,6 +2292,22 @@ class VideoDetailView : ConstraintLayout {
|
||||
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_NOSOURCES", context.getString(R.string.video_without_source), context.getString(R.string.there_was_a_in_your_queue_videoname_by_authorname_without_the_required_source_being_enabled_playback_was_skipped).replace("{videoName}", video?.name ?: "").replace("{authorName}", video?.author?.name ?: ""), AnnouncementType.SESSION)
|
||||
}
|
||||
}
|
||||
.exception<ScriptLoginRequiredException> {
|
||||
Logger.w(TAG, "exception<ScriptLoginRequiredException>", it);
|
||||
|
||||
UIDialogs.showDialog(context, R.drawable.ic_security, "Authentication", it.message, null, 0,
|
||||
UIDialogs.Action("Cancel", {}),
|
||||
UIDialogs.Action("Login", {
|
||||
val id = it.config?.let { if(it is SourcePluginConfig) it.id else null };
|
||||
val didLogin = if(id == null)
|
||||
false
|
||||
else StatePlugins.instance.loginPlugin(context, id) {
|
||||
fetchVideo();
|
||||
}
|
||||
if(!didLogin)
|
||||
UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, "Failed to login");
|
||||
}));
|
||||
}
|
||||
.exception<ContentNotAvailableYetException> {
|
||||
Logger.w(TAG, "exception<ContentNotAvailableYetException>", it)
|
||||
|
||||
|
||||
@@ -59,23 +59,28 @@ public class PolycentricModelLoader implements ModelLoader<String, ByteBuffer> {
|
||||
|
||||
@Override
|
||||
public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback<? super ByteBuffer> callback) {
|
||||
Log.i("PolycentricModelLoader", this._model);
|
||||
_deferred = PolycentricCache.getInstance().getDataAsync(_model);
|
||||
_deferred.invokeOnCompletion(throwable -> {
|
||||
if (throwable != null) {
|
||||
Log.e("PolycentricModelLoader", "getDataAsync failed throwable: " + throwable.toString());
|
||||
callback.onLoadFailed(new Exception(throwable));
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
|
||||
Deferred<ByteBuffer> deferred = _deferred;
|
||||
if (deferred == null) {
|
||||
Log.e("PolycentricModelLoader", "getDataAsync failed deferred is null");
|
||||
callback.onLoadFailed(new Exception("Deferred is null"));
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
|
||||
ByteBuffer completed = deferred.getCompleted();
|
||||
if (completed != null) {
|
||||
Log.e("PolycentricModelLoader", "getDataAsync success loaded " + completed.remaining() + " bytes");
|
||||
callback.onDataReady(completed);
|
||||
} else {
|
||||
Log.e("PolycentricModelLoader", "getDataAsync failed completed is null");
|
||||
callback.onLoadFailed(new Exception("Completed is null"));
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
|
||||
@@ -17,6 +17,7 @@ import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.nio.ByteBuffer
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class PolycentricCache {
|
||||
data class CachedOwnedClaims(val ownedClaims: List<OwnedClaim>?, val creationTime: OffsetDateTime = OffsetDateTime.now()) {
|
||||
@@ -29,8 +30,15 @@ class PolycentricCache {
|
||||
|
||||
private val _cache = hashMapOf<PlatformID, CachedOwnedClaims>()
|
||||
private val _profileCache = hashMapOf<PublicKey, CachedPolycentricProfile>()
|
||||
private val _profileUrlCache = FragmentedStorage.get<CachedPolycentricProfileStorage>("profileUrlCache")
|
||||
private val _profileUrlCache: CachedPolycentricProfileStorage;
|
||||
private val _scope = CoroutineScope(Dispatchers.IO);
|
||||
init {
|
||||
Logger.i(TAG, "Initializing Polycentric cache");
|
||||
val time = measureTimeMillis {
|
||||
_profileUrlCache = FragmentedStorage.get<CachedPolycentricProfileStorage>("profileUrlCache")
|
||||
}
|
||||
Logger.i(TAG, "Initialized Polycentric cache (${_profileUrlCache.map.size}, ${time}ms)");
|
||||
}
|
||||
|
||||
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope,
|
||||
{ system ->
|
||||
@@ -222,7 +230,7 @@ class PolycentricCache {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getProfileAsync(id: PlatformID): CachedPolycentricProfile? {
|
||||
suspend fun getProfileAsync(id: PlatformID, urlNullCache: String? = null): CachedPolycentricProfile? {
|
||||
if (!StatePolycentric.instance.enabled || id.claimType <= 0) {
|
||||
return CachedPolycentricProfile(null);
|
||||
}
|
||||
@@ -243,6 +251,8 @@ class PolycentricCache {
|
||||
Logger.v(TAG, "getProfileAsync (id: $id) != null (with retrieved valid claims)")
|
||||
return getProfileAsync(claims.ownedClaims.first().system).await()
|
||||
} else {
|
||||
if(urlNullCache != null)
|
||||
_profileUrlCache.setAndSave(urlNullCache, PolycentricCache.CachedPolycentricProfile(null));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,12 +341,12 @@ class MediaPlaybackService : Service() {
|
||||
|
||||
if (Settings.instance.playback.restartPlaybackAfterLoss == 1) {
|
||||
val lossTime_ms = _audioFocusLossTime_ms
|
||||
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 30) {
|
||||
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 10) {
|
||||
MediaControlReceiver.onPlayReceived.emit()
|
||||
}
|
||||
} else if (Settings.instance.playback.restartPlaybackAfterLoss == 2) {
|
||||
val lossTime_ms = _audioFocusLossTime_ms
|
||||
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 10) {
|
||||
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 30) {
|
||||
MediaControlReceiver.onPlayReceived.emit()
|
||||
}
|
||||
} else if (Settings.instance.playback.restartPlaybackAfterLoss == 3) {
|
||||
|
||||
@@ -31,6 +31,7 @@ import com.futo.platformplayer.background.BackgroundWorker
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||
import com.futo.platformplayer.logging.AndroidLogConsumer
|
||||
@@ -751,6 +752,9 @@ class StateApp {
|
||||
})
|
||||
}
|
||||
}
|
||||
fun handleLoginException(client: JSClient, exception: ScriptLoginRequiredException, onSuccess: (client: JSClient)->Unit) {
|
||||
|
||||
}
|
||||
|
||||
fun getLocaleContext(baseContext: Context?): Context? {
|
||||
val locale = getLocaleSetting(baseContext);
|
||||
|
||||
@@ -99,7 +99,7 @@ class StateCache {
|
||||
|
||||
if(existing != null && doUpdate) {
|
||||
_subscriptionCache.update(existing.id!!, serialized);
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
else if(existing == null) {
|
||||
_subscriptionCache.insert(serialized);
|
||||
|
||||
@@ -609,7 +609,7 @@ class StatePlatform {
|
||||
//Video
|
||||
fun hasEnabledVideoClient(url: String) : Boolean = getEnabledClients().any { it.isContentDetailsUrl(url) };
|
||||
fun getContentClient(url: String) : IPlatformClient = getContentClientOrNull(url)
|
||||
?: throw NoPlatformClientException("No client enabled that supports this channel url (${url})");
|
||||
?: throw NoPlatformClientException("No client enabled that supports this content url (${url})");
|
||||
fun getContentClientOrNull(url: String) : IPlatformClient? = getEnabledClients().find { it.isContentDetailsUrl(url) };
|
||||
fun getContentDetails(url: String, forceRefetch: Boolean = false): Deferred<IPlatformContentDetails> {
|
||||
Logger.i(TAG, "Platform - getContentDetails (${url})");
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.futo.platformplayer.states
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
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
|
||||
@@ -55,12 +57,16 @@ class StatePlayer {
|
||||
var queueShuffle: Boolean = false
|
||||
private set;
|
||||
|
||||
val hasQueue: Boolean get() {
|
||||
val queueSize: Int get() {
|
||||
synchronized(_queue) {
|
||||
return _queue.isNotEmpty()
|
||||
return _queue.size
|
||||
}
|
||||
}
|
||||
|
||||
val hasQueue: Boolean get() {
|
||||
return queueSize > 1
|
||||
}
|
||||
|
||||
val queueName: String get() = _queueName ?: _queueType;
|
||||
|
||||
//Events
|
||||
@@ -268,7 +274,12 @@ class StatePlayer {
|
||||
setQueueWithPosition(videos, _queueType, index, withFocus);
|
||||
}
|
||||
fun addToQueue(video: IPlatformVideo) {
|
||||
var didAdd = false;
|
||||
synchronized(_queue) {
|
||||
if(_queue.any { it.url == video.url }) {
|
||||
return@synchronized;
|
||||
}
|
||||
|
||||
if(_queue.isEmpty()) {
|
||||
setQueueType(TYPE_QUEUE);
|
||||
currentVideo?.let {
|
||||
@@ -284,8 +295,19 @@ class StatePlayer {
|
||||
if (_queuePosition < 0) {
|
||||
_queuePosition = 0;
|
||||
}
|
||||
didAdd = true;
|
||||
}
|
||||
onQueueChanged.emit(true);
|
||||
if(didAdd) {
|
||||
onQueueChanged.emit(true);
|
||||
StateApp.instance.contextOrNull?.let { context ->
|
||||
val name = if (video.name.length > 20) (video.name.subSequence(0, 20).toString() + "...") else video.name;
|
||||
UIDialogs.toast(context, context.getString(R.string.queued) + " [$name]", false);
|
||||
}
|
||||
}
|
||||
else
|
||||
StateApp.instance.contextOrNull?.let { context ->
|
||||
UIDialogs.toast(context, context.getString(R.string.already_queued), false);
|
||||
}
|
||||
}
|
||||
fun insertToQueue(video: IPlatformVideo, playNow: Boolean = false) {
|
||||
synchronized(_queue) {
|
||||
@@ -373,10 +395,43 @@ class StatePlayer {
|
||||
return null;
|
||||
}
|
||||
|
||||
fun getNextQueueItem() : IPlatformVideo? {
|
||||
if(loopVideo)
|
||||
return currentVideo;
|
||||
/***
|
||||
* Checks what the prev queue item would without consuming it.
|
||||
* @param forceLoop If start of queue should be ignored and loop around to end without queueRepeat being true
|
||||
*/
|
||||
fun getPrevQueueItem(forceLoop: Boolean = false) : IPlatformVideo? {
|
||||
synchronized(_queue) {
|
||||
if(_queue.size == 1)
|
||||
return null;
|
||||
|
||||
val shuffledQueue = _queueShuffled;
|
||||
val queue = if (queueShuffle && shuffledQueue != null) {
|
||||
shuffledQueue;
|
||||
} else {
|
||||
_queue;
|
||||
}
|
||||
|
||||
//Init Behavior
|
||||
if(_queuePosition == -1 && queue.isNotEmpty())
|
||||
return queue[0];
|
||||
//Standard Behavior
|
||||
if(_queuePosition - 1 >= 0)
|
||||
return queue[_queuePosition - 1];
|
||||
//Repeat Behavior (End of queue)
|
||||
if(_queuePosition - 1 < 0 && queue.isNotEmpty() && (forceLoop || queueRepeat))
|
||||
return queue[_queue.size - 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/***
|
||||
* Checks what the next queue item would without consuming it.
|
||||
* @param forceLoop If end of queue should be ignored and loop around to start without queueRepeat being true
|
||||
*/
|
||||
fun getNextQueueItem(forceLoop: Boolean = false) : IPlatformVideo? {
|
||||
synchronized(_queue) {
|
||||
if(_queue.size == 1)
|
||||
return null;
|
||||
|
||||
val shuffledQueue = _queueShuffled;
|
||||
val queue = if (queueShuffle && shuffledQueue != null) {
|
||||
shuffledQueue;
|
||||
@@ -391,7 +446,7 @@ class StatePlayer {
|
||||
if(_queuePosition + 1 < queue.size)
|
||||
return queue[_queuePosition + 1];
|
||||
//Repeat Behavior (End of queue)
|
||||
if(_queuePosition + 1 == queue.size && queue.isNotEmpty() && queueRepeat)
|
||||
if(_queuePosition + 1 == queue.size && queue.isNotEmpty() && (forceLoop || queueRepeat))
|
||||
return queue[0];
|
||||
}
|
||||
return null;
|
||||
@@ -399,11 +454,17 @@ class StatePlayer {
|
||||
fun restartQueue() : IPlatformVideo? {
|
||||
synchronized(_queue) {
|
||||
_queuePosition = -1;
|
||||
return nextQueueItem();
|
||||
return nextQueueItem(false, true);
|
||||
}
|
||||
};
|
||||
fun nextQueueItem(withoutRemoval: Boolean = false) : IPlatformVideo? {
|
||||
if(loopVideo)
|
||||
|
||||
/***
|
||||
* Triggers the next queue item, removing it depending on the queue type, should ONLY be used if you're directly consuming this item
|
||||
* @param withoutRemoval Prevents the removal behavior of certain playlists, should be true for manual user actions like next
|
||||
* @param bypassVideoLoop Bypasses any single-video-looping behavior, should be true for manual user actions like next
|
||||
*/
|
||||
fun nextQueueItem(withoutRemoval: Boolean = false, bypassVideoLoop: Boolean = false) : IPlatformVideo? {
|
||||
if(loopVideo && !bypassVideoLoop)
|
||||
return currentVideo;
|
||||
synchronized(_queue) {
|
||||
if (_queue.isEmpty())
|
||||
@@ -438,6 +499,10 @@ class StatePlayer {
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* Triggers the prev queue item, removing it depending on the queue type
|
||||
* @param withoutRemoval Prevents the removal behavior of certain playlists, should be true for manual user actions like next
|
||||
*/
|
||||
fun prevQueueItem(withoutRemoval: Boolean = false) : IPlatformVideo? {
|
||||
synchronized(_queue) {
|
||||
if (_queue.size == 0) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.futo.platformplayer.states
|
||||
import android.content.Context
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.LoginActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||
@@ -53,6 +54,25 @@ class StatePlugins {
|
||||
.load();
|
||||
}
|
||||
|
||||
fun loginPlugin(context: Context, id: String, afterLogin: ()->Unit): Boolean {
|
||||
val descriptor = getPlugin(id) ?: return false;
|
||||
val config = descriptor.config;
|
||||
|
||||
if(config.authentication == null)
|
||||
return false;
|
||||
|
||||
LoginActivity.showLogin(context, config) {
|
||||
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||
|
||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||
StatePlatform.instance.reloadClient(context, id);
|
||||
afterLogin.invoke();
|
||||
}
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private fun getResourceIdFromString(resourceName: String, c: Class<*> = R.drawable::class.java): Int? {
|
||||
return try {
|
||||
val idField = c.getDeclaredField(resourceName)
|
||||
|
||||
@@ -169,18 +169,24 @@ class StatePolycentric {
|
||||
}
|
||||
}
|
||||
|
||||
fun getChannelUrls(url: String, channelId: PlatformID? = null, cacheOnly: Boolean = false): List<String> {
|
||||
fun getChannelUrls(url: String, channelId: PlatformID? = null, cacheOnly: Boolean = false, doCacheNull: Boolean = false): List<String> {
|
||||
return getChannelUrlsWithUpdateResult(url, channelId, cacheOnly, doCacheNull).second;
|
||||
}
|
||||
fun getChannelUrlsWithUpdateResult(url: String, channelId: PlatformID? = null, cacheOnly: Boolean = false, doCacheNull: Boolean = false): Pair<Boolean, List<String>> {
|
||||
var didUpdate = false;
|
||||
if (!enabled) {
|
||||
return listOf(url);
|
||||
return Pair(false, listOf(url));
|
||||
}
|
||||
|
||||
var polycentricProfile: PolycentricProfile? = null;
|
||||
try {
|
||||
polycentricProfile = PolycentricCache.instance.getCachedProfile(url)?.profile;
|
||||
if (polycentricProfile == null && channelId != null) {
|
||||
val polycentricCached = PolycentricCache.instance.getCachedProfile(url, cacheOnly)
|
||||
polycentricProfile = polycentricCached?.profile;
|
||||
if (polycentricCached == null && channelId != null) {
|
||||
Logger.i("StateSubscriptions", "Get polycentric profile not cached");
|
||||
if(!cacheOnly)
|
||||
polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(channelId) }?.profile;
|
||||
if(!cacheOnly) {
|
||||
polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(channelId, if(doCacheNull) url else null) }?.profile;
|
||||
didUpdate = true;
|
||||
}
|
||||
} else {
|
||||
Logger.i("StateSubscriptions", "Get polycentric profile cached");
|
||||
}
|
||||
@@ -193,12 +199,12 @@ class StatePolycentric {
|
||||
val urls = polycentricProfile.ownedClaims.groupBy { it.claim.claimType }
|
||||
.mapNotNull { it.value.firstOrNull()?.claim?.resolveChannelUrl() }.toMutableList();
|
||||
if(urls.any { it.equals(url, true) })
|
||||
return urls;
|
||||
return Pair(didUpdate, urls);
|
||||
else
|
||||
return listOf(url) + urls;
|
||||
return Pair(didUpdate, listOf(url) + urls);
|
||||
}
|
||||
else
|
||||
return listOf(url);
|
||||
return Pair(didUpdate, listOf(url));
|
||||
}
|
||||
|
||||
fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? {
|
||||
|
||||
@@ -254,9 +254,18 @@ class StateSubscriptions {
|
||||
}
|
||||
|
||||
val usePolycentric = true;
|
||||
val lock = Object();
|
||||
var polycentricBudget: Int = 10;
|
||||
val subUrls = getSubscriptions().parallelStream().map {
|
||||
if(usePolycentric)
|
||||
Pair(it, StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id));
|
||||
if(usePolycentric) {
|
||||
val result = StatePolycentric.instance.getChannelUrlsWithUpdateResult(it.channel.url, it.channel.id, polycentricBudget <= 0, true);
|
||||
if(result.first) {
|
||||
synchronized(lock) {
|
||||
polycentricBudget--;
|
||||
}
|
||||
}
|
||||
Pair(it, result.second);
|
||||
}
|
||||
else
|
||||
Pair(it, listOf(it.channel.url));
|
||||
}.asSequence()
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.futo.platformplayer.views
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.futo.platformplayer.R
|
||||
|
||||
class NoResultsView: ConstraintLayout {
|
||||
|
||||
val textTitle: TextView;
|
||||
val textCentered: TextView;
|
||||
val icon: ImageView;
|
||||
val containerExtraViews: LinearLayout;
|
||||
|
||||
|
||||
constructor(context: Context, title: String, text: String, iconId: Int, extraViews: List<View>) : super(context) {
|
||||
inflate(context, R.layout.view_no_results, this);
|
||||
textTitle = findViewById(R.id.text_title)
|
||||
textCentered = findViewById(R.id.text_centered);
|
||||
icon = findViewById(R.id.icon);
|
||||
containerExtraViews = findViewById(R.id.container_extra_views);
|
||||
textTitle.text = title;
|
||||
textCentered.text = text;
|
||||
icon.setImageResource(iconId);
|
||||
if(iconId < 0)
|
||||
icon.visibility = GONE;
|
||||
|
||||
for(view in extraViews)
|
||||
containerExtraViews.addView(view);
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -214,11 +214,12 @@ class PreviewPostView : LinearLayout {
|
||||
.load(image)
|
||||
.placeholder(R.drawable.placeholder_video_thumbnail)
|
||||
.listener(object: RequestListener<Drawable> {
|
||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
|
||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>, isFirstResource: Boolean): Boolean {
|
||||
imageImage.visibility = View.GONE;
|
||||
return false;
|
||||
}
|
||||
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
|
||||
|
||||
override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable>?, dataSource: DataSource, isFirstResource: Boolean): Boolean {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
|
||||
@@ -330,11 +330,21 @@ class GestureControlView : LinearLayout {
|
||||
_controlsVisible = false;
|
||||
}
|
||||
|
||||
fun stopAllGestures() {
|
||||
stopAdjustingFullscreenDown()
|
||||
stopAdjustingBrightness()
|
||||
stopAdjustingSound()
|
||||
stopAdjustingFullscreenUp()
|
||||
stopFastForward()
|
||||
stopAutoFastForward()
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
_jobExitFastForward?.cancel();
|
||||
_jobExitFastForward = null;
|
||||
_jobAutoFastForward?.cancel();
|
||||
_jobAutoFastForward = null;
|
||||
stopAllGestures();
|
||||
cancelHideJob();
|
||||
_scope.cancel();
|
||||
}
|
||||
|
||||
@@ -76,6 +76,11 @@ open class BigButton : LinearLayout {
|
||||
_textSecondary.text = attrTextSecondary;
|
||||
}
|
||||
|
||||
fun withMargin(bottom: Int, side: Int = 0): BigButton {
|
||||
setPadding(side, 0, side, bottom)
|
||||
return this;
|
||||
}
|
||||
|
||||
fun setSecondaryText(text: String?) {
|
||||
_textSecondary.text = text
|
||||
}
|
||||
|
||||
@@ -102,6 +102,14 @@ class CastView : ConstraintLayout {
|
||||
_updateTimeJob = null;
|
||||
}
|
||||
|
||||
fun stopAllGestures() {
|
||||
_gestureControlView.stopAllGestures();
|
||||
}
|
||||
|
||||
fun setLoopVisible(visible: Boolean) {
|
||||
_buttonLoop.visibility = if (visible) View.VISIBLE else View.GONE;
|
||||
}
|
||||
|
||||
fun setIsPlaying(isPlaying: Boolean) {
|
||||
_updateTimeJob?.cancel();
|
||||
|
||||
|
||||
@@ -22,7 +22,10 @@ class QueueEditorOverlay : LinearLayout {
|
||||
|
||||
_topbar.onClose.subscribe(this, onClose::emit);
|
||||
_editor.onVideoOrderChanged.subscribe { StatePlayer.instance.setQueueWithExisting(it) }
|
||||
_editor.onVideoRemoved.subscribe { v -> StatePlayer.instance.removeFromQueue(v) }
|
||||
_editor.onVideoRemoved.subscribe { v ->
|
||||
StatePlayer.instance.removeFromQueue(v);
|
||||
_topbar.setInfo(context.getString(R.string.queue), "${StatePlayer.instance.queueSize} " + context.getString(R.string.videos));
|
||||
}
|
||||
_editor.onVideoClicked.subscribe { v -> StatePlayer.instance.setQueuePosition(v) }
|
||||
|
||||
_topbar.setInfo(context.getString(R.string.queue), "");
|
||||
|
||||
@@ -54,10 +54,8 @@ class PillRatingLikesDislikes : LinearLayout {
|
||||
_loaderViewLikes = findViewById(R.id.loader_likes)
|
||||
_loaderViewDislikes = findViewById(R.id.loader_dislikes)
|
||||
|
||||
_iconLikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; };
|
||||
_textLikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; };
|
||||
_iconDislikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; };
|
||||
_textDislikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; };
|
||||
findViewById<LinearLayout>(R.id.layout_like).setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; };
|
||||
findViewById<LinearLayout>(R.id.layout_dislike).setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; };
|
||||
}
|
||||
|
||||
fun setLoading(loading: Boolean) {
|
||||
|
||||
@@ -35,7 +35,10 @@ import com.google.android.exoplayer2.ui.PlayerControlView
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
import com.google.android.exoplayer2.ui.TimeBar
|
||||
import com.google.android.exoplayer2.video.VideoSize
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -300,6 +303,18 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
}
|
||||
|
||||
StatePlayer.instance.onQueueChanged.subscribe(this) {
|
||||
CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main) {
|
||||
setLoopVisible(!StatePlayer.instance.hasQueue)
|
||||
updateNextPrevious();
|
||||
}
|
||||
}
|
||||
StatePlayer.instance.onVideoChanging.subscribe(this) {
|
||||
CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main) {
|
||||
updateNextPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
updateLoopVideoUI();
|
||||
|
||||
if(!isInEditMode) {
|
||||
@@ -307,10 +322,14 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
}
|
||||
|
||||
/*fun updateNextPrevious(hasNext: Boolean, hasPrevious: Boolean) {
|
||||
_buttonNext.visibility = if (hasNext) View.VISIBLE else View.GONE
|
||||
_buttonPrevious.visibility = if (hasPrevious) View.VISIBLE else View.GONE
|
||||
}*/
|
||||
fun updateNextPrevious() {
|
||||
val vidPrev = StatePlayer.instance.getPrevQueueItem(true);
|
||||
val vidNext = StatePlayer.instance.getNextQueueItem(true);
|
||||
_buttonNext.visibility = if (vidNext != null) View.VISIBLE else View.GONE
|
||||
_buttonNext_fullscreen.visibility = if (vidNext != null) View.VISIBLE else View.GONE
|
||||
_buttonPrevious.visibility = if (vidPrev != null) View.VISIBLE else View.GONE
|
||||
_buttonPrevious_fullscreen.visibility = if (vidPrev != null) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private val _currentChapterUpdateInterval: Long = 1000L / Settings.instance.playback.getChapterUpdateFrames();
|
||||
private var _currentChapterUpdateLastPos = 0L;
|
||||
@@ -343,6 +362,15 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
_currentChapterLoopActive = false;
|
||||
}
|
||||
|
||||
fun setLoopVisible(visible: Boolean) {
|
||||
_control_loop.visibility = if (visible) View.VISIBLE else View.GONE;
|
||||
_control_loop_fullscreen.visibility = if (visible) View.VISIBLE else View.GONE;
|
||||
}
|
||||
|
||||
fun stopAllGestures() {
|
||||
gestureControl.stopAllGestures();
|
||||
}
|
||||
|
||||
fun attachPlayer() {
|
||||
exoPlayer?.attach(_videoView, PLAYER_STATE_NAME);
|
||||
}
|
||||
@@ -369,11 +397,11 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
fun setArtwork(drawable: Drawable?) {
|
||||
if (drawable != null) {
|
||||
_videoView.defaultArtwork = drawable;
|
||||
_videoView.useArtwork = true;
|
||||
_videoView.artworkDisplayMode = StyledPlayerView.ARTWORK_DISPLAY_MODE_FILL;
|
||||
fitOrFill(isFullScreen);
|
||||
} else {
|
||||
_videoView.defaultArtwork = null;
|
||||
_videoView.useArtwork = false;
|
||||
_videoView.artworkDisplayMode = StyledPlayerView.ARTWORK_DISPLAY_MODE_OFF;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
|
||||
Logger.v(TAG, "Attached onConnectionAvailable listener.");
|
||||
StateApp.instance.onConnectionAvailable.subscribe(_referenceObject) {
|
||||
Logger.v(TAG, "onConnectionAvailable");
|
||||
Logger.v(TAG, "onConnectionAvailable connectivityLossTime = $_connectivityLossTime_ms, position = $position, duration = $duration");
|
||||
|
||||
val pos = position;
|
||||
val dur = duration;
|
||||
@@ -158,25 +158,37 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
if (_shouldPlaybackRestartOnConnectivity && abs(pos - dur) > 2000) {
|
||||
if (Settings.instance.playback.restartPlaybackAfterConnectivityLoss == 1) {
|
||||
val lossTime_ms = _connectivityLossTime_ms
|
||||
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 30) {
|
||||
shouldRestartPlayback = true
|
||||
if (lossTime_ms != null) {
|
||||
val lossDuration_ms = System.currentTimeMillis() - lossTime_ms
|
||||
Logger.v(TAG, "onConnectionAvailable lossDuration=$lossDuration_ms")
|
||||
if (lossDuration_ms < 1000 * 10) {
|
||||
shouldRestartPlayback = true
|
||||
}
|
||||
}
|
||||
} else if (Settings.instance.playback.restartPlaybackAfterConnectivityLoss == 2) {
|
||||
val lossTime_ms = _connectivityLossTime_ms
|
||||
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 10) {
|
||||
shouldRestartPlayback = true
|
||||
if (lossTime_ms != null) {
|
||||
val lossDuration_ms = System.currentTimeMillis() - lossTime_ms
|
||||
Logger.v(TAG, "onConnectionAvailable lossDuration=$lossDuration_ms")
|
||||
if (lossDuration_ms < 1000 * 30) {
|
||||
shouldRestartPlayback = true
|
||||
}
|
||||
}
|
||||
} else if (Settings.instance.playback.restartPlaybackAfterConnectivityLoss == 3) {
|
||||
shouldRestartPlayback = true
|
||||
}
|
||||
}
|
||||
|
||||
Logger.v(TAG, "onConnectionAvailable shouldRestartPlayback = $shouldRestartPlayback");
|
||||
|
||||
if (shouldRestartPlayback) {
|
||||
Logger.i(TAG, "Playback ended due to connection loss, resuming playback since connection is restored.");
|
||||
exoPlayer?.player?.playWhenReady = true;
|
||||
exoPlayer?.player?.prepare();
|
||||
exoPlayer?.player?.play();
|
||||
}
|
||||
|
||||
_connectivityLossTime_ms = null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -524,6 +536,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
}
|
||||
|
||||
protected open fun onPlayerError(error: PlaybackException) {
|
||||
Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss");
|
||||
|
||||
when (error.errorCode) {
|
||||
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
|
||||
onDatasourceError.emit(error);
|
||||
@@ -536,9 +550,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
//PlaybackException.ERROR_CODE_IO_NO_PERMISSION,
|
||||
//PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
|
||||
PlaybackException.ERROR_CODE_IO_UNSPECIFIED -> {
|
||||
Logger.i(TAG, "IO error, set _shouldPlaybackRestartOnConnectivity=true");
|
||||
_shouldPlaybackRestartOnConnectivity = true;
|
||||
_connectivityLossTime_ms = System.currentTimeMillis()
|
||||
Logger.i(TAG, "IO error, set _shouldPlaybackRestartOnConnectivity=true _connectivityLossTime_ms=$_connectivityLossTime_ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#142B66" />
|
||||
<corners android:radius="4dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,840Q405.46,840 339.77,811.58Q274.08,783.15 225.46,734.54Q176.85,685.92 148.42,620.23Q120,554.54 120,480Q120,405.46 148.42,339.77Q176.85,274.08 225.46,225.46Q274.08,176.85 339.77,148.42Q405.46,120 480,120Q619.85,120 721.27,212.27Q822.69,304.54 837.39,440.23L796.92,440.23Q786.39,353.38 734.19,284.35Q682,215.31 600,182.46L600,200Q600,233 576.5,256.5Q553,280 520,280L440,280L440,360Q440,377 428.5,388.5Q417,400 400,400L320,400L320,480L393.85,480L393.85,600L360,600L168,408Q165,426 162.5,444Q160,462 160,480Q160,611 252,705Q344,799 480,800L480,840ZM840.92,829.23L702.92,692.77Q685,705.54 664.08,712.77Q643.15,720 620,720Q561.15,720 520.58,679.42Q480,638.85 480,580Q480,521.15 520.58,480.58Q561.15,440 620,440Q678.85,440 719.42,480.58Q760,521.15 760,580Q760,603.92 752.39,625.23Q744.77,646.54 731.23,664.46L869.23,800.92L840.92,829.23ZM620,680Q662,680 691,651Q720,622 720,580Q720,538 691,509Q662,480 620,480Q578,480 549,509Q520,538 520,580Q520,622 549,651Q578,680 620,680Z"/>
|
||||
</vector>
|
||||
@@ -71,6 +71,16 @@
|
||||
android:paddingTop="10dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="10dp">
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_import_platform"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleY="0.9"
|
||||
android:scaleX="0.9"
|
||||
app:buttonIcon="@drawable/ic_sources_filled"
|
||||
app:buttonText="Import from Platform"
|
||||
app:buttonBackground="@drawable/background_big_button_black"
|
||||
app:buttonSubText="Import your data from a specific source" />
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_import_zip"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -106,6 +106,11 @@
|
||||
android:gravity="center"
|
||||
android:textColor="@color/gray_ac"
|
||||
android:textSize="12dp" />
|
||||
<FrameLayout
|
||||
android:visibility="gone"
|
||||
android:id="@+id/empty_pager_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</FrameLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
|
||||
@@ -39,7 +39,16 @@
|
||||
<Space android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
<TextView
|
||||
android:id="@+id/text_load_progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textSize="15dp"
|
||||
android:text=""
|
||||
android:textColor="#58595B" />
|
||||
|
||||
<!--
|
||||
<TextView
|
||||
android:id="@+id/text_load_more"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -47,7 +56,7 @@
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textSize="15dp"
|
||||
android:text="@string/load_more"
|
||||
android:textColor="@color/colorPrimary" />
|
||||
android:textColor="@color/colorPrimary" /> -->
|
||||
|
||||
<Space android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -70,10 +70,11 @@
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintLeft_toLeftOf="@id/text_body"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_body"
|
||||
android:layout_marginLeft="-10dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
@@ -83,7 +84,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginStart="10dp" />
|
||||
android:layout_marginStart="9dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_rating"
|
||||
|
||||
@@ -4,56 +4,78 @@
|
||||
android:layout_height="32dp"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="7dp"
|
||||
android:paddingLeft="7dp"
|
||||
android:paddingRight="12dp"
|
||||
android:background="@drawable/background_pill"
|
||||
android:gravity="center_vertical">
|
||||
<ImageView
|
||||
android:id="@+id/pill_like_icon"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="18dp"
|
||||
app:srcCompat="@drawable/ic_thumb_up" />
|
||||
<TextView
|
||||
android:id="@+id/pill_likes"
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_like"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="13dp"
|
||||
android:gravity="center_vertical"
|
||||
tools:text="500K" />
|
||||
<com.futo.platformplayer.views.LoaderView
|
||||
android:id="@+id/loader_likes"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
app:isWhite="true" />
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="7dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="7dp"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pill_like_icon"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="18dp"
|
||||
app:srcCompat="@drawable/ic_thumb_up" />
|
||||
<TextView
|
||||
android:id="@+id/pill_likes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="13dp"
|
||||
android:gravity="center_vertical"
|
||||
tools:text="50K" />
|
||||
|
||||
<com.futo.platformplayer.views.LoaderView
|
||||
android:id="@+id/loader_likes"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
app:isWhite="true" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/pill_seperator"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginBottom="7dp"
|
||||
|
||||
android:background="#808080"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pill_dislike_icon"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginTop="2dp"
|
||||
app:srcCompat="@drawable/ic_thumb_down" />
|
||||
<TextView
|
||||
android:id="@+id/pill_dislikes"
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_dislike"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textColor="@color/white"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="13dp"
|
||||
tools:text="500K" />
|
||||
<com.futo.platformplayer.views.LoaderView
|
||||
android:id="@+id/loader_dislikes"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
app:isWhite="true" />
|
||||
android:orientation="horizontal"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="7dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pill_dislike_icon"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginTop="2dp"
|
||||
app:srcCompat="@drawable/ic_thumb_down" />
|
||||
<TextView
|
||||
android:id="@+id/pill_dislikes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textColor="@color/white"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="13dp"
|
||||
tools:text="50K" />
|
||||
<com.futo.platformplayer.views.LoaderView
|
||||
android:id="@+id/loader_dislikes"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
app:isWhite="true" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:scaleType="fitCenter" />
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:fontFamily="@font/inter_bold"
|
||||
android:text="No results"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="22dp" />
|
||||
<TextView
|
||||
android:id="@+id/text_centered"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="No results were found for this page"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/gray_ac"
|
||||
android:textSize="13dp" />
|
||||
<LinearLayout
|
||||
android:id="@+id/container_extra_views"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_margin="10dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -537,6 +537,7 @@
|
||||
<string name="play_feed_as_queue">Play Feed as Queue</string>
|
||||
<string name="play_entire_feed">Play entire feed</string>
|
||||
<string name="queued">Queued</string>
|
||||
<string name="already_queued">Already queued</string>
|
||||
<string name="used">Used</string>
|
||||
<string name="available">Available</string>
|
||||
<string name="failed_to_load_next_page">Failed to load next page</string>
|
||||
@@ -576,6 +577,7 @@
|
||||
<string name="import_your_subscriptions_from_this_source">Import your subscriptions from this source</string>
|
||||
<string name="import_your_playlists_from_this_source">Import your playlists from this source</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="login_required">Login Required</string>
|
||||
<string name="sign_into_the_platform_of_this_source">Sign into the platform of this source</string>
|
||||
<string name="management">Management</string>
|
||||
<string name="uninstall">Uninstall</string>
|
||||
|
||||
Submodule app/src/stable/assets/sources/nebula updated: 1b08b18c74...863d0be132
Submodule app/src/stable/assets/sources/odysee updated: a05feced80...a21ad56829
Submodule app/src/stable/assets/sources/youtube updated: 07aa5a9aab...fc5d17e190
Submodule app/src/unstable/assets/sources/nebula updated: 1b08b18c74...863d0be132
Submodule app/src/unstable/assets/sources/odysee updated: a05feced80...a21ad56829
Submodule app/src/unstable/assets/sources/youtube updated: 07aa5a9aab...fc5d17e190
+6
-5
@@ -1,7 +1,8 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version '7.4.2' apply false
|
||||
id 'com.android.library' version '7.4.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
|
||||
id 'com.google.protobuf' version '0.9.3' apply false
|
||||
}
|
||||
id 'com.android.application' version '8.2.0' apply false
|
||||
id 'com.android.library' version '8.2.0' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
|
||||
id 'com.google.protobuf' version '0.9.4' apply false
|
||||
id 'com.google.devtools.ksp' version '1.9.0-1.0.13' apply false
|
||||
}
|
||||
|
||||
+1
-1
Submodule dep/futopay updated: 68c9ae36fd...4268917697
+1
-1
Submodule dep/polycentricandroid updated: 62328514ec...86cd96c41f
+1
-1
@@ -21,4 +21,4 @@ kotlin.code.style=official
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
android.nonFinalResIds=false
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#Fri Nov 11 13:25:09 CET 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
+2
-2
@@ -16,13 +16,13 @@ dependencyResolutionManagement {
|
||||
|
||||
includeBuild('dep/polycentricandroid') {
|
||||
dependencySubstitution {
|
||||
substitute module('com.polycentric.core:app') with project(':app')
|
||||
substitute module('com.polycentric.core:app') using project(':app')
|
||||
}
|
||||
}
|
||||
|
||||
includeBuild('dep/futopay/android') {
|
||||
dependencySubstitution {
|
||||
substitute module('com.futo.futopay:app') with project(':app')
|
||||
substitute module('com.futo.futopay:app') using project(':app')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user