Compare commits

..

26 Commits

Author SHA1 Message Date
Koen 150a7d5006 Added proper permissions for download and export service (A14). 2023-12-10 09:29:33 +01:00
Koen a0a73a8e5c Fixes Android SDK. 2023-12-10 00:34:34 +01:00
Kelvin 4723a0b29a Fix up next view 2023-12-09 19:03:47 +01:00
Kelvin adbe0357ba Refs 2023-12-09 18:12:36 +01:00
Kelvin b0a35bcf3f No notification on known item, Polycentric logging, refs 2023-12-09 18:11:45 +01:00
Kelvin 0e7482321c Import platform redirect and disabled buttons, minor ui fixes 2023-12-09 16:31:34 +01:00
Koen e50d195b85 Fixed artwork not displaying properly. Loop button now hidden if you have a queue. Videos on queue editor now properly updates the amount of videos when a video is deleted. 2023-12-08 21:42:27 +01:00
Koen 33780f1046 Cleanup. 2023-12-08 14:53:57 +01:00
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
45 changed files with 652 additions and 209 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'
}
@@ -0,0 +1,94 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "ffba56c2f572c25080ce8596e8bb8945",
"entities": [
{
"tableName": "history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `url` TEXT NOT NULL, `position` INTEGER NOT NULL, `datetime` INTEGER NOT NULL, `name` TEXT NOT NULL, `serialized` BLOB)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serialized",
"columnName": "serialized",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_history_url",
"unique": false,
"columnNames": [
"url"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_url` ON `${TABLE_NAME}` (`url`)"
},
{
"name": "index_history_name",
"unique": false,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_name` ON `${TABLE_NAME}` (`name`)"
},
{
"name": "index_history_datetime",
"unique": false,
"columnNames": [
"datetime"
],
"orders": [
"DESC"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_datetime` ON `${TABLE_NAME}` (`datetime` DESC)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ffba56c2f572c25080ce8596e8bb8945')"
]
}
}
@@ -0,0 +1,88 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "eb813d54b9c44d29f1d7bb198a16d4d1",
"entities": [
{
"tableName": "subscription_cache",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `url` TEXT, `channelUrl` TEXT, `datetime` INTEGER, `serialized` BLOB)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "channelUrl",
"columnName": "channelUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "datetime",
"columnName": "datetime",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "serialized",
"columnName": "serialized",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_subscription_cache_url",
"unique": false,
"columnNames": [
"url"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_url` ON `${TABLE_NAME}` (`url`)"
},
{
"name": "index_subscription_cache_channelUrl",
"unique": false,
"columnNames": [
"channelUrl"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_channelUrl` ON `${TABLE_NAME}` (`channelUrl`)"
},
{
"name": "index_subscription_cache_datetime",
"unique": false,
"columnNames": [
"datetime"
],
"orders": [
"DESC"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_datetime` ON `${TABLE_NAME}` (`datetime` DESC)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eb813d54b9c44d29f1d7bb198a16d4d1')"
]
}
}
@@ -0,0 +1,52 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "6e3b2d286325c4ea8a7a4c94c290daec",
"entities": [
{
"tableName": "testing",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`someString` TEXT NOT NULL, `someNum` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `serialized` BLOB)",
"fields": [
{
"fieldPath": "someString",
"columnName": "someString",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "someNum",
"columnName": "someNum",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "serialized",
"columnName": "serialized",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6e3b2d286325c4ea8a7a4c94c290daec')"
]
}
}
+6 -2
View File
@@ -11,6 +11,8 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<application
android:allowBackup="true"
@@ -35,9 +37,11 @@
android:enabled="true"
android:foregroundServiceType="mediaPlayback" />
<service android:name=".services.DownloadService"
android:enabled="true" />
android:enabled="true"
android:foregroundServiceType="dataSync" />
<service android:name=".services.ExportingService"
android:enabled="true" />
android:enabled="true"
android:foregroundServiceType="dataSync" />
<receiver android:name=".receivers.MediaControlReceiver" />
<receiver android:name=".receivers.AudioNoisyReceiver" />
@@ -555,7 +555,7 @@ class Settings : FragmentedStorageFileJson() {
val cookieManager: CookieManager = CookieManager.getInstance();
cookieManager.removeAllCookies(null);
}
@FormField(R.string.reinstall_embedded_plugins, FieldForm.BUTTON, R.string.also_removes_any_data_related_plugin_like_login_or_settings, 1)
/*@FormField(R.string.reinstall_embedded_plugins, FieldForm.BUTTON, R.string.also_removes_any_data_related_plugin_like_login_or_settings, 1)
fun reinstallEmbedded() {
StateApp.instance.scopeOrNull!!.launch(Dispatchers.IO) {
try {
@@ -574,7 +574,7 @@ class Settings : FragmentedStorageFileJson() {
}
}
}
}
}*/
}
@@ -2,6 +2,8 @@ package com.futo.platformplayer.api.media.structures
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.assume
import kotlin.streams.asSequence
import kotlin.streams.toList
/**
@@ -28,6 +30,7 @@ class PlatformContentPager : IPager<IPlatformContent> {
_page++;
_currentItems = _items.stream()
.skip((_page * _pageSize).toLong())
.asSequence()
.toList()
.take(_pageSize)
.toList();
@@ -7,6 +7,7 @@ import android.view.LayoutInflater
import android.widget.Button
import com.futo.platformplayer.R
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
import com.futo.platformplayer.readBytes
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
@@ -19,6 +20,7 @@ class ImportOptionsDialog: AlertDialog {
private lateinit var _button_import_ezip: BigButton;
private lateinit var _button_import_txt: BigButton;
private lateinit var _button_import_newpipe_subs: BigButton;
private lateinit var _button_import_platform: BigButton;
private lateinit var _button_close: Button;
@@ -33,6 +35,7 @@ class ImportOptionsDialog: AlertDialog {
_button_import_ezip = findViewById(R.id.button_import_ezip);
_button_import_txt = findViewById(R.id.button_import_txt);
_button_import_newpipe_subs = findViewById(R.id.button_import_newpipe_subs);
_button_import_platform = findViewById(R.id.button_import_platform);
_button_close = findViewById(R.id.button_cancel);
_button_import_zip.onClick.subscribe {
@@ -61,6 +64,10 @@ class ImportOptionsDialog: AlertDialog {
StateBackup.importNewPipeSubs(_context, json);
};
};
_button_import_platform.onClick.subscribe {
dismiss();
_context.navigate(_context.getFragment<SourcesFragment>());
};
_button_close.setOnClickListener {
dismiss();
}
@@ -207,8 +207,6 @@ class ChannelFragment : MainFragment() {
adapter.onAddToQueueClicked.subscribe { content ->
if(content is IPlatformVideo) {
StatePlayer.instance.addToQueue(content);
val name = if (content.name.length > 20) (content.name.subSequence(0, 20).toString() + "...") else content.name;
UIDialogs.toast(context, "Queued [$name]", false);
}
}
adapter.onUrlClicked.subscribe { url ->
@@ -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) {
@@ -264,6 +264,31 @@ class SourceDetailFragment : MainFragment() {
}
)
);
val migrationButtons = mutableListOf<BigButton>();
if (isEnabled && source.capabilities.hasGetUserSubscriptions) {
migrationButtons.add(
BigButton(c, context.getString(R.string.import_subscriptions), context.getString(R.string.login_required), R.drawable.ic_subscriptions) {
}.apply { this.alpha = 0.5f }
);
}
if (isEnabled && source.capabilities.hasGetUserPlaylists && source.capabilities.hasGetPlaylist) {
val bigButton = BigButton(c, context.getString(R.string.import_playlists), context.getString(R.string.login_required), R.drawable.ic_playlist) {
}.apply { this.alpha = 0.5f };
bigButton.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
};
migrationButtons.add(bigButton);
}
if (migrationButtons.size > 0) {
groups.add(BigButtonGroup(c, context.getString(R.string.migration), *migrationButtons.toTypedArray()));
}
}
}
@@ -280,6 +305,8 @@ class SourceDetailFragment : MainFragment() {
if(clientIfExists?.captchaEncrypted != null)
BigButton(c, context.getString(R.string.delete_captcha), context.getString(R.string.deletes_stored_captcha_answer_for_this_plugin), R.drawable.ic_block) {
clientIfExists.updateCaptcha(null);
updateButtons();
UIDialogs.toast(context, "Captcha data deleted");
}.apply {
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
@@ -468,8 +468,8 @@ class VideoDetailView : ConstraintLayout {
nextVideo();
};
_player.onDatasourceError.subscribe(::onDataSourceError);
_player.onNext.subscribe { nextVideo(true) };
_player.onPrevious.subscribe { previousVideo(true) };
_player.onNext.subscribe { nextVideo(true, true, true) };
_player.onPrevious.subscribe { prevVideo(true) };
_minimize_controls_play.setOnClickListener { handlePlay(); };
_minimize_controls_pause.setOnClickListener { handlePause(); };
@@ -538,6 +538,7 @@ class VideoDetailView : ConstraintLayout {
if(!_destroyed) {
updateQueueState();
StatePlayer.instance.updateMediaSession(null);
_cast.setLoopVisible(!StatePlayer.instance.hasQueue);
}
};
StatePlayer.instance.onVideoChanging.subscribe(this) {
@@ -546,8 +547,8 @@ class VideoDetailView : ConstraintLayout {
MediaControlReceiver.onLowerVolumeReceived.subscribe(this) { handleLowerVolume() };
MediaControlReceiver.onPlayReceived.subscribe(this) { handlePlay() };
MediaControlReceiver.onPauseReceived.subscribe(this) { handlePause() };
MediaControlReceiver.onNextReceived.subscribe(this) { nextVideo(true) };
MediaControlReceiver.onPreviousReceived.subscribe(this) { previousVideo(true) };
MediaControlReceiver.onNextReceived.subscribe(this) { nextVideo(true, true, true) };
MediaControlReceiver.onPreviousReceived.subscribe(this) { prevVideo(true) };
MediaControlReceiver.onCloseReceived.subscribe(this) {
Logger.i(TAG, "MediaControlReceiver.onCloseReceived")
onClose.emit()
@@ -565,9 +566,7 @@ class VideoDetailView : ConstraintLayout {
};
_upNext.onNextItem.subscribe {
val item = StatePlayer.instance.nextQueueItem();
if(item != null)
setVideoOverview(item, true);
nextVideo(true, true, true);
};
_upNext.onOpenQueueClick.subscribe {
_container_content_queue.updateQueue();
@@ -1332,6 +1331,7 @@ class VideoDetailView : ConstraintLayout {
if(video.isLive && video.live == null && !video.video.videoSources.any())
startLiveTry(video);
_player.updateNextPrevious();
updateMoreButtons();
}
fun loadLiveChat(video: IPlatformVideoDetails) {
@@ -1542,63 +1542,26 @@ class VideoDetailView : ConstraintLayout {
_slideUpOverlay = _overlay_quality_selector;
}
private fun getPreviousVideo(withoutRemoval: Boolean, forceLoop: Boolean = false): IPlatformVideo? {
if (!StatePlayer.instance.hasQueue) {
if (forceLoop) {
return StatePlayer.instance.currentVideo
} else {
return null
}
}
val shouldNotRemove = _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9
var previous = StatePlayer.instance.prevQueueItem(withoutRemoval || shouldNotRemove);
if(previous == null && forceLoop)
previous = StatePlayer.instance.getQueue().last();
return previous;
fun prevVideo(withoutRemoval: Boolean = false) {
Logger.i(TAG, "prevVideo")
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
if(next != null) {
setVideoOverview(next);
}
}
private fun getNextVideo(withoutRemoval: Boolean, forceLoop: Boolean = false): IPlatformVideo? {
if (!StatePlayer.instance.hasQueue) {
if (forceLoop) {
return StatePlayer.instance.currentVideo
} else {
return null
}
}
val shouldNotRemove = _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9
var next = StatePlayer.instance.nextQueueItem(withoutRemoval || shouldNotRemove);
fun nextVideo(forceLoop: Boolean = false, withoutRemoval: Boolean = false, bypassVideoLoop: Boolean = false): Boolean {
Logger.i(TAG, "nextVideo")
var next = StatePlayer.instance.nextQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9, bypassVideoLoop);
if(next == null && forceLoop)
next = StatePlayer.instance.restartQueue();
return next;
}
fun previousVideo(forceLoop: Boolean = false): Boolean {
Logger.i(TAG, "previousVideo")
val previous = getPreviousVideo(false, forceLoop);
if(previous != null) {
setVideoOverview(previous);
return true;
} else {
StatePlayer.instance.setCurrentlyPlaying(null);
}
return false;
}
fun nextVideo(forceLoop: Boolean = false): Boolean {
Logger.i(TAG, "nextVideo")
val next = getNextVideo(false, forceLoop);
if(next != null) {
setVideoOverview(next);
return true;
} else {
StatePlayer.instance.setCurrentlyPlaying(null);
}
else
StatePlayer.instance.setCurrentlyPlaying(null);
return false;
}
@@ -59,23 +59,28 @@ public class PolycentricModelLoader implements ModelLoader<String, ByteBuffer> {
@Override
public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback<? super ByteBuffer> callback) {
Log.i("PolycentricModelLoader", this._model);
_deferred = PolycentricCache.getInstance().getDataAsync(_model);
_deferred.invokeOnCompletion(throwable -> {
if (throwable != null) {
Log.e("PolycentricModelLoader", "getDataAsync failed throwable: " + throwable.toString());
callback.onLoadFailed(new Exception(throwable));
return Unit.INSTANCE;
}
Deferred<ByteBuffer> deferred = _deferred;
if (deferred == null) {
Log.e("PolycentricModelLoader", "getDataAsync failed deferred is null");
callback.onLoadFailed(new Exception("Deferred is null"));
return Unit.INSTANCE;
}
ByteBuffer completed = deferred.getCompleted();
if (completed != null) {
Log.e("PolycentricModelLoader", "getDataAsync success loaded " + completed.remaining() + " bytes");
callback.onDataReady(completed);
} else {
Log.e("PolycentricModelLoader", "getDataAsync failed completed is null");
callback.onLoadFailed(new Exception("Completed is null"));
}
return Unit.INSTANCE;
@@ -17,6 +17,7 @@ import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
import java.nio.ByteBuffer
import java.time.OffsetDateTime
import kotlin.system.measureTimeMillis
class PolycentricCache {
data class CachedOwnedClaims(val ownedClaims: List<OwnedClaim>?, val creationTime: OffsetDateTime = OffsetDateTime.now()) {
@@ -29,8 +30,15 @@ class PolycentricCache {
private val _cache = hashMapOf<PlatformID, CachedOwnedClaims>()
private val _profileCache = hashMapOf<PublicKey, CachedPolycentricProfile>()
private val _profileUrlCache = FragmentedStorage.get<CachedPolycentricProfileStorage>("profileUrlCache")
private val _profileUrlCache: CachedPolycentricProfileStorage;
private val _scope = CoroutineScope(Dispatchers.IO);
init {
Logger.i(TAG, "Initializing Polycentric cache");
val time = measureTimeMillis {
_profileUrlCache = FragmentedStorage.get<CachedPolycentricProfileStorage>("profileUrlCache")
}
Logger.i(TAG, "Initialized Polycentric cache (${_profileUrlCache.map.size}, ${time}ms)");
}
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope,
{ system ->
@@ -222,7 +230,7 @@ class PolycentricCache {
}
}
suspend fun getProfileAsync(id: PlatformID): CachedPolycentricProfile? {
suspend fun getProfileAsync(id: PlatformID, urlNullCache: String? = null): CachedPolycentricProfile? {
if (!StatePolycentric.instance.enabled || id.claimType <= 0) {
return CachedPolycentricProfile(null);
}
@@ -243,6 +251,8 @@ class PolycentricCache {
Logger.v(TAG, "getProfileAsync (id: $id) != null (with retrieved valid claims)")
return getProfileAsync(claims.ownedClaims.first().system).await()
} else {
if(urlNullCache != null)
_profileUrlCache.setAndSave(urlNullCache, PolycentricCache.CachedPolycentricProfile(null));
return null;
}
}
@@ -6,6 +6,9 @@ import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.futo.platformplayer.*
@@ -250,7 +253,11 @@ class DownloadService : Service() {
val notif = builder.build();
notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR;
startForeground(DOWNLOAD_NOTIF_ID, notif);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(DOWNLOAD_NOTIF_ID, notif, FOREGROUND_SERVICE_TYPE_DATA_SYNC);
} else {
startForeground(DOWNLOAD_NOTIF_ID, notif);
}
}
fun closeDownloadSession() {
@@ -7,6 +7,8 @@ import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.content.FileProvider
@@ -173,7 +175,11 @@ class ExportingService : Service() {
val notif = builder.build();
notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR;
startForeground(EXPORT_NOTIF_ID, notif);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(EXPORT_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
} else {
startForeground(EXPORT_NOTIF_ID, notif);
}
}
fun closeExportSession() {
@@ -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) {
@@ -1,6 +1,7 @@
package com.futo.platformplayer.states
import android.content.Context
import kotlin.streams.asSequence
import kotlin.streams.toList
/***
@@ -26,7 +27,7 @@ class StateAssets {
else
break;
}
return (parts1 + parts2.stream().skip(toSkip.toLong()).toList()).joinToString("/");
return (parts1 + parts2.stream().skip(toSkip.toLong()).asSequence().toList()).joinToString("/");
}
/**
@@ -99,7 +99,7 @@ class StateCache {
if(existing != null && doUpdate) {
_subscriptionCache.update(existing.id!!, serialized);
return true;
return false;
}
else if(existing == null) {
_subscriptionCache.insert(serialized);
@@ -609,7 +609,7 @@ class StatePlatform {
//Video
fun hasEnabledVideoClient(url: String) : Boolean = getEnabledClients().any { it.isContentDetailsUrl(url) };
fun getContentClient(url: String) : IPlatformClient = getContentClientOrNull(url)
?: throw NoPlatformClientException("No client enabled that supports this channel url (${url})");
?: throw NoPlatformClientException("No client enabled that supports this content url (${url})");
fun getContentClientOrNull(url: String) : IPlatformClient? = getEnabledClients().find { it.isContentDetailsUrl(url) };
fun getContentDetails(url: String, forceRefetch: Boolean = false): Deferred<IPlatformContentDetails> {
Logger.i(TAG, "Platform - getContentDetails (${url})");
@@ -2,6 +2,8 @@ package com.futo.platformplayer.states
import android.content.Context
import android.util.Log
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
@@ -55,12 +57,16 @@ class StatePlayer {
var queueShuffle: Boolean = false
private set;
val hasQueue: Boolean get() {
val queueSize: Int get() {
synchronized(_queue) {
return _queue.isNotEmpty()
return _queue.size
}
}
val hasQueue: Boolean get() {
return queueSize > 1
}
val queueName: String get() = _queueName ?: _queueType;
//Events
@@ -268,7 +274,12 @@ class StatePlayer {
setQueueWithPosition(videos, _queueType, index, withFocus);
}
fun addToQueue(video: IPlatformVideo) {
var didAdd = false;
synchronized(_queue) {
if(_queue.any { it.url == video.url }) {
return@synchronized;
}
if(_queue.isEmpty()) {
setQueueType(TYPE_QUEUE);
currentVideo?.let {
@@ -284,8 +295,19 @@ class StatePlayer {
if (_queuePosition < 0) {
_queuePosition = 0;
}
didAdd = true;
}
onQueueChanged.emit(true);
if(didAdd) {
onQueueChanged.emit(true);
StateApp.instance.contextOrNull?.let { context ->
val name = if (video.name.length > 20) (video.name.subSequence(0, 20).toString() + "...") else video.name;
UIDialogs.toast(context, context.getString(R.string.queued) + " [$name]", false);
}
}
else
StateApp.instance.contextOrNull?.let { context ->
UIDialogs.toast(context, context.getString(R.string.already_queued), false);
}
}
fun insertToQueue(video: IPlatformVideo, playNow: Boolean = false) {
synchronized(_queue) {
@@ -373,10 +395,43 @@ class StatePlayer {
return null;
}
fun getNextQueueItem() : IPlatformVideo? {
if(loopVideo)
return currentVideo;
/***
* Checks what the prev queue item would without consuming it.
* @param forceLoop If start of queue should be ignored and loop around to end without queueRepeat being true
*/
fun getPrevQueueItem(forceLoop: Boolean = false) : IPlatformVideo? {
synchronized(_queue) {
if(_queue.size == 1)
return null;
val shuffledQueue = _queueShuffled;
val queue = if (queueShuffle && shuffledQueue != null) {
shuffledQueue;
} else {
_queue;
}
//Init Behavior
if(_queuePosition == -1 && queue.isNotEmpty())
return queue[0];
//Standard Behavior
if(_queuePosition - 1 >= 0)
return queue[_queuePosition - 1];
//Repeat Behavior (End of queue)
if(_queuePosition - 1 < 0 && queue.isNotEmpty() && (forceLoop || queueRepeat))
return queue[_queue.size - 1];
}
return null;
}
/***
* Checks what the next queue item would without consuming it.
* @param forceLoop If end of queue should be ignored and loop around to start without queueRepeat being true
*/
fun getNextQueueItem(forceLoop: Boolean = false) : IPlatformVideo? {
synchronized(_queue) {
if(_queue.size == 1)
return null;
val shuffledQueue = _queueShuffled;
val queue = if (queueShuffle && shuffledQueue != null) {
shuffledQueue;
@@ -391,7 +446,7 @@ class StatePlayer {
if(_queuePosition + 1 < queue.size)
return queue[_queuePosition + 1];
//Repeat Behavior (End of queue)
if(_queuePosition + 1 == queue.size && queue.isNotEmpty() && queueRepeat)
if(_queuePosition + 1 == queue.size && queue.isNotEmpty() && (forceLoop || queueRepeat))
return queue[0];
}
return null;
@@ -399,11 +454,17 @@ class StatePlayer {
fun restartQueue() : IPlatformVideo? {
synchronized(_queue) {
_queuePosition = -1;
return nextQueueItem();
return nextQueueItem(false, true);
}
};
fun nextQueueItem(withoutRemoval: Boolean = false) : IPlatformVideo? {
if(loopVideo)
/***
* Triggers the next queue item, removing it depending on the queue type, should ONLY be used if you're directly consuming this item
* @param withoutRemoval Prevents the removal behavior of certain playlists, should be true for manual user actions like next
* @param bypassVideoLoop Bypasses any single-video-looping behavior, should be true for manual user actions like next
*/
fun nextQueueItem(withoutRemoval: Boolean = false, bypassVideoLoop: Boolean = false) : IPlatformVideo? {
if(loopVideo && !bypassVideoLoop)
return currentVideo;
synchronized(_queue) {
if (_queue.isEmpty())
@@ -438,6 +499,10 @@ class StatePlayer {
}
}
/***
* Triggers the prev queue item, removing it depending on the queue type
* @param withoutRemoval Prevents the removal behavior of certain playlists, should be true for manual user actions like next
*/
fun prevQueueItem(withoutRemoval: Boolean = false) : IPlatformVideo? {
synchronized(_queue) {
if (_queue.size == 0) {
@@ -169,18 +169,24 @@ class StatePolycentric {
}
}
fun getChannelUrls(url: String, channelId: PlatformID? = null, cacheOnly: Boolean = false): List<String> {
fun getChannelUrls(url: String, channelId: PlatformID? = null, cacheOnly: Boolean = false, doCacheNull: Boolean = false): List<String> {
return getChannelUrlsWithUpdateResult(url, channelId, cacheOnly, doCacheNull).second;
}
fun getChannelUrlsWithUpdateResult(url: String, channelId: PlatformID? = null, cacheOnly: Boolean = false, doCacheNull: Boolean = false): Pair<Boolean, List<String>> {
var didUpdate = false;
if (!enabled) {
return listOf(url);
return Pair(false, listOf(url));
}
var polycentricProfile: PolycentricProfile? = null;
try {
polycentricProfile = PolycentricCache.instance.getCachedProfile(url)?.profile;
if (polycentricProfile == null && channelId != null) {
val polycentricCached = PolycentricCache.instance.getCachedProfile(url, cacheOnly)
polycentricProfile = polycentricCached?.profile;
if (polycentricCached == null && channelId != null) {
Logger.i("StateSubscriptions", "Get polycentric profile not cached");
if(!cacheOnly)
polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(channelId) }?.profile;
if(!cacheOnly) {
polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(channelId, if(doCacheNull) url else null) }?.profile;
didUpdate = true;
}
} else {
Logger.i("StateSubscriptions", "Get polycentric profile cached");
}
@@ -193,12 +199,12 @@ class StatePolycentric {
val urls = polycentricProfile.ownedClaims.groupBy { it.claim.claimType }
.mapNotNull { it.value.firstOrNull()?.claim?.resolveChannelUrl() }.toMutableList();
if(urls.any { it.equals(url, true) })
return urls;
return Pair(didUpdate, urls);
else
return listOf(url) + urls;
return Pair(didUpdate, listOf(url) + urls);
}
else
return listOf(url);
return Pair(didUpdate, listOf(url));
}
fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? {
@@ -254,9 +254,18 @@ class StateSubscriptions {
}
val usePolycentric = true;
val lock = Object();
var polycentricBudget: Int = 10;
val subUrls = getSubscriptions().parallelStream().map {
if(usePolycentric)
Pair(it, StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id));
if(usePolycentric) {
val result = StatePolycentric.instance.getChannelUrlsWithUpdateResult(it.channel.url, it.channel.id, polycentricBudget <= 0, true);
if(result.first) {
synchronized(lock) {
polycentricBudget--;
}
}
Pair(it, result.second);
}
else
Pair(it, listOf(it.channel.url));
}.asSequence()
@@ -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;
}
})
@@ -106,6 +106,10 @@ class CastView : ConstraintLayout {
_gestureControlView.stopAllGestures();
}
fun setLoopVisible(visible: Boolean) {
_buttonLoop.visibility = if (visible) View.VISIBLE else View.GONE;
}
fun setIsPlaying(isPlaying: Boolean) {
_updateTimeJob?.cancel();
@@ -22,7 +22,10 @@ class QueueEditorOverlay : LinearLayout {
_topbar.onClose.subscribe(this, onClose::emit);
_editor.onVideoOrderChanged.subscribe { StatePlayer.instance.setQueueWithExisting(it) }
_editor.onVideoRemoved.subscribe { v -> StatePlayer.instance.removeFromQueue(v) }
_editor.onVideoRemoved.subscribe { v ->
StatePlayer.instance.removeFromQueue(v);
_topbar.setInfo(context.getString(R.string.queue), "${StatePlayer.instance.queueSize} " + context.getString(R.string.videos));
}
_editor.onVideoClicked.subscribe { v -> StatePlayer.instance.setQueuePosition(v) }
_topbar.setInfo(context.getString(R.string.queue), "");
@@ -54,10 +54,8 @@ class PillRatingLikesDislikes : LinearLayout {
_loaderViewLikes = findViewById(R.id.loader_likes)
_loaderViewDislikes = findViewById(R.id.loader_dislikes)
_iconLikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; };
_textLikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; };
_iconDislikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; };
_textDislikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; };
findViewById<LinearLayout>(R.id.layout_like).setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; };
findViewById<LinearLayout>(R.id.layout_dislike).setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; };
}
fun setLoading(loading: Boolean) {
@@ -35,7 +35,10 @@ import com.google.android.exoplayer2.ui.PlayerControlView
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.ui.TimeBar
import com.google.android.exoplayer2.video.VideoSize
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
@@ -300,6 +303,18 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
}
StatePlayer.instance.onQueueChanged.subscribe(this) {
CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main) {
setLoopVisible(!StatePlayer.instance.hasQueue)
updateNextPrevious();
}
}
StatePlayer.instance.onVideoChanging.subscribe(this) {
CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main) {
updateNextPrevious();
}
}
updateLoopVideoUI();
if(!isInEditMode) {
@@ -307,10 +322,14 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
}
/*fun updateNextPrevious(hasNext: Boolean, hasPrevious: Boolean) {
_buttonNext.visibility = if (hasNext) View.VISIBLE else View.GONE
_buttonPrevious.visibility = if (hasPrevious) View.VISIBLE else View.GONE
}*/
fun updateNextPrevious() {
val vidPrev = StatePlayer.instance.getPrevQueueItem(true);
val vidNext = StatePlayer.instance.getNextQueueItem(true);
_buttonNext.visibility = if (vidNext != null) View.VISIBLE else View.GONE
_buttonNext_fullscreen.visibility = if (vidNext != null) View.VISIBLE else View.GONE
_buttonPrevious.visibility = if (vidPrev != null) View.VISIBLE else View.GONE
_buttonPrevious_fullscreen.visibility = if (vidPrev != null) View.VISIBLE else View.GONE
}
private val _currentChapterUpdateInterval: Long = 1000L / Settings.instance.playback.getChapterUpdateFrames();
private var _currentChapterUpdateLastPos = 0L;
@@ -343,6 +362,11 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_currentChapterLoopActive = false;
}
fun setLoopVisible(visible: Boolean) {
_control_loop.visibility = if (visible) View.VISIBLE else View.GONE;
_control_loop_fullscreen.visibility = if (visible) View.VISIBLE else View.GONE;
}
fun stopAllGestures() {
gestureControl.stopAllGestures();
}
@@ -373,11 +397,11 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
fun setArtwork(drawable: Drawable?) {
if (drawable != null) {
_videoView.defaultArtwork = drawable;
_videoView.useArtwork = true;
_videoView.artworkDisplayMode = StyledPlayerView.ARTWORK_DISPLAY_MODE_FILL;
fitOrFill(isFullScreen);
} else {
_videoView.defaultArtwork = null;
_videoView.useArtwork = false;
_videoView.artworkDisplayMode = StyledPlayerView.ARTWORK_DISPLAY_MODE_OFF;
}
}
@@ -150,7 +150,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
Logger.v(TAG, "Attached onConnectionAvailable listener.");
StateApp.instance.onConnectionAvailable.subscribe(_referenceObject) {
Logger.v(TAG, "onConnectionAvailable");
Logger.v(TAG, "onConnectionAvailable connectivityLossTime = $_connectivityLossTime_ms, position = $position, duration = $duration");
val pos = position;
val dur = duration;
@@ -158,25 +158,37 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
if (_shouldPlaybackRestartOnConnectivity && abs(pos - dur) > 2000) {
if (Settings.instance.playback.restartPlaybackAfterConnectivityLoss == 1) {
val lossTime_ms = _connectivityLossTime_ms
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 30) {
shouldRestartPlayback = true
if (lossTime_ms != null) {
val lossDuration_ms = System.currentTimeMillis() - lossTime_ms
Logger.v(TAG, "onConnectionAvailable lossDuration=$lossDuration_ms")
if (lossDuration_ms < 1000 * 10) {
shouldRestartPlayback = true
}
}
} else if (Settings.instance.playback.restartPlaybackAfterConnectivityLoss == 2) {
val lossTime_ms = _connectivityLossTime_ms
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 10) {
shouldRestartPlayback = true
if (lossTime_ms != null) {
val lossDuration_ms = System.currentTimeMillis() - lossTime_ms
Logger.v(TAG, "onConnectionAvailable lossDuration=$lossDuration_ms")
if (lossDuration_ms < 1000 * 30) {
shouldRestartPlayback = true
}
}
} else if (Settings.instance.playback.restartPlaybackAfterConnectivityLoss == 3) {
shouldRestartPlayback = true
}
}
Logger.v(TAG, "onConnectionAvailable shouldRestartPlayback = $shouldRestartPlayback");
if (shouldRestartPlayback) {
Logger.i(TAG, "Playback ended due to connection loss, resuming playback since connection is restored.");
exoPlayer?.player?.playWhenReady = true;
exoPlayer?.player?.prepare();
exoPlayer?.player?.play();
}
_connectivityLossTime_ms = null;
};
}
@@ -524,6 +536,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
}
protected open fun onPlayerError(error: PlaybackException) {
Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss");
when (error.errorCode) {
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
onDatasourceError.emit(error);
@@ -536,9 +550,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
//PlaybackException.ERROR_CODE_IO_NO_PERMISSION,
//PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED -> {
Logger.i(TAG, "IO error, set _shouldPlaybackRestartOnConnectivity=true");
_shouldPlaybackRestartOnConnectivity = true;
_connectivityLossTime_ms = System.currentTimeMillis()
Logger.i(TAG, "IO error, set _shouldPlaybackRestartOnConnectivity=true _connectivityLossTime_ms=$_connectivityLossTime_ms");
}
}
}
@@ -71,6 +71,16 @@
android:paddingTop="10dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="10dp">
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_import_platform"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleY="0.9"
android:scaleX="0.9"
app:buttonIcon="@drawable/ic_sources_filled"
app:buttonText="Import from Platform"
app:buttonBackground="@drawable/background_big_button_black"
app:buttonSubText="Import your data from a specific source" />
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_import_zip"
android:layout_width="match_parent"
+1 -1
View File
@@ -45,7 +45,7 @@
android:layout_height="wrap_content"
android:fontFamily="@font/inter_light"
android:textSize="15dp"
android:text="(0/0)"
android:text=""
android:textColor="#58595B" />
<!--
+4 -3
View File
@@ -70,10 +70,11 @@
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintLeft_toLeftOf="@id/text_body"
app:layout_constraintTop_toBottomOf="@id/text_body"
android:layout_marginLeft="-10dp"
android:layout_marginTop="8dp"
android:gravity="center_vertical">
@@ -83,7 +84,7 @@
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginStart="10dp" />
android:layout_marginStart="9dp" />
<LinearLayout
android:id="@+id/layout_rating"
@@ -4,56 +4,78 @@
android:layout_height="32dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:paddingTop="6dp"
android:paddingBottom="7dp"
android:paddingLeft="7dp"
android:paddingRight="12dp"
android:background="@drawable/background_pill"
android:gravity="center_vertical">
<ImageView
android:id="@+id/pill_like_icon"
android:layout_width="30dp"
android:layout_height="18dp"
app:srcCompat="@drawable/ic_thumb_up" />
<TextView
android:id="@+id/pill_likes"
<LinearLayout
android:id="@+id/layout_like"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/white"
android:textSize="13dp"
android:gravity="center_vertical"
tools:text="500K" />
<com.futo.platformplayer.views.LoaderView
android:id="@+id/loader_likes"
android:layout_width="14dp"
android:layout_height="14dp"
app:isWhite="true" />
android:orientation="horizontal"
android:paddingStart="7dp"
android:paddingTop="6dp"
android:paddingBottom="7dp"
android:paddingEnd="8dp">
<ImageView
android:id="@+id/pill_like_icon"
android:layout_width="30dp"
android:layout_height="18dp"
app:srcCompat="@drawable/ic_thumb_up" />
<TextView
android:id="@+id/pill_likes"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/white"
android:textSize="13dp"
android:gravity="center_vertical"
tools:text="50K" />
<com.futo.platformplayer.views.LoaderView
android:id="@+id/loader_likes"
android:layout_width="14dp"
android:layout_height="14dp"
app:isWhite="true" />
</LinearLayout>
<View
android:id="@+id/pill_seperator"
android:layout_width="1dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="7dp"
android:layout_marginBottom="7dp"
android:background="#808080"/>
<ImageView
android:id="@+id/pill_dislike_icon"
android:layout_width="30dp"
android:layout_height="18dp"
android:layout_marginTop="2dp"
app:srcCompat="@drawable/ic_thumb_down" />
<TextView
android:id="@+id/pill_dislikes"
<LinearLayout
android:id="@+id/layout_dislike"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/white"
android:gravity="center_vertical"
android:textSize="13dp"
tools:text="500K" />
<com.futo.platformplayer.views.LoaderView
android:id="@+id/loader_dislikes"
android:layout_width="14dp"
android:layout_height="14dp"
app:isWhite="true" />
android:orientation="horizontal"
android:paddingEnd="12dp"
android:paddingTop="6dp"
android:paddingBottom="7dp">
<ImageView
android:id="@+id/pill_dislike_icon"
android:layout_width="30dp"
android:layout_height="18dp"
android:layout_marginTop="2dp"
app:srcCompat="@drawable/ic_thumb_down" />
<TextView
android:id="@+id/pill_dislikes"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/white"
android:gravity="center_vertical"
android:textSize="13dp"
tools:text="50K" />
<com.futo.platformplayer.views.LoaderView
android:id="@+id/loader_dislikes"
android:layout_width="14dp"
android:layout_height="14dp"
app:isWhite="true" />
</LinearLayout>
</LinearLayout>
+2
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>
@@ -576,6 +577,7 @@
<string name="import_your_subscriptions_from_this_source">Import your subscriptions from this source</string>
<string name="import_your_playlists_from_this_source">Import your playlists from this source</string>
<string name="login">Login</string>
<string name="login_required">Login Required</string>
<string name="sign_into_the_platform_of_this_source">Sign into the platform of this source</string>
<string name="management">Management</string>
<string name="uninstall">Uninstall</string>
+16 -3
View File
@@ -24,6 +24,12 @@
<data android:host="www.youtube.com" />
<data android:host="m.youtube.com" />
<data android:host="rumble.com" />
<data android:host="kick.com" />
<data android:host="nebula.tv" />
<data android:host="odysee.com" />
<data android:host="patreon.com" />
<data android:host="soundcloud.com" />
<data android:host="twitch.tv" />
<data android:pathPrefix="/" />
</intent-filter>
<intent-filter android:autoVerify="true">
@@ -33,11 +39,18 @@
<data android:mimeType="text/plain" />
<data android:host="youtube.com" />
<data android:host="m.youtube.com" />
<data android:host="you.be" />
<data android:host="youtu.be" />
<data android:host="www.you.be" />
<data android:host="youtube.com" />
<data android:host="www.youtube.com" />
<data android:host="m.youtube.com" />
<data android:host="rumble.com" />
<data android:host="kick.com" />
<data android:host="nebula.tv" />
<data android:host="odysee.com" />
<data android:host="patreon.com" />
<data android:host="soundcloud.com" />
<data android:host="twitch.tv" />
</intent-filter>
</activity>
</application>
+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')
}
}