Compare commits

...

34 Commits

Author SHA1 Message Date
Koen 8b20b4909f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-08 14:43:33 +01:00
Koen 71a3828fe4 Migration to new deps. 2023-12-08 14:43:24 +01:00
Kelvin d713f2bd55 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-08 13:24:06 +01:00
Kelvin 069a615193 Polycentric persistent cache fixes for subscriptions 2023-12-08 13:23:58 +01:00
Koen f7d2cb4055 Updated Odysee. 2023-12-08 12:03:24 +01:00
Koen f109d82537 Fixed clickable area of likes/dislikes. 2023-12-08 12:02:17 +01:00
Kelvin ab49d4749b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-08 11:45:27 +01:00
Kelvin 507eed4f53 fix error message 2023-12-08 11:45:24 +01:00
Kelvin 23ca4addf9 Prevent dup queue items, handle toast more centrally 2023-12-08 11:40:06 +01:00
Koen 331ed09775 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-08 11:27:51 +01:00
Koen 85303b54bc Fixed bug in audio focus loss timers using the wrong time. 2023-12-08 11:27:42 +01:00
Kelvin f224cd1ca5 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-08 11:26:07 +01:00
Kelvin d433d6e774 Elaboration on prev/next queue behavior 2023-12-08 11:25:35 +01:00
Koen 90de54ac5c Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-08 11:24:25 +01:00
Koen 5ff8f1ba6d Connectivity loss fixes. 2023-12-08 11:24:17 +01:00
Kelvin bc00b12b8c Hide prev/next for single item queue, Fullscreen next/prev button show/hide, Bypass loop for next controls 2023-12-08 11:17:36 +01:00
Kelvin 1c0cfa89a3 Fixing Queue and hiding next/prev buttons 2023-12-08 10:44:20 +01:00
Kelvin efa1361fbe Remove (0/0) import, captcha delete update buttons 2023-12-07 22:04:24 +01:00
Kelvin 73918a8d76 refs 2023-12-07 20:18:39 +01:00
Kelvin a3c8bbb21f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-07 20:17:49 +01:00
Kelvin 53525cb365 Improved import flow, Empty pager view support, No subscriptions result view, LoginRequiredException support 2023-12-07 20:17:35 +01:00
Koen e4d39cbec4 Added stop all gestures flow. 2023-12-07 17:16:25 +01:00
Koen a15e4beafb Updated youtube ref for stable. 2023-12-07 17:02:19 +01:00
Koen d47298102e Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-07 17:00:57 +01:00
Koen 280feea06e Default language fixes. 2023-12-07 17:00:47 +01:00
Kelvin f649d62e38 Logging and refs 2023-12-07 16:04:22 +01:00
Kelvin 0ae05e7cd4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-06 19:46:14 +01:00
Kelvin b284176072 Loop support, Improve add to queue behavior, home retry, fix history search pager, defaults progressbar 2023-12-06 19:46:09 +01:00
Koen 5fffaf2f4e Added next/previous skip buttons. 2023-12-06 19:40:05 +01:00
Koen 58da91eae8 Made history properly reload. 2023-12-06 16:47:22 +01:00
Koen 98d92d3fe2 Updated HistoryView to use pager. 2023-12-06 16:32:17 +01:00
Koen c5d35b27f0 Cleanup on store PR. 2023-12-06 13:41:07 +01:00
Koen aee5b75c2f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-06 10:34:32 +01:00
Koen fe02197bd8 Fixes to Polycentric flows. 2023-12-06 10:34:20 +01:00
83 changed files with 1330 additions and 563 deletions
+43 -36
View File
@@ -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'
}
+5
View File
@@ -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 }));
@@ -181,7 +181,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = false;
var progressBar: Boolean = true;
@FormField(R.string.clear_hidden, FieldForm.BUTTON, R.string.clear_hidden_description, 8)
@@ -212,7 +212,7 @@ class Settings : FragmentedStorageFileJson() {
var previewFeedItems: Boolean = true;
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = false;
var progressBar: Boolean = true;
fun getSearchFeedStyle(): FeedStyle {
@@ -230,7 +230,7 @@ class Settings : FragmentedStorageFileJson() {
class ChannelSettings {
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = false;
var progressBar: Boolean = true;
}
@FormField(R.string.subscriptions, "group", R.string.configure_how_your_subscriptions_works_and_feels, 4)
@@ -252,7 +252,7 @@ class Settings : FragmentedStorageFileJson() {
var previewFeedItems: Boolean = true;
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = false;
var progressBar: Boolean = true;
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 7)
@Serializable(with = FlexibleBooleanSerializer::class)
@@ -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
)
@@ -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);
@@ -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"));
}
}
}
@@ -101,10 +101,6 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
return@TaskHandler it.getResults();
}).success {
setLoading(false);
if (it.isEmpty()) {
return@success;
}
val posBefore = _results.size;
val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
_results.addAll(toAdd);
@@ -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) {
@@ -54,6 +54,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import okhttp3.internal.platform.Platform
@Serializable
data class PolycentricProfile(val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>);
@@ -206,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 ->
@@ -298,7 +297,7 @@ class ChannelFragment : MainFragment() {
Glide.with(_imageBanner)
.clear(_imageBanner);
_taskLoadPolycentricProfile.run(parameter.id);
loadPolycentricProfile(parameter.id, parameter.url)
};
_url = parameter.url;
@@ -311,7 +310,7 @@ class ChannelFragment : MainFragment() {
Glide.with(_imageBanner)
.clear(_imageBanner);
_taskLoadPolycentricProfile.run(parameter.channel.id);
loadPolycentricProfile(parameter.channel.id, parameter.channel.url)
};
_url = parameter.channel.url;
@@ -327,6 +326,18 @@ class ChannelFragment : MainFragment() {
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex));
}
private fun loadPolycentricProfile(id: PlatformID, url: String) {
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = true)
if (cachedPolycentricProfile.expired) {
_taskLoadPolycentricProfile.run(id);
}
} else {
_taskLoadPolycentricProfile.run(id);
}
}
private fun setLoading(isLoading: Boolean) {
if (_isLoading == isLoading) {
return;
@@ -448,8 +459,6 @@ class ChannelFragment : MainFragment() {
}
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
Log.i(TAG, "setPolycentricProfile(cachedPolycentricProfile = $cachedPolycentricProfile, animate = $animate)")
val dp_35 = 35.dp(resources)
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
@@ -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) {
@@ -168,7 +166,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
protected open fun onContentClicked(content: IPlatformContent, time: Long) {
if(content is IPlatformVideo) {
if (StatePlayer.instance.hasQueue) {
StatePlayer.instance.addToQueue(content)
StatePlayer.instance.insertToQueue(content, true);
} else {
if (Settings.instance.playback.shouldResumePreview(time))
fragment.navigate<VideoDetailFragment>(content.withTimestamp(time)).maximizeVideoDetail();
@@ -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;
@@ -132,10 +134,6 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
}).success {
setLoading(false);
if (it.isEmpty()) {
return@success;
}
val posBefore = recyclerData.results.size;
val filteredResults = filterResults(it);
recyclerData.results.addAll(filteredResults);
@@ -203,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
@@ -424,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()) {
@@ -433,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;
}
}
@@ -1,5 +1,6 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
@@ -8,88 +9,285 @@ import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.structures.IAsyncPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.PlatformContentPager
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.views.others.TagsView
import com.futo.platformplayer.views.adapters.HistoryListAdapter
import com.futo.platformplayer.views.adapters.HistoryListViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class HistoryFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
private var _adapter: HistoryListAdapter? = null;
private var _view: HistoryView? = null;
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_history, container, false);
val inputMethodManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
val recyclerHistory = view.findViewById<RecyclerView>(R.id.recycler_history);
val clearSearch = view.findViewById<ImageButton>(R.id.button_clear_search);
val editSearch = view.findViewById<EditText>(R.id.edit_search);
var tagsView = view.findViewById<TagsView>(R.id.tags_text);
tagsView.setPairs(listOf(
Pair(getString(R.string.last_hour), 60L),
Pair(getString(R.string.last_24_hours), 24L * 60L),
Pair(getString(R.string.last_week), 7L * 24L * 60L),
Pair(getString(R.string.last_30_days), 30L * 24L * 60L),
Pair(getString(R.string.last_year), 365L * 30L * 24L * 60L),
Pair(getString(R.string.all_time), -1L)));
val adapter = HistoryListAdapter();
adapter.onClick.subscribe { v ->
val diff = v.video.duration - v.position;
val vid: Any = if (diff > 5) { v.video.withTimestamp(v.position) } else { v.video };
StatePlayer.instance.clearQueue();
navigate<VideoDetailFragment>(vid).maximizeVideoDetail();
editSearch.clearFocus();
inputMethodManager.hideSoftInputFromWindow(editSearch.windowToken, 0);
};
_adapter = adapter;
recyclerHistory.adapter = adapter;
recyclerHistory.isSaveEnabled = false;
recyclerHistory.layoutManager = LinearLayoutManager(context);
tagsView.onClick.subscribe { timeMinutesToErase ->
UIDialogs.showConfirmationDialog(requireContext(), getString(R.string.are_you_sure_delete_historical), {
StateHistory.instance.removeHistoryRange(timeMinutesToErase.second as Long);
UIDialogs.toast(view.context, timeMinutesToErase.first + " " + getString(R.string.removed));
adapter.updateFilteredVideos();
adapter.notifyDataSetChanged();
});
};
clearSearch.setOnClickListener {
editSearch.text.clear();
clearSearch.visibility = View.GONE;
adapter.setQuery("");
editSearch.clearFocus();
inputMethodManager.hideSoftInputFromWindow(editSearch.windowToken, 0);
};
editSearch.addTextChangedListener { _ ->
val text = editSearch.text;
clearSearch.visibility = if (text.isEmpty()) { View.GONE } else { View.VISIBLE };
adapter.setQuery(text.toString());
};
val view = HistoryView(this, inflater);
_view = view;
return view;
}
override fun onDestroyMainView() {
super.onDestroyMainView();
_adapter?.cleanup();
_adapter = null;
_view = null;
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack)
_view?.setPager(StateHistory.instance.getHistoryPager());
}
@SuppressLint("ViewConstructor")
class HistoryView : LinearLayout {
private val _fragment: HistoryFragment;
private val _adapter: InsertedViewAdapterWithLoader<HistoryListViewHolder>;
private val _recyclerHistory: RecyclerView;
private val _clearSearch: ImageButton;
private val _editSearch: EditText;
private val _tagsView: TagsView;
private val _llmHistory: LinearLayoutManager;
private val _pagerLock = Object();
private var _nextPageHandler: TaskHandler<IPager<HistoryVideo>, List<HistoryVideo>>;
private var _pager: IPager<HistoryVideo>? = null;
private val _results = arrayListOf<HistoryVideo>();
private var _loading = false;
private var _automaticNextPageCounter = 0;
constructor(fragment: HistoryFragment, inflater: LayoutInflater) : super(inflater.context) {
_fragment = fragment;
inflater.inflate(R.layout.fragment_history, this);
_recyclerHistory = findViewById(R.id.recycler_history);
_clearSearch = findViewById(R.id.button_clear_search);
_editSearch = findViewById(R.id.edit_search);
_tagsView = findViewById(R.id.tags_text);
_tagsView.setPairs(listOf(
Pair(context.getString(R.string.last_hour), 60L),
Pair(context.getString(R.string.last_24_hours), 24L * 60L),
Pair(context.getString(R.string.last_week), 7L * 24L * 60L),
Pair(context.getString(R.string.last_30_days), 30L * 24L * 60L),
Pair(context.getString(R.string.last_year), 365L * 30L * 24L * 60L),
Pair(context.getString(R.string.all_time), -1L)
));
_adapter = InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
{ _results.size },
{ view, _ ->
val holder = HistoryListViewHolder(view);
holder.onRemove.subscribe(::onHistoryVideoRemove);
holder.onClick.subscribe(::onHistoryVideoClick);
return@InsertedViewAdapterWithLoader holder;
},
{ viewHolder, position ->
var watchTime: String? = null;
if (position == 0) {
watchTime = _results[position].date.toHumanNowDiffStringMinDay();
} else {
val previousWatchTime = _results[position - 1].date.toHumanNowDiffStringMinDay();
val currentWatchTime = _results[position].date.toHumanNowDiffStringMinDay();
if (previousWatchTime != currentWatchTime) {
watchTime = currentWatchTime;
}
}
viewHolder.bind(_results[position], watchTime);
}
);
_recyclerHistory.adapter = _adapter;
_recyclerHistory.isSaveEnabled = false;
_llmHistory = LinearLayoutManager(context);
_recyclerHistory.layoutManager = _llmHistory;
_tagsView.onClick.subscribe { timeMinutesToErase ->
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_delete_historical), {
StateHistory.instance.removeHistoryRange(timeMinutesToErase.second as Long);
UIDialogs.toast(context, timeMinutesToErase.first + " " + context.getString(R.string.removed));
updatePager();
});
};
_clearSearch.setOnClickListener {
val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
_editSearch.text.clear();
_clearSearch.visibility = View.GONE;
setPager(StateHistory.instance.getHistoryPager());
_editSearch.clearFocus();
inputMethodManager.hideSoftInputFromWindow(_editSearch.windowToken, 0);
};
_editSearch.addTextChangedListener { _ ->
val text = _editSearch.text;
_clearSearch.visibility = if (text.isEmpty()) { View.GONE } else { View.VISIBLE };
updatePager();
};
_recyclerHistory.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy);
val visibleItemCount = _recyclerHistory.childCount;
val firstVisibleItem = _llmHistory.findFirstVisibleItemPosition();
Logger.i(TAG, "onScrolled _loading = $_loading, firstVisibleItem = $firstVisibleItem, visibleItemCount = $visibleItemCount, _results.size = ${_results.size}")
val visibleThreshold = 15;
if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= _results.size && firstVisibleItem > 0) {
loadNextPage();
}
}
});
_nextPageHandler = TaskHandler<IPager<HistoryVideo>, List<HistoryVideo>>({fragment.lifecycleScope}, {
if (it is IAsyncPager<*>)
it.nextPageAsync();
else
it.nextPage();
return@TaskHandler it.getResults();
}).success {
setLoading(false);
val posBefore = _results.size;
_results.addAll(it);
_adapter.notifyItemRangeInserted(_adapter.childToParentPosition(posBefore), it.size);
ensureEnoughContentVisible(it)
}.exception<Throwable> {
Logger.w(TAG, "Failed to load next page.", it);
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
loadNextPage();
});
};
}
private fun updatePager() {
val query = _editSearch.text.toString();
if (_editSearch.text.isNotEmpty()) {
setPager(StateHistory.instance.getHistorySearchPager(query));
//setPager(StateHistory.instance.getHistorySearchPager(query));
} else {
setPager(StateHistory.instance.getHistoryPager());
}
}
fun setPager(pager: IPager<HistoryVideo>) {
Logger.i(TAG, "setPager()");
synchronized(_pagerLock) {
loadPagerInternal(pager);
}
}
private fun onHistoryVideoRemove(v: HistoryVideo) {
val index = _results.indexOf(v);
if (index == -1) {
return;
}
StateHistory.instance.removeHistory(v.video.url);
_results.removeAt(index);
_adapter.notifyItemRemoved(index);
}
private fun onHistoryVideoClick(v: HistoryVideo) {
val index = _results.indexOf(v);
if (index == -1) {
return;
}
val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
val diff = v.video.duration - v.position;
val vid: Any = if (diff > 5) { v.video.withTimestamp(v.position) } else { v.video };
StatePlayer.instance.clearQueue();
_fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail();
_editSearch.clearFocus();
inputMethodManager.hideSoftInputFromWindow(_editSearch.windowToken, 0);
_fragment.lifecycleScope.launch(Dispatchers.Main) {
delay(2000)
updatePager()
}
}
private fun loadNextPage() {
synchronized(_pagerLock) {
val pager: IPager<HistoryVideo> = _pager ?: return;
val hasMorePages = pager.hasMorePages();
Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages");
if (pager.hasMorePages()) {
setLoading(true);
_nextPageHandler.run(pager);
}
}
}
private fun setLoading(loading: Boolean) {
Logger.v(TAG, "setLoading loading=${loading}");
_loading = loading;
_adapter.setLoading(loading);
}
private fun loadPagerInternal(pager: IPager<HistoryVideo>) {
Logger.i(TAG, "Setting new internal pager on feed");
_results.clear();
val toAdd = pager.getResults();
_results.addAll(toAdd);
_adapter.notifyDataSetChanged();
ensureEnoughContentVisible(toAdd)
_pager = pager;
}
private fun ensureEnoughContentVisible(results: List<HistoryVideo>) {
val canScroll = if (_results.isEmpty()) false else {
val layoutManager = _llmHistory
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
if (firstVisibleItemPosition != RecyclerView.NO_POSITION) {
val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition)
val itemHeight = firstVisibleView?.height ?: 0
val occupiedSpace = _results.size * itemHeight
val recyclerViewHeight = _recyclerHistory.height
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage occupiedSpace=$occupiedSpace recyclerViewHeight=$recyclerViewHeight")
occupiedSpace >= recyclerViewHeight
} else {
false
}
}
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter")
if (!canScroll || results.isEmpty()) {
_automaticNextPageCounter++
if(_automaticNextPageCounter <= 4)
loadNextPage()
} else {
_automaticNextPageCounter = 0;
}
}
}
companion object {
fun newInstance() = HistoryFragment().apply {}
private const val TAG = "HistoryFragment"
}
}
@@ -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 {}
}
}
@@ -596,9 +596,12 @@ class PostDetailFragment : MainFragment {
private fun fetchPolycentricProfile() {
val author = _post?.author ?: _postOverview?.author ?: return;
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(author.url);
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(author.url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = false);
if (cachedPolycentricProfile.expired) {
_taskLoadPolycentricProfile.run(author.id);
}
} else {
setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(author.id);
@@ -280,6 +280,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);
@@ -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 {
@@ -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;
@@ -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,6 +468,8 @@ class VideoDetailView : ConstraintLayout {
nextVideo();
};
_player.onDatasourceError.subscribe(::onDataSourceError);
_player.onNext.subscribe { nextVideo(true, true, true) };
_player.onPrevious.subscribe { prevVideo(true) };
_minimize_controls_play.setOnClickListener { handlePlay(); };
_minimize_controls_pause.setOnClickListener { handlePause(); };
@@ -541,8 +546,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) { prevVideo() };
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()
@@ -681,6 +686,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 {
@@ -1003,14 +1013,17 @@ class VideoDetailView : ConstraintLayout {
_descriptionContainer.visibility = View.GONE;
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
_channelName.text = video.author.name;
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url);
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = false);
if (cachedPolycentricProfile.expired) {
_taskLoadPolycentricProfile.run(video.author.id);
}
} else {
setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(video.author.id);
_channelName.text = video.author.name;
}
_player.clear();
@@ -1157,7 +1170,7 @@ class VideoDetailView : ConstraintLayout {
setDescription(video.description.fixHtmlLinks());
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url);
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = false);
} else {
@@ -1319,6 +1332,7 @@ class VideoDetailView : ConstraintLayout {
if(video.isLive && video.live == null && !video.video.videoSources.any())
startLiveTry(video);
_player.updateNextPrevious();
updateMoreButtons();
}
fun loadLiveChat(video: IPlatformVideoDetails) {
@@ -1529,17 +1543,18 @@ class VideoDetailView : ConstraintLayout {
_slideUpOverlay = _overlay_quality_selector;
}
fun prevVideo() {
fun prevVideo(withoutRemoval: Boolean = false) {
Logger.i(TAG, "prevVideo")
val next = StatePlayer.instance.prevQueueItem(_player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
if(next != null) {
setVideoOverview(next);
}
}
fun nextVideo(forceLoop: Boolean = false): Boolean {
fun nextVideo(forceLoop: Boolean = false, withoutRemoval: Boolean = false, bypassVideoLoop: Boolean = false): Boolean {
Logger.i(TAG, "nextVideo")
var next = StatePlayer.instance.nextQueueItem(_player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
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();
if(next != null) {
@@ -1687,6 +1702,10 @@ class VideoDetailView : ConstraintLayout {
private fun updateQueueState() {
_upNext.update();
/*_player.updateNextPrevious(
getPreviousVideo(withoutRemoval = true, forceLoop = true) != null,
getNextVideo(withoutRemoval = true, forceLoop = true) != null
)*/
}
//Handlers
@@ -1894,6 +1913,7 @@ class VideoDetailView : ConstraintLayout {
video?.let { updateQualitySourcesOverlay(it, videoLocal); };
val changed = _isCasting != isCasting;
_isCasting = isCasting;
if(isCasting) {
@@ -1901,8 +1921,7 @@ class VideoDetailView : ConstraintLayout {
_player.stop();
_player.hideControls(false);
_cast.visibility = View.VISIBLE;
}
else {
} else {
StateCasting.instance.stopVideo();
_cast.stopTimeJob();
_cast.visibility = View.GONE;
@@ -1911,6 +1930,10 @@ class VideoDetailView : ConstraintLayout {
_player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed());
}
}
if (changed) {
stopAllGestures();
}
}
fun setFullscreen(fullscreen : Boolean) {
@@ -2189,6 +2212,11 @@ class VideoDetailView : ConstraintLayout {
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
val username = cachedPolycentricProfile?.profile?.systemState?.username
if (username != null) {
_channelName.text = username
}
_monetization.setPolycentricProfile(cachedPolycentricProfile, animate);
}
@@ -2265,6 +2293,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;
@@ -50,7 +50,9 @@ class PolycentricCache {
ContentType.MEMBERSHIP_URLS.value,
ContentType.DONATION_DESTINATIONS.value
)
).eventsList.map { e -> SignedEvent.fromProto(e) };
).eventsList.map { e -> SignedEvent.fromProto(e) }
.groupBy { e -> e.event.contentType }
.map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } };
val storageSystemState = StorageTypeSystemState.create()
for (signedEvent in signedProfileEvents) {
@@ -220,7 +222,7 @@ class PolycentricCache {
}
}
suspend fun getProfileAsync(id: PlatformID): CachedPolycentricProfile? {
suspend fun getProfileAsync(id: PlatformID, url: String? = null): CachedPolycentricProfile? {
if (!StatePolycentric.instance.enabled || id.claimType <= 0) {
return CachedPolycentricProfile(null);
}
@@ -241,6 +243,8 @@ class PolycentricCache {
Logger.v(TAG, "getProfileAsync (id: $id) != null (with retrieved valid claims)")
return getProfileAsync(claims.ownedClaims.first().system).await()
} else {
if(url != null)
_profileUrlCache.setAndSave(url, 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
@@ -553,27 +554,6 @@ class StateApp {
if(StateHistory.instance.shouldMigrateLegacyHistory())
StateHistory.instance.migrateLegacyHistory();
if(false) {
/*
Logger.i(TAG, "TEST:--------(200)---------");
testHistoryDB(200);
Logger.i(TAG, "TEST:--------(1000)---------");
testHistoryDB(1000);
Logger.i(TAG, "TEST:--------(2000)---------");
testHistoryDB(2000);
Logger.i(TAG, "TEST:--------(4000)---------");
testHistoryDB(4000);
Logger.i(TAG, "TEST:--------(6000)---------");
testHistoryDB(6000);
Logger.i(TAG, "TEST:--------(100000)---------");
scope.launch(Dispatchers.Default) {
StateHistory.instance.testHistoryDB(100000);
}
*/
}
}
fun mainAppStartedWithExternalFiles(context: Context) {
@@ -772,6 +752,9 @@ class StateApp {
})
}
}
fun handleLoginException(client: JSClient, exception: ScriptLoginRequiredException, onSuccess: (client: JSClient)->Unit) {
}
fun getLocaleContext(baseContext: Context?): Context? {
val locale = getLocaleSetting(baseContext);
@@ -39,27 +39,16 @@ class StateCache {
}
fun getChannelCachePager(channelUrl: String): IPager<IPlatformContent> {
val result: IPager<IPlatformContent>;
val time = measureTimeMillis {
result = _subscriptionCache.queryPager(DBSubscriptionCache.Index::channelUrl, channelUrl, 20) {
it.obj;
}
}
return result;
return _subscriptionCache.queryPager(DBSubscriptionCache.Index::channelUrl, channelUrl, 20) { it.obj }
}
fun getAllChannelCachePager(channelUrls: List<String>): IPager<IPlatformContent> {
val result: IPager<IPlatformContent>;
val time = measureTimeMillis {
result = _subscriptionCache.queryInPager(DBSubscriptionCache.Index::channelUrl, channelUrls, 20) {
it.obj;
}
}
return result;
return _subscriptionCache.queryInPager(DBSubscriptionCache.Index::channelUrl, channelUrls, 20) { it.obj }
}
fun getChannelCachePager(channelUrls: List<String>): IPager<IPlatformContent> {
val pagers = MultiChronoContentPager(channelUrls.map { _subscriptionCache.queryPager(DBSubscriptionCache.Index::channelUrl, it, 20) {
fun getChannelCachePager(channelUrls: List<String>, pageSize: Int = 20): IPager<IPlatformContent> {
val pagers = MultiChronoContentPager(channelUrls.map { _subscriptionCache.queryPager(DBSubscriptionCache.Index::channelUrl, it, pageSize) {
it.obj;
} }, false, 20);
} }, false, pageSize);
return DedupContentPager(pagers, StatePlatform.instance.getEnabledClients().map { it.id });
}
fun getSubscriptionCachePager(): DedupContentPager {
@@ -67,7 +56,7 @@ class StateCache {
val subs = StateSubscriptions.instance.getSubscriptions();
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
val allUrls = subs.map {
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
if(!otherUrls.contains(it.channel.url))
return@map listOf(listOf(it.channel.url), otherUrls).flatten();
else
@@ -79,13 +68,6 @@ class StateCache {
val timeCacheRetrieving = measureTimeMillis {
pagers = listOf(getAllChannelCachePager(allUrls));
/*allUrls.parallelStream()
.map {
getChannelCachePager(it)
}
.asSequence()
.toList();*/
}
Logger.i(TAG, "Subscriptions CachePager compiling (retrieved in ${timeCacheRetrieving}ms)");
@@ -100,7 +100,7 @@ class StateHistory {
return _historyDBStore.getObjectPager();
}
fun getHistorySearchPager(query: String): IPager<HistoryVideo> {
return _historyDBStore.queryLikeObjectPager(DBHistory.Index::url, "%${query}%", 10);
return _historyDBStore.queryLikeObjectPager(DBHistory.Index::name, "%${query}%", 10);
}
fun getHistoryIndexByUrl(url: String): DBHistory.Index? {
return historyIndex[url];
@@ -40,6 +40,7 @@ import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.stores.*
import kotlinx.coroutines.*
import okhttp3.internal.concat
import java.lang.Thread.sleep
import java.time.OffsetDateTime
import kotlin.reflect.jvm.internal.impl.builtins.jvm.JavaToKotlinClassMap.PlatformMutabilityMapping
import kotlin.streams.asSequence
@@ -405,7 +406,12 @@ class StatePlatform {
val deferred: List<Pair<IPlatformClient, Deferred<IPager<IPlatformContent>?>>> = clients.map {
return@map Pair(it, scope.async(Dispatchers.IO) {
try {
val searchResult = it.fromPool(_pagerClientPool).getHome();
var searchResult = it.fromPool(_pagerClientPool).getHome();
if(searchResult.getResults().size == 0) {
Logger.i(TAG, "No home results, retrying");
sleep(500);
searchResult = it.fromPool(_pagerClientPool).getHome();
}
return@async searchResult;
} catch(ex: Throwable) {
Logger.e(TAG, "getHomeRefresh", ex);
@@ -603,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
@@ -37,6 +39,7 @@ class StatePlayer {
//Video Status
var rotationLock : Boolean = false;
var loopVideo : Boolean = false;
val isPlaying: Boolean get() = _exoplayer?.player?.playWhenReady ?: false;
@@ -267,7 +270,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 {
@@ -280,11 +288,47 @@ class StatePlayer {
addToShuffledQueue(video);
}
if (_queuePosition < 0) {
_queuePosition = 0;
}
didAdd = 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) {
if(_queue.isEmpty()) {
setQueueType(TYPE_QUEUE);
currentVideo?.let {
_queue.add(it);
}
}
if(_queue.isEmpty())
_queue.add(video);
else
_queue.add(_queuePosition.coerceAtLeast(0).coerceAtMost(_queue.size - 1), video);
if (queueShuffle) {
addToShuffledQueue(video);
}
if (_queuePosition < 0) {
_queuePosition = 0;
}
}
onQueueChanged.emit(true);
if(playNow)
setQueuePosition(video);
}
fun setQueuePosition(video: IPlatformVideo) {
synchronized(_queue) {
@@ -347,8 +391,43 @@ class StatePlayer {
return null;
}
fun getNextQueueItem() : IPlatformVideo? {
/***
* 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;
@@ -363,7 +442,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;
@@ -371,10 +450,18 @@ class StatePlayer {
fun restartQueue() : IPlatformVideo? {
synchronized(_queue) {
_queuePosition = -1;
return nextQueueItem();
return nextQueueItem(false, true);
}
};
fun nextQueueItem(withoutRemoval: Boolean = false) : IPlatformVideo? {
/***
* 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())
return null;
@@ -408,6 +495,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)
@@ -170,17 +170,23 @@ class StatePolycentric {
}
fun getChannelUrls(url: String, channelId: PlatformID? = null, cacheOnly: Boolean = false): List<String> {
return getChannelUrlsWithUpdateResult(url, channelId, cacheOnly).second;
}
fun getChannelUrlsWithUpdateResult(url: String, channelId: PlatformID? = null, cacheOnly: 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, url) }?.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,17 @@ 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);
if(result.first)
synchronized(lock) {
polycentricBudget--;
}
Pair(it, result.second);
}
else
Pair(it, listOf(it.channel.url));
}.asSequence()
@@ -1,27 +0,0 @@
package com.futo.platformplayer.stores.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
/*
@Dao
class ManagedDBContext<T, I: ManagedDBIndex<T>> {
fun get(id: Int): I;
fun gets(vararg id: Int): List<I>;
fun getAll(): List<I>;
@Insert
fun insert(index: I);
@Insert
fun insertAll(vararg indexes: I)
@Update
fun update(index: I);
@Delete
fun delete(index: I);
}*/
@@ -1,42 +1,23 @@
package com.futo.platformplayer.subscription
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.structures.DedupContentPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.PlatformContentPager
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.toSafeFileName
import kotlinx.coroutines.CoroutineScope
import java.util.concurrent.ForkJoinPool
class CachedSubscriptionAlgorithm(pageSize: Int = 150, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = true, threadPool: ForkJoinPool? = null)
class CachedSubscriptionAlgorithm(scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = true, threadPool: ForkJoinPool? = null, pageSize: Int = 50)
: SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool) {
private val _pageSize: Int = pageSize;
override fun countRequests(subs: Map<Subscription, List<String>>): Map<JSClient, Int> {
return mapOf<JSClient, Int>();
return mapOf();
}
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
val validSubIds = subs.flatMap { it.value } .map { it.toSafeFileName() }.toHashSet();
/*
val validStores = StateCache.instance._channelContents
.filter { validSubIds.contains(it.key) }
.map { it.value };*/
/*
val items = validStores.flatMap { it.getItems() }
.sortedByDescending { it.datetime };
*/
return Result(DedupContentPager(StateCache.instance.getChannelCachePager(subs.flatMap { it.value }.distinct()), StatePlatform.instance.getEnabledClients().map { it.id }), listOf());
return Result(DedupContentPager(StateCache.instance.getChannelCachePager(subs.flatMap { it.value }.distinct(), _pageSize), StatePlatform.instance.getEnabledClients().map { it.id }), listOf());
}
}
@@ -38,7 +38,7 @@ abstract class SubscriptionFetchAlgorithm(
fun getAlgorithm(algo: SubscriptionFetchAlgorithms, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = false, pool: ForkJoinPool? = null): SubscriptionFetchAlgorithm {
return when(algo) {
SubscriptionFetchAlgorithms.CACHE -> CachedSubscriptionAlgorithm(150, scope, allowFailure, withCacheFallback, pool);
SubscriptionFetchAlgorithms.CACHE -> CachedSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, 50);
SubscriptionFetchAlgorithms.SIMPLE -> SimpleSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
SubscriptionFetchAlgorithms.SMART -> SmartSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
else -> throw IllegalStateException("Unknown algorithm ${algo}");
@@ -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);
}
}
@@ -1,120 +0,0 @@
package com.futo.platformplayer.views.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlaylists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
private lateinit var _filteredVideos: MutableList<HistoryVideo>;
val onClick = Event1<HistoryVideo>();
private var _query: String = "";
constructor() : super() {
updateFilteredVideos();
StateHistory.instance.onHistoricVideoChanged.subscribe(this) { video, position ->
StateApp.instance.scope.launch(Dispatchers.Main) {
val index = _filteredVideos.indexOfFirst { v -> v.video.url == video.url };
if (index == -1) {
return@launch;
}
_filteredVideos[index].position = position;
if (index < _filteredVideos.size - 2) {
notifyItemRangeChanged(index, 2);
} else {
notifyItemChanged(index);
}
}
};
}
fun setQuery(query: String) {
_query = query;
updateFilteredVideos();
}
fun updateFilteredVideos() {
val videos = StateHistory.instance.getHistory();
val pager = StateHistory.instance.getHistoryPager();
//filtered val pager = StateHistory.instance.getHistorySearchPager("querrryyyyy");
if (_query.isBlank()) {
_filteredVideos = videos.toMutableList();
} else {
_filteredVideos = videos.filter { v -> v.video.name.lowercase().contains(_query); }.toMutableList();
}
notifyDataSetChanged();
}
fun cleanup() {
StateHistory.instance.onHistoricVideoChanged.remove(this);
}
override fun getItemCount() = _filteredVideos.size;
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): HistoryListViewHolder {
val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.list_history, viewGroup, false);
val holder = HistoryListViewHolder(view);
holder.onRemove.subscribe { v ->
val videos = _filteredVideos;
val index = videos.indexOf(v);
if (index == -1) {
return@subscribe;
}
StateHistory.instance.removeHistory(v.video.url);
_filteredVideos.removeAt(index);
notifyItemRemoved(index);
};
holder.onClick.subscribe { v ->
val videos = _filteredVideos;
val index = videos.indexOf(v);
if (index == -1) {
return@subscribe;
}
_filteredVideos.removeAt(index);
_filteredVideos.add(0, v);
notifyItemMoved(index, 0);
notifyItemRangeChanged(0, 2);
onClick.emit(v);
};
return holder;
}
override fun onBindViewHolder(viewHolder: HistoryListViewHolder, position: Int) {
val videos = _filteredVideos;
var watchTime: String? = null;
if (position == 0) {
watchTime = videos[position].date.toHumanNowDiffStringMinDay();
} else {
val previousWatchTime = videos[position - 1].date.toHumanNowDiffStringMinDay();
val currentWatchTime = videos[position].date.toHumanNowDiffStringMinDay();
if (previousWatchTime != currentWatchTime) {
watchTime = currentWatchTime;
}
}
viewHolder.bind(videos[position], watchTime);
}
companion object {
val TAG = "HistoryListAdapter";
}
}
@@ -1,6 +1,8 @@
package com.futo.platformplayer.views.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
@@ -35,26 +37,26 @@ class HistoryListViewHolder : ViewHolder {
val onClick = Event1<HistoryVideo>();
val onRemove = Event1<HistoryVideo>();
constructor(view: View) : super(view) {
_root = view.findViewById(R.id.root);
_imageThumbnail = view.findViewById(R.id.image_video_thumbnail);
_imageThumbnail?.clipToOutline = true;
_textName = view.findViewById(R.id.text_video_name);
_textAuthor = view.findViewById(R.id.text_author);
_textMetadata = view.findViewById(R.id.text_video_metadata);
_textVideoDuration = view.findViewById(R.id.thumbnail_duration);
_containerDuration = view.findViewById(R.id.thumbnail_duration_container);
_containerLive = view.findViewById(R.id.thumbnail_live_container);
_imageRemove = view.findViewById(R.id.image_trash);
_textHeader = view.findViewById(R.id.text_header);
_timeBar = view.findViewById(R.id.time_bar);
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_history, viewGroup, false)) {
_root = itemView.findViewById(R.id.root);
_imageThumbnail = itemView.findViewById(R.id.image_video_thumbnail);
_imageThumbnail.clipToOutline = true;
_textName = itemView.findViewById(R.id.text_video_name);
_textAuthor = itemView.findViewById(R.id.text_author);
_textMetadata = itemView.findViewById(R.id.text_video_metadata);
_textVideoDuration = itemView.findViewById(R.id.thumbnail_duration);
_containerDuration = itemView.findViewById(R.id.thumbnail_duration_container);
_containerLive = itemView.findViewById(R.id.thumbnail_live_container);
_imageRemove = itemView.findViewById(R.id.image_trash);
_textHeader = itemView.findViewById(R.id.text_header);
_timeBar = itemView.findViewById(R.id.time_bar);
_root.setOnClickListener {
val v = video ?: return@setOnClickListener;
onClick.emit(v);
};
_imageRemove?.setOnClickListener {
_imageRemove.setOnClickListener {
val v = video ?: return@setOnClickListener;
onRemove.emit(v);
};
@@ -82,7 +82,6 @@ class SubscriptionViewHolder : ViewHolder {
this.subscription = sub;
_creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
_taskLoadProfile.run(sub.channel.id);
_textName.text = sub.channel.name;
bindViewMetrics(sub);
_platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId);
@@ -93,6 +92,8 @@ class SubscriptionViewHolder : ViewHolder {
if (cachedProfile.expired) {
_taskLoadProfile.run(sub.channel.id);
}
} else {
_taskLoadProfile.run(sub.channel.id);
}
}
@@ -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;
}
})
@@ -184,7 +184,7 @@ open class PreviewVideoView : LinearLayout {
.placeholder(R.drawable.placeholder_channel_thumbnail)
.into(_imageChannel);
}
_taskLoadProfile.run(content.author.id);
_textChannelName.text = content.author.name
val cachedProfile = PolycentricCache.instance.getCachedProfile(content.author.url, true);
@@ -193,6 +193,8 @@ open class PreviewVideoView : LinearLayout {
if (cachedProfile.expired) {
_taskLoadProfile.run(content.author.id);
}
} else {
_taskLoadProfile.run(content.author.id);
}
_imageChannel?.clipToOutline = true;
@@ -66,7 +66,6 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
_taskLoadProfile.cancel();
_creatorThumbnail.setThumbnail(authorLink.thumbnail, false);
_taskLoadProfile.run(authorLink.id);
_textName.text = authorLink.name;
val cachedProfile = PolycentricCache.instance.getCachedProfile(authorLink.url, true);
@@ -75,6 +74,8 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
if (cachedProfile.expired) {
_taskLoadProfile.run(authorLink.id);
}
} else {
_taskLoadProfile.run(authorLink.id);
}
if(authorLink.subscribers == null || (authorLink.subscribers ?: 0) <= 0L)
@@ -52,7 +52,6 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
_channel = subscription.channel;
_creatorThumbnail.setThumbnail(subscription.channel.thumbnail, false);
_taskLoadProfile.run(subscription.channel.id);
_name.text = subscription.channel.name;
val cachedProfile = PolycentricCache.instance.getCachedProfile(subscription.channel.url, true);
@@ -61,6 +60,8 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
if (cachedProfile.expired) {
_taskLoadProfile.run(subscription.channel.id);
}
} else {
_taskLoadProfile.run(subscription.channel.id);
}
_subscription = subscription;
@@ -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
}
@@ -26,6 +26,7 @@ class CastView : ConstraintLayout {
private val _thumbnail: ImageView;
private val _buttonMinimize: ImageButton;
private val _buttonSettings: ImageButton;
private val _buttonLoop: ImageButton;
private val _buttonPlay: ImageButton;
private val _buttonPause: ImageButton;
private val _buttonCast: CastButton;
@@ -49,6 +50,7 @@ class CastView : ConstraintLayout {
_thumbnail = findViewById(R.id.image_thumbnail);
_buttonMinimize = findViewById(R.id.button_minimize);
_buttonSettings = findViewById(R.id.button_settings);
_buttonLoop = findViewById(R.id.button_loop);
_buttonPlay = findViewById(R.id.button_play);
_buttonPause = findViewById(R.id.button_pause);
_buttonCast = findViewById(R.id.button_cast);
@@ -65,6 +67,12 @@ class CastView : ConstraintLayout {
StateCasting.instance.videoSeekTo(d.expectedCurrentTime + it / 1000);
};
_buttonLoop.setOnClickListener {
StatePlayer.instance.loopVideo = !StatePlayer.instance.loopVideo;
_buttonLoop.setImageResource(if(StatePlayer.instance.loopVideo) R.drawable.ic_loop_active else R.drawable.ic_loop);
}
_buttonLoop.setImageResource(if(StatePlayer.instance.loopVideo) R.drawable.ic_loop_active else R.drawable.ic_loop);
_timeBar.addListener(object : OnScrubListener {
override fun onScrubStart(timeBar: TimeBar, position: Long) {
StateCasting.instance.videoSeekTo(position.toDouble());
@@ -94,6 +102,10 @@ class CastView : ConstraintLayout {
_updateTimeJob = null;
}
fun stopAllGestures() {
_gestureControlView.stopAllGestures();
}
fun setIsPlaying(isPlaying: Boolean) {
_updateTimeJob?.cancel();
@@ -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) {
@@ -4,7 +4,6 @@ import android.content.Context
import android.content.res.Resources
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Handler
import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue
@@ -17,7 +16,6 @@ import android.widget.RelativeLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.setMargins
import androidx.lifecycle.LifecycleOwner
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
@@ -37,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
@@ -68,20 +69,26 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
private val _control_videosettings: ImageButton;
private val _control_minimize: ImageButton;
private val _control_rotate_lock: ImageButton;
private val _control_loop: ImageButton;
private val _control_cast: ImageButton;
private val _control_play: ImageButton;
private val _control_chapter: TextView;
private val _time_bar: TimeBar;
private val _buttonPrevious: ImageButton;
private val _buttonNext: ImageButton;
private val _control_fullscreen_fullscreen: ImageButton;
private val _control_videosettings_fullscreen: ImageButton;
private val _control_minimize_fullscreen: ImageButton;
private val _control_rotate_lock_fullscreen: ImageButton;
private val _control_loop_fullscreen: ImageButton;
private val _control_cast_fullscreen: ImageButton;
private val _control_play_fullscreen: ImageButton;
private val _time_bar_fullscreen: TimeBar;
private val _overlay_brightness: FrameLayout;
private val _control_chapter_fullscreen: TextView;
private val _buttonPrevious_fullscreen: ImageButton;
private val _buttonNext_fullscreen: ImageButton;
private val _title_fullscreen: TextView;
private val _author_fullscreen: TextView;
@@ -110,6 +117,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
val onToggleFullScreen = Event1<Boolean>();
val onSourceChanged = Event3<IVideoSource?, IAudioSource?, Boolean>();
val onSourceEnded = Event0();
val onPrevious = Event0();
val onNext = Event0();
val onChapterChanged = Event2<IChapter?, Boolean>();
@@ -128,25 +137,36 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_control_videosettings = videoControls.findViewById(R.id.exo_settings);
_control_minimize = videoControls.findViewById(R.id.exo_minimize);
_control_rotate_lock = videoControls.findViewById(R.id.exo_rotate_lock);
_control_loop = videoControls.findViewById(R.id.exo_loop);
_control_cast = videoControls.findViewById(R.id.exo_cast);
_control_play = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_play);
_time_bar = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress);
_control_chapter = videoControls.findViewById(R.id.text_chapter_current);
_buttonNext = videoControls.findViewById(R.id.button_next);
_buttonPrevious = videoControls.findViewById(R.id.button_previous);
_videoControls_fullscreen = findViewById(R.id.video_player_controller_fullscreen);
_control_fullscreen_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_fullscreen);
_control_minimize_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_minimize);
_control_videosettings_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_settings);
_control_rotate_lock_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_rotate_lock);
_control_loop_fullscreen = videoControls.findViewById(R.id.exo_loop);
_control_cast_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_cast);
_control_play_fullscreen = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_play);
_control_chapter_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_chapter_current);
_time_bar_fullscreen = _videoControls_fullscreen.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress);
_buttonPrevious_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_previous);
_buttonNext_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_next);
val castVisibility = if (Settings.instance.casting.enabled) View.VISIBLE else View.GONE
_control_cast.visibility = castVisibility
_control_cast_fullscreen.visibility = castVisibility
_buttonPrevious.setOnClickListener { onPrevious.emit() };
_buttonNext.setOnClickListener { onNext.emit() };
_buttonPrevious_fullscreen.setOnClickListener { onPrevious.emit() };
_buttonNext_fullscreen.setOnClickListener { onNext.emit() };
_overlay_brightness = findViewById(R.id.overlay_brightness);
_title_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_title);
@@ -244,6 +264,16 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
UIDialogs.showCastingDialog(context);
};
_control_loop.setOnClickListener {
StatePlayer.instance.loopVideo = !StatePlayer.instance.loopVideo;
updateLoopVideoUI();
}
_control_loop_fullscreen.setOnClickListener {
StatePlayer.instance.loopVideo = !StatePlayer.instance.loopVideo;
updateLoopVideoUI();
}
_control_minimize_fullscreen.setOnClickListener {
onMinimize.emit(this);
};
@@ -273,11 +303,33 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
}
StatePlayer.instance.onQueueChanged.subscribe(this) {
CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main) {
updateNextPrevious();
}
}
StatePlayer.instance.onVideoChanging.subscribe(this) {
CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main) {
updateNextPrevious();
}
}
updateLoopVideoUI();
if(!isInEditMode) {
gestureControl.hideControls();
}
}
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;
private val _currentChapterUpdateExecuter = Executors.newSingleThreadScheduledExecutor();
@@ -309,6 +361,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_currentChapterLoopActive = false;
}
fun stopAllGestures() {
gestureControl.stopAllGestures();
}
fun attachPlayer() {
exoPlayer?.attach(_videoView, PLAYER_STATE_NAME);
}
@@ -555,6 +611,17 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_control_rotate_lock.setImageResource(R.drawable.ic_screen_lock_rotation);
}
}
fun updateLoopVideoUI() {
if(StatePlayer.instance.loopVideo) {
_control_loop.setImageResource(R.drawable.ic_loop_active);
_control_loop_fullscreen.setImageResource(R.drawable.ic_loop_active);
}
else {
_control_loop.setImageResource(R.drawable.ic_loop);
_control_loop_fullscreen.setImageResource(R.drawable.ic_loop);
}
}
fun setGestureSoundFactor(soundFactor: Float) {
gestureControl.setSoundFactor(soundFactor);
@@ -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>
+9
View File
@@ -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>
+9
View File
@@ -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="M292.31,840L160,707.69L292.31,575.39L320.61,604.15L237.08,687.69L692.31,687.69L692.31,527.69L732.31,527.69L732.31,727.69L237.08,727.69L320.61,811.23L292.31,840ZM227.69,432.31L227.69,232.31L722.92,232.31L639.39,148.77L667.69,120L800,252.31L667.69,384.61L639.39,355.85L722.92,272.31L267.69,272.31L267.69,432.31L227.69,432.31Z"/>
</vector>
@@ -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="@color/colorPrimary"
android:pathData="M292.31,840L160,707.69L292.31,575.39L320.61,604.15L237.08,687.69L692.31,687.69L692.31,527.69L732.31,527.69L732.31,727.69L237.08,727.69L320.61,811.23L292.31,840ZM227.69,432.31L227.69,232.31L722.92,232.31L639.39,148.77L667.69,120L800,252.31L667.69,384.61L639.39,355.85L722.92,272.31L267.69,272.31L267.69,432.31L227.69,432.31Z"/>
</vector>
@@ -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="M660,720L660,240L740,240L740,720L660,720ZM220,720L220,240L580,480L220,720Z"/>
</vector>
@@ -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="M220,720L220,240L300,240L300,720L220,720ZM740,720L380,480L740,240L740,720Z"/>
</vector>
@@ -3,11 +3,17 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.futo.platformplayer.views.SupportView
android:id="@+id/support"
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
android:layout_height="match_parent">
<com.futo.platformplayer.views.SupportView
android:id="@+id/support"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"/>
</androidx.core.widget.NestedScrollView>
<TextView
android:id="@+id/text_monetization"
@@ -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>
+10 -1
View File
@@ -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"
+10 -3
View File
@@ -15,11 +15,18 @@
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<com.futo.platformplayer.views.SupportView
android:id="@+id/support"
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/topbar"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintBottom_toBottomOf="parent">
<com.futo.platformplayer.views.SupportView
android:id="@+id/support"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -4,56 +4,76 @@
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="500K" />
<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: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="500K" />
<com.futo.platformplayer.views.LoaderView
android:id="@+id/loader_dislikes"
android:layout_width="14dp"
android:layout_height="14dp"
app:isWhite="true" />
</LinearLayout>
</LinearLayout>
+54 -14
View File
@@ -45,6 +45,14 @@
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_screen_lock_rotation" />
<ImageButton
android:id="@+id/exo_loop"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_loop" />
<ImageButton
android:id="@+id/exo_settings"
android:layout_width="50dp"
@@ -56,28 +64,60 @@
</LinearLayout>
<ImageButton
android:id="@id/exo_play"
android:id="@id/button_previous"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:scaleType="centerCrop"
android:clickable="true"
app:srcCompat="@drawable/ic_play_white_nopad" />
android:layout_marginRight="40dp"
android:padding="5dp"
app:srcCompat="@drawable/ic_skip_previous"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/layout_play_pause"
app:layout_constraintBottom_toBottomOf="parent" />
<FrameLayout
android:id="@+id/layout_play_pause"
android:layout_width="60dp"
android:layout_height="60dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent">
<ImageButton
android:id="@id/exo_play"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
android:clickable="true"
app:srcCompat="@drawable/ic_play_white_nopad" />
<ImageButton
android:id="@id/exo_pause"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
android:clickable="true"
app:srcCompat="@drawable/ic_pause_white"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</FrameLayout>
<ImageButton
android:id="@id/exo_pause"
android:id="@id/button_next"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:clickable="true"
app:srcCompat="@drawable/ic_pause_white" />
android:scaleType="centerCrop"
android:padding="5dp"
android:layout_marginLeft="40dp"
app:srcCompat="@drawable/ic_skip_next"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@id/layout_play_pause"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/exo_fullscreen"
@@ -73,6 +73,14 @@
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_screen_lock_rotation" />
<ImageButton
android:id="@+id/exo_loop"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_loop" />
<ImageButton
android:id="@+id/exo_settings"
android:layout_width="50dp"
@@ -84,28 +92,60 @@
</LinearLayout>
<ImageButton
android:id="@id/exo_play"
android:id="@id/button_previous"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
android:scaleType="centerCrop"
android:clickable="true"
app:srcCompat="@drawable/ic_play_white_nopad"
android:layout_marginRight="40dp"
android:padding="5dp"
app:srcCompat="@drawable/ic_skip_previous"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/layout_play_pause"
app:layout_constraintBottom_toBottomOf="parent" />
<FrameLayout
android:id="@+id/layout_play_pause"
android:layout_width="60dp"
android:layout_height="60dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
app:layout_constraintRight_toRightOf="parent">
<ImageButton
android:id="@id/exo_play"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
android:clickable="true"
app:srcCompat="@drawable/ic_play_white_nopad" />
<ImageButton
android:id="@id/exo_pause"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
android:clickable="true"
app:srcCompat="@drawable/ic_pause_white"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</FrameLayout>
<ImageButton
android:id="@id/exo_pause"
android:id="@id/button_next"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
android:clickable="true"
app:srcCompat="@drawable/ic_pause_white"
android:scaleType="centerCrop"
android:padding="5dp"
android:layout_marginLeft="40dp"
app:srcCompat="@drawable/ic_skip_next"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
app:layout_constraintLeft_toRightOf="@id/layout_play_pause"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/exo_fullscreen"
+9
View File
@@ -54,6 +54,15 @@
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_cast" />
<ImageButton
android:id="@+id/button_loop"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_loop" />
<ImageButton
android:id="@+id/button_settings"
android:layout_width="50dp"
@@ -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>
+3 -6
View File
@@ -1,11 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_margin="18dp"
android:showDividers="middle"
@@ -187,5 +185,4 @@
android:divider="@drawable/divider_transparent_vertical_8dp">
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
+1
View File
@@ -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>
+6 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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')
}
}