mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-17 05:22:40 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b20b4909f | |||
| 71a3828fe4 | |||
| d713f2bd55 | |||
| 069a615193 | |||
| f7d2cb4055 | |||
| f109d82537 | |||
| ab49d4749b | |||
| 507eed4f53 | |||
| 23ca4addf9 | |||
| 331ed09775 | |||
| 85303b54bc | |||
| f224cd1ca5 | |||
| d433d6e774 | |||
| 90de54ac5c | |||
| 5ff8f1ba6d | |||
| bc00b12b8c | |||
| 1c0cfa89a3 | |||
| efa1361fbe |
+43
-36
@@ -1,11 +1,11 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
||||
id 'org.ajoberstar.grgit' version '1.7.2'
|
||||
id 'com.google.protobuf'
|
||||
id 'kotlin-parcelize'
|
||||
id 'kotlin-kapt'
|
||||
id 'com.google.devtools.ksp'
|
||||
}
|
||||
|
||||
ext {
|
||||
@@ -24,7 +24,7 @@ if (keystorePropertiesFile.exists()) {
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.22.3'
|
||||
artifact = 'com.google.protobuf:protoc:3.25.1'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
@@ -97,11 +97,15 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
minSdk 28
|
||||
targetSdk 33
|
||||
targetSdk 34
|
||||
versionCode gitVersionCode
|
||||
versionName gitVersionName
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -137,43 +141,46 @@ android {
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
//Core
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
|
||||
//Images
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1'
|
||||
implementation 'com.github.bumptech.glide:glide:4.15.1'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.16.0'
|
||||
|
||||
//Async
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
||||
|
||||
//HTTP
|
||||
implementation "com.squareup.okhttp3:okhttp:4.10.0"
|
||||
implementation "com.squareup.okhttp3:okhttp:4.11.0"
|
||||
|
||||
//JSON
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" //Used for structured json
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" //Used for structured json
|
||||
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
||||
|
||||
//JS
|
||||
implementation("com.caoccao.javet:javet-android:2.2.1")
|
||||
|
||||
//Exoplayer
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.18.7'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.18.7'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.18.7'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.18.7'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-rtsp:2.18.7'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.18.7'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-transformer:2.18.7'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.19.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-rtsp:2.19.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.19.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-transformer:2.19.1'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'
|
||||
|
||||
//Other
|
||||
implementation 'org.jmdns:jmdns:3.5.1'
|
||||
@@ -181,34 +188,34 @@ dependencies {
|
||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||
implementation 'com.google.zxing:core:3.4.1'
|
||||
implementation 'com.journeyapps:zxing-android-embedded:4.2.0'
|
||||
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
|
||||
//Protobuf
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.22.3'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
|
||||
|
||||
implementation 'com.polycentric.core:app:1.0'
|
||||
implementation 'com.futo.futopay:app:1.0'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
||||
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
|
||||
|
||||
//Database
|
||||
implementation("androidx.room:room-runtime:2.6.0")
|
||||
annotationProcessor("androidx.room:room-compiler:2.6.0")
|
||||
kapt("androidx.room:room-compiler:2.6.0")
|
||||
implementation("androidx.room:room-ktx:2.6.0")
|
||||
implementation("androidx.room:room-runtime:2.6.1")
|
||||
annotationProcessor("androidx.room:room-compiler:2.6.1")
|
||||
ksp("androidx.room:room-compiler:2.6.1")
|
||||
implementation("androidx.room:room-ktx:2.6.1")
|
||||
|
||||
//Payment
|
||||
implementation 'com.stripe:stripe-android:20.28.3'
|
||||
implementation 'com.stripe:stripe-android:20.35.1'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2'
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.20"
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.22"
|
||||
testImplementation "org.xmlunit:xmlunit-core:2.9.1"
|
||||
testImplementation "org.mockito:mockito-core:5.4.0"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
}
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
}
|
||||
|
||||
-2
@@ -207,8 +207,6 @@ class ChannelFragment : MainFragment() {
|
||||
adapter.onAddToQueueClicked.subscribe { content ->
|
||||
if(content is IPlatformVideo) {
|
||||
StatePlayer.instance.addToQueue(content);
|
||||
val name = if (content.name.length > 20) (content.name.subSequence(0, 20).toString() + "...") else content.name;
|
||||
UIDialogs.toast(context, "Queued [$name]", false);
|
||||
}
|
||||
}
|
||||
adapter.onUrlClicked.subscribe { url ->
|
||||
|
||||
-2
@@ -79,8 +79,6 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
adapter.onAddToQueueClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlayer.instance.addToQueue(it);
|
||||
val name = if (it.name.length > 20) (it.name.subSequence(0, 20).toString() + "...") else it.name;
|
||||
UIDialogs.toast(context, context.getString(R.string.queued) + " [$name]", false);
|
||||
}
|
||||
};
|
||||
adapter.onLongPress.subscribe(this) {
|
||||
|
||||
+2
@@ -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);
|
||||
|
||||
+16
-52
@@ -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(); };
|
||||
@@ -546,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) { 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()
|
||||
@@ -1332,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) {
|
||||
@@ -1542,63 +1543,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;
|
||||
|
||||
@@ -222,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);
|
||||
}
|
||||
@@ -243,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) {
|
||||
|
||||
@@ -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
|
||||
@@ -268,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 {
|
||||
@@ -284,8 +291,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 +391,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 +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;
|
||||
@@ -399,11 +450,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 +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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
+3
-2
@@ -214,11 +214,12 @@ class PreviewPostView : LinearLayout {
|
||||
.load(image)
|
||||
.placeholder(R.drawable.placeholder_video_thumbnail)
|
||||
.listener(object: RequestListener<Drawable> {
|
||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
|
||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>, isFirstResource: Boolean): Boolean {
|
||||
imageImage.visibility = View.GONE;
|
||||
return false;
|
||||
}
|
||||
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
|
||||
|
||||
override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable>?, dataSource: DataSource, isFirstResource: Boolean): Boolean {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,17 @@ 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) {
|
||||
@@ -307,10 +321,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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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>
|
||||
@@ -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>
|
||||
|
||||
Submodule app/src/stable/assets/sources/odysee updated: a05feced80...a21ad56829
Submodule app/src/unstable/assets/sources/odysee updated: a05feced80...a21ad56829
+6
-5
@@ -1,7 +1,8 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version '7.4.2' apply false
|
||||
id 'com.android.library' version '7.4.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
|
||||
id 'com.google.protobuf' version '0.9.3' apply false
|
||||
}
|
||||
id 'com.android.application' version '8.2.0' apply false
|
||||
id 'com.android.library' version '8.2.0' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
|
||||
id 'com.google.protobuf' version '0.9.4' apply false
|
||||
id 'com.google.devtools.ksp' version '1.9.0-1.0.13' apply false
|
||||
}
|
||||
|
||||
+1
-1
Submodule dep/futopay updated: 68c9ae36fd...4268917697
+1
-1
Submodule dep/polycentricandroid updated: 62328514ec...86cd96c41f
+1
-1
@@ -21,4 +21,4 @@ kotlin.code.style=official
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
android.nonFinalResIds=false
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#Fri Nov 11 13:25:09 CET 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
+2
-2
@@ -16,13 +16,13 @@ dependencyResolutionManagement {
|
||||
|
||||
includeBuild('dep/polycentricandroid') {
|
||||
dependencySubstitution {
|
||||
substitute module('com.polycentric.core:app') with project(':app')
|
||||
substitute module('com.polycentric.core:app') using project(':app')
|
||||
}
|
||||
}
|
||||
|
||||
includeBuild('dep/futopay/android') {
|
||||
dependencySubstitution {
|
||||
substitute module('com.futo.futopay:app') with project(':app')
|
||||
substitute module('com.futo.futopay:app') using project(':app')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user