mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-24 05:24:45 +00:00
Compare commits
No commits in common. "master" and "283" have entirely different histories.
198 changed files with 2126 additions and 5344 deletions
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -88,9 +88,3 @@
|
||||||
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
|
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
|
||||||
path = app/src/unstable/assets/sources/apple-podcasts
|
path = app/src/unstable/assets/sources/apple-podcasts
|
||||||
url = ../plugins/apple-podcasts.git
|
url = ../plugins/apple-podcasts.git
|
||||||
[submodule "app/src/stable/assets/sources/tedtalks"]
|
|
||||||
path = app/src/stable/assets/sources/tedtalks
|
|
||||||
url = ../plugins/tedtalks.git
|
|
||||||
[submodule "app/src/unstable/assets/sources/tedtalks"]
|
|
||||||
path = app/src/unstable/assets/sources/tedtalks
|
|
||||||
url = ../plugins/tedtalks.git
|
|
||||||
|
|
|
@ -197,7 +197,7 @@ dependencies {
|
||||||
implementation 'org.jsoup:jsoup:1.15.3'
|
implementation 'org.jsoup:jsoup:1.15.3'
|
||||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS'
|
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||||
implementation 'com.google.zxing:core:3.4.1'
|
implementation 'com.google.zxing:core:3.4.1'
|
||||||
|
|
|
@ -1,266 +0,0 @@
|
||||||
package com.futo.platformplayer
|
|
||||||
|
|
||||||
import com.futo.platformplayer.noise.protocol.Noise
|
|
||||||
import com.futo.platformplayer.sync.internal.*
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import org.junit.Assert.*
|
|
||||||
import org.junit.Test
|
|
||||||
import java.net.Socket
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import kotlin.random.Random
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
class SyncServerTests {
|
|
||||||
|
|
||||||
//private val relayHost = "relay.grayjay.app"
|
|
||||||
//private val relayKey = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw="
|
|
||||||
private val relayKey = "XlUaSpIlRaCg0TGzZ7JYmPupgUHDqTZXUUBco2K7ejw="
|
|
||||||
private val relayHost = "192.168.1.175"
|
|
||||||
private val relayPort = 9000
|
|
||||||
|
|
||||||
/** Creates a client connected to the live relay server. */
|
|
||||||
private suspend fun createClient(
|
|
||||||
onHandshakeComplete: ((SyncSocketSession) -> Unit)? = null,
|
|
||||||
onData: ((SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit)? = null,
|
|
||||||
onNewChannel: ((SyncSocketSession, ChannelRelayed) -> Unit)? = null,
|
|
||||||
isHandshakeAllowed: ((SyncSocketSession, String, String?) -> Boolean)? = null
|
|
||||||
): SyncSocketSession = withContext(Dispatchers.IO) {
|
|
||||||
val p = Noise.createDH("25519")
|
|
||||||
p.generateKeyPair()
|
|
||||||
val socket = Socket(relayHost, relayPort)
|
|
||||||
val inputStream = LittleEndianDataInputStream(socket.getInputStream())
|
|
||||||
val outputStream = LittleEndianDataOutputStream(socket.getOutputStream())
|
|
||||||
val tcs = CompletableDeferred<Boolean>()
|
|
||||||
val socketSession = SyncSocketSession(
|
|
||||||
relayHost,
|
|
||||||
p,
|
|
||||||
inputStream,
|
|
||||||
outputStream,
|
|
||||||
onClose = { socket.close() },
|
|
||||||
onHandshakeComplete = { s ->
|
|
||||||
onHandshakeComplete?.invoke(s)
|
|
||||||
tcs.complete(true)
|
|
||||||
},
|
|
||||||
onData = onData ?: { _, _, _, _ -> },
|
|
||||||
onNewChannel = onNewChannel ?: { _, _ -> },
|
|
||||||
isHandshakeAllowed = isHandshakeAllowed ?: { _, _, _ -> true }
|
|
||||||
)
|
|
||||||
socketSession.authorizable = AlwaysAuthorized()
|
|
||||||
socketSession.startAsInitiator(relayKey)
|
|
||||||
withTimeout(5000.milliseconds) { tcs.await() }
|
|
||||||
return@withContext socketSession
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun multipleClientsHandshake_Success() = runBlocking {
|
|
||||||
val client1 = createClient()
|
|
||||||
val client2 = createClient()
|
|
||||||
assertNotNull(client1.remotePublicKey, "Client 1 handshake failed")
|
|
||||||
assertNotNull(client2.remotePublicKey, "Client 2 handshake failed")
|
|
||||||
client1.stop()
|
|
||||||
client2.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun publishAndRequestConnectionInfo_Authorized_Success() = runBlocking {
|
|
||||||
val clientA = createClient()
|
|
||||||
val clientB = createClient()
|
|
||||||
val clientC = createClient()
|
|
||||||
clientA.publishConnectionInformation(arrayOf(clientB.localPublicKey), 12345, true, true, true, true)
|
|
||||||
delay(100.milliseconds)
|
|
||||||
val infoB = clientB.requestConnectionInfo(clientA.localPublicKey)
|
|
||||||
val infoC = clientC.requestConnectionInfo(clientA.localPublicKey)
|
|
||||||
assertNotNull("Client B should receive connection info", infoB)
|
|
||||||
assertEquals(12345.toUShort(), infoB!!.port)
|
|
||||||
assertNull("Client C should not receive connection info (unauthorized)", infoC)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
clientC.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun relayedTransport_Bidirectional_Success() = runBlocking {
|
|
||||||
val tcsA = CompletableDeferred<ChannelRelayed>()
|
|
||||||
val tcsB = CompletableDeferred<ChannelRelayed>()
|
|
||||||
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
|
|
||||||
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
|
|
||||||
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
|
|
||||||
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
|
|
||||||
channelA.authorizable = AlwaysAuthorized()
|
|
||||||
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
|
|
||||||
channelB.authorizable = AlwaysAuthorized()
|
|
||||||
channelTask.await()
|
|
||||||
|
|
||||||
val tcsDataB = CompletableDeferred<ByteArray>()
|
|
||||||
channelB.setDataHandler { _, _, o, so, d ->
|
|
||||||
val b = ByteArray(d.remaining())
|
|
||||||
d.get(b)
|
|
||||||
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
|
|
||||||
}
|
|
||||||
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(1, 2, 3)))
|
|
||||||
|
|
||||||
val tcsDataA = CompletableDeferred<ByteArray>()
|
|
||||||
channelA.setDataHandler { _, _, o, so, d ->
|
|
||||||
val b = ByteArray(d.remaining())
|
|
||||||
d.get(b)
|
|
||||||
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataA.complete(b)
|
|
||||||
}
|
|
||||||
channelB.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(4, 5, 6)))
|
|
||||||
|
|
||||||
val receivedB = withTimeout(5000.milliseconds) { tcsDataB.await() }
|
|
||||||
val receivedA = withTimeout(5000.milliseconds) { tcsDataA.await() }
|
|
||||||
assertArrayEquals(byteArrayOf(1, 2, 3), receivedB)
|
|
||||||
assertArrayEquals(byteArrayOf(4, 5, 6), receivedA)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun relayedTransport_MaximumMessageSize_Success() = runBlocking {
|
|
||||||
val MAX_DATA_PER_PACKET = SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE - 8 - 16 - 16
|
|
||||||
val maxSizeData = ByteArray(MAX_DATA_PER_PACKET).apply { Random.nextBytes(this) }
|
|
||||||
val tcsA = CompletableDeferred<ChannelRelayed>()
|
|
||||||
val tcsB = CompletableDeferred<ChannelRelayed>()
|
|
||||||
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
|
|
||||||
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
|
|
||||||
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
|
|
||||||
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
|
|
||||||
channelA.authorizable = AlwaysAuthorized()
|
|
||||||
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
|
|
||||||
channelB.authorizable = AlwaysAuthorized()
|
|
||||||
channelTask.await()
|
|
||||||
|
|
||||||
val tcsDataB = CompletableDeferred<ByteArray>()
|
|
||||||
channelB.setDataHandler { _, _, o, so, d ->
|
|
||||||
val b = ByteArray(d.remaining())
|
|
||||||
d.get(b)
|
|
||||||
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
|
|
||||||
}
|
|
||||||
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxSizeData))
|
|
||||||
val receivedData = withTimeout(5000.milliseconds) { tcsDataB.await() }
|
|
||||||
assertArrayEquals(maxSizeData, receivedData)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun publishAndGetRecord_Success() = runBlocking {
|
|
||||||
val clientA = createClient()
|
|
||||||
val clientB = createClient()
|
|
||||||
val clientC = createClient()
|
|
||||||
val data = byteArrayOf(1, 2, 3)
|
|
||||||
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "testKey", data)
|
|
||||||
val recordB = clientB.getRecord(clientA.localPublicKey, "testKey")
|
|
||||||
val recordC = clientC.getRecord(clientA.localPublicKey, "testKey")
|
|
||||||
assertTrue(success)
|
|
||||||
assertNotNull(recordB)
|
|
||||||
assertArrayEquals(data, recordB!!.first)
|
|
||||||
assertNull("Unauthorized client should not access record", recordC)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
clientC.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun getNonExistentRecord_ReturnsNull() = runBlocking {
|
|
||||||
val clientA = createClient()
|
|
||||||
val clientB = createClient()
|
|
||||||
val record = clientB.getRecord(clientA.localPublicKey, "nonExistentKey")
|
|
||||||
assertNull("Getting non-existent record should return null", record)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun updateRecord_TimestampUpdated() = runBlocking {
|
|
||||||
val clientA = createClient()
|
|
||||||
val clientB = createClient()
|
|
||||||
val key = "updateKey"
|
|
||||||
val data1 = byteArrayOf(1)
|
|
||||||
val data2 = byteArrayOf(2)
|
|
||||||
clientA.publishRecords(listOf(clientB.localPublicKey), key, data1)
|
|
||||||
val record1 = clientB.getRecord(clientA.localPublicKey, key)
|
|
||||||
delay(1000.milliseconds)
|
|
||||||
clientA.publishRecords(listOf(clientB.localPublicKey), key, data2)
|
|
||||||
val record2 = clientB.getRecord(clientA.localPublicKey, key)
|
|
||||||
assertNotNull(record1)
|
|
||||||
assertNotNull(record2)
|
|
||||||
assertTrue(record2!!.second > record1!!.second)
|
|
||||||
assertArrayEquals(data2, record2.first)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun deleteRecord_Success() = runBlocking {
|
|
||||||
val clientA = createClient()
|
|
||||||
val clientB = createClient()
|
|
||||||
val data = byteArrayOf(1, 2, 3)
|
|
||||||
clientA.publishRecords(listOf(clientB.localPublicKey), "toDelete", data)
|
|
||||||
val success = clientB.deleteRecords(clientA.localPublicKey, clientB.localPublicKey, listOf("toDelete"))
|
|
||||||
val record = clientB.getRecord(clientA.localPublicKey, "toDelete")
|
|
||||||
assertTrue(success)
|
|
||||||
assertNull(record)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun listRecordKeys_Success() = runBlocking {
|
|
||||||
val clientA = createClient()
|
|
||||||
val clientB = createClient()
|
|
||||||
val keys = arrayOf("key1", "key2", "key3")
|
|
||||||
keys.forEach { key ->
|
|
||||||
clientA.publishRecords(listOf(clientB.localPublicKey), key, byteArrayOf(1))
|
|
||||||
}
|
|
||||||
val listedKeys = clientB.listRecordKeys(clientA.localPublicKey, clientB.localPublicKey)
|
|
||||||
assertArrayEquals(keys, listedKeys.map { it.first }.toTypedArray())
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun singleLargeMessageViaRelayedChannel_Success() = runBlocking {
|
|
||||||
val largeData = ByteArray(100000).apply { Random.nextBytes(this) }
|
|
||||||
val tcsA = CompletableDeferred<ChannelRelayed>()
|
|
||||||
val tcsB = CompletableDeferred<ChannelRelayed>()
|
|
||||||
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
|
|
||||||
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
|
|
||||||
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
|
|
||||||
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
|
|
||||||
channelA.authorizable = AlwaysAuthorized()
|
|
||||||
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
|
|
||||||
channelB.authorizable = AlwaysAuthorized()
|
|
||||||
channelTask.await()
|
|
||||||
|
|
||||||
val tcsDataB = CompletableDeferred<ByteArray>()
|
|
||||||
channelB.setDataHandler { _, _, o, so, d ->
|
|
||||||
val b = ByteArray(d.remaining())
|
|
||||||
d.get(b)
|
|
||||||
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
|
|
||||||
}
|
|
||||||
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
|
|
||||||
val receivedData = withTimeout(10000.milliseconds) { tcsDataB.await() }
|
|
||||||
assertArrayEquals(largeData, receivedData)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun publishAndGetLargeRecord_Success() = runBlocking {
|
|
||||||
val largeData = ByteArray(1000000).apply { Random.nextBytes(this) }
|
|
||||||
val clientA = createClient()
|
|
||||||
val clientB = createClient()
|
|
||||||
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "largeRecord", largeData)
|
|
||||||
val record = clientB.getRecord(clientA.localPublicKey, "largeRecord")
|
|
||||||
assertTrue(success)
|
|
||||||
assertNotNull(record)
|
|
||||||
assertArrayEquals(largeData, record!!.first)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AlwaysAuthorized : IAuthorizable {
|
|
||||||
override val isAuthorized: Boolean get() = true
|
|
||||||
}
|
|
|
@ -156,6 +156,7 @@
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SettingsActivity"
|
android:name=".activities.SettingsActivity"
|
||||||
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.DeveloperActivity"
|
android:name=".activities.DeveloperActivity"
|
||||||
|
|
|
@ -263,10 +263,6 @@ class PlatformVideoDetails extends PlatformVideo {
|
||||||
this.rating = obj.rating ?? null; //IRating
|
this.rating = obj.rating ?? null; //IRating
|
||||||
this.subtitles = obj.subtitles ?? [];
|
this.subtitles = obj.subtitles ?? [];
|
||||||
this.isShort = !!obj.isShort ?? false;
|
this.isShort = !!obj.isShort ?? false;
|
||||||
|
|
||||||
if (obj.getContentRecommendations) {
|
|
||||||
this.getContentRecommendations = obj.getContentRecommendations
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -216,14 +216,9 @@ private fun ByteArray.toInetAddress(): InetAddress {
|
||||||
return InetAddress.getByAddress(this);
|
return InetAddress.getByAddress(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
|
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||||
val timeout = 2000
|
val timeout = 2000
|
||||||
|
|
||||||
|
|
||||||
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
|
|
||||||
if(addresses.isEmpty())
|
|
||||||
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
|
|
||||||
|
|
||||||
if (addresses.isEmpty()) {
|
if (addresses.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
import com.futo.polycentric.core.base64UrlToByteArray
|
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
@ -40,25 +40,33 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
|
||||||
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
|
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.getDataLinkFromUrl(): Protocol.URLInfoDataLink? {
|
|
||||||
val urlData = if (this.startsWith("polycentric://")) {
|
|
||||||
this.substring("polycentric://".length)
|
|
||||||
} else this;
|
|
||||||
|
|
||||||
val urlBytes = urlData.base64UrlToByteArray();
|
|
||||||
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
|
||||||
if (urlInfo.urlType != 4L) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
|
||||||
return dataLink
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Protocol.Claim.resolveChannelUrl(): String? {
|
fun Protocol.Claim.resolveChannelUrl(): String? {
|
||||||
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||||
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
||||||
|
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
||||||
|
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
|
||||||
|
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
|
||||||
|
addServer(PolycentricCache.SERVER)
|
||||||
|
}
|
||||||
|
|
||||||
|
val exceptions = fullyBackfillServers()
|
||||||
|
for (pair in exceptions) {
|
||||||
|
val server = pair.key
|
||||||
|
val exception = pair.value
|
||||||
|
|
||||||
|
StateAnnouncement.instance.registerAnnouncement(
|
||||||
|
"backfill-failed",
|
||||||
|
"Backfill failed",
|
||||||
|
"Failed to backfill server $server. $exception",
|
||||||
|
AnnouncementType.SESSION_RECURRING
|
||||||
|
);
|
||||||
|
|
||||||
|
Logger.e("Backfill", "Failed to backfill server $server.", exception)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -205,7 +205,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
var home = HomeSettings();
|
var home = HomeSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class HomeSettings {
|
class HomeSettings {
|
||||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 3)
|
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5)
|
||||||
@DropdownFieldOptionsId(R.array.feed_style)
|
@DropdownFieldOptionsId(R.array.feed_style)
|
||||||
var homeFeedStyle: Int = 1;
|
var homeFeedStyle: Int = 1;
|
||||||
|
|
||||||
|
@ -216,11 +216,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
return FeedStyle.THUMBNAIL;
|
return FeedStyle.THUMBNAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.show_home_filters, FieldForm.TOGGLE, R.string.show_home_filters_description, 4)
|
|
||||||
var showHomeFilters: Boolean = true;
|
|
||||||
@FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
|
|
||||||
var showHomeFiltersPluginNames: Boolean = false;
|
|
||||||
|
|
||||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||||
var previewFeedItems: Boolean = true;
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
|
@ -299,9 +294,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
||||||
var showSubscriptionGroups: Boolean = true;
|
var showSubscriptionGroups: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
|
|
||||||
var useSubscriptionExchange: Boolean = false;
|
|
||||||
|
|
||||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||||
var previewFeedItems: Boolean = true;
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
|
@ -364,7 +356,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
var playback = PlaybackSettings();
|
var playback = PlaybackSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class PlaybackSettings {
|
class PlaybackSettings {
|
||||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -2)
|
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
|
||||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||||
var primaryLanguage: Int = 0;
|
var primaryLanguage: Int = 0;
|
||||||
|
|
||||||
|
@ -388,8 +380,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
|
|
||||||
var preferOriginalAudio: Boolean = true;
|
|
||||||
|
|
||||||
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
||||||
|
|
||||||
|
@ -583,15 +573,10 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var keepScreenOn: Boolean = true;
|
var keepScreenOn: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
|
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 1)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var alwaysProxyRequests: Boolean = false;
|
var alwaysProxyRequests: Boolean = false;
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
|
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
|
||||||
var allowIpv6: Boolean = false;
|
|
||||||
|
|
||||||
/*TODO: Should we have a different casting quality?
|
/*TODO: Should we have a different casting quality?
|
||||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
|
@ -936,15 +921,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
|
|
||||||
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
|
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
|
||||||
var connectLast: Boolean = true;
|
var connectLast: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.discover_through_relay, FieldForm.TOGGLE, R.string.discover_through_relay_description, 3)
|
|
||||||
var discoverThroughRelay: Boolean = true;
|
|
||||||
|
|
||||||
@FormField(R.string.pair_through_relay, FieldForm.TOGGLE, R.string.pair_through_relay_description, 3)
|
|
||||||
var pairThroughRelay: Boolean = true;
|
|
||||||
|
|
||||||
@FormField(R.string.connect_through_relay, FieldForm.TOGGLE, R.string.connect_through_relay_description, 3)
|
|
||||||
var connectThroughRelay: Boolean = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.Animatable
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.Layout
|
import android.text.Layout
|
||||||
import android.text.method.ScrollingMovementMethod
|
import android.text.method.ScrollingMovementMethod
|
||||||
|
@ -200,21 +199,16 @@ class UIDialogs {
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
|
||||||
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
|
|
||||||
}
|
|
||||||
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
|
||||||
val builder = AlertDialog.Builder(context);
|
val builder = AlertDialog.Builder(context);
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
||||||
builder.setView(view);
|
builder.setView(view);
|
||||||
builder.setCancelable(defaultCloseAction > -2);
|
|
||||||
val dialog = builder.create();
|
val dialog = builder.create();
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
|
|
||||||
view.findViewById<ImageView>(R.id.dialog_icon).apply {
|
view.findViewById<ImageView>(R.id.dialog_icon).apply {
|
||||||
this.setImageResource(icon);
|
this.setImageResource(icon);
|
||||||
if(animated)
|
|
||||||
this.drawable.assume<Animatable, Unit> { it.start() };
|
|
||||||
}
|
}
|
||||||
view.findViewById<TextView>(R.id.dialog_text).apply {
|
view.findViewById<TextView>(R.id.dialog_text).apply {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
|
@ -281,7 +275,6 @@ class UIDialogs {
|
||||||
registerDialogClosed(dialog);
|
registerDialogClosed(dialog);
|
||||||
}
|
}
|
||||||
dialog.show();
|
dialog.show();
|
||||||
return dialog;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) {
|
fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) {
|
||||||
|
|
|
@ -402,7 +402,7 @@ class UISlideOverlays {
|
||||||
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else if (source is IHLSManifestAudioSource) {
|
} else if (source is IHLSManifestAudioSource) {
|
||||||
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, sourceUrl), null)
|
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, sourceUrl), null)
|
||||||
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else {
|
} else {
|
||||||
|
@ -1075,9 +1075,8 @@ class UISlideOverlays {
|
||||||
StatePlayer.TYPE_WATCHLATER,
|
StatePlayer.TYPE_WATCHLATER,
|
||||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "watch later",
|
tag = "watch later",
|
||||||
call = {
|
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true);
|
||||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
UIDialogs.appToast("Added to watch later", false);
|
||||||
UIDialogs.appToast("Added to watch later", false);
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -1148,7 +1147,7 @@ class UISlideOverlays {
|
||||||
container.context.getString(R.string.decide_which_buttons_should_be_pinned),
|
container.context.getString(R.string.decide_which_buttons_should_be_pinned),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }, {
|
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
|
||||||
val selected = it
|
val selected = it
|
||||||
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
||||||
.filter { it != null }
|
.filter { it != null }
|
||||||
|
@ -1156,7 +1155,7 @@ class UISlideOverlays {
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
|
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
invokeParent = false
|
invokeParent = false
|
||||||
))
|
))
|
||||||
|
@ -1164,40 +1163,29 @@ class UISlideOverlays {
|
||||||
|
|
||||||
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
||||||
}
|
}
|
||||||
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit, description: String? = null) {
|
|
||||||
|
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit) {
|
||||||
val selection: MutableList<Any> = mutableListOf();
|
val selection: MutableList<Any> = mutableListOf();
|
||||||
|
|
||||||
var overlay: SlideUpMenuOverlay? = null;
|
var overlay: SlideUpMenuOverlay? = null;
|
||||||
|
|
||||||
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
|
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
|
||||||
listOf(
|
options.map { SlideUpMenuItem(
|
||||||
if(!description.isNullOrEmpty()) SlideUpMenuGroup(container.context, "", description, "", listOf()) else null,
|
|
||||||
).filterNotNull() +
|
|
||||||
(options.map { SlideUpMenuItem(
|
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_move_up,
|
R.drawable.ic_move_up,
|
||||||
it.first,
|
it.first,
|
||||||
"",
|
"",
|
||||||
tag = it.second,
|
tag = it.second,
|
||||||
call = {
|
call = {
|
||||||
val overlayItem = overlay?.getSlideUpItemByTag(it.second);
|
|
||||||
if(overlay!!.selectOption(null, it.second, true, true)) {
|
if(overlay!!.selectOption(null, it.second, true, true)) {
|
||||||
if(!selection.contains(it.second)) {
|
if(!selection.contains(it.second))
|
||||||
selection.add(it.second);
|
selection.add(it.second);
|
||||||
if(overlayItem != null) {
|
} else
|
||||||
overlayItem.setSubText(selection.indexOf(it.second).toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selection.remove(it.second);
|
selection.remove(it.second);
|
||||||
if(overlayItem != null) {
|
|
||||||
overlayItem.setSubText("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
invokeParent = false
|
invokeParent = false
|
||||||
)
|
)
|
||||||
}));
|
});
|
||||||
overlay.onOK.subscribe {
|
overlay.onOK.subscribe {
|
||||||
onOrdered.invoke(selection);
|
onOrdered.invoke(selection);
|
||||||
overlay.hide();
|
overlay.hide();
|
||||||
|
|
|
@ -27,17 +27,14 @@ import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.PlatformVideoWithTime
|
import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||||
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.security.SecureRandom
|
import java.nio.ByteOrder
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
import java.util.zip.GZIPInputStream
|
|
||||||
import java.util.zip.GZIPOutputStream
|
|
||||||
|
|
||||||
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
|
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
|
||||||
fun getRandomString(sizeOfRandomString: Int): String {
|
fun getRandomString(sizeOfRandomString: Int): String {
|
||||||
|
@ -282,46 +279,3 @@ fun ByteBuffer.toUtf8String(): String {
|
||||||
get(remainingBytes)
|
get(remainingBytes)
|
||||||
return String(remainingBytes, Charsets.UTF_8)
|
return String(remainingBytes, Charsets.UTF_8)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateReadablePassword(length: Int): String {
|
|
||||||
val validChars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"
|
|
||||||
val secureRandom = SecureRandom()
|
|
||||||
val randomBytes = ByteArray(length)
|
|
||||||
secureRandom.nextBytes(randomBytes)
|
|
||||||
val sb = StringBuilder(length)
|
|
||||||
for (byte in randomBytes) {
|
|
||||||
val index = (byte.toInt() and 0xFF) % validChars.length
|
|
||||||
sb.append(validChars[index])
|
|
||||||
}
|
|
||||||
return sb.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ByteArray.toGzip(): ByteArray {
|
|
||||||
if (this == null || this.isEmpty()) return ByteArray(0)
|
|
||||||
|
|
||||||
val gzipTimeStart = OffsetDateTime.now();
|
|
||||||
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
GZIPOutputStream(outputStream).use { gzip ->
|
|
||||||
gzip.write(this)
|
|
||||||
}
|
|
||||||
val result = outputStream.toByteArray();
|
|
||||||
Logger.i("Utility", "Gzip compression time: ${gzipTimeStart.getNowDiffMiliseconds()}ms");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ByteArray.fromGzip(): ByteArray {
|
|
||||||
if (this == null || this.isEmpty()) return ByteArray(0)
|
|
||||||
|
|
||||||
val inputStream = ByteArrayInputStream(this)
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
|
|
||||||
GZIPInputStream(inputStream).use { gzip ->
|
|
||||||
val buffer = ByteArray(1024)
|
|
||||||
var bytesRead: Int
|
|
||||||
while (gzip.read(buffer).also { bytesRead = it } != -1) {
|
|
||||||
outputStream.write(buffer, 0, bytesRead)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return outputStream.toByteArray()
|
|
||||||
}
|
|
|
@ -10,13 +10,11 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
|
|
||||||
class AddSourceOptionsActivity : AppCompatActivity() {
|
class AddSourceOptionsActivity : AppCompatActivity() {
|
||||||
lateinit var _buttonBack: ImageButton;
|
lateinit var _buttonBack: ImageButton;
|
||||||
|
|
||||||
lateinit var _overlayContainer: FrameLayout;
|
|
||||||
lateinit var _buttonQR: BigButton;
|
lateinit var _buttonQR: BigButton;
|
||||||
lateinit var _buttonBrowse: BigButton;
|
lateinit var _buttonBrowse: BigButton;
|
||||||
lateinit var _buttonURL: BigButton;
|
lateinit var _buttonURL: BigButton;
|
||||||
|
@ -56,7 +54,6 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||||
setContentView(R.layout.activity_add_source_options);
|
setContentView(R.layout.activity_add_source_options);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
_overlayContainer = findViewById(R.id.overlay_container);
|
|
||||||
_buttonBack = findViewById(R.id.button_back);
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
|
|
||||||
_buttonQR = findViewById(R.id.option_qr);
|
_buttonQR = findViewById(R.id.option_qr);
|
||||||
|
@ -84,25 +81,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonURL.onClick.subscribe {
|
_buttonURL.onClick.subscribe {
|
||||||
val nameInput = SlideUpMenuTextInput(this, "ex. https://yourplugin.com/config.json");
|
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
||||||
UISlideOverlays.showOverlay(_overlayContainer, "Enter your url", "Install", {
|
|
||||||
|
|
||||||
val content = nameInput.text;
|
|
||||||
|
|
||||||
val url = if (content.startsWith("https://")) {
|
|
||||||
content
|
|
||||||
} else if (content.startsWith("grayjay://plugin/")) {
|
|
||||||
content.substring("grayjay://plugin/".length)
|
|
||||||
} else {
|
|
||||||
UIDialogs.toast(this, getString(R.string.not_a_plugin_url))
|
|
||||||
return@showOverlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
|
||||||
data = Uri.parse(url);
|
|
||||||
};
|
|
||||||
startActivity(intent);
|
|
||||||
}, nameInput)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -113,7 +113,7 @@ class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "LoginActivity";
|
private val TAG = "LoginActivity";
|
||||||
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#:_ ]*");
|
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#_ ]*");
|
||||||
|
|
||||||
private var _callback: ((SourceAuth?) -> Unit)? = null;
|
private var _callback: ((SourceAuth?) -> Unit)? = null;
|
||||||
|
|
||||||
|
|
|
@ -11,16 +11,16 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.LoaderView
|
import com.futo.platformplayer.views.LoaderView
|
||||||
import com.futo.polycentric.core.ApiMethods
|
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -87,7 +87,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
||||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
processHandle.addServer(ApiMethods.SERVER);
|
processHandle.addServer(PolycentricCache.SERVER);
|
||||||
processHandle.setUsername(username);
|
processHandle.setUsername(username);
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|
|
@ -12,12 +12,12 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
import com.futo.polycentric.core.ApiMethods
|
|
||||||
import com.futo.polycentric.core.KeyPair
|
import com.futo.polycentric.core.KeyPair
|
||||||
import com.futo.polycentric.core.Process
|
import com.futo.polycentric.core.Process
|
||||||
import com.futo.polycentric.core.ProcessSecret
|
import com.futo.polycentric.core.ProcessSecret
|
||||||
|
@ -145,7 +145,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
processHandle.fullyBackfillClient(ApiMethods.SERVER);
|
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
||||||
finish();
|
finish();
|
||||||
|
|
|
@ -21,8 +21,10 @@ import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
@ -30,10 +32,8 @@ import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
import com.futo.polycentric.core.ApiMethods
|
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
import com.futo.polycentric.core.toBase64Url
|
import com.futo.polycentric.core.toBase64Url
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
|
@ -145,7 +145,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
processHandle.fullyBackfillClient(ApiMethods.SERVER)
|
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateUI();
|
updateUI();
|
||||||
|
|
|
@ -100,10 +100,8 @@ class SyncHomeActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
|
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
|
||||||
val connected = session?.connected ?: false
|
val connected = session?.connected ?: false
|
||||||
|
syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None)
|
||||||
syncDeviceView.setLinkType(session?.linkType ?: LinkType.None)
|
.setName(publicKey)
|
||||||
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
|
|
||||||
//TODO: also display public key?
|
|
||||||
.setStatus(if (connected) "Connected" else "Disconnected")
|
.setStatus(if (connected) "Connected" else "Disconnected")
|
||||||
return syncDeviceView
|
return syncDeviceView
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,9 +109,9 @@ class SyncPairActivity : AppCompatActivity() {
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
StateSync.instance.connect(deviceInfo) { complete, message ->
|
StateSync.instance.connect(deviceInfo) { session, complete, message ->
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
if (complete != null && complete) {
|
if (complete) {
|
||||||
_layoutPairingSuccess.visibility = View.VISIBLE
|
_layoutPairingSuccess.visibility = View.VISIBLE
|
||||||
_layoutPairing.visibility = View.GONE
|
_layoutPairing.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -67,7 +67,7 @@ class SyncShowPairingCodeActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val ips = getIPs()
|
val ips = getIPs()
|
||||||
val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT, StateSync.instance.pairingCode)
|
val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT)
|
||||||
val json = Json.encodeToString(selfDeviceInfo)
|
val json = Json.encodeToString(selfDeviceInfo)
|
||||||
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
||||||
val url = "grayjay://sync/${base64}"
|
val url = "grayjay://sync/${base64}"
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.futo.platformplayer.api.media
|
package com.futo.platformplayer.api.media
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
|
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
@ -67,11 +66,6 @@ interface IPlatformClient {
|
||||||
*/
|
*/
|
||||||
fun searchChannels(query: String): IPager<PlatformAuthorLink>;
|
fun searchChannels(query: String): IPager<PlatformAuthorLink>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches for channels and returns a content pager
|
|
||||||
*/
|
|
||||||
fun searchChannelsAsContent(query: String): IPager<IPlatformContent>;
|
|
||||||
|
|
||||||
|
|
||||||
//Video Pages
|
//Video Pages
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,10 +2,7 @@ package com.futo.platformplayer.api.media.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSContent
|
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
@ -45,21 +42,4 @@ open class PlatformAuthorLink {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
interface IPlatformChannelContent : IPlatformContent {
|
|
||||||
val thumbnail: String?
|
|
||||||
val subscribers: Long?
|
|
||||||
}
|
|
||||||
|
|
||||||
open class JSChannelContent : JSContent, IPlatformChannelContent {
|
|
||||||
override val contentType: ContentType get() = ContentType.CHANNEL
|
|
||||||
override val thumbnail: String?
|
|
||||||
override val subscribers: Long?
|
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
|
|
||||||
val contextName = "Channel";
|
|
||||||
thumbnail = obj.getOrDefault<String>(config, "thumbnail", contextName, null)
|
|
||||||
subscribers = if(obj.has("subscribers")) obj.getOrThrow(config,"subscribers", contextName) else null
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -12,7 +12,6 @@ enum class ContentType(val value: Int) {
|
||||||
URL(9),
|
URL(9),
|
||||||
|
|
||||||
NESTED_VIDEO(11),
|
NESTED_VIDEO(11),
|
||||||
CHANNEL(60),
|
|
||||||
|
|
||||||
LOCKED(70),
|
LOCKED(70),
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,6 @@ package com.futo.platformplayer.api.media.models.contents
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.JsonNames
|
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
interface IPlatformContent {
|
interface IPlatformContent {
|
||||||
|
|
|
@ -3,7 +3,7 @@ package com.futo.platformplayer.api.media.models.streams
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
class DownloadedVideoMuxedSourceDescriptor(
|
class LocalVideoMuxedSourceDescriptor(
|
||||||
private val video: VideoLocal
|
private val video: VideoLocal
|
||||||
) : VideoMuxedSourceDescriptor() {
|
) : VideoMuxedSourceDescriptor() {
|
||||||
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
|
@ -13,8 +13,7 @@ class AudioUrlSource(
|
||||||
override val codec: String = "",
|
override val codec: String = "",
|
||||||
override val language: String = Language.UNKNOWN,
|
override val language: String = Language.UNKNOWN,
|
||||||
override val duration: Long? = null,
|
override val duration: Long? = null,
|
||||||
override var priority: Boolean = false,
|
override var priority: Boolean = false
|
||||||
override var original: Boolean = false
|
|
||||||
) : IAudioUrlSource, IStreamMetaDataSource{
|
) : IAudioUrlSource, IStreamMetaDataSource{
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
|
@ -37,9 +36,7 @@ class AudioUrlSource(
|
||||||
source.container,
|
source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.language,
|
source.language,
|
||||||
source.duration,
|
source.duration
|
||||||
source.priority,
|
|
||||||
source.original
|
|
||||||
);
|
);
|
||||||
ret.streamMetaData = streamData;
|
ret.streamMetaData = streamData;
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,6 @@ class HLSVariantAudioUrlSource(
|
||||||
override val language: String,
|
override val language: String,
|
||||||
override val duration: Long?,
|
override val duration: Long?,
|
||||||
override val priority: Boolean,
|
override val priority: Boolean,
|
||||||
override val original: Boolean,
|
|
||||||
val url: String
|
val url: String
|
||||||
) : IAudioUrlSource {
|
) : IAudioUrlSource {
|
||||||
override fun getAudioUrl(): String {
|
override fun getAudioUrl(): String {
|
||||||
|
|
|
@ -8,5 +8,4 @@ interface IAudioSource {
|
||||||
val language : String;
|
val language : String;
|
||||||
val duration : Long?;
|
val duration : Long?;
|
||||||
val priority: Boolean;
|
val priority: Boolean;
|
||||||
val original: Boolean;
|
|
||||||
}
|
}
|
|
@ -15,7 +15,6 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
||||||
override val duration: Long? = null;
|
override val duration: Long? = null;
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
override val original: Boolean = false;
|
|
||||||
|
|
||||||
val filePath : String;
|
val filePath : String;
|
||||||
val fileSize: Long;
|
val fileSize: Long;
|
||||||
|
|
|
@ -10,18 +10,15 @@ import com.futo.polycentric.core.combineHashCodes
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonNames
|
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
open class SerializedPlatformVideo(
|
open class SerializedPlatformVideo(
|
||||||
override val contentType: ContentType = ContentType.MEDIA,
|
|
||||||
override val id: PlatformID,
|
override val id: PlatformID,
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val thumbnails: Thumbnails,
|
override val thumbnails: Thumbnails,
|
||||||
override val author: PlatformAuthorLink,
|
override val author: PlatformAuthorLink,
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
@JsonNames("datetime", "dateTime")
|
|
||||||
override val datetime: OffsetDateTime? = null,
|
override val datetime: OffsetDateTime? = null,
|
||||||
override val url: String,
|
override val url: String,
|
||||||
override val shareUrl: String = "",
|
override val shareUrl: String = "",
|
||||||
|
@ -30,6 +27,7 @@ open class SerializedPlatformVideo(
|
||||||
override val viewCount: Long,
|
override val viewCount: Long,
|
||||||
override val isShort: Boolean = false
|
override val isShort: Boolean = false
|
||||||
) : IPlatformVideo, SerializedPlatformContent {
|
) : IPlatformVideo, SerializedPlatformContent {
|
||||||
|
override val contentType: ContentType = ContentType.MEDIA;
|
||||||
|
|
||||||
override val isLive: Boolean = false;
|
override val isLive: Boolean = false;
|
||||||
|
|
||||||
|
@ -46,7 +44,6 @@ open class SerializedPlatformVideo(
|
||||||
companion object {
|
companion object {
|
||||||
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
|
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
|
||||||
return SerializedPlatformVideo(
|
return SerializedPlatformVideo(
|
||||||
ContentType.MEDIA,
|
|
||||||
video.id,
|
video.id,
|
||||||
video.name,
|
video.name,
|
||||||
video.thumbnails,
|
video.thumbnails,
|
||||||
|
|
|
@ -10,7 +10,6 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
|
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
@ -32,7 +31,6 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
|
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelContentPager
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSComment
|
import com.futo.platformplayer.api.media.platforms.js.models.JSComment
|
||||||
|
@ -363,10 +361,6 @@ open class JSClient : IPlatformClient {
|
||||||
return@isBusyWith JSChannelPager(config, this,
|
return@isBusyWith JSChannelPager(config, this,
|
||||||
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
||||||
}
|
}
|
||||||
override fun searchChannelsAsContent(query: String): IPager<IPlatformContent> = isBusyWith("searchChannels") {
|
|
||||||
ensureEnabled();
|
|
||||||
return@isBusyWith JSChannelContentPager(config, this, plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"), );
|
|
||||||
}
|
|
||||||
|
|
||||||
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
|
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
|
||||||
@JSDocsParameter("url", "A channel url (May not be your platform)")
|
@JSDocsParameter("url", "A channel url (May not be your platform)")
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.futo.platformplayer.api.media.platforms.js.models
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.JSChannelContent
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
@ -27,7 +26,6 @@ interface IJSContent: IPlatformContent {
|
||||||
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
|
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
|
||||||
ContentType.PLAYLIST -> JSPlaylist(config, obj);
|
ContentType.PLAYLIST -> JSPlaylist(config, obj);
|
||||||
ContentType.LOCKED -> JSLockedContent(config, obj);
|
ContentType.LOCKED -> JSLockedContent(config, obj);
|
||||||
ContentType.CHANNEL -> JSChannelContent(config, obj)
|
|
||||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
|
||||||
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
|
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
|
||||||
|
|
||||||
|
|
|
@ -49,8 +49,8 @@ open class JSContent : IPlatformContent, IPluginSourced {
|
||||||
else
|
else
|
||||||
author = PlatformAuthorLink.UNKNOWN;
|
author = PlatformAuthorLink.UNKNOWN;
|
||||||
|
|
||||||
val datetimeInt = _content.getOrDefault<Int>(config, "datetime", contextName, null)?.toLong();
|
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
||||||
if(datetimeInt == null || datetimeInt == 0.toLong())
|
if(datetimeInt == 0.toLong())
|
||||||
datetime = null;
|
datetime = null;
|
||||||
else
|
else
|
||||||
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
||||||
|
|
|
@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.IPluginSourced
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
import com.futo.platformplayer.api.media.models.JSChannelContent
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
@ -16,14 +15,4 @@ class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
|
||||||
override fun convertResult(obj: V8ValueObject): IPlatformContent {
|
override fun convertResult(obj: V8ValueObject): IPlatformContent {
|
||||||
return IJSContent.fromV8(plugin, obj);
|
return IJSContent.fromV8(plugin, obj);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class JSChannelContentPager : JSPager<IPlatformContent>, IPluginSourced {
|
|
||||||
override val sourceConfig: SourcePluginConfig get() = config;
|
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
|
|
||||||
|
|
||||||
override fun convertResult(obj: V8ValueObject): IPlatformContent {
|
|
||||||
return JSChannelContent(config, obj);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -21,8 +21,6 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
|
||||||
override var original: Boolean = false;
|
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
|
||||||
val contextName = "AudioUrlSource";
|
val contextName = "AudioUrlSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
|
@ -37,7 +35,6 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
||||||
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
|
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
|
||||||
|
|
||||||
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
|
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
|
||||||
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAudioUrl() : String {
|
override fun getAudioUrl() : String {
|
||||||
|
|
|
@ -23,7 +23,6 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||||
override val bitrate: Int;
|
override val bitrate: Int;
|
||||||
override val duration: Long;
|
override val duration: Long;
|
||||||
override val priority: Boolean;
|
override val priority: Boolean;
|
||||||
override var original: Boolean = false;
|
|
||||||
|
|
||||||
override val language: String;
|
override val language: String;
|
||||||
|
|
||||||
|
@ -46,7 +45,6 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||||
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
||||||
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
||||||
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
|
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
|
||||||
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
|
||||||
hasGenerate = _obj.has("generate");
|
hasGenerate = _obj.has("generate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
||||||
override val language: String;
|
override val language: String;
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
override var original: Boolean = false;
|
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
|
||||||
val contextName = "HLSAudioSource";
|
val contextName = "HLSAudioSource";
|
||||||
|
@ -33,7 +32,6 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
||||||
language = _obj.getOrThrow(config, "language", contextName);
|
language = _obj.getOrThrow(config, "language", contextName);
|
||||||
|
|
||||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||||
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
package com.futo.platformplayer.api.media.platforms.local
|
|
||||||
|
|
||||||
class LocalClient {
|
|
||||||
//TODO
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
package com.futo.platformplayer.api.media.platforms.local.models
|
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
|
||||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
|
||||||
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
|
||||||
import java.io.File
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.time.ZoneId
|
|
||||||
|
|
||||||
class LocalVideoDetails: IPlatformVideoDetails {
|
|
||||||
|
|
||||||
override val contentType: ContentType get() = ContentType.UNKNOWN;
|
|
||||||
|
|
||||||
override val id: PlatformID;
|
|
||||||
override val name: String;
|
|
||||||
override val author: PlatformAuthorLink;
|
|
||||||
|
|
||||||
override val datetime: OffsetDateTime?;
|
|
||||||
|
|
||||||
override val url: String;
|
|
||||||
override val shareUrl: String;
|
|
||||||
override val rating: IRating = RatingLikes(0);
|
|
||||||
override val description: String = "";
|
|
||||||
|
|
||||||
override val video: IVideoSourceDescriptor;
|
|
||||||
override val preview: IVideoSourceDescriptor? = null;
|
|
||||||
override val live: IVideoSource? = null;
|
|
||||||
override val dash: IDashManifestSource? = null;
|
|
||||||
override val hls: IHLSManifestSource? = null;
|
|
||||||
override val subtitles: List<ISubtitleSource> = listOf()
|
|
||||||
|
|
||||||
override val thumbnails: Thumbnails;
|
|
||||||
override val duration: Long;
|
|
||||||
override val viewCount: Long = 0;
|
|
||||||
override val isLive: Boolean = false;
|
|
||||||
override val isShort: Boolean = false;
|
|
||||||
|
|
||||||
constructor(file: File) {
|
|
||||||
id = PlatformID("Local", file.path, "LOCAL")
|
|
||||||
name = file.name;
|
|
||||||
author = PlatformAuthorLink.UNKNOWN;
|
|
||||||
|
|
||||||
url = file.canonicalPath;
|
|
||||||
shareUrl = "";
|
|
||||||
|
|
||||||
duration = 0;
|
|
||||||
thumbnails = Thumbnails(arrayOf());
|
|
||||||
|
|
||||||
datetime = OffsetDateTime.ofInstant(
|
|
||||||
Instant.ofEpochMilli(file.lastModified()),
|
|
||||||
ZoneId.systemDefault()
|
|
||||||
);
|
|
||||||
video = LocalVideoMuxedSourceDescriptor(LocalVideoFileSource(file));
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPlaybackTracker(): IPlaybackTracker? {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
package com.futo.platformplayer.api.media.platforms.local.models
|
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
|
||||||
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
|
||||||
|
|
||||||
class LocalVideoMuxedSourceDescriptor(
|
|
||||||
private val video: LocalVideoFileSource
|
|
||||||
) : VideoMuxedSourceDescriptor() {
|
|
||||||
override val videoSources: Array<IVideoSource> get() = arrayOf(video);
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
package com.futo.platformplayer.api.media.platforms.local.models
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.provider.MediaStore.Video
|
|
||||||
|
|
||||||
class MediaStoreVideo {
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val URI = MediaStore.Files.getContentUri("external");
|
|
||||||
val PROJECTION = arrayOf(Video.Media._ID, Video.Media.TITLE, Video.Media.DURATION, Video.Media.HEIGHT, Video.Media.WIDTH, Video.Media.MIME_TYPE);
|
|
||||||
val ORDER = MediaStore.Video.Media.TITLE;
|
|
||||||
|
|
||||||
fun readMediaStoreVideo(cursor: Cursor) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun query(context: Context, selection: String, args: Array<String>, order: String? = null): Cursor? {
|
|
||||||
val cursor = context.contentResolver.query(URI, PROJECTION, selection, args, order ?: ORDER, null);
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package com.futo.platformplayer.api.media.platforms.local.models.sources
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.provider.MediaStore.Video
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class LocalVideoFileSource: IVideoSource {
|
|
||||||
|
|
||||||
|
|
||||||
override val name: String;
|
|
||||||
override val width: Int;
|
|
||||||
override val height: Int;
|
|
||||||
override val container: String;
|
|
||||||
override val codec: String = ""
|
|
||||||
override val bitrate: Int = 0
|
|
||||||
override val duration: Long;
|
|
||||||
override val priority: Boolean = false;
|
|
||||||
|
|
||||||
constructor(file: File) {
|
|
||||||
name = file.name;
|
|
||||||
width = 0;
|
|
||||||
height = 0;
|
|
||||||
container = VideoHelper.videoExtensionToMimetype(file.extension) ?: "";
|
|
||||||
duration = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -6,7 +6,7 @@ import com.futo.platformplayer.constructs.Event1
|
||||||
* A RefreshPager represents a pager that can be modified overtime (eg. By getting more results later, by recreating the pager)
|
* A RefreshPager represents a pager that can be modified overtime (eg. By getting more results later, by recreating the pager)
|
||||||
* When the onPagerChanged event is emitted, a new pager instance is passed, or requested via getCurrentPager
|
* When the onPagerChanged event is emitted, a new pager instance is passed, or requested via getCurrentPager
|
||||||
*/
|
*/
|
||||||
interface IRefreshPager<T>: IPager<T> {
|
interface IRefreshPager<T> {
|
||||||
val onPagerChanged: Event1<IPager<T>>;
|
val onPagerChanged: Event1<IPager<T>>;
|
||||||
val onPagerError: Event1<Throwable>;
|
val onPagerError: Event1<Throwable>;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package com.futo.platformplayer.api.media.structures
|
package com.futo.platformplayer.api.media.structures
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.structures.ReusablePager.Window
|
|
||||||
import com.futo.platformplayer.constructs.Event1
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,8 +9,8 @@ import com.futo.platformplayer.logging.Logger
|
||||||
* A "Window" is effectively a pager that just reads previous results from the shared results, but when the end is reached, it will call nextPage on the parent if possible for new results.
|
* A "Window" is effectively a pager that just reads previous results from the shared results, but when the end is reached, it will call nextPage on the parent if possible for new results.
|
||||||
* This allows multiple Windows to exist of the same pager, without messing with position, or duplicate requests
|
* This allows multiple Windows to exist of the same pager, without messing with position, or duplicate requests
|
||||||
*/
|
*/
|
||||||
open class ReusablePager<T>: INestedPager<T>, IReusablePager<T> {
|
class ReusablePager<T>: INestedPager<T>, IPager<T> {
|
||||||
protected var _pager: IPager<T>;
|
private val _pager: IPager<T>;
|
||||||
val previousResults = arrayListOf<T>();
|
val previousResults = arrayListOf<T>();
|
||||||
|
|
||||||
constructor(subPager: IPager<T>) {
|
constructor(subPager: IPager<T>) {
|
||||||
|
@ -46,7 +44,7 @@ open class ReusablePager<T>: INestedPager<T>, IReusablePager<T> {
|
||||||
return previousResults;
|
return previousResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getWindow(): Window<T> {
|
fun getWindow(): Window<T> {
|
||||||
return Window(this);
|
return Window(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,118 +95,4 @@ open class ReusablePager<T>: INestedPager<T>, IReusablePager<T> {
|
||||||
return ReusablePager(this);
|
return ReusablePager(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class ReusableRefreshPager<T>: INestedPager<T>, IReusablePager<T> {
|
|
||||||
protected var _pager: IRefreshPager<T>;
|
|
||||||
val previousResults = arrayListOf<T>();
|
|
||||||
|
|
||||||
private var _currentPage: IPager<T>;
|
|
||||||
|
|
||||||
|
|
||||||
val onPagerChanged = Event1<IPager<T>>()
|
|
||||||
val onPagerError = Event1<Throwable>()
|
|
||||||
|
|
||||||
constructor(subPager: IRefreshPager<T>) {
|
|
||||||
this._pager = subPager;
|
|
||||||
_currentPage = this;
|
|
||||||
synchronized(previousResults) {
|
|
||||||
previousResults.addAll(subPager.getResults());
|
|
||||||
}
|
|
||||||
_pager.onPagerError.subscribe(onPagerError::emit);
|
|
||||||
_pager.onPagerChanged.subscribe {
|
|
||||||
_currentPage = it;
|
|
||||||
synchronized(previousResults) {
|
|
||||||
previousResults.clear();
|
|
||||||
previousResults.addAll(it.getResults());
|
|
||||||
}
|
|
||||||
|
|
||||||
onPagerChanged.emit(_currentPage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun findPager(query: (IPager<T>) -> Boolean): IPager<T>? {
|
|
||||||
if(query(_pager))
|
|
||||||
return _pager;
|
|
||||||
else if(_pager is INestedPager<*>)
|
|
||||||
return (_pager as INestedPager<T>).findPager(query);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hasMorePages(): Boolean {
|
|
||||||
return _pager.hasMorePages();
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun nextPage() {
|
|
||||||
_pager.nextPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getResults(): List<T> {
|
|
||||||
val results = _pager.getResults();
|
|
||||||
synchronized(previousResults) {
|
|
||||||
previousResults.addAll(results);
|
|
||||||
}
|
|
||||||
return previousResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getWindow(): RefreshWindow<T> {
|
|
||||||
return RefreshWindow(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RefreshWindow<T>: IPager<T>, INestedPager<T>, IRefreshPager<T> {
|
|
||||||
private val _parent: ReusableRefreshPager<T>;
|
|
||||||
private var _position: Int = 0;
|
|
||||||
private var _read: Int = 0;
|
|
||||||
|
|
||||||
private var _currentResults: List<T>;
|
|
||||||
|
|
||||||
override val onPagerChanged = Event1<IPager<T>>();
|
|
||||||
override val onPagerError = Event1<Throwable>();
|
|
||||||
|
|
||||||
|
|
||||||
override fun getCurrentPager(): IPager<T> {
|
|
||||||
return _parent.getWindow();
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(parent: ReusableRefreshPager<T>) {
|
|
||||||
_parent = parent;
|
|
||||||
|
|
||||||
synchronized(_parent.previousResults) {
|
|
||||||
_currentResults = _parent.previousResults.toList();
|
|
||||||
_read += _currentResults.size;
|
|
||||||
}
|
|
||||||
parent.onPagerChanged.subscribe(onPagerChanged::emit);
|
|
||||||
parent.onPagerError.subscribe(onPagerError::emit);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun hasMorePages(): Boolean {
|
|
||||||
return _parent.previousResults.size > _read || _parent.hasMorePages();
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun nextPage() {
|
|
||||||
synchronized(_parent.previousResults) {
|
|
||||||
if (_parent.previousResults.size <= _read) {
|
|
||||||
_parent.nextPage();
|
|
||||||
_parent.getResults();
|
|
||||||
}
|
|
||||||
_currentResults = _parent.previousResults.drop(_read).toList();
|
|
||||||
_read += _currentResults.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getResults(): List<T> {
|
|
||||||
return _currentResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun findPager(query: (IPager<T>) -> Boolean): IPager<T>? {
|
|
||||||
return _parent.findPager(query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IReusablePager<T>: IPager<T> {
|
|
||||||
fun getWindow(): IPager<T>;
|
|
||||||
}
|
}
|
|
@ -3,7 +3,6 @@ package com.futo.platformplayer.casting
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.futo.platformplayer.Settings
|
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
||||||
|
@ -33,7 +32,6 @@ import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.net.Inet4Address
|
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
|
@ -92,7 +90,7 @@ class FCastCastingDevice : CastingDevice {
|
||||||
private var _version: Long = 1;
|
private var _version: Long = 1;
|
||||||
private var _thread: Thread? = null
|
private var _thread: Thread? = null
|
||||||
private var _pingThread: Thread? = null
|
private var _pingThread: Thread? = null
|
||||||
@Volatile private var _lastPongTime = System.currentTimeMillis()
|
private var _lastPongTime = -1L
|
||||||
private var _outputStreamLock = Object()
|
private var _outputStreamLock = Object()
|
||||||
|
|
||||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||||
|
@ -326,9 +324,9 @@ class FCastCastingDevice : CastingDevice {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
localAddress = _socket?.localAddress
|
localAddress = _socket?.localAddress;
|
||||||
_lastPongTime = System.currentTimeMillis()
|
connectionState = CastConnectionState.CONNECTED;
|
||||||
connectionState = CastConnectionState.CONNECTED
|
_lastPongTime = -1L
|
||||||
|
|
||||||
val buffer = ByteArray(4096);
|
val buffer = ByteArray(4096);
|
||||||
|
|
||||||
|
@ -404,32 +402,36 @@ class FCastCastingDevice : CastingDevice {
|
||||||
|
|
||||||
_pingThread = Thread {
|
_pingThread = Thread {
|
||||||
Logger.i(TAG, "Started ping loop.")
|
Logger.i(TAG, "Started ping loop.")
|
||||||
|
|
||||||
while (_scopeIO?.isActive == true) {
|
while (_scopeIO?.isActive == true) {
|
||||||
if (connectionState == CastConnectionState.CONNECTED) {
|
try {
|
||||||
|
send(Opcode.Ping)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.w(TAG, "Failed to send ping.")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
send(Opcode.Ping)
|
_socket?.close()
|
||||||
if (System.currentTimeMillis() - _lastPongTime > 15000) {
|
_inputStream?.close()
|
||||||
Logger.w(TAG, "Closing socket due to last pong time being larger than 15 seconds.")
|
_outputStream?.close()
|
||||||
try {
|
|
||||||
_socket?.close()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.w(TAG, "Failed to close socket.", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.w(TAG, "Failed to send ping.")
|
Log.w(TAG, "Failed to close socket.", e)
|
||||||
try {
|
|
||||||
_socket?.close()
|
|
||||||
_inputStream?.close()
|
|
||||||
_outputStream?.close()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.w(TAG, "Failed to close socket.", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Thread.sleep(5000)
|
|
||||||
|
/*if (_lastPongTime != -1L && System.currentTimeMillis() - _lastPongTime > 6000) {
|
||||||
|
Logger.w(TAG, "Closing socket due to last pong time being larger than 6 seconds.")
|
||||||
|
|
||||||
|
try {
|
||||||
|
_socket?.close()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.w(TAG, "Failed to close socket.", e)
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
Thread.sleep(2000)
|
||||||
}
|
}
|
||||||
Logger.i(TAG, "Stopped ping loop.")
|
|
||||||
|
Logger.i(TAG, "Stopped ping loop.");
|
||||||
}.apply { start() }
|
}.apply { start() }
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "Thread was still alive, not restarted")
|
Log.i(TAG, "Thread was still alive, not restarted")
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.futo.platformplayer.casting
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -10,7 +9,6 @@ import android.util.Log
|
||||||
import android.util.Xml
|
import android.util.Xml
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.futo.platformplayer.R
|
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
@ -241,9 +239,6 @@ class StateCasting {
|
||||||
Logger.i(TAG, "CastingService stopped.")
|
Logger.i(TAG, "CastingService stopped.")
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _castingDialogLock = Any();
|
|
||||||
private var _currentDialog: AlertDialog? = null;
|
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun connectDevice(device: CastingDevice) {
|
fun connectDevice(device: CastingDevice) {
|
||||||
if (activeDevice == device)
|
if (activeDevice == device)
|
||||||
|
@ -277,41 +272,10 @@ class StateCasting {
|
||||||
invokeInMainScopeIfRequired {
|
invokeInMainScopeIfRequired {
|
||||||
StateApp.withContext(false) { context ->
|
StateApp.withContext(false) { context ->
|
||||||
context.let {
|
context.let {
|
||||||
Logger.i(TAG, "Casting state changed to ${castConnectionState}");
|
|
||||||
when (castConnectionState) {
|
when (castConnectionState) {
|
||||||
CastConnectionState.CONNECTED -> {
|
CastConnectionState.CONNECTED -> UIDialogs.toast(it, "Connected to device")
|
||||||
Logger.i(TAG, "Casting connected to [${device.name}]");
|
CastConnectionState.CONNECTING -> UIDialogs.toast(it, "Connecting to device...")
|
||||||
UIDialogs.appToast("Connected to device")
|
CastConnectionState.DISCONNECTED -> UIDialogs.toast(it, "Disconnected from device")
|
||||||
synchronized(_castingDialogLock) {
|
|
||||||
if(_currentDialog != null) {
|
|
||||||
_currentDialog?.hide();
|
|
||||||
_currentDialog = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CastConnectionState.CONNECTING -> {
|
|
||||||
Logger.i(TAG, "Casting connecting to [${device.name}]");
|
|
||||||
UIDialogs.toast(it, "Connecting to device...")
|
|
||||||
synchronized(_castingDialogLock) {
|
|
||||||
if(_currentDialog == null) {
|
|
||||||
_currentDialog = UIDialogs.showDialog(context, R.drawable.ic_loader_animated, true,
|
|
||||||
"Connecting to [${device.name}]",
|
|
||||||
"Make sure you are on the same network\n\nVPNs and guest networks can cause issues", null, -2,
|
|
||||||
UIDialogs.Action("Disconnect", {
|
|
||||||
device.stop();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CastConnectionState.DISCONNECTED -> {
|
|
||||||
UIDialogs.toast(it, "Disconnected from device")
|
|
||||||
synchronized(_castingDialogLock) {
|
|
||||||
if(_currentDialog != null) {
|
|
||||||
_currentDialog?.hide();
|
|
||||||
_currentDialog = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,6 +22,7 @@ import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComm
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
@ -29,7 +30,6 @@ import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.polycentric.core.ClaimType
|
import com.futo.polycentric.core.ClaimType
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
|
|
|
@ -73,11 +73,11 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
};
|
};
|
||||||
_rememberedAdapter.onConnect.subscribe { _ ->
|
_rememberedAdapter.onConnect.subscribe { _ ->
|
||||||
dismiss()
|
dismiss()
|
||||||
//UIDialogs.showCastingDialog(context)
|
UIDialogs.showCastingDialog(context)
|
||||||
}
|
}
|
||||||
_adapter.onConnect.subscribe { _ ->
|
_adapter.onConnect.subscribe { _ ->
|
||||||
dismiss()
|
dismiss()
|
||||||
//UIDialogs.showCastingDialog(context)
|
UIDialogs.showCastingDialog(context)
|
||||||
}
|
}
|
||||||
_recyclerRememberedDevices.adapter = _rememberedAdapter;
|
_recyclerRememberedDevices.adapter = _rememberedAdapter;
|
||||||
_recyclerRememberedDevices.layoutManager = LinearLayoutManager(context);
|
_recyclerRememberedDevices.layoutManager = LinearLayoutManager(context);
|
||||||
|
|
|
@ -10,7 +10,7 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.LocalVideoMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
|
@ -57,7 +57,7 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
||||||
override val video: IVideoSourceDescriptor get() = if(audioSource.isNotEmpty())
|
override val video: IVideoSourceDescriptor get() = if(audioSource.isNotEmpty())
|
||||||
LocalVideoUnMuxedSourceDescriptor(this)
|
LocalVideoUnMuxedSourceDescriptor(this)
|
||||||
else
|
else
|
||||||
DownloadedVideoMuxedSourceDescriptor(this);
|
LocalVideoMuxedSourceDescriptor(this);
|
||||||
override val preview: IVideoSourceDescriptor? get() = videoSerialized.preview;
|
override val preview: IVideoSourceDescriptor? get() = videoSerialized.preview;
|
||||||
|
|
||||||
override val live: IVideoSource? get() = videoSerialized.live;
|
override val live: IVideoSource? get() = videoSerialized.live;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fixHtmlLinks
|
import com.futo.platformplayer.fixHtmlLinks
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
|
@ -20,7 +21,6 @@ import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.toHumanNumber
|
import com.futo.platformplayer.toHumanNumber
|
||||||
import com.futo.platformplayer.views.platform.PlatformLinkView
|
import com.futo.platformplayer.views.platform.PlatformLinkView
|
||||||
import com.futo.polycentric.core.PolycentricProfile
|
|
||||||
import com.futo.polycentric.core.toName
|
import com.futo.polycentric.core.toName
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
|
|
||||||
|
@ -134,7 +134,9 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!map.containsKey("Harbor"))
|
if(!map.containsKey("Harbor"))
|
||||||
map.set("Harbor", polycentricProfile.getHarborUrl());
|
this.context?.let {
|
||||||
|
map.set("Harbor", polycentricProfile.getHarborUrl(it));
|
||||||
|
}
|
||||||
|
|
||||||
if (map.isNotEmpty())
|
if (map.isNotEmpty())
|
||||||
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
|
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
|
||||||
|
|
|
@ -29,6 +29,7 @@ import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateCache
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
@ -38,7 +39,6 @@ import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
import com.futo.polycentric.core.PolycentricProfile
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
|
@ -16,12 +16,12 @@ import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder
|
import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder
|
||||||
import com.futo.polycentric.core.PolycentricProfile
|
|
||||||
|
|
||||||
class ChannelListFragment : Fragment, IChannelTabFragment {
|
class ChannelListFragment : Fragment, IChannelTabFragment {
|
||||||
private var _channels: ArrayList<IPlatformChannel> = arrayListOf();
|
private var _channels: ArrayList<IPlatformChannel> = arrayListOf();
|
||||||
|
|
|
@ -8,8 +8,8 @@ import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
import com.futo.platformplayer.views.SupportView
|
import com.futo.platformplayer.views.SupportView
|
||||||
import com.futo.polycentric.core.PolycentricProfile
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.polycentric.core.PolycentricProfile
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
|
|
||||||
interface IChannelTabFragment {
|
interface IChannelTabFragment {
|
||||||
fun setChannel(channel: IPlatformChannel)
|
fun setChannel(channel: IPlatformChannel)
|
||||||
|
|
|
@ -42,6 +42,7 @@ import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.SearchType
|
import com.futo.platformplayer.models.SearchType
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.selectHighestResolutionImage
|
import com.futo.platformplayer.selectHighestResolutionImage
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
@ -54,14 +55,29 @@ import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||||
import com.futo.polycentric.core.ApiMethods
|
import com.futo.polycentric.core.OwnedClaim
|
||||||
import com.futo.polycentric.core.PolycentricProfile
|
import com.futo.polycentric.core.PublicKey
|
||||||
|
import com.futo.polycentric.core.Store
|
||||||
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PolycentricProfile(
|
||||||
|
val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>
|
||||||
|
) {
|
||||||
|
fun getHarborUrl(context: Context): String{
|
||||||
|
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system));
|
||||||
|
val url = system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
|
||||||
|
return "https://harbor.social/" + url.substring("polycentric://".length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ChannelFragment : MainFragment() {
|
class ChannelFragment : MainFragment() {
|
||||||
override val isMainView: Boolean = true
|
override val isMainView: Boolean = true
|
||||||
|
@ -128,14 +144,15 @@ class ChannelFragment : MainFragment() {
|
||||||
|
|
||||||
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
|
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricProfile?>
|
private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>
|
||||||
private val _taskGetChannel: TaskHandler<String, IPlatformChannel>
|
private val _taskGetChannel: TaskHandler<String, IPlatformChannel>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
inflater.inflate(R.layout.fragment_channel, this)
|
inflater.inflate(R.layout.fragment_channel, this)
|
||||||
_taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>({ fragment.lifecycleScope },
|
_taskLoadPolycentricProfile =
|
||||||
|
TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>({ fragment.lifecycleScope },
|
||||||
{ id ->
|
{ id ->
|
||||||
return@TaskHandler ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, id.claimFieldType.toLong(), id.claimType.toLong(), id.value!!)
|
return@TaskHandler PolycentricCache.instance.getProfileAsync(id)
|
||||||
}).success { setPolycentricProfile(it, animate = true) }.exception<Throwable> {
|
}).success { setPolycentricProfile(it, animate = true) }.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load polycentric profile.", it)
|
Logger.w(TAG, "Failed to load polycentric profile.", it)
|
||||||
}
|
}
|
||||||
|
@ -221,8 +238,8 @@ class ChannelFragment : MainFragment() {
|
||||||
}
|
}
|
||||||
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||||
if (content is IPlatformVideo) {
|
if (content is IPlatformVideo) {
|
||||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
|
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)
|
||||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
adapter.onUrlClicked.subscribe { url ->
|
adapter.onUrlClicked.subscribe { url ->
|
||||||
|
@ -311,7 +328,7 @@ class ChannelFragment : MainFragment() {
|
||||||
_creatorThumbnail.setThumbnail(parameter.thumbnail, true)
|
_creatorThumbnail.setThumbnail(parameter.thumbnail, true)
|
||||||
Glide.with(_imageBanner).clear(_imageBanner)
|
Glide.with(_imageBanner).clear(_imageBanner)
|
||||||
|
|
||||||
loadPolycentricProfile(parameter.id)
|
loadPolycentricProfile(parameter.id, parameter.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
_url = parameter.url
|
_url = parameter.url
|
||||||
|
@ -325,7 +342,7 @@ class ChannelFragment : MainFragment() {
|
||||||
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true)
|
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true)
|
||||||
Glide.with(_imageBanner).clear(_imageBanner)
|
Glide.with(_imageBanner).clear(_imageBanner)
|
||||||
|
|
||||||
loadPolycentricProfile(parameter.channel.id)
|
loadPolycentricProfile(parameter.channel.id, parameter.channel.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
_url = parameter.channel.url
|
_url = parameter.channel.url
|
||||||
|
@ -342,8 +359,16 @@ class ChannelFragment : MainFragment() {
|
||||||
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
|
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadPolycentricProfile(id: PlatformID) {
|
private fun loadPolycentricProfile(id: PlatformID, url: String) {
|
||||||
_taskLoadPolycentricProfile.run(id)
|
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true)
|
||||||
|
if (cachedPolycentricProfile != null) {
|
||||||
|
setPolycentricProfile(cachedPolycentricProfile, animate = true)
|
||||||
|
if (cachedPolycentricProfile.expired) {
|
||||||
|
_taskLoadPolycentricProfile.run(id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_taskLoadPolycentricProfile.run(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setLoading(isLoading: Boolean) {
|
private fun setLoading(isLoading: Boolean) {
|
||||||
|
@ -508,13 +533,20 @@ class ChannelFragment : MainFragment() {
|
||||||
|
|
||||||
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
|
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
|
||||||
setPolycentricProfile(null, animate = false)
|
setPolycentricProfile(null, animate = false)
|
||||||
or()
|
|
||||||
|
val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(url) }
|
||||||
|
if (cachedProfile != null) {
|
||||||
|
setPolycentricProfile(cachedProfile, animate = false)
|
||||||
|
} else {
|
||||||
|
or()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPolycentricProfile(
|
private fun setPolycentricProfile(
|
||||||
profile: PolycentricProfile?, animate: Boolean
|
cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean
|
||||||
) {
|
) {
|
||||||
val dp35 = 35.dp(resources)
|
val dp35 = 35.dp(resources)
|
||||||
|
val profile = cachedPolycentricProfile?.profile
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let {
|
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let {
|
||||||
it.toURLInfoSystemLinkUrl(
|
it.toURLInfoSystemLinkUrl(
|
||||||
profile.system.toProto(), it.process, profile.systemState.servers.toList()
|
profile.system.toProto(), it.process, profile.systemState.servers.toList()
|
||||||
|
|
|
@ -23,6 +23,7 @@ import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
@ -31,7 +32,6 @@ import com.futo.platformplayer.views.adapters.CommentWithReferenceViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||||
import com.futo.polycentric.core.PublicKey
|
import com.futo.polycentric.core.PublicKey
|
||||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
|
|
|
@ -82,8 +82,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||||
};
|
};
|
||||||
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
||||||
if(it is IPlatformVideo) {
|
if(it is IPlatformVideo) {
|
||||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
||||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
adapter.onLongPress.subscribe(this) {
|
adapter.onLongPress.subscribe(this) {
|
||||||
|
@ -201,12 +201,11 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||||
protected open fun onContentUrlClicked(url: String, contentType: ContentType) {
|
protected open fun onContentUrlClicked(url: String, contentType: ContentType) {
|
||||||
when(contentType) {
|
when(contentType) {
|
||||||
ContentType.MEDIA -> {
|
ContentType.MEDIA -> {
|
||||||
StatePlayer.instance.clearQueue()
|
StatePlayer.instance.clearQueue();
|
||||||
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
|
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail();
|
||||||
}
|
};
|
||||||
ContentType.PLAYLIST -> fragment.navigate<RemotePlaylistFragment>(url)
|
ContentType.PLAYLIST -> fragment.navigate<RemotePlaylistFragment>(url);
|
||||||
ContentType.URL -> fragment.navigate<BrowserFragment>(url)
|
ContentType.URL -> fragment.navigate<BrowserFragment>(url);
|
||||||
ContentType.CHANNEL -> fragment.navigate<ChannelFragment>(url)
|
|
||||||
else -> {};
|
else -> {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,7 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.UISlideOverlays
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
|
@ -19,7 +17,6 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
import com.futo.platformplayer.isHttpUrl
|
import com.futo.platformplayer.isHttpUrl
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.SearchType
|
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
@ -86,7 +83,6 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||||
private var _filterValues: HashMap<String, List<String>> = hashMapOf();
|
private var _filterValues: HashMap<String, List<String>> = hashMapOf();
|
||||||
private var _enabledClientIds: List<String>? = null;
|
private var _enabledClientIds: List<String>? = null;
|
||||||
private var _channelUrl: String? = null;
|
private var _channelUrl: String? = null;
|
||||||
private var _searchType: SearchType? = null;
|
|
||||||
|
|
||||||
private val _taskSearch: TaskHandler<String, IPager<IPlatformContent>>;
|
private val _taskSearch: TaskHandler<String, IPager<IPlatformContent>>;
|
||||||
override val shouldShowTimeBar: Boolean get() = Settings.instance.search.progressBar
|
override val shouldShowTimeBar: Boolean get() = Settings.instance.search.progressBar
|
||||||
|
@ -98,13 +94,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||||
if (channelUrl != null) {
|
if (channelUrl != null) {
|
||||||
StatePlatform.instance.searchChannel(channelUrl, query, null, _sortBy, _filterValues, _enabledClientIds)
|
StatePlatform.instance.searchChannel(channelUrl, query, null, _sortBy, _filterValues, _enabledClientIds)
|
||||||
} else {
|
} else {
|
||||||
when (_searchType)
|
StatePlatform.instance.searchRefresh(fragment.lifecycleScope, query, null, _sortBy, _filterValues, _enabledClientIds)
|
||||||
{
|
|
||||||
SearchType.VIDEO -> StatePlatform.instance.searchRefresh(fragment.lifecycleScope, query, null, _sortBy, _filterValues, _enabledClientIds)
|
|
||||||
SearchType.CREATOR -> StatePlatform.instance.searchChannelsAsContent(query)
|
|
||||||
SearchType.PLAYLIST -> StatePlatform.instance.searchPlaylist(query)
|
|
||||||
else -> throw Exception("Search type must be specified")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> { }
|
.success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> { }
|
||||||
|
@ -125,7 +115,6 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||||
if(parameter is SuggestionsFragmentData) {
|
if(parameter is SuggestionsFragmentData) {
|
||||||
setQuery(parameter.query, false);
|
setQuery(parameter.query, false);
|
||||||
setChannelUrl(parameter.channelUrl, false);
|
setChannelUrl(parameter.channelUrl, false);
|
||||||
setSearchType(parameter.searchType, false)
|
|
||||||
|
|
||||||
fragment.topBar?.apply {
|
fragment.topBar?.apply {
|
||||||
if (this is SearchTopBarFragment) {
|
if (this is SearchTopBarFragment) {
|
||||||
|
@ -171,14 +160,8 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||||
navigate<RemotePlaylistFragment>(it);
|
navigate<RemotePlaylistFragment>(it);
|
||||||
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||||
navigate<ChannelFragment>(it);
|
navigate<ChannelFragment>(it);
|
||||||
else {
|
else
|
||||||
val url = it;
|
navigate<VideoDetailFragment>(it);
|
||||||
activity?.let {
|
|
||||||
close()
|
|
||||||
if(it is MainActivity)
|
|
||||||
it.navigate(it.getFragment<VideoDetailFragment>(), url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
setQuery(it, true);
|
setQuery(it, true);
|
||||||
|
@ -268,15 +251,6 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setSearchType(searchType: SearchType, updateResults: Boolean = true) {
|
|
||||||
_searchType = searchType
|
|
||||||
|
|
||||||
if (updateResults) {
|
|
||||||
clearResults();
|
|
||||||
loadResults();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setSortBy(sortBy: String?, updateResults: Boolean = true) {
|
private fun setSortBy(sortBy: String?, updateResults: Boolean = true) {
|
||||||
_sortBy = sortBy;
|
_sortBy = sortBy;
|
||||||
|
|
||||||
|
|
|
@ -21,8 +21,6 @@ import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
|
||||||
import com.futo.platformplayer.stores.StringStorage
|
|
||||||
import com.futo.platformplayer.toHumanBytesSize
|
import com.futo.platformplayer.toHumanBytesSize
|
||||||
import com.futo.platformplayer.toHumanDuration
|
import com.futo.platformplayer.toHumanDuration
|
||||||
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||||
|
@ -105,15 +103,12 @@ class DownloadsFragment : MainFragment() {
|
||||||
private val _listDownloaded: AnyInsertedAdapterView<VideoLocal, VideoDownloadViewHolder>;
|
private val _listDownloaded: AnyInsertedAdapterView<VideoLocal, VideoDownloadViewHolder>;
|
||||||
|
|
||||||
private var lastDownloads: List<VideoLocal>? = null;
|
private var lastDownloads: List<VideoLocal>? = null;
|
||||||
private var ordering = FragmentedStorage.get<StringStorage>("downloads_ordering")
|
private var ordering: String? = "nameAsc";
|
||||||
|
|
||||||
constructor(frag: DownloadsFragment, inflater: LayoutInflater): super(frag.requireContext()) {
|
constructor(frag: DownloadsFragment, inflater: LayoutInflater): super(frag.requireContext()) {
|
||||||
inflater.inflate(R.layout.fragment_downloads, this);
|
inflater.inflate(R.layout.fragment_downloads, this);
|
||||||
_frag = frag;
|
_frag = frag;
|
||||||
|
|
||||||
if(ordering.value.isNullOrBlank())
|
|
||||||
ordering.value = "nameAsc";
|
|
||||||
|
|
||||||
_usageUsed = findViewById(R.id.downloads_usage_used);
|
_usageUsed = findViewById(R.id.downloads_usage_used);
|
||||||
_usageAvailable = findViewById(R.id.downloads_usage_available);
|
_usageAvailable = findViewById(R.id.downloads_usage_available);
|
||||||
_usageProgress = findViewById(R.id.downloads_usage_progress);
|
_usageProgress = findViewById(R.id.downloads_usage_progress);
|
||||||
|
@ -137,23 +132,22 @@ class DownloadsFragment : MainFragment() {
|
||||||
spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.downloads_sortby_array)).also {
|
spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.downloads_sortby_array)).also {
|
||||||
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
||||||
};
|
};
|
||||||
val options = listOf("nameAsc", "nameDesc", "downloadDateAsc", "downloadDateDesc", "releasedAsc", "releasedDesc");
|
spinnerSortBy.setSelection(0);
|
||||||
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
|
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
|
||||||
when(pos) {
|
when(pos) {
|
||||||
0 -> ordering.setAndSave("nameAsc")
|
0 -> ordering = "nameAsc"
|
||||||
1 -> ordering.setAndSave("nameDesc")
|
1 -> ordering = "nameDesc"
|
||||||
2 -> ordering.setAndSave("downloadDateAsc")
|
2 -> ordering = "downloadDateAsc"
|
||||||
3 -> ordering.setAndSave("downloadDateDesc")
|
3 -> ordering = "downloadDateDesc"
|
||||||
4 -> ordering.setAndSave("releasedAsc")
|
4 -> ordering = "releasedAsc"
|
||||||
5 -> ordering.setAndSave("releasedDesc")
|
5 -> ordering = "releasedDesc"
|
||||||
else -> ordering.setAndSave("")
|
else -> ordering = null
|
||||||
}
|
}
|
||||||
updateContentFilters()
|
updateContentFilters()
|
||||||
}
|
}
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||||
};
|
};
|
||||||
spinnerSortBy.setSelection(Math.max(0, options.indexOf(ordering.value)));
|
|
||||||
|
|
||||||
_listDownloaded = findViewById<RecyclerView>(R.id.list_downloaded)
|
_listDownloaded = findViewById<RecyclerView>(R.id.list_downloaded)
|
||||||
.asAnyWithTop(findViewById(R.id.downloads_top)) {
|
.asAnyWithTop(findViewById(R.id.downloads_top)) {
|
||||||
|
@ -235,9 +229,9 @@ class DownloadsFragment : MainFragment() {
|
||||||
fun filterDownloads(vids: List<VideoLocal>): List<VideoLocal>{
|
fun filterDownloads(vids: List<VideoLocal>): List<VideoLocal>{
|
||||||
var vidsToReturn = vids;
|
var vidsToReturn = vids;
|
||||||
if(!_listDownloadSearch.text.isNullOrEmpty())
|
if(!_listDownloadSearch.text.isNullOrEmpty())
|
||||||
vidsToReturn = vids.filter { it.name.contains(_listDownloadSearch.text, true) || it.author.name.contains(_listDownloadSearch.text, true) };
|
vidsToReturn = vids.filter { it.name.contains(_listDownloadSearch.text, true) };
|
||||||
if(!ordering.value.isNullOrEmpty()) {
|
if(!ordering.isNullOrEmpty()) {
|
||||||
vidsToReturn = when(ordering.value){
|
vidsToReturn = when(ordering){
|
||||||
"downloadDateAsc" -> vidsToReturn.sortedBy { it.downloadDate ?: OffsetDateTime.MAX };
|
"downloadDateAsc" -> vidsToReturn.sortedBy { it.downloadDate ?: OffsetDateTime.MAX };
|
||||||
"downloadDateDesc" -> vidsToReturn.sortedByDescending { it.downloadDate ?: OffsetDateTime.MIN };
|
"downloadDateDesc" -> vidsToReturn.sortedByDescending { it.downloadDate ?: OffsetDateTime.MIN };
|
||||||
"nameAsc" -> vidsToReturn.sortedBy { it.name.lowercase() }
|
"nameAsc" -> vidsToReturn.sortedBy { it.name.lowercase() }
|
||||||
|
|
|
@ -3,15 +3,12 @@ package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.util.DisplayMetrics
|
|
||||||
import android.view.Display
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.LayoutManager
|
import androidx.recyclerview.widget.RecyclerView.LayoutManager
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
@ -23,7 +20,6 @@ import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.others.ProgressBar
|
import com.futo.platformplayer.views.others.ProgressBar
|
||||||
import com.futo.platformplayer.views.others.TagsView
|
import com.futo.platformplayer.views.others.TagsView
|
||||||
|
@ -32,9 +28,7 @@ import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||||
import com.futo.platformplayer.views.announcements.AnnouncementView
|
import com.futo.platformplayer.views.announcements.AnnouncementView
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@ -74,7 +68,6 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||||
|
|
||||||
private val _scrollListener: RecyclerView.OnScrollListener;
|
private val _scrollListener: RecyclerView.OnScrollListener;
|
||||||
private var _automaticNextPageCounter = 0;
|
private var _automaticNextPageCounter = 0;
|
||||||
private val _automaticBackoff = arrayOf(0, 500, 1000, 1000, 2000, 5000, 5000, 5000);
|
|
||||||
|
|
||||||
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>? = null) : super(inflater.context) {
|
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>? = null) : super(inflater.context) {
|
||||||
this.fragment = fragment;
|
this.fragment = fragment;
|
||||||
|
@ -189,61 +182,29 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||||
|
|
||||||
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
|
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
|
||||||
val canScroll = if (recyclerData.results.isEmpty()) false else {
|
val canScroll = if (recyclerData.results.isEmpty()) false else {
|
||||||
val height = resources.displayMetrics.heightPixels;
|
|
||||||
|
|
||||||
val layoutManager = recyclerData.layoutManager
|
val layoutManager = recyclerData.layoutManager
|
||||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||||
val firstVisibleItemView = if(firstVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(firstVisibleItemPosition) else null;
|
|
||||||
val lastVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition();
|
if (firstVisibleItemPosition != RecyclerView.NO_POSITION) {
|
||||||
val lastVisibleItemView = if(lastVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(lastVisibleItemPosition) else null;
|
val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition)
|
||||||
val rows = if(recyclerData.layoutManager is GridLayoutManager) Math.max(1, recyclerData.results.size / recyclerData.layoutManager.spanCount) else 1;
|
val itemHeight = firstVisibleView?.height ?: 0
|
||||||
val rowsHeight = (firstVisibleItemView?.height ?: 0) * rows;
|
val occupiedSpace = recyclerData.results.size / recyclerData.layoutManager.spanCount * itemHeight
|
||||||
if(lastVisibleItemView != null && lastVisibleItemPosition == (recyclerData.results.size - 1)) {
|
val recyclerViewHeight = _recyclerResults.height
|
||||||
false;
|
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage occupiedSpace=$occupiedSpace recyclerViewHeight=$recyclerViewHeight")
|
||||||
}
|
occupiedSpace >= recyclerViewHeight
|
||||||
else if (firstVisibleItemView != null && height != null && rowsHeight < height) {
|
|
||||||
false;
|
|
||||||
} else {
|
} else {
|
||||||
true;
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter")
|
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter")
|
||||||
if (!canScroll || filteredResults.isEmpty()) {
|
if (!canScroll || filteredResults.isEmpty()) {
|
||||||
_automaticNextPageCounter++
|
_automaticNextPageCounter++
|
||||||
if(_automaticNextPageCounter < _automaticBackoff.size) {
|
if(_automaticNextPageCounter <= 4)
|
||||||
if(_automaticNextPageCounter > 0) {
|
loadNextPage()
|
||||||
val automaticNextPageCounterSaved = _automaticNextPageCounter;
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Default) {
|
|
||||||
val backoff = _automaticBackoff[Math.min(_automaticBackoff.size - 1, _automaticNextPageCounter)];
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
setLoading(true);
|
|
||||||
}
|
|
||||||
delay(backoff.toLong());
|
|
||||||
if(automaticNextPageCounterSaved == _automaticNextPageCounter) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
loadNextPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
loadNextPage();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "ensureEnoughContentVisible automaticNextPageCounter reset");
|
|
||||||
_automaticNextPageCounter = 0;
|
_automaticNextPageCounter = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun resetAutomaticNextPageCounter(){
|
|
||||||
_automaticNextPageCounter = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun setTextCentered(text: String?) {
|
protected fun setTextCentered(text: String?) {
|
||||||
_textCentered.text = text;
|
_textCentered.text = text;
|
||||||
|
|
|
@ -5,38 +5,28 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.allViews
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.UISlideOverlays.Companion.showOrderOverlay
|
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.IRefreshPager
|
|
||||||
import com.futo.platformplayer.api.media.structures.IReusablePager
|
|
||||||
import com.futo.platformplayer.api.media.structures.ReusablePager
|
|
||||||
import com.futo.platformplayer.api.media.structures.ReusableRefreshPager
|
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateHistory
|
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
|
||||||
import com.futo.platformplayer.stores.StringArrayStorage
|
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.NoResultsView
|
import com.futo.platformplayer.views.NoResultsView
|
||||||
import com.futo.platformplayer.views.ToggleBar
|
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||||
|
import com.futo.platformplayer.views.announcements.AnnouncementView
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
@ -48,12 +38,6 @@ class HomeFragment : MainFragment() {
|
||||||
|
|
||||||
private var _view: HomeView? = null;
|
private var _view: HomeView? = null;
|
||||||
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
||||||
private var _cachedLastPager: IReusablePager<IPlatformContent>? = null
|
|
||||||
|
|
||||||
private var _toggleRecent = false;
|
|
||||||
private var _toggleWatched = false;
|
|
||||||
private var _togglePluginsDisabled = mutableListOf<String>();
|
|
||||||
|
|
||||||
|
|
||||||
fun reloadFeed() {
|
fun reloadFeed() {
|
||||||
_view?.reloadFeed()
|
_view?.reloadFeed()
|
||||||
|
@ -79,7 +63,7 @@ class HomeFragment : MainFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val view = HomeView(this, inflater, _cachedRecyclerData, _cachedLastPager);
|
val view = HomeView(this, inflater, _cachedRecyclerData);
|
||||||
_view = view;
|
_view = view;
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
@ -97,7 +81,6 @@ class HomeFragment : MainFragment() {
|
||||||
val view = _view;
|
val view = _view;
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
_cachedRecyclerData = view.recyclerData;
|
_cachedRecyclerData = view.recyclerData;
|
||||||
_cachedLastPager = view.lastPager;
|
|
||||||
view.cleanup();
|
view.cleanup();
|
||||||
_view = null;
|
_view = null;
|
||||||
}
|
}
|
||||||
|
@ -107,32 +90,18 @@ class HomeFragment : MainFragment() {
|
||||||
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.home.previewFeedItems);
|
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.home.previewFeedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
class HomeView : ContentFeedView<HomeFragment> {
|
class HomeView : ContentFeedView<HomeFragment> {
|
||||||
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
|
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
|
||||||
|
|
||||||
private var _toggleBar: ToggleBar? = null;
|
|
||||||
|
|
||||||
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
|
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
|
||||||
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
|
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
|
||||||
|
|
||||||
var lastPager: IReusablePager<IPlatformContent>? = null;
|
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||||
|
|
||||||
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null, cachedLastPager: IReusablePager<IPlatformContent>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
|
||||||
lastPager = cachedLastPager
|
|
||||||
_taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({ fragment.lifecycleScope }, {
|
_taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({ fragment.lifecycleScope }, {
|
||||||
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
|
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
|
||||||
})
|
})
|
||||||
.success {
|
.success { loadedResult(it); }
|
||||||
val wrappedPager = if(it is IRefreshPager)
|
|
||||||
ReusableRefreshPager(it);
|
|
||||||
else
|
|
||||||
ReusablePager(it);
|
|
||||||
lastPager = wrappedPager;
|
|
||||||
resetAutomaticNextPageCounter();
|
|
||||||
loadedResult(wrappedPager.getWindow());
|
|
||||||
}
|
|
||||||
.exception<ScriptCaptchaRequiredException> { }
|
.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exception<ScriptExecutionException> {
|
.exception<ScriptExecutionException> {
|
||||||
Logger.w(ChannelFragment.TAG, "Plugin failure.", it);
|
Logger.w(ChannelFragment.TAG, "Plugin failure.", it);
|
||||||
|
@ -158,8 +127,6 @@ class HomeFragment : MainFragment() {
|
||||||
}, fragment);
|
}, fragment);
|
||||||
};
|
};
|
||||||
|
|
||||||
initializeToolbarContent();
|
|
||||||
|
|
||||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||||
showAnnouncementView()
|
showAnnouncementView()
|
||||||
}
|
}
|
||||||
|
@ -234,119 +201,13 @@ class HomeFragment : MainFragment() {
|
||||||
loadResults();
|
loadResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _filterLock = Object();
|
|
||||||
private var _togglesConfig = FragmentedStorage.get<StringArrayStorage>("home_toggles");
|
|
||||||
fun initializeToolbarContent() {
|
|
||||||
if(_toolbarContentView.allViews.any { it is ToggleBar })
|
|
||||||
_toolbarContentView.removeView(_toolbarContentView.allViews.find { it is ToggleBar });
|
|
||||||
|
|
||||||
if(Settings.instance.home.showHomeFilters) {
|
|
||||||
|
|
||||||
if (!_togglesConfig.any()) {
|
|
||||||
_togglesConfig.set("today", "watched", "plugins");
|
|
||||||
_togglesConfig.save();
|
|
||||||
}
|
|
||||||
_toggleBar = ToggleBar(context).apply {
|
|
||||||
layoutParams =
|
|
||||||
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_filterLock) {
|
|
||||||
var buttonsPlugins: List<ToggleBar.Toggle> = listOf()
|
|
||||||
buttonsPlugins = (if (_togglesConfig.contains("plugins"))
|
|
||||||
(StatePlatform.instance.getEnabledClients()
|
|
||||||
.filter { it is JSClient && it.enableInHome }
|
|
||||||
.map { plugin ->
|
|
||||||
ToggleBar.Toggle(if(Settings.instance.home.showHomeFiltersPluginNames) plugin.name else "", plugin.icon, !fragment._togglePluginsDisabled.contains(plugin.id), { view, active ->
|
|
||||||
var dontSwap = false;
|
|
||||||
if (active) {
|
|
||||||
if (fragment._togglePluginsDisabled.contains(plugin.id))
|
|
||||||
fragment._togglePluginsDisabled.remove(plugin.id);
|
|
||||||
} else {
|
|
||||||
if (!fragment._togglePluginsDisabled.contains(plugin.id)) {
|
|
||||||
val enabledClients = StatePlatform.instance.getEnabledClients();
|
|
||||||
val availableAfterDisable = enabledClients.count { !fragment._togglePluginsDisabled.contains(it.id) && it.id != plugin.id };
|
|
||||||
if(availableAfterDisable > 0)
|
|
||||||
fragment._togglePluginsDisabled.add(plugin.id);
|
|
||||||
else {
|
|
||||||
UIDialogs.appToast("Home needs atleast 1 plugin active");
|
|
||||||
dontSwap = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(!dontSwap)
|
|
||||||
reloadForFilters();
|
|
||||||
else {
|
|
||||||
view.setToggle(!active);
|
|
||||||
}
|
|
||||||
}).withTag("plugins")
|
|
||||||
})
|
|
||||||
else listOf())
|
|
||||||
val buttons = (listOf<ToggleBar.Toggle?>(
|
|
||||||
(if (_togglesConfig.contains("today"))
|
|
||||||
ToggleBar.Toggle("Today", fragment._toggleRecent) { view, active ->
|
|
||||||
fragment._toggleRecent = active; reloadForFilters()
|
|
||||||
}
|
|
||||||
.withTag("today") else null),
|
|
||||||
(if (_togglesConfig.contains("watched"))
|
|
||||||
ToggleBar.Toggle("Unwatched", fragment._toggleWatched) { view, active ->
|
|
||||||
fragment._toggleWatched = active; reloadForFilters()
|
|
||||||
}
|
|
||||||
.withTag("watched") else null),
|
|
||||||
).filterNotNull() + buttonsPlugins)
|
|
||||||
.sortedBy { _togglesConfig.indexOf(it.tag ?: "") } ?: listOf()
|
|
||||||
|
|
||||||
val buttonSettings = ToggleBar.Toggle("", R.drawable.ic_settings, true, { view, active ->
|
|
||||||
showOrderOverlay(_overlayContainer,
|
|
||||||
"Visible home filters",
|
|
||||||
listOf(
|
|
||||||
Pair("Plugins", "plugins"),
|
|
||||||
Pair("Today", "today"),
|
|
||||||
Pair("Watched", "watched")
|
|
||||||
),
|
|
||||||
{
|
|
||||||
val newArray = it.map { it.toString() }.toTypedArray();
|
|
||||||
_togglesConfig.set(*(if (newArray.any()) newArray else arrayOf("none")));
|
|
||||||
_togglesConfig.save();
|
|
||||||
initializeToolbarContent();
|
|
||||||
},
|
|
||||||
"Select which toggles you want to see in order. You can also choose to hide filters in the Grayjay Settings"
|
|
||||||
);
|
|
||||||
}).asButton();
|
|
||||||
|
|
||||||
val buttonsOrder = (buttons + listOf(buttonSettings)).toTypedArray();
|
|
||||||
_toggleBar?.setToggles(*buttonsOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
_toolbarContentView.addView(_toggleBar, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun reloadForFilters() {
|
|
||||||
lastPager?.let { loadedResult(it.getWindow()) };
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
return results.filter {
|
return results.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) };
|
||||||
if(StateMeta.instance.isVideoHidden(it.url))
|
|
||||||
return@filter false;
|
|
||||||
if(StateMeta.instance.isCreatorHidden(it.author.url))
|
|
||||||
return@filter false;
|
|
||||||
|
|
||||||
if(fragment._toggleRecent && (it.datetime?.getNowDiffHours() ?: 0) > 25)
|
|
||||||
return@filter false;
|
|
||||||
if(fragment._toggleWatched && StateHistory.instance.isHistoryWatched(it.url, 0))
|
|
||||||
return@filter false;
|
|
||||||
if(fragment._togglePluginsDisabled.any() && it.id.pluginId != null && fragment._togglePluginsDisabled.contains(it.id.pluginId)) {
|
|
||||||
return@filter false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return@filter true;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadResults(withRefetch: Boolean = true) {
|
private fun loadResults() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
_taskGetPager.run(withRefetch);
|
_taskGetPager.run(true);
|
||||||
}
|
}
|
||||||
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
||||||
if (pager is EmptyPager<IPlatformContent>) {
|
if (pager is EmptyPager<IPlatformContent>) {
|
||||||
|
|
|
@ -326,10 +326,6 @@ class PlaylistFragment : MainFragment() {
|
||||||
playlist.videos = ArrayList(playlist.videos.filter { it != video });
|
playlist.videos = ArrayList(playlist.videos.filter { it != video });
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVideoOptions(video: IPlatformVideo) {
|
|
||||||
UISlideOverlays.showVideoOptionsOverlay(video, overlayContainer);
|
|
||||||
}
|
|
||||||
override fun onVideoClicked(video: IPlatformVideo) {
|
override fun onVideoClicked(video: IPlatformVideo) {
|
||||||
val playlist = _playlist;
|
val playlist = _playlist;
|
||||||
if (playlist != null) {
|
if (playlist != null) {
|
||||||
|
|
|
@ -6,17 +6,12 @@ import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.Spinner
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.widget.addTextChangedListener
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -26,15 +21,11 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
|
||||||
import com.futo.platformplayer.stores.StringStorage
|
|
||||||
import com.futo.platformplayer.views.SearchView
|
|
||||||
import com.futo.platformplayer.views.adapters.*
|
import com.futo.platformplayer.views.adapters.*
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.OffsetDateTime
|
|
||||||
|
|
||||||
|
|
||||||
class PlaylistsFragment : MainFragment() {
|
class PlaylistsFragment : MainFragment() {
|
||||||
|
@ -74,7 +65,6 @@ class PlaylistsFragment : MainFragment() {
|
||||||
private val _fragment: PlaylistsFragment;
|
private val _fragment: PlaylistsFragment;
|
||||||
|
|
||||||
var watchLater: ArrayList<IPlatformVideo> = arrayListOf();
|
var watchLater: ArrayList<IPlatformVideo> = arrayListOf();
|
||||||
var allPlaylists: ArrayList<Playlist> = arrayListOf();
|
|
||||||
var playlists: ArrayList<Playlist> = arrayListOf();
|
var playlists: ArrayList<Playlist> = arrayListOf();
|
||||||
private var _appBar: AppBarLayout;
|
private var _appBar: AppBarLayout;
|
||||||
private var _adapterWatchLater: VideoListHorizontalAdapter;
|
private var _adapterWatchLater: VideoListHorizontalAdapter;
|
||||||
|
@ -82,20 +72,12 @@ class PlaylistsFragment : MainFragment() {
|
||||||
private var _layoutWatchlist: ConstraintLayout;
|
private var _layoutWatchlist: ConstraintLayout;
|
||||||
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
||||||
|
|
||||||
private var _listPlaylistsSearch: EditText;
|
|
||||||
|
|
||||||
private var _ordering = FragmentedStorage.get<StringStorage>("playlists_ordering")
|
|
||||||
|
|
||||||
|
|
||||||
constructor(fragment: PlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
|
constructor(fragment: PlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||||
_fragment = fragment;
|
_fragment = fragment;
|
||||||
inflater.inflate(R.layout.fragment_playlists, this);
|
inflater.inflate(R.layout.fragment_playlists, this);
|
||||||
|
|
||||||
_listPlaylistsSearch = findViewById(R.id.playlists_search);
|
|
||||||
|
|
||||||
watchLater = ArrayList();
|
watchLater = ArrayList();
|
||||||
playlists = ArrayList();
|
playlists = ArrayList();
|
||||||
allPlaylists = ArrayList();
|
|
||||||
|
|
||||||
val recyclerWatchLater = findViewById<RecyclerView>(R.id.recycler_watch_later);
|
val recyclerWatchLater = findViewById<RecyclerView>(R.id.recycler_watch_later);
|
||||||
|
|
||||||
|
@ -123,7 +105,6 @@ class PlaylistsFragment : MainFragment() {
|
||||||
buttonCreatePlaylist.setOnClickListener {
|
buttonCreatePlaylist.setOnClickListener {
|
||||||
_slideUpOverlay = UISlideOverlays.showCreatePlaylistOverlay(findViewById<FrameLayout>(R.id.overlay_create_playlist)) {
|
_slideUpOverlay = UISlideOverlays.showCreatePlaylistOverlay(findViewById<FrameLayout>(R.id.overlay_create_playlist)) {
|
||||||
val playlist = Playlist(it, arrayListOf());
|
val playlist = Playlist(it, arrayListOf());
|
||||||
allPlaylists.add(0, playlist);
|
|
||||||
playlists.add(0, playlist);
|
playlists.add(0, playlist);
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
|
|
||||||
|
@ -139,35 +120,6 @@ class PlaylistsFragment : MainFragment() {
|
||||||
_appBar = findViewById(R.id.app_bar);
|
_appBar = findViewById(R.id.app_bar);
|
||||||
_layoutWatchlist = findViewById(R.id.layout_watchlist);
|
_layoutWatchlist = findViewById(R.id.layout_watchlist);
|
||||||
|
|
||||||
|
|
||||||
_listPlaylistsSearch.addTextChangedListener {
|
|
||||||
updatePlaylistsFiltering();
|
|
||||||
}
|
|
||||||
val spinnerSortBy: Spinner = findViewById(R.id.spinner_sortby);
|
|
||||||
spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.playlists_sortby_array)).also {
|
|
||||||
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
|
||||||
};
|
|
||||||
val options = listOf("nameAsc", "nameDesc", "dateEditAsc", "dateEditDesc", "dateCreateAsc", "dateCreateDesc", "datePlayAsc", "datePlayDesc");
|
|
||||||
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
|
|
||||||
when(pos) {
|
|
||||||
0 -> _ordering.setAndSave("nameAsc")
|
|
||||||
1 -> _ordering.setAndSave("nameDesc")
|
|
||||||
2 -> _ordering.setAndSave("dateEditAsc")
|
|
||||||
3 -> _ordering.setAndSave("dateEditDesc")
|
|
||||||
4 -> _ordering.setAndSave("dateCreateAsc")
|
|
||||||
5 -> _ordering.setAndSave("dateCreateDesc")
|
|
||||||
6 -> _ordering.setAndSave("datePlayAsc")
|
|
||||||
7 -> _ordering.setAndSave("datePlayDesc")
|
|
||||||
else -> _ordering.setAndSave("")
|
|
||||||
}
|
|
||||||
updatePlaylistsFiltering()
|
|
||||||
}
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
|
||||||
};
|
|
||||||
spinnerSortBy.setSelection(Math.max(0, options.indexOf(_ordering.value)));
|
|
||||||
|
|
||||||
|
|
||||||
findViewById<TextView>(R.id.text_view_all).setOnClickListener { _fragment.navigate<WatchLaterFragment>(context.getString(R.string.watch_later)); };
|
findViewById<TextView>(R.id.text_view_all).setOnClickListener { _fragment.navigate<WatchLaterFragment>(context.getString(R.string.watch_later)); };
|
||||||
StatePlaylists.instance.onWatchLaterChanged.subscribe(this) {
|
StatePlaylists.instance.onWatchLaterChanged.subscribe(this) {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
@ -182,12 +134,10 @@ class PlaylistsFragment : MainFragment() {
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun onShown() {
|
fun onShown() {
|
||||||
allPlaylists.clear();
|
|
||||||
playlists.clear()
|
playlists.clear()
|
||||||
allPlaylists.addAll(
|
playlists.addAll(
|
||||||
StatePlaylists.instance.getPlaylists().sortedByDescending { maxOf(it.datePlayed, it.dateUpdate, it.dateCreation) }
|
StatePlaylists.instance.getPlaylists().sortedByDescending { maxOf(it.datePlayed, it.dateUpdate, it.dateCreation) }
|
||||||
);
|
);
|
||||||
playlists.addAll(filterPlaylists(allPlaylists));
|
|
||||||
_adapterPlaylist.notifyDataSetChanged();
|
_adapterPlaylist.notifyDataSetChanged();
|
||||||
|
|
||||||
updateWatchLater();
|
updateWatchLater();
|
||||||
|
@ -207,32 +157,6 @@ class PlaylistsFragment : MainFragment() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlaylistsFiltering() {
|
|
||||||
val toFilter = allPlaylists ?: return;
|
|
||||||
playlists.clear();
|
|
||||||
playlists.addAll(filterPlaylists(toFilter));
|
|
||||||
_adapterPlaylist.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
private fun filterPlaylists(pls: List<Playlist>): List<Playlist> {
|
|
||||||
var playlistsToReturn = pls;
|
|
||||||
if(!_listPlaylistsSearch.text.isNullOrEmpty())
|
|
||||||
playlistsToReturn = playlistsToReturn.filter { it.name.contains(_listPlaylistsSearch.text, true) };
|
|
||||||
if(!_ordering.value.isNullOrEmpty()){
|
|
||||||
playlistsToReturn = when(_ordering.value){
|
|
||||||
"nameAsc" -> playlistsToReturn.sortedBy { it.name.lowercase() }
|
|
||||||
"nameDesc" -> playlistsToReturn.sortedByDescending { it.name.lowercase() };
|
|
||||||
"dateEditAsc" -> playlistsToReturn.sortedBy { it.dateUpdate ?: OffsetDateTime.MAX };
|
|
||||||
"dateEditDesc" -> playlistsToReturn.sortedByDescending { it.dateUpdate ?: OffsetDateTime.MIN }
|
|
||||||
"dateCreateAsc" -> playlistsToReturn.sortedBy { it.dateCreation ?: OffsetDateTime.MAX };
|
|
||||||
"dateCreateDesc" -> playlistsToReturn.sortedByDescending { it.dateCreation ?: OffsetDateTime.MIN }
|
|
||||||
"datePlayAsc" -> playlistsToReturn.sortedBy { it.datePlayed ?: OffsetDateTime.MAX };
|
|
||||||
"datePlayDesc" -> playlistsToReturn.sortedByDescending { it.datePlayed ?: OffsetDateTime.MIN }
|
|
||||||
else -> playlistsToReturn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return playlistsToReturn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateWatchLater() {
|
private fun updateWatchLater() {
|
||||||
val watchList = StatePlaylists.instance.getWatchLater();
|
val watchList = StatePlaylists.instance.getWatchLater();
|
||||||
if (watchList.isNotEmpty()) {
|
if (watchList.isNotEmpty()) {
|
||||||
|
@ -240,7 +164,7 @@ class PlaylistsFragment : MainFragment() {
|
||||||
|
|
||||||
_appBar.let { appBar ->
|
_appBar.let { appBar ->
|
||||||
val layoutParams: CoordinatorLayout.LayoutParams = appBar.layoutParams as CoordinatorLayout.LayoutParams;
|
val layoutParams: CoordinatorLayout.LayoutParams = appBar.layoutParams as CoordinatorLayout.LayoutParams;
|
||||||
layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 315.0f, resources.displayMetrics).toInt();
|
layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 230.0f, resources.displayMetrics).toInt();
|
||||||
appBar.layoutParams = layoutParams;
|
appBar.layoutParams = layoutParams;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -248,7 +172,7 @@ class PlaylistsFragment : MainFragment() {
|
||||||
|
|
||||||
_appBar.let { appBar ->
|
_appBar.let { appBar ->
|
||||||
val layoutParams: CoordinatorLayout.LayoutParams = appBar.layoutParams as CoordinatorLayout.LayoutParams;
|
val layoutParams: CoordinatorLayout.LayoutParams = appBar.layoutParams as CoordinatorLayout.LayoutParams;
|
||||||
layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 110.0f, resources.displayMetrics).toInt();
|
layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25.0f, resources.displayMetrics).toInt();
|
||||||
appBar.layoutParams = layoutParams;
|
appBar.layoutParams = layoutParams;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,8 +33,10 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fixHtmlWhitespace
|
import com.futo.platformplayer.fixHtmlWhitespace
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
@ -45,6 +47,7 @@ import com.futo.platformplayer.views.adapters.ChannelTab
|
||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView
|
||||||
import com.futo.platformplayer.views.comments.AddCommentView
|
import com.futo.platformplayer.views.comments.AddCommentView
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.others.Toggle
|
||||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
@ -54,8 +57,6 @@ import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.ContentType
|
||||||
import com.futo.polycentric.core.Models
|
import com.futo.polycentric.core.Models
|
||||||
import com.futo.polycentric.core.Opinion
|
import com.futo.polycentric.core.Opinion
|
||||||
import com.futo.polycentric.core.PolycentricProfile
|
|
||||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.google.android.flexbox.FlexboxLayout
|
import com.google.android.flexbox.FlexboxLayout
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.google.android.material.shape.CornerFamily
|
import com.google.android.material.shape.CornerFamily
|
||||||
|
@ -111,7 +112,7 @@ class PostDetailFragment : MainFragment {
|
||||||
private var _isLoading = false;
|
private var _isLoading = false;
|
||||||
private var _post: IPlatformPostDetails? = null;
|
private var _post: IPlatformPostDetails? = null;
|
||||||
private var _postOverview: IPlatformPost? = null;
|
private var _postOverview: IPlatformPost? = null;
|
||||||
private var _polycentricProfile: PolycentricProfile? = null;
|
private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null;
|
||||||
private var _version = 0;
|
private var _version = 0;
|
||||||
private var _isRepliesVisible: Boolean = false;
|
private var _isRepliesVisible: Boolean = false;
|
||||||
private var _repliesAnimator: ViewPropertyAnimator? = null;
|
private var _repliesAnimator: ViewPropertyAnimator? = null;
|
||||||
|
@ -168,7 +169,7 @@ class PostDetailFragment : MainFragment {
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment);
|
||||||
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) })
|
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
||||||
.success { it -> setPolycentricProfile(it, animate = true) }
|
.success { it -> setPolycentricProfile(it, animate = true) }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load claims.", it);
|
Logger.w(TAG, "Failed to load claims.", it);
|
||||||
|
@ -273,7 +274,7 @@ class PostDetailFragment : MainFragment {
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonStore.setOnClickListener {
|
_buttonStore.setOnClickListener {
|
||||||
_polycentricProfile?.systemState?.store?.let {
|
_polycentricProfile?.profile?.systemState?.store?.let {
|
||||||
try {
|
try {
|
||||||
val uri = Uri.parse(it);
|
val uri = Uri.parse(it);
|
||||||
val intent = Intent(Intent.ACTION_VIEW);
|
val intent = Intent(Intent.ACTION_VIEW);
|
||||||
|
@ -333,7 +334,7 @@ class PostDetailFragment : MainFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val queryReferencesResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, ref, null,null,
|
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
|
||||||
arrayListOf(
|
arrayListOf(
|
||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
||||||
ContentType.OPINION.value).setValue(
|
ContentType.OPINION.value).setValue(
|
||||||
|
@ -603,8 +604,16 @@ class PostDetailFragment : MainFragment {
|
||||||
|
|
||||||
private fun fetchPolycentricProfile() {
|
private fun fetchPolycentricProfile() {
|
||||||
val author = _post?.author ?: _postOverview?.author ?: return;
|
val author = _post?.author ?: _postOverview?.author ?: return;
|
||||||
|
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(author.url, true);
|
||||||
|
if (cachedPolycentricProfile != null) {
|
||||||
|
setPolycentricProfile(cachedPolycentricProfile, animate = false);
|
||||||
|
if (cachedPolycentricProfile.expired) {
|
||||||
|
_taskLoadPolycentricProfile.run(author.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
setPolycentricProfile(null, animate = false);
|
setPolycentricProfile(null, animate = false);
|
||||||
_taskLoadPolycentricProfile.run(author.id);
|
_taskLoadPolycentricProfile.run(author.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setChannelMeta(value: IPlatformPost?) {
|
private fun setChannelMeta(value: IPlatformPost?) {
|
||||||
|
@ -630,18 +639,17 @@ class PostDetailFragment : MainFragment {
|
||||||
_repliesOverlay.cleanup();
|
_repliesOverlay.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) {
|
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||||
_polycentricProfile = polycentricProfile;
|
_polycentricProfile = cachedPolycentricProfile;
|
||||||
|
|
||||||
val pp = _polycentricProfile;
|
if (cachedPolycentricProfile?.profile == null) {
|
||||||
if (pp == null) {
|
|
||||||
_layoutMonetization.visibility = View.GONE;
|
_layoutMonetization.visibility = View.GONE;
|
||||||
_creatorThumbnail.setHarborAvailable(false, animate, null);
|
_creatorThumbnail.setHarborAvailable(false, animate, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_layoutMonetization.visibility = View.VISIBLE;
|
_layoutMonetization.visibility = View.VISIBLE;
|
||||||
_creatorThumbnail.setHarborAvailable(true, animate, pp.system.toProto());
|
_creatorThumbnail.setHarborAvailable(true, animate, cachedPolycentricProfile.profile.system.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchPost() {
|
private fun fetchPost() {
|
||||||
|
|
|
@ -556,7 +556,7 @@ class SourceDetailFragment : MainFragment() {
|
||||||
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
||||||
|
|
||||||
val config = SourcePluginConfig.fromJson(configJson);
|
val config = SourcePluginConfig.fromJson(configJson);
|
||||||
if (config.version <= c.version) {
|
if (config.version <= c.version && config.name != "Youtube") {
|
||||||
Logger.i(TAG, "Plugin is up to date.");
|
Logger.i(TAG, "Plugin is up to date.");
|
||||||
withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.plugin_is_fully_up_to_date)); };
|
withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.plugin_is_fully_up_to_date)); };
|
||||||
return@launch;
|
return@launch;
|
||||||
|
|
|
@ -256,8 +256,6 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||||
val sub = StateSubscriptions.instance.getSubscription(sub) ?: StateSubscriptions.instance.getSubscriptionOther(sub);
|
val sub = StateSubscriptions.instance.getSubscription(sub) ?: StateSubscriptions.instance.getSubscriptionOther(sub);
|
||||||
if(sub != null && sub.channel.thumbnail != null) {
|
if(sub != null && sub.channel.thumbnail != null) {
|
||||||
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
||||||
if(g.image != null)
|
|
||||||
g.image!!.subscriptionUrl = sub.channel.url;
|
|
||||||
g.image?.setImageView(_imageGroup);
|
g.image?.setImageView(_imageGroup);
|
||||||
g.image?.setImageView(_imageGroupBackground);
|
g.image?.setImageView(_imageGroupBackground);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -18,7 +18,6 @@ import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.exceptions.RateLimitException
|
import com.futo.platformplayer.exceptions.RateLimitException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment.SubscriptionsFeedView.FeedFilterSettings
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.SearchType
|
import com.futo.platformplayer.models.SearchType
|
||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
|
@ -57,9 +56,6 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
private var _group: SubscriptionGroup? = null;
|
private var _group: SubscriptionGroup? = null;
|
||||||
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
||||||
|
|
||||||
private val _filterLock = Object();
|
|
||||||
private val _filterSettings = FragmentedStorage.get<FeedFilterSettings>("subFeedFilter");
|
|
||||||
|
|
||||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
super.onShownWithView(parameter, isBack);
|
super.onShownWithView(parameter, isBack);
|
||||||
_view?.onShown();
|
_view?.onShown();
|
||||||
|
@ -188,6 +184,8 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
return Json.encodeToString(this);
|
return Json.encodeToString(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private val _filterLock = Object();
|
||||||
|
private val _filterSettings = FragmentedStorage.get<FeedFilterSettings>("subFeedFilter");
|
||||||
|
|
||||||
private var _bypassRateLimit = false;
|
private var _bypassRateLimit = false;
|
||||||
private val _lastExceptions: List<Throwable>? = null;
|
private val _lastExceptions: List<Throwable>? = null;
|
||||||
|
@ -286,18 +284,13 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
fragment.navigate<SubscriptionGroupFragment>(g);
|
fragment.navigate<SubscriptionGroupFragment>(g);
|
||||||
};
|
};
|
||||||
|
|
||||||
synchronized(fragment._filterLock) {
|
synchronized(_filterLock) {
|
||||||
_subscriptionBar?.setToggles(
|
_subscriptionBar?.setToggles(
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.videos), fragment._filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { view, active ->
|
SubscriptionBar.Toggle(context.getString(R.string.videos), _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); },
|
||||||
toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), active); },
|
SubscriptionBar.Toggle(context.getString(R.string.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); },
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.posts), fragment._filterSettings.allowContentTypes.contains(ContentType.POST)) { view, active ->
|
SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); },
|
||||||
toggleFilterContentType(ContentType.POST, active); },
|
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); },
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.live), fragment._filterSettings.allowLive) { view, active ->
|
SubscriptionBar.Toggle(context.getString(R.string.watched), _filterSettings.allowWatched) { _filterSettings.allowWatched = it; _filterSettings.save(); loadResults(false); }
|
||||||
fragment._filterSettings.allowLive = active; fragment._filterSettings.save(); loadResults(false); },
|
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.planned), fragment._filterSettings.allowPlanned) { view, active ->
|
|
||||||
fragment._filterSettings.allowPlanned = active; fragment._filterSettings.save(); loadResults(false); },
|
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.watched), fragment._filterSettings.allowWatched) { view, active ->
|
|
||||||
fragment._filterSettings.allowWatched = active; fragment._filterSettings.save(); loadResults(false); }
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -308,13 +301,13 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
toggleFilterContentType(contentType, isTrue);
|
toggleFilterContentType(contentType, isTrue);
|
||||||
}
|
}
|
||||||
private fun toggleFilterContentType(contentType: ContentType, isTrue: Boolean) {
|
private fun toggleFilterContentType(contentType: ContentType, isTrue: Boolean) {
|
||||||
synchronized(fragment._filterLock) {
|
synchronized(_filterLock) {
|
||||||
if(!isTrue) {
|
if(!isTrue) {
|
||||||
fragment._filterSettings.allowContentTypes.remove(contentType);
|
_filterSettings.allowContentTypes.remove(contentType);
|
||||||
} else if(!fragment._filterSettings.allowContentTypes.contains(contentType)) {
|
} else if(!_filterSettings.allowContentTypes.contains(contentType)) {
|
||||||
fragment._filterSettings.allowContentTypes.add(contentType)
|
_filterSettings.allowContentTypes.add(contentType)
|
||||||
}
|
}
|
||||||
fragment._filterSettings.save();
|
_filterSettings.save();
|
||||||
};
|
};
|
||||||
if(Settings.instance.subscriptions.fetchOnTabOpen) { //TODO: Do this different, temporary workaround
|
if(Settings.instance.subscriptions.fetchOnTabOpen) { //TODO: Do this different, temporary workaround
|
||||||
loadResults(false);
|
loadResults(false);
|
||||||
|
@ -327,9 +320,9 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
val nowSoon = OffsetDateTime.now().plusMinutes(5);
|
val nowSoon = OffsetDateTime.now().plusMinutes(5);
|
||||||
val filterGroup = subGroup;
|
val filterGroup = subGroup;
|
||||||
return results.filter {
|
return results.filter {
|
||||||
val allowedContentType = fragment._filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
|
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
|
||||||
|
|
||||||
if(it is IPlatformVideo && it.duration > 0 && !fragment._filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration))
|
if(it is IPlatformVideo && it.duration > 0 && !_filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration))
|
||||||
return@filter false;
|
return@filter false;
|
||||||
|
|
||||||
//TODO: Check against a sub cache
|
//TODO: Check against a sub cache
|
||||||
|
@ -338,11 +331,11 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
|
|
||||||
|
|
||||||
if(it.datetime?.isAfter(nowSoon) == true) {
|
if(it.datetime?.isAfter(nowSoon) == true) {
|
||||||
if(!fragment._filterSettings.allowPlanned)
|
if(!_filterSettings.allowPlanned)
|
||||||
return@filter false;
|
return@filter false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(fragment._filterSettings.allowLive) { //If allowLive, always show live
|
if(_filterSettings.allowLive) { //If allowLive, always show live
|
||||||
if(it is IPlatformVideo && it.isLive)
|
if(it is IPlatformVideo && it.isLive)
|
||||||
return@filter true;
|
return@filter true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
@ -18,8 +17,6 @@ import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.SearchHistoryStorage
|
import com.futo.platformplayer.stores.SearchHistoryStorage
|
||||||
import com.futo.platformplayer.views.adapters.SearchSuggestionAdapter
|
import com.futo.platformplayer.views.adapters.SearchSuggestionAdapter
|
||||||
import com.futo.platformplayer.views.others.RadioGroupView
|
|
||||||
import com.futo.platformplayer.views.others.TagsView
|
|
||||||
|
|
||||||
data class SuggestionsFragmentData(val query: String, val searchType: SearchType, val channelUrl: String? = null);
|
data class SuggestionsFragmentData(val query: String, val searchType: SearchType, val channelUrl: String? = null);
|
||||||
|
|
||||||
|
@ -30,7 +27,6 @@ class SuggestionsFragment : MainFragment {
|
||||||
|
|
||||||
private var _recyclerSuggestions: RecyclerView? = null;
|
private var _recyclerSuggestions: RecyclerView? = null;
|
||||||
private var _llmSuggestions: LinearLayoutManager? = null;
|
private var _llmSuggestions: LinearLayoutManager? = null;
|
||||||
private var _radioGroupView: RadioGroupView? = null;
|
|
||||||
private val _suggestions: ArrayList<String> = ArrayList();
|
private val _suggestions: ArrayList<String> = ArrayList();
|
||||||
private var _query: String? = null;
|
private var _query: String? = null;
|
||||||
private var _searchType: SearchType = SearchType.VIDEO;
|
private var _searchType: SearchType = SearchType.VIDEO;
|
||||||
|
@ -52,7 +48,14 @@ class SuggestionsFragment : MainFragment {
|
||||||
_adapterSuggestions.onClicked.subscribe { suggestion ->
|
_adapterSuggestions.onClicked.subscribe { suggestion ->
|
||||||
val storage = FragmentedStorage.get<SearchHistoryStorage>();
|
val storage = FragmentedStorage.get<SearchHistoryStorage>();
|
||||||
storage.add(suggestion);
|
storage.add(suggestion);
|
||||||
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(suggestion, _searchType, _channelUrl));
|
|
||||||
|
if (_searchType == SearchType.CREATOR) {
|
||||||
|
navigate<CreatorSearchResultsFragment>(suggestion);
|
||||||
|
} else if (_searchType == SearchType.PLAYLIST) {
|
||||||
|
navigate<PlaylistSearchResultsFragment>(suggestion);
|
||||||
|
} else {
|
||||||
|
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(suggestion, SearchType.VIDEO, _channelUrl));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_adapterSuggestions.onRemove.subscribe { suggestion ->
|
_adapterSuggestions.onRemove.subscribe { suggestion ->
|
||||||
val index = _suggestions.indexOf(suggestion);
|
val index = _suggestions.indexOf(suggestion);
|
||||||
|
@ -76,15 +79,6 @@ class SuggestionsFragment : MainFragment {
|
||||||
recyclerSuggestions.adapter = _adapterSuggestions;
|
recyclerSuggestions.adapter = _adapterSuggestions;
|
||||||
_recyclerSuggestions = recyclerSuggestions;
|
_recyclerSuggestions = recyclerSuggestions;
|
||||||
|
|
||||||
_radioGroupView = view.findViewById<RadioGroupView>(R.id.radio_group).apply {
|
|
||||||
onSelectedChange.subscribe {
|
|
||||||
if (it.size != 1)
|
|
||||||
_searchType = SearchType.VIDEO
|
|
||||||
else
|
|
||||||
_searchType = (it[0] ?: SearchType.VIDEO) as SearchType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSuggestions();
|
loadSuggestions();
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
@ -115,27 +109,25 @@ class SuggestionsFragment : MainFragment {
|
||||||
_channelUrl = null;
|
_channelUrl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_radioGroupView?.setOptions(listOf(Pair("Media", SearchType.VIDEO), Pair("Creators", SearchType.CREATOR), Pair("Playlists", SearchType.PLAYLIST)), listOf(_searchType), false, true)
|
|
||||||
|
|
||||||
topBar?.apply {
|
topBar?.apply {
|
||||||
if (this is SearchTopBarFragment) {
|
if (this is SearchTopBarFragment) {
|
||||||
onSearch.subscribe(this) {
|
onSearch.subscribe(this) {
|
||||||
if(it.isHttpUrl()) {
|
if (_searchType == SearchType.CREATOR) {
|
||||||
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
|
navigate<CreatorSearchResultsFragment>(it);
|
||||||
navigate<RemotePlaylistFragment>(it);
|
} else if (_searchType == SearchType.PLAYLIST) {
|
||||||
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
navigate<PlaylistSearchResultsFragment>(it);
|
||||||
navigate<ChannelFragment>(it);
|
} else {
|
||||||
else {
|
if(it.isHttpUrl()) {
|
||||||
val url = it;
|
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
|
||||||
activity?.let {
|
navigate<RemotePlaylistFragment>(it);
|
||||||
close()
|
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||||
if(it is MainActivity)
|
navigate<ChannelFragment>(it);
|
||||||
it.navigate(it.getFragment<VideoDetailFragment>(), url);
|
else
|
||||||
}
|
navigate<VideoDetailFragment>(it);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
|
||||||
}
|
}
|
||||||
else
|
|
||||||
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, _searchType, _channelUrl));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onTextChange.subscribe(this) {
|
onTextChange.subscribe(this) {
|
||||||
|
@ -197,7 +189,6 @@ class SuggestionsFragment : MainFragment {
|
||||||
super.onDestroyMainView();
|
super.onDestroyMainView();
|
||||||
_getSuggestions.onError.clear();
|
_getSuggestions.onError.clear();
|
||||||
_recyclerSuggestions = null;
|
_recyclerSuggestions = null;
|
||||||
_radioGroupView = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|
|
@ -94,10 +94,12 @@ import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||||
import com.futo.platformplayer.fixHtmlLinks
|
import com.futo.platformplayer.fixHtmlLinks
|
||||||
import com.futo.platformplayer.fixHtmlWhitespace
|
import com.futo.platformplayer.fixHtmlWhitespace
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.getNowDiffSeconds
|
import com.futo.platformplayer.getNowDiffSeconds
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
|
@ -132,7 +134,6 @@ import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
|
||||||
import com.futo.platformplayer.views.casting.CastView
|
import com.futo.platformplayer.views.casting.CastView
|
||||||
import com.futo.platformplayer.views.comments.AddCommentView
|
import com.futo.platformplayer.views.comments.AddCommentView
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.overlays.ChaptersOverlay
|
|
||||||
import com.futo.platformplayer.views.overlays.DescriptionOverlay
|
import com.futo.platformplayer.views.overlays.DescriptionOverlay
|
||||||
import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
||||||
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
||||||
|
@ -148,7 +149,6 @@ import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||||
import com.futo.platformplayer.views.pills.RoundButton
|
import com.futo.platformplayer.views.pills.RoundButton
|
||||||
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
||||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
import com.futo.platformplayer.views.segments.ChaptersList
|
|
||||||
import com.futo.platformplayer.views.segments.CommentsList
|
import com.futo.platformplayer.views.segments.CommentsList
|
||||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||||
import com.futo.platformplayer.views.video.FutoVideoPlayer
|
import com.futo.platformplayer.views.video.FutoVideoPlayer
|
||||||
|
@ -158,8 +158,6 @@ import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.ContentType
|
||||||
import com.futo.polycentric.core.Models
|
import com.futo.polycentric.core.Models
|
||||||
import com.futo.polycentric.core.Opinion
|
import com.futo.polycentric.core.Opinion
|
||||||
import com.futo.polycentric.core.PolycentricProfile
|
|
||||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -197,8 +195,6 @@ class VideoDetailView : ConstraintLayout {
|
||||||
private var _liveChat: LiveChatManager? = null;
|
private var _liveChat: LiveChatManager? = null;
|
||||||
private var _videoResumePositionMilliseconds : Long = 0L;
|
private var _videoResumePositionMilliseconds : Long = 0L;
|
||||||
|
|
||||||
private var _chapters: List<IChapter>? = null;
|
|
||||||
|
|
||||||
private val _player: FutoVideoPlayer;
|
private val _player: FutoVideoPlayer;
|
||||||
private val _cast: CastView;
|
private val _cast: CastView;
|
||||||
private val _playerProgress: PlayerControlView;
|
private val _playerProgress: PlayerControlView;
|
||||||
|
@ -267,7 +263,6 @@ class VideoDetailView : ConstraintLayout {
|
||||||
private val _container_content_liveChat: LiveChatOverlay;
|
private val _container_content_liveChat: LiveChatOverlay;
|
||||||
private val _container_content_browser: WebviewOverlay;
|
private val _container_content_browser: WebviewOverlay;
|
||||||
private val _container_content_support: SupportOverlay;
|
private val _container_content_support: SupportOverlay;
|
||||||
private val _container_content_chapters: ChaptersOverlay;
|
|
||||||
|
|
||||||
private var _container_content_current: View;
|
private var _container_content_current: View;
|
||||||
|
|
||||||
|
@ -299,7 +294,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
private set;
|
private set;
|
||||||
private var _historicalPosition: Long = 0;
|
private var _historicalPosition: Long = 0;
|
||||||
private var _commentsCount = 0;
|
private var _commentsCount = 0;
|
||||||
private var _polycentricProfile: PolycentricProfile? = null;
|
private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null;
|
||||||
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
||||||
private var _autoplayVideo: IPlatformVideo? = null
|
private var _autoplayVideo: IPlatformVideo? = null
|
||||||
|
|
||||||
|
@ -379,7 +374,6 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
||||||
_container_content_support = findViewById(R.id.videodetail_container_support);
|
_container_content_support = findViewById(R.id.videodetail_container_support);
|
||||||
_container_content_browser = findViewById(R.id.videodetail_container_webview)
|
_container_content_browser = findViewById(R.id.videodetail_container_webview)
|
||||||
_container_content_chapters = findViewById(R.id.videodetail_container_chapters);
|
|
||||||
|
|
||||||
_addCommentView = findViewById(R.id.add_comment_view);
|
_addCommentView = findViewById(R.id.add_comment_view);
|
||||||
_commentsList = findViewById(R.id.comments_list);
|
_commentsList = findViewById(R.id.comments_list);
|
||||||
|
@ -404,10 +398,6 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_monetization = findViewById(R.id.monetization);
|
_monetization = findViewById(R.id.monetization);
|
||||||
_player.attachPlayer();
|
_player.attachPlayer();
|
||||||
|
|
||||||
_player.onChapterClicked.subscribe {
|
|
||||||
showChaptersUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
_buttonSubscribe.onSubscribed.subscribe {
|
_buttonSubscribe.onSubscribed.subscribe {
|
||||||
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||||
|
@ -419,12 +409,12 @@ class VideoDetailView : ConstraintLayout {
|
||||||
};
|
};
|
||||||
|
|
||||||
_monetization.onSupportTap.subscribe {
|
_monetization.onSupportTap.subscribe {
|
||||||
_container_content_support.setPolycentricProfile(_polycentricProfile);
|
_container_content_support.setPolycentricProfile(_polycentricProfile?.profile);
|
||||||
switchContentView(_container_content_support);
|
switchContentView(_container_content_support);
|
||||||
};
|
};
|
||||||
|
|
||||||
_monetization.onStoreTap.subscribe {
|
_monetization.onStoreTap.subscribe {
|
||||||
_polycentricProfile?.systemState?.store?.let {
|
_polycentricProfile?.profile?.systemState?.store?.let {
|
||||||
try {
|
try {
|
||||||
val uri = Uri.parse(it);
|
val uri = Uri.parse(it);
|
||||||
val intent = Intent(Intent.ACTION_VIEW);
|
val intent = Intent(Intent.ACTION_VIEW);
|
||||||
|
@ -589,14 +579,6 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_minimize_title.setOnClickListener { onMaximize.emit(false) };
|
_minimize_title.setOnClickListener { onMaximize.emit(false) };
|
||||||
_minimize_meta.setOnClickListener { onMaximize.emit(false) };
|
_minimize_meta.setOnClickListener { onMaximize.emit(false) };
|
||||||
|
|
||||||
_player.onStateChange.subscribe {
|
|
||||||
if (_player.activelyPlaying) {
|
|
||||||
Logger.i(TAG, "Play changed, resetting error counter _didTriggerDatasourceErrorCount = 0 (_player.activelyPlaying: ${_player.activelyPlaying})")
|
|
||||||
_didTriggerDatasourceErrorCount = 0;
|
|
||||||
_didTriggerDatasourceError = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_player.onPlayChanged.subscribe {
|
_player.onPlayChanged.subscribe {
|
||||||
if (StateCasting.instance.activeDevice == null) {
|
if (StateCasting.instance.activeDevice == null) {
|
||||||
handlePlayChanged(it);
|
handlePlayChanged(it);
|
||||||
|
@ -693,17 +675,9 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_container_content_description.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_description.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_liveChat.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_liveChat.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_queue.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_queue.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_queue.onOptions.subscribe {
|
|
||||||
UISlideOverlays.showVideoOptionsOverlay(it, _overlayContainer);
|
|
||||||
}
|
|
||||||
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_browser.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_browser.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_chapters.onClose.subscribe { switchContentView(_container_content_main); };
|
|
||||||
|
|
||||||
_container_content_chapters.onClick.subscribe {
|
|
||||||
handleSeek(it.timeStart.toLong() * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
_description_viewMore.setOnClickListener {
|
_description_viewMore.setOnClickListener {
|
||||||
switchContentView(_container_content_description);
|
switchContentView(_container_content_description);
|
||||||
|
@ -870,22 +844,6 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_cast.stopAllGestures();
|
_cast.stopAllGestures();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showChaptersUI(){
|
|
||||||
video?.let {
|
|
||||||
try {
|
|
||||||
_chapters?.let {
|
|
||||||
if(it.size == 0)
|
|
||||||
return@let;
|
|
||||||
_container_content_chapters.setChapters(_chapters);
|
|
||||||
switchContentView(_container_content_chapters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateMoreButtons() {
|
fun updateMoreButtons() {
|
||||||
val isLimitedVersion = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
|
val isLimitedVersion = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
|
||||||
if (it is JSClient)
|
if (it is JSClient)
|
||||||
|
@ -899,13 +857,6 @@ class VideoDetailView : ConstraintLayout {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_chapters?.let {
|
|
||||||
if(it != null && it.size > 0)
|
|
||||||
RoundButton(context, R.drawable.ic_list, "Chapters", TAG_CHAPTERS) {
|
|
||||||
showChaptersUI();
|
|
||||||
}
|
|
||||||
else null
|
|
||||||
},
|
|
||||||
if(video?.isLive ?: false)
|
if(video?.isLive ?: false)
|
||||||
RoundButton(context, R.drawable.ic_chat, context.getString(R.string.live_chat), TAG_LIVECHAT) {
|
RoundButton(context, R.drawable.ic_chat, context.getString(R.string.live_chat), TAG_LIVECHAT) {
|
||||||
video?.let {
|
video?.let {
|
||||||
|
@ -971,7 +922,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
} else if(devices.size == 1){
|
} else if(devices.size == 1){
|
||||||
val device = devices.first();
|
val device = devices.first();
|
||||||
Logger.i(TAG, "Send to device? (public key: ${device.remotePublicKey}): " + videoToSend.url)
|
Logger.i(TAG, "Send to device? (public key: ${device.remotePublicKey}): " + videoToSend.url)
|
||||||
UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non '${device.displayName}'" , {
|
UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non ${device.remotePublicKey}" , {
|
||||||
Logger.i(TAG, "Send to device confirmed (public key: ${device.remotePublicKey}): " + videoToSend.url)
|
Logger.i(TAG, "Send to device confirmed (public key: ${device.remotePublicKey}): " + videoToSend.url)
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
@ -1012,7 +963,6 @@ class VideoDetailView : ConstraintLayout {
|
||||||
throw IllegalStateException("Expected media content, found ${video.contentType}");
|
throw IllegalStateException("Expected media content, found ${video.contentType}");
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_videoResumePositionMilliseconds = _player.position
|
|
||||||
setVideoDetails(video);
|
setVideoDetails(video);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1277,8 +1227,16 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
||||||
_channelName.text = video.author.name;
|
_channelName.text = video.author.name;
|
||||||
|
|
||||||
setPolycentricProfile(null, animate = false);
|
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url, true);
|
||||||
_taskLoadPolycentricProfile.run(video.author.id);
|
if (cachedPolycentricProfile != null) {
|
||||||
|
setPolycentricProfile(cachedPolycentricProfile, animate = false);
|
||||||
|
if (cachedPolycentricProfile.expired) {
|
||||||
|
_taskLoadPolycentricProfile.run(video.author.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setPolycentricProfile(null, animate = false);
|
||||||
|
_taskLoadPolycentricProfile.run(video.author.id);
|
||||||
|
}
|
||||||
|
|
||||||
_player.clear();
|
_player.clear();
|
||||||
|
|
||||||
|
@ -1307,6 +1265,8 @@ class VideoDetailView : ConstraintLayout {
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
||||||
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
||||||
|
_didTriggerDatasourceErrroCount = 0;
|
||||||
|
_didTriggerDatasourceError = false;
|
||||||
_autoplayVideo = null
|
_autoplayVideo = null
|
||||||
Logger.i(TAG, "Autoplay video cleared (setVideoDetails)")
|
Logger.i(TAG, "Autoplay video cleared (setVideoDetails)")
|
||||||
|
|
||||||
|
@ -1317,10 +1277,6 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_lastVideoSource = null;
|
_lastVideoSource = null;
|
||||||
_lastAudioSource = null;
|
_lastAudioSource = null;
|
||||||
_lastSubtitleSource = null;
|
_lastSubtitleSource = null;
|
||||||
|
|
||||||
Logger.i(TAG, "_didTriggerDatasourceErrorCount reset to 0 because new video")
|
|
||||||
_didTriggerDatasourceErrorCount = 0;
|
|
||||||
_didTriggerDatasourceError = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now())
|
if (videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now())
|
||||||
|
@ -1381,12 +1337,10 @@ class VideoDetailView : ConstraintLayout {
|
||||||
val chapters = null ?: StatePlatform.instance.getContentChapters(video.url);
|
val chapters = null ?: StatePlatform.instance.getContentChapters(video.url);
|
||||||
_player.setChapters(chapters);
|
_player.setChapters(chapters);
|
||||||
_cast.setChapters(chapters);
|
_cast.setChapters(chapters);
|
||||||
_chapters = _player.getChapters();
|
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
Logger.e(TAG, "Failed to get chapters", ex);
|
Logger.e(TAG, "Failed to get chapters", ex);
|
||||||
_player.setChapters(null);
|
_player.setChapters(null);
|
||||||
_cast.setChapters(null);
|
_cast.setChapters(null);
|
||||||
_chapters = null;
|
|
||||||
|
|
||||||
/*withContext(Dispatchers.Main) {
|
/*withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
||||||
|
@ -1425,10 +1379,6 @@ class VideoDetailView : ConstraintLayout {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
updateMoreButtons();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1444,8 +1394,11 @@ class VideoDetailView : ConstraintLayout {
|
||||||
setTabIndex(2, true)
|
setTabIndex(2, true)
|
||||||
} else {
|
} else {
|
||||||
when (Settings.instance.comments.defaultCommentSection) {
|
when (Settings.instance.comments.defaultCommentSection) {
|
||||||
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true)
|
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(
|
||||||
1 -> setTabIndex(1, true)
|
0,
|
||||||
|
true
|
||||||
|
) else setTabIndex(1, true);
|
||||||
|
1 -> setTabIndex(1, true);
|
||||||
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
|
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1483,8 +1436,16 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_buttonSubscribe.setSubscribeChannel(video.author.url);
|
_buttonSubscribe.setSubscribeChannel(video.author.url);
|
||||||
setDescription(video.description.fixHtmlLinks());
|
setDescription(video.description.fixHtmlLinks());
|
||||||
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
||||||
setPolycentricProfile(null, animate = false);
|
|
||||||
_taskLoadPolycentricProfile.run(video.author.id);
|
|
||||||
|
val cachedPolycentricProfile =
|
||||||
|
PolycentricCache.instance.getCachedProfile(video.author.url, true);
|
||||||
|
if (cachedPolycentricProfile != null) {
|
||||||
|
setPolycentricProfile(cachedPolycentricProfile, animate = false);
|
||||||
|
} else {
|
||||||
|
setPolycentricProfile(null, animate = false);
|
||||||
|
_taskLoadPolycentricProfile.run(video.author.id);
|
||||||
|
}
|
||||||
|
|
||||||
_platform.setPlatformFromClientID(video.id.pluginId);
|
_platform.setPlatformFromClientID(video.id.pluginId);
|
||||||
val subTitleSegments: ArrayList<String> = ArrayList();
|
val subTitleSegments: ArrayList<String> = ArrayList();
|
||||||
|
@ -1513,7 +1474,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val queryReferencesResponse = ApiMethods.getQueryReferences(
|
val queryReferencesResponse = ApiMethods.getQueryReferences(
|
||||||
ApiMethods.SERVER, ref, null, null,
|
PolycentricCache.SERVER, ref, null, null,
|
||||||
arrayListOf(
|
arrayListOf(
|
||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
||||||
.setFromType(ContentType.OPINION.value).setValue(
|
.setFromType(ContentType.OPINION.value).setValue(
|
||||||
|
@ -1529,8 +1490,10 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
val likes = queryReferencesResponse.countsList[0];
|
val likes = queryReferencesResponse.countsList[0];
|
||||||
val dislikes = queryReferencesResponse.countsList[1];
|
val dislikes = queryReferencesResponse.countsList[1];
|
||||||
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
val hasLiked =
|
||||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
||||||
|
val hasDisliked =
|
||||||
|
StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_rating.visibility = View.VISIBLE;
|
_rating.visibility = View.VISIBLE;
|
||||||
|
@ -1868,7 +1831,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var _didTriggerDatasourceErrorCount = 0;
|
private var _didTriggerDatasourceErrroCount = 0;
|
||||||
private var _didTriggerDatasourceError = false;
|
private var _didTriggerDatasourceError = false;
|
||||||
private fun onDataSourceError(exception: Throwable) {
|
private fun onDataSourceError(exception: Throwable) {
|
||||||
Logger.e(TAG, "onDataSourceError", exception);
|
Logger.e(TAG, "onDataSourceError", exception);
|
||||||
|
@ -1878,53 +1841,32 @@ class VideoDetailView : ConstraintLayout {
|
||||||
return;
|
return;
|
||||||
val config = currentVideo.sourceConfig;
|
val config = currentVideo.sourceConfig;
|
||||||
|
|
||||||
if(_didTriggerDatasourceErrorCount <= 3) {
|
if(_didTriggerDatasourceErrroCount <= 3) {
|
||||||
_didTriggerDatasourceError = true;
|
_didTriggerDatasourceError = true;
|
||||||
_didTriggerDatasourceErrorCount++;
|
_didTriggerDatasourceErrroCount++;
|
||||||
|
|
||||||
UIDialogs.toast("Detected video error, attempting automatic reload (${_didTriggerDatasourceErrorCount})");
|
|
||||||
Logger.i(TAG, "Block detected, attempting bypass (_didTriggerDatasourceErrorCount = ${_didTriggerDatasourceErrorCount})");
|
|
||||||
|
|
||||||
|
UIDialogs.toast("Block detected, attempting bypass");
|
||||||
//return;
|
//return;
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
|
||||||
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
|
val previousVideoSource = _lastVideoSource;
|
||||||
val previousVideoSource = _lastVideoSource;
|
val previousAudioSource = _lastAudioSource;
|
||||||
val previousAudioSource = _lastAudioSource;
|
|
||||||
|
|
||||||
if (newDetails is IPlatformVideoDetails) {
|
if(newDetails is IPlatformVideoDetails) {
|
||||||
val newVideoSource = if (previousVideoSource != null)
|
val newVideoSource = if(previousVideoSource != null)
|
||||||
VideoHelper.selectBestVideoSource(
|
VideoHelper.selectBestVideoSource(newDetails.video, previousVideoSource.height * previousVideoSource.width, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS);
|
||||||
newDetails.video,
|
else null;
|
||||||
previousVideoSource.height * previousVideoSource.width,
|
val newAudioSource = if(previousAudioSource != null)
|
||||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
VideoHelper.selectBestAudioSource(newDetails.video, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS, previousAudioSource.language, previousAudioSource.bitrate.toLong());
|
||||||
);
|
else null;
|
||||||
else null;
|
withContext(Dispatchers.Main) {
|
||||||
val newAudioSource = if (previousAudioSource != null)
|
video = newDetails;
|
||||||
VideoHelper.selectBestAudioSource(
|
_player.setSource(newVideoSource, newAudioSource, true, true);
|
||||||
newDetails.video,
|
|
||||||
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
|
||||||
previousAudioSource.language,
|
|
||||||
previousAudioSource.bitrate.toLong()
|
|
||||||
);
|
|
||||||
else null;
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
video = newDetails;
|
|
||||||
_player.setSource(newVideoSource, newAudioSource, true, true, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to get video details, attempting retrying without reloading.", e)
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
video?.let {
|
|
||||||
_videoResumePositionMilliseconds = _player.position
|
|
||||||
setVideoDetails(it, false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(_didTriggerDatasourceErrorCount > 3) {
|
else if(_didTriggerDatasourceErrroCount > 3) {
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_error_pred,
|
UIDialogs.showDialog(context, R.drawable.ic_error_pred,
|
||||||
context.getString(R.string.media_error),
|
context.getString(R.string.media_error),
|
||||||
context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental),
|
context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental),
|
||||||
|
@ -2648,21 +2590,13 @@ class VideoDetailView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
onChannelClicked.subscribe {
|
onChannelClicked.subscribe {
|
||||||
if(it.url.isNotBlank())
|
fragment.navigate<ChannelFragment>(it)
|
||||||
fragment.navigate<ChannelFragment>(it)
|
|
||||||
else
|
|
||||||
UIDialogs.appToast("No author url present");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddToWatchLaterClicked.subscribe(this) {
|
onAddToWatchLaterClicked.subscribe(this) {
|
||||||
if(it is IPlatformVideo) {
|
if(it is IPlatformVideo) {
|
||||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
||||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||||
}
|
|
||||||
}
|
|
||||||
onAddToQueueClicked.subscribe(this) {
|
|
||||||
if(it is IPlatformVideo) {
|
|
||||||
StatePlayer.instance.addToQueue(it);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -2834,12 +2768,13 @@ class VideoDetailView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
|
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||||
_polycentricProfile = profile
|
_polycentricProfile = cachedPolycentricProfile;
|
||||||
|
|
||||||
val dp_35 = 35.dp(context.resources)
|
val dp_35 = 35.dp(context.resources)
|
||||||
|
val profile = cachedPolycentricProfile?.profile;
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
|
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
|
||||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }
|
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||||
|
|
||||||
if (avatar != null) {
|
if (avatar != null) {
|
||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
|
@ -2848,12 +2783,12 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
val username = profile?.systemState?.username
|
val username = cachedPolycentricProfile?.profile?.systemState?.username
|
||||||
if (username != null) {
|
if (username != null) {
|
||||||
_channelName.text = username
|
_channelName.text = username
|
||||||
}
|
}
|
||||||
|
|
||||||
_monetization.setPolycentricProfile(profile);
|
_monetization.setPolycentricProfile(cachedPolycentricProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
||||||
|
@ -3041,7 +2976,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
Logger.w(TAG, "Failed to load recommendations.", it);
|
Logger.w(TAG, "Failed to load recommendations.", it);
|
||||||
};
|
};
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) })
|
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
||||||
.success { it -> setPolycentricProfile(it, animate = true) }
|
.success { it -> setPolycentricProfile(it, animate = true) }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load claims.", it);
|
Logger.w(TAG, "Failed to load claims.", it);
|
||||||
|
@ -3127,7 +3062,6 @@ class VideoDetailView : ConstraintLayout {
|
||||||
const val TAG_SHARE = "share";
|
const val TAG_SHARE = "share";
|
||||||
const val TAG_OVERLAY = "overlay";
|
const val TAG_OVERLAY = "overlay";
|
||||||
const val TAG_LIVECHAT = "livechat";
|
const val TAG_LIVECHAT = "livechat";
|
||||||
const val TAG_CHAPTERS = "chapters";
|
|
||||||
const val TAG_OPEN = "open";
|
const val TAG_OPEN = "open";
|
||||||
const val TAG_SEND_TO_DEVICE = "send_to_device";
|
const val TAG_SEND_TO_DEVICE = "send_to_device";
|
||||||
const val TAG_MORE = "MORE";
|
const val TAG_MORE = "MORE";
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.view.setPadding
|
import androidx.core.view.setPadding
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
@ -25,7 +22,6 @@ import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.toHumanDuration
|
import com.futo.platformplayer.toHumanDuration
|
||||||
import com.futo.platformplayer.toHumanTime
|
import com.futo.platformplayer.toHumanTime
|
||||||
import com.futo.platformplayer.views.SearchView
|
|
||||||
import com.futo.platformplayer.views.lists.VideoListEditorView
|
import com.futo.platformplayer.views.lists.VideoListEditorView
|
||||||
|
|
||||||
abstract class VideoListEditorView : LinearLayout {
|
abstract class VideoListEditorView : LinearLayout {
|
||||||
|
@ -41,20 +37,9 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
protected var _buttonExport: ImageButton;
|
protected var _buttonExport: ImageButton;
|
||||||
private var _buttonShare: ImageButton;
|
private var _buttonShare: ImageButton;
|
||||||
private var _buttonEdit: ImageButton;
|
private var _buttonEdit: ImageButton;
|
||||||
private var _buttonSearch: ImageButton;
|
|
||||||
|
|
||||||
private var _search: SearchView;
|
|
||||||
|
|
||||||
private var _onShare: (()->Unit)? = null;
|
private var _onShare: (()->Unit)? = null;
|
||||||
|
|
||||||
private var _loadedVideos: List<IPlatformVideo>? = null;
|
|
||||||
private var _loadedVideosCanEdit: Boolean = false;
|
|
||||||
|
|
||||||
fun hideSearchKeyboard() {
|
|
||||||
(context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager)?.hideSoftInputFromWindow(_search.textSearch.windowToken, 0)
|
|
||||||
_search.textSearch.clearFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(inflater: LayoutInflater) : super(inflater.context) {
|
constructor(inflater: LayoutInflater) : super(inflater.context) {
|
||||||
inflater.inflate(R.layout.fragment_video_list_editor, this);
|
inflater.inflate(R.layout.fragment_video_list_editor, this);
|
||||||
|
|
||||||
|
@ -72,48 +57,26 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
_buttonDownload.visibility = View.GONE;
|
_buttonDownload.visibility = View.GONE;
|
||||||
_buttonExport = findViewById(R.id.button_export);
|
_buttonExport = findViewById(R.id.button_export);
|
||||||
_buttonExport.visibility = View.GONE;
|
_buttonExport.visibility = View.GONE;
|
||||||
_buttonSearch = findViewById(R.id.button_search);
|
|
||||||
|
|
||||||
_search = findViewById(R.id.search_bar);
|
|
||||||
_search.visibility = View.GONE;
|
|
||||||
_search.onSearchChanged.subscribe {
|
|
||||||
updateVideoFilters();
|
|
||||||
}
|
|
||||||
|
|
||||||
_buttonSearch.setOnClickListener {
|
|
||||||
if(_search.isVisible) {
|
|
||||||
_search.visibility = View.GONE;
|
|
||||||
_search.textSearch.text = "";
|
|
||||||
updateVideoFilters();
|
|
||||||
_buttonSearch.setImageResource(R.drawable.ic_search);
|
|
||||||
hideSearchKeyboard();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
_search.visibility = View.VISIBLE;
|
|
||||||
_buttonSearch.setImageResource(R.drawable.ic_search_off);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_buttonShare = findViewById(R.id.button_share);
|
_buttonShare = findViewById(R.id.button_share);
|
||||||
val onShare = _onShare;
|
val onShare = _onShare;
|
||||||
if(onShare != null) {
|
if(onShare != null) {
|
||||||
_buttonShare.setOnClickListener { hideSearchKeyboard(); onShare.invoke() };
|
_buttonShare.setOnClickListener { onShare.invoke() };
|
||||||
_buttonShare.visibility = View.VISIBLE;
|
_buttonShare.visibility = View.VISIBLE;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
_buttonShare.visibility = View.GONE;
|
_buttonShare.visibility = View.GONE;
|
||||||
|
|
||||||
buttonPlayAll.setOnClickListener { hideSearchKeyboard();onPlayAllClick(); hideSearchKeyboard(); };
|
buttonPlayAll.setOnClickListener { onPlayAllClick(); };
|
||||||
buttonShuffle.setOnClickListener { hideSearchKeyboard();onShuffleClick(); hideSearchKeyboard(); };
|
buttonShuffle.setOnClickListener { onShuffleClick(); };
|
||||||
|
|
||||||
_buttonEdit.setOnClickListener { hideSearchKeyboard(); onEditClick(); };
|
_buttonEdit.setOnClickListener { onEditClick(); };
|
||||||
setButtonExportVisible(false);
|
setButtonExportVisible(false);
|
||||||
setButtonDownloadVisible(canEdit());
|
setButtonDownloadVisible(canEdit());
|
||||||
|
|
||||||
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
|
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
|
||||||
videoListEditorView.onVideoRemoved.subscribe(::onVideoRemoved);
|
videoListEditorView.onVideoRemoved.subscribe(::onVideoRemoved);
|
||||||
videoListEditorView.onVideoOptions.subscribe(::onVideoOptions);
|
videoListEditorView.onVideoClicked.subscribe(::onVideoClicked);
|
||||||
videoListEditorView.onVideoClicked.subscribe { hideSearchKeyboard(); onVideoClicked(it)};
|
|
||||||
|
|
||||||
_videoListEditorView = videoListEditorView;
|
_videoListEditorView = videoListEditorView;
|
||||||
}
|
}
|
||||||
|
@ -121,7 +84,6 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
fun setOnShare(onShare: (()-> Unit)? = null) {
|
fun setOnShare(onShare: (()-> Unit)? = null) {
|
||||||
_onShare = onShare;
|
_onShare = onShare;
|
||||||
_buttonShare.setOnClickListener {
|
_buttonShare.setOnClickListener {
|
||||||
hideSearchKeyboard();
|
|
||||||
onShare?.invoke();
|
onShare?.invoke();
|
||||||
};
|
};
|
||||||
_buttonShare.visibility = View.VISIBLE;
|
_buttonShare.visibility = View.VISIBLE;
|
||||||
|
@ -132,7 +94,6 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
open fun onShuffleClick() { }
|
open fun onShuffleClick() { }
|
||||||
open fun onEditClick() { }
|
open fun onEditClick() { }
|
||||||
open fun onVideoRemoved(video: IPlatformVideo) {}
|
open fun onVideoRemoved(video: IPlatformVideo) {}
|
||||||
open fun onVideoOptions(video: IPlatformVideo) {}
|
|
||||||
open fun onVideoOrderChanged(videos : List<IPlatformVideo>) {}
|
open fun onVideoOrderChanged(videos : List<IPlatformVideo>) {}
|
||||||
open fun onVideoClicked(video: IPlatformVideo) {
|
open fun onVideoClicked(video: IPlatformVideo) {
|
||||||
|
|
||||||
|
@ -154,7 +115,7 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
setButtonExportVisible(false);
|
setButtonExportVisible(false);
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
||||||
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
||||||
_buttonDownload.setOnClickListener { hideSearchKeyboard();
|
_buttonDownload.setOnClickListener {
|
||||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||||
StateDownloads.instance.deleteCachedPlaylist(playlistId);
|
StateDownloads.instance.deleteCachedPlaylist(playlistId);
|
||||||
});
|
});
|
||||||
|
@ -163,7 +124,7 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
else if(isDownloaded) {
|
else if(isDownloaded) {
|
||||||
setButtonExportVisible(true)
|
setButtonExportVisible(true)
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
||||||
_buttonDownload.setOnClickListener { hideSearchKeyboard();
|
_buttonDownload.setOnClickListener {
|
||||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||||
StateDownloads.instance.deleteCachedPlaylist(playlistId);
|
StateDownloads.instance.deleteCachedPlaylist(playlistId);
|
||||||
});
|
});
|
||||||
|
@ -172,7 +133,7 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
else {
|
else {
|
||||||
setButtonExportVisible(false);
|
setButtonExportVisible(false);
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_download);
|
_buttonDownload.setImageResource(R.drawable.ic_download);
|
||||||
_buttonDownload.setOnClickListener { hideSearchKeyboard();
|
_buttonDownload.setOnClickListener {
|
||||||
onDownload();
|
onDownload();
|
||||||
//UISlideOverlays.showDownloadPlaylistOverlay(playlist, overlayContainer);
|
//UISlideOverlays.showDownloadPlaylistOverlay(playlist, overlayContainer);
|
||||||
}
|
}
|
||||||
|
@ -210,21 +171,8 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
.load(R.drawable.placeholder_video_thumbnail)
|
.load(R.drawable.placeholder_video_thumbnail)
|
||||||
.into(_imagePlaylistThumbnail)
|
.into(_imagePlaylistThumbnail)
|
||||||
}
|
}
|
||||||
_loadedVideos = videos;
|
|
||||||
_loadedVideosCanEdit = canEdit;
|
|
||||||
_videoListEditorView.setVideos(videos, canEdit);
|
|
||||||
}
|
|
||||||
fun filterVideos(videos: List<IPlatformVideo>): List<IPlatformVideo> {
|
|
||||||
var toReturn = videos;
|
|
||||||
val searchStr = _search.textSearch.text
|
|
||||||
if(!searchStr.isNullOrBlank())
|
|
||||||
toReturn = toReturn.filter { it.name.contains(searchStr, true) || it.author.name.contains(searchStr, true) };
|
|
||||||
return toReturn;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateVideoFilters() {
|
_videoListEditorView.setVideos(videos, canEdit);
|
||||||
val videos = _loadedVideos ?: return;
|
|
||||||
_videoListEditorView.setVideos(filterVideos(videos), _loadedVideosCanEdit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun setButtonDownloadVisible(isVisible: Boolean) {
|
protected fun setButtonDownloadVisible(isVisible: Boolean) {
|
||||||
|
|
|
@ -103,9 +103,6 @@ class WatchLaterFragment : MainFragment() {
|
||||||
StatePlaylists.instance.removeFromWatchLater(video, true);
|
StatePlaylists.instance.removeFromWatchLater(video, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun onVideoOptions(video: IPlatformVideo) {
|
|
||||||
UISlideOverlays.showVideoOptionsOverlay(video, overlayContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onVideoClicked(video: IPlatformVideo) {
|
override fun onVideoClicked(video: IPlatformVideo) {
|
||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
|
|
|
@ -14,9 +14,9 @@ import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.views.casting.CastButton
|
import com.futo.platformplayer.views.casting.CastButton
|
||||||
import com.futo.polycentric.core.PolycentricProfile
|
|
||||||
|
|
||||||
class NavigationTopBarFragment : TopFragment() {
|
class NavigationTopBarFragment : TopFragment() {
|
||||||
private var _buttonBack: ImageButton? = null;
|
private var _buttonBack: ImageButton? = null;
|
||||||
|
|
|
@ -9,7 +9,6 @@ import androidx.media3.datasource.ResolvingDataSource
|
||||||
import androidx.media3.exoplayer.dash.DashMediaSource
|
import androidx.media3.exoplayer.dash.DashMediaSource
|
||||||
import androidx.media3.exoplayer.dash.manifest.DashManifestParser
|
import androidx.media3.exoplayer.dash.manifest.DashManifestParser
|
||||||
import androidx.media3.exoplayer.source.MediaSource
|
import androidx.media3.exoplayer.source.MediaSource
|
||||||
import com.futo.platformplayer.Settings
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
@ -86,17 +85,12 @@ class VideoHelper {
|
||||||
|
|
||||||
return selectBestAudioSource((desc as VideoUnMuxedSourceDescriptor).audioSources.toList(), prefContainers, prefLanguage, targetBitrate);
|
return selectBestAudioSource((desc as VideoUnMuxedSourceDescriptor).audioSources.toList(), prefContainers, prefLanguage, targetBitrate);
|
||||||
}
|
}
|
||||||
fun selectBestAudioSource(sources : Iterable<IAudioSource>, prefContainers : Array<String>, preferredLanguage: String? = null, targetBitrate: Long? = null) : IAudioSource? {
|
fun selectBestAudioSource(altSources : Iterable<IAudioSource>, prefContainers : Array<String>, preferredLanguage: String? = null, targetBitrate: Long? = null) : IAudioSource? {
|
||||||
val hasPriority = sources.any { it.priority };
|
|
||||||
var altSources = if(hasPriority) sources.filter { it.priority } else sources;
|
|
||||||
val hasOriginal = altSources.any { it.original };
|
|
||||||
if(hasOriginal && Settings.instance.playback.preferOriginalAudio)
|
|
||||||
altSources = altSources.filter { it.original };
|
|
||||||
val languageToFilter = if(preferredLanguage != null && altSources.any { it.language == preferredLanguage }) {
|
val languageToFilter = if(preferredLanguage != null && altSources.any { it.language == preferredLanguage }) {
|
||||||
preferredLanguage
|
preferredLanguage
|
||||||
} else {
|
} else {
|
||||||
if(altSources.any { it.language == Language.ENGLISH })
|
if(altSources.any { it.language == Language.ENGLISH })
|
||||||
Language.ENGLISH;
|
Language.ENGLISH
|
||||||
else
|
else
|
||||||
Language.UNKNOWN;
|
Language.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
@ -214,38 +208,5 @@ class VideoHelper {
|
||||||
}
|
}
|
||||||
else return 0;
|
else return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mediaExtensionToMimetype(extension: String): String? {
|
|
||||||
return videoExtensionToMimetype(extension) ?: audioExtensionToMimetype(extension);
|
|
||||||
}
|
|
||||||
fun videoExtensionToMimetype(extension: String): String? {
|
|
||||||
val extensionTrimmed = extension.trim('.').lowercase();
|
|
||||||
return when (extensionTrimmed) {
|
|
||||||
"mp4" -> return "video/mp4";
|
|
||||||
"webm" -> return "video/webm";
|
|
||||||
"m3u8" -> return "video/x-mpegURL";
|
|
||||||
"3gp" -> return "video/3gpp";
|
|
||||||
"mov" -> return "video/quicktime";
|
|
||||||
"mkv" -> return "video/x-matroska";
|
|
||||||
"mp4a" -> return "audio/vnd.apple.mpegurl";
|
|
||||||
"mpga" -> return "audio/mpga";
|
|
||||||
"mp3" -> return "audio/mp3";
|
|
||||||
"webm" -> return "audio/webm";
|
|
||||||
"3gp" -> return "audio/3gpp";
|
|
||||||
else -> null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun audioExtensionToMimetype(extension: String): String? {
|
|
||||||
val extensionTrimmed = extension.trim('.').lowercase();
|
|
||||||
return when (extensionTrimmed) {
|
|
||||||
"mkv" -> return "audio/x-matroska";
|
|
||||||
"mp4a" -> return "audio/vnd.apple.mpegurl";
|
|
||||||
"mpga" -> return "audio/mpga";
|
|
||||||
"mp3" -> return "audio/mp3";
|
|
||||||
"webm" -> return "audio/webm";
|
|
||||||
"3gp" -> return "audio/3gpp";
|
|
||||||
else -> null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package com.futo.platformplayer.images;
|
package com.futo.platformplayer.images;
|
||||||
|
|
||||||
import static com.futo.platformplayer.Extensions_PolycentricKt.getDataLinkFromUrl;
|
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
@ -14,14 +12,10 @@ import com.bumptech.glide.load.model.ModelLoader;
|
||||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||||
import com.bumptech.glide.signature.ObjectKey;
|
import com.bumptech.glide.signature.ObjectKey;
|
||||||
import com.futo.polycentric.core.ApiMethods;
|
import com.futo.platformplayer.polycentric.PolycentricCache;
|
||||||
|
|
||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
import kotlinx.coroutines.CoroutineScopeKt;
|
|
||||||
import kotlinx.coroutines.Deferred;
|
import kotlinx.coroutines.Deferred;
|
||||||
import kotlinx.coroutines.Dispatchers;
|
|
||||||
import userpackage.Protocol;
|
|
||||||
|
|
||||||
import java.lang.Exception;
|
import java.lang.Exception;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
|
@ -66,14 +60,7 @@ public class PolycentricModelLoader implements ModelLoader<String, ByteBuffer> {
|
||||||
@Override
|
@Override
|
||||||
public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback<? super ByteBuffer> callback) {
|
public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback<? super ByteBuffer> callback) {
|
||||||
Log.i("PolycentricModelLoader", this._model);
|
Log.i("PolycentricModelLoader", this._model);
|
||||||
|
_deferred = PolycentricCache.getInstance().getDataAsync(_model);
|
||||||
Protocol.URLInfoDataLink dataLink = getDataLinkFromUrl(_model);
|
|
||||||
if (dataLink == null) {
|
|
||||||
callback.onLoadFailed(new Exception("Data link cannot be null"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_deferred = ApiMethods.Companion.getDataFromServerAndReassemble(CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()), dataLink);
|
|
||||||
_deferred.invokeOnCompletion(throwable -> {
|
_deferred.invokeOnCompletion(throwable -> {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
Log.e("PolycentricModelLoader", "getDataAsync failed throwable: " + throwable.toString());
|
Log.e("PolycentricModelLoader", "getDataAsync failed throwable: " + throwable.toString());
|
||||||
|
|
|
@ -3,7 +3,6 @@ package com.futo.platformplayer.models
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
@ -47,7 +46,6 @@ class HistoryVideo {
|
||||||
val name = str.substring(indexNext + 3);
|
val name = str.substring(indexNext + 3);
|
||||||
|
|
||||||
val video = resolve?.invoke(url) ?: SerializedPlatformVideo(
|
val video = resolve?.invoke(url) ?: SerializedPlatformVideo(
|
||||||
ContentType.MEDIA,
|
|
||||||
id = PlatformID.asUrlID(url),
|
id = PlatformID.asUrlID(url),
|
||||||
name = name,
|
name = name,
|
||||||
thumbnails = Thumbnails(),
|
thumbnails = Thumbnails(),
|
||||||
|
|
|
@ -7,8 +7,6 @@ import android.widget.ImageView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.PresetImages
|
import com.futo.platformplayer.PresetImages
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -20,8 +18,7 @@ data class ImageVariable(
|
||||||
@Transient
|
@Transient
|
||||||
@Contextual
|
@Contextual
|
||||||
private val bitmap: Bitmap? = null,
|
private val bitmap: Bitmap? = null,
|
||||||
val presetName: String? = null,
|
val presetName: String? = null) {
|
||||||
var subscriptionUrl: String? = null) {
|
|
||||||
|
|
||||||
@SuppressLint("DiscouragedApi")
|
@SuppressLint("DiscouragedApi")
|
||||||
fun setImageView(imageView: ImageView, fallbackResId: Int = -1) {
|
fun setImageView(imageView: ImageView, fallbackResId: Int = -1) {
|
||||||
|
@ -36,12 +33,6 @@ data class ImageVariable(
|
||||||
} else if(!url.isNullOrEmpty()) {
|
} else if(!url.isNullOrEmpty()) {
|
||||||
Glide.with(imageView)
|
Glide.with(imageView)
|
||||||
.load(url)
|
.load(url)
|
||||||
.error(if(!subscriptionUrl.isNullOrBlank()) StateSubscriptions.instance.getSubscription(subscriptionUrl!!)?.channel?.thumbnail else null)
|
|
||||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
|
||||||
.into(imageView);
|
|
||||||
} else if(!subscriptionUrl.isNullOrEmpty()) {
|
|
||||||
Glide.with(imageView)
|
|
||||||
.load(StateSubscriptions.instance.getSubscription(subscriptionUrl!!)?.channel?.thumbnail)
|
|
||||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
.into(imageView);
|
.into(imageView);
|
||||||
} else if(!presetName.isNullOrEmpty()) {
|
} else if(!presetName.isNullOrEmpty()) {
|
||||||
|
@ -72,13 +63,7 @@ data class ImageVariable(
|
||||||
return ImageVariable(null, null, null, str);
|
return ImageVariable(null, null, null, str);
|
||||||
}
|
}
|
||||||
fun fromFile(file: File): ImageVariable {
|
fun fromFile(file: File): ImageVariable {
|
||||||
try {
|
return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath));
|
||||||
return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath));
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e("ImageVariable", "Unsupported image format? " + ex.message, ex);
|
|
||||||
return fromResource(R.drawable.ic_error_pred);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -119,7 +119,7 @@ class HLS {
|
||||||
return if (source is IHLSManifestSource) {
|
return if (source is IHLSManifestSource) {
|
||||||
listOf()
|
listOf()
|
||||||
} else if (source is IHLSManifestAudioSource) {
|
} else if (source is IHLSManifestAudioSource) {
|
||||||
listOf(HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, url))
|
listOf(HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, url))
|
||||||
} else {
|
} else {
|
||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
}
|
}
|
||||||
|
@ -340,7 +340,7 @@ class HLS {
|
||||||
|
|
||||||
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
||||||
return@mapNotNull when (it.type) {
|
return@mapNotNull when (it.type) {
|
||||||
"AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, false, it.uri)
|
"AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,353 @@
|
||||||
|
package com.futo.platformplayer.polycentric
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.constructs.BatchedTaskHandler
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
|
import com.futo.platformplayer.getNowDiffSeconds
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.resolveChannelUrls
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.stores.CachedPolycentricProfileStorage
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
|
import com.futo.polycentric.core.ContentType
|
||||||
|
import com.futo.polycentric.core.OwnedClaim
|
||||||
|
import com.futo.polycentric.core.PublicKey
|
||||||
|
import com.futo.polycentric.core.SignedEvent
|
||||||
|
import com.futo.polycentric.core.StorageTypeSystemState
|
||||||
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.base64ToByteArray
|
||||||
|
import com.futo.polycentric.core.base64UrlToByteArray
|
||||||
|
import com.futo.polycentric.core.getClaimIfValid
|
||||||
|
import com.futo.polycentric.core.getValidClaims
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import userpackage.Protocol
|
||||||
|
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()) {
|
||||||
|
val expired get() = creationTime.getNowDiffSeconds() > CACHE_EXPIRATION_SECONDS
|
||||||
|
}
|
||||||
|
@Serializable
|
||||||
|
data class CachedPolycentricProfile(val profile: PolycentricProfile?, @Serializable(with = OffsetDateTimeSerializer::class) val creationTime: OffsetDateTime = OffsetDateTime.now()) {
|
||||||
|
val expired get() = creationTime.getNowDiffSeconds() > CACHE_EXPIRATION_SECONDS
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _cache = hashMapOf<PlatformID, CachedOwnedClaims>()
|
||||||
|
private val _profileCache = hashMapOf<PublicKey, CachedPolycentricProfile>()
|
||||||
|
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 ->
|
||||||
|
val signedEventsList = ApiMethods.getQueryLatest(
|
||||||
|
SERVER,
|
||||||
|
system.toProto(),
|
||||||
|
listOf(
|
||||||
|
ContentType.BANNER.value,
|
||||||
|
ContentType.AVATAR.value,
|
||||||
|
ContentType.USERNAME.value,
|
||||||
|
ContentType.DESCRIPTION.value,
|
||||||
|
ContentType.STORE.value,
|
||||||
|
ContentType.SERVER.value,
|
||||||
|
ContentType.STORE_DATA.value,
|
||||||
|
ContentType.PROMOTION_BANNER.value,
|
||||||
|
ContentType.PROMOTION.value,
|
||||||
|
ContentType.MEMBERSHIP_URLS.value,
|
||||||
|
ContentType.DONATION_DESTINATIONS.value
|
||||||
|
)
|
||||||
|
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||||
|
|
||||||
|
val signedProfileEvents = signedEventsList.groupBy { e -> e.event.contentType }
|
||||||
|
.map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } };
|
||||||
|
|
||||||
|
val storageSystemState = StorageTypeSystemState.create()
|
||||||
|
for (signedEvent in signedProfileEvents) {
|
||||||
|
storageSystemState.update(signedEvent.event)
|
||||||
|
}
|
||||||
|
|
||||||
|
val signedClaimEvents = ApiMethods.getQueryIndex(
|
||||||
|
SERVER,
|
||||||
|
system.toProto(),
|
||||||
|
ContentType.CLAIM.value,
|
||||||
|
limit = 200
|
||||||
|
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||||
|
|
||||||
|
val ownedClaims: ArrayList<OwnedClaim> = arrayListOf()
|
||||||
|
for (signedEvent in signedClaimEvents) {
|
||||||
|
if (signedEvent.event.contentType != ContentType.CLAIM.value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = ApiMethods.getQueryReferences(
|
||||||
|
SERVER,
|
||||||
|
Protocol.Reference.newBuilder()
|
||||||
|
.setReference(signedEvent.toPointer().toProto().toByteString())
|
||||||
|
.setReferenceType(2)
|
||||||
|
.build(),
|
||||||
|
null,
|
||||||
|
Protocol.QueryReferencesRequestEvents.newBuilder()
|
||||||
|
.setFromType(ContentType.VOUCH.value)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
val ownedClaim = response.itemsList.map { SignedEvent.fromProto(it.event) }.getClaimIfValid(signedEvent);
|
||||||
|
if (ownedClaim != null) {
|
||||||
|
ownedClaims.add(ownedClaim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "Retrieved profile (ownedClaims = $ownedClaims)");
|
||||||
|
val systemState = SystemState.fromStorageTypeSystemState(storageSystemState);
|
||||||
|
return@BatchedTaskHandler CachedPolycentricProfile(PolycentricProfile(system, systemState, ownedClaims));
|
||||||
|
},
|
||||||
|
{ system -> return@BatchedTaskHandler getCachedProfile(system); },
|
||||||
|
{ system, result ->
|
||||||
|
synchronized(_cache) {
|
||||||
|
_profileCache[system] = result;
|
||||||
|
|
||||||
|
if (result.profile != null) {
|
||||||
|
for (claim in result.profile.ownedClaims) {
|
||||||
|
val urls = claim.claim.resolveChannelUrls();
|
||||||
|
for (url in urls)
|
||||||
|
_profileUrlCache.map[url] = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_profileUrlCache.save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
private val _batchTaskGetClaims = BatchedTaskHandler<PlatformID, CachedOwnedClaims>(_scope,
|
||||||
|
{ id ->
|
||||||
|
val resolved = if (id.claimFieldType == -1) ApiMethods.getResolveClaim(SERVER, system, id.claimType.toLong(), id.value!!)
|
||||||
|
else ApiMethods.getResolveClaim(SERVER, system, id.claimType.toLong(), id.claimFieldType.toLong(), id.value!!);
|
||||||
|
Logger.v(TAG, "getResolveClaim(url = $SERVER, system = $system, id = $id, claimType = ${id.claimType}, matchAnyField = ${id.value})");
|
||||||
|
val protoEvents = resolved.matchesList.flatMap { arrayListOf(it.claim).apply { addAll(it.proofChainList) } }
|
||||||
|
val resolvedEvents = protoEvents.map { i -> SignedEvent.fromProto(i) };
|
||||||
|
return@BatchedTaskHandler CachedOwnedClaims(resolvedEvents.getValidClaims());
|
||||||
|
},
|
||||||
|
{ id -> return@BatchedTaskHandler getCachedValidClaims(id); },
|
||||||
|
{ id, result ->
|
||||||
|
synchronized(_cache) {
|
||||||
|
_cache[id] = result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
private val _batchTaskGetData = BatchedTaskHandler<String, ByteBuffer>(_scope,
|
||||||
|
{
|
||||||
|
val dataLink = getDataLinkFromUrl(it) ?: throw Exception("Only URLInfoDataLink is supported");
|
||||||
|
return@BatchedTaskHandler ApiMethods.getDataFromServerAndReassemble(dataLink);
|
||||||
|
},
|
||||||
|
{ return@BatchedTaskHandler null },
|
||||||
|
{ _, _ -> });
|
||||||
|
|
||||||
|
fun getCachedValidClaims(id: PlatformID, ignoreExpired: Boolean = false): CachedOwnedClaims? {
|
||||||
|
if (!StatePolycentric.instance.enabled || id.claimType <= 0) {
|
||||||
|
return CachedOwnedClaims(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(_cache) {
|
||||||
|
val cached = _cache[id]
|
||||||
|
if (cached == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ignoreExpired && cached.expired) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Review all return null in this file, perhaps it should be CachedX(null) instead
|
||||||
|
fun getValidClaimsAsync(id: PlatformID): Deferred<CachedOwnedClaims> {
|
||||||
|
if (!StatePolycentric.instance.enabled || id.value == null || id.claimType <= 0) {
|
||||||
|
return _scope.async { CachedOwnedClaims(null) };
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.v(TAG, "getValidClaims (id: $id)")
|
||||||
|
val def = _batchTaskGetClaims.execute(id);
|
||||||
|
def.invokeOnCompletion {
|
||||||
|
if (it == null) {
|
||||||
|
return@invokeOnCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
handleException(it, handleNetworkException = { /* Do nothing (do not cache) */ }, handleOtherException = {
|
||||||
|
//Cache failed result
|
||||||
|
synchronized(_cache) {
|
||||||
|
_cache[id] = CachedOwnedClaims(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDataAsync(url: String): Deferred<ByteBuffer> {
|
||||||
|
StatePolycentric.instance.ensureEnabled()
|
||||||
|
return _batchTaskGetData.execute(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCachedProfile(url: String, ignoreExpired: Boolean = false): CachedPolycentricProfile? {
|
||||||
|
if (!StatePolycentric.instance.enabled) {
|
||||||
|
return CachedPolycentricProfile(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (_profileCache) {
|
||||||
|
val cached = _profileUrlCache.get(url) ?: return null;
|
||||||
|
if (!ignoreExpired && cached.expired) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCachedProfile(system: PublicKey, ignoreExpired: Boolean = false): CachedPolycentricProfile? {
|
||||||
|
if (!StatePolycentric.instance.enabled) {
|
||||||
|
return CachedPolycentricProfile(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(_profileCache) {
|
||||||
|
val cached = _profileCache[system] ?: return null;
|
||||||
|
if (!ignoreExpired && cached.expired) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getProfileAsync(id: PlatformID, urlNullCache: String? = null): CachedPolycentricProfile? {
|
||||||
|
if (!StatePolycentric.instance.enabled || id.claimType <= 0) {
|
||||||
|
return CachedPolycentricProfile(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
val cachedClaims = getCachedValidClaims(id);
|
||||||
|
if (cachedClaims != null) {
|
||||||
|
if (!cachedClaims.ownedClaims.isNullOrEmpty()) {
|
||||||
|
Logger.v(TAG, "getProfileAsync (id: $id) != null (with cached valid claims)")
|
||||||
|
return getProfileAsync(cachedClaims.ownedClaims.first().system).await();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.v(TAG, "getProfileAsync (id: $id) no cached valid claims, will be retrieved")
|
||||||
|
|
||||||
|
val claims = getValidClaimsAsync(id).await()
|
||||||
|
if (!claims.ownedClaims.isNullOrEmpty()) {
|
||||||
|
Logger.v(TAG, "getProfileAsync (id: $id) != null (with retrieved valid claims)")
|
||||||
|
return getProfileAsync(claims.ownedClaims.first().system).await()
|
||||||
|
} else {
|
||||||
|
synchronized (_cache) {
|
||||||
|
if (urlNullCache != null) {
|
||||||
|
_profileUrlCache.setAndSave(urlNullCache, CachedPolycentricProfile(null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProfileAsync(system: PublicKey): Deferred<CachedPolycentricProfile?> {
|
||||||
|
if (!StatePolycentric.instance.enabled) {
|
||||||
|
return _scope.async { CachedPolycentricProfile(null) };
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "getProfileAsync (system: ${system})")
|
||||||
|
val def = _taskGetProfile.execute(system);
|
||||||
|
def.invokeOnCompletion {
|
||||||
|
if (it == null) {
|
||||||
|
return@invokeOnCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
handleException(it, handleNetworkException = { /* Do nothing (do not cache) */ }, handleOtherException = {
|
||||||
|
//Cache failed result
|
||||||
|
synchronized(_cache) {
|
||||||
|
val cachedProfile = CachedPolycentricProfile(null);
|
||||||
|
_profileCache[system] = cachedProfile;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleException(e: Throwable, handleNetworkException: () -> Unit, handleOtherException: () -> Unit) {
|
||||||
|
val isNetworkException = when(e) {
|
||||||
|
is java.net.UnknownHostException,
|
||||||
|
is java.net.SocketTimeoutException,
|
||||||
|
is java.net.ConnectException -> true
|
||||||
|
else -> when(e.cause) {
|
||||||
|
is java.net.UnknownHostException,
|
||||||
|
is java.net.SocketTimeoutException,
|
||||||
|
is java.net.ConnectException -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNetworkException) {
|
||||||
|
handleNetworkException()
|
||||||
|
} else {
|
||||||
|
handleOtherException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val system = Protocol.PublicKey.newBuilder()
|
||||||
|
.setKeyType(1)
|
||||||
|
.setKey(ByteString.copyFrom("gX0eCWctTm6WHVGot4sMAh7NDAIwWsIM5tRsOz9dX04=".base64ToByteArray())) //Production key
|
||||||
|
//.setKey(ByteString.copyFrom("LeQkzn1j625YZcZHayfCmTX+6ptrzsA+CdAyq+BcEdQ".base64ToByteArray())) //Test key koen-futo
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private const val TAG = "PolycentricCache"
|
||||||
|
const val SERVER = "https://srv1-prod.polycentric.io"
|
||||||
|
private var _instance: PolycentricCache? = null;
|
||||||
|
private val CACHE_EXPIRATION_SECONDS = 60 * 5;
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val instance: PolycentricCache
|
||||||
|
get(){
|
||||||
|
if(_instance == null)
|
||||||
|
_instance = PolycentricCache();
|
||||||
|
return _instance!!;
|
||||||
|
};
|
||||||
|
|
||||||
|
fun finish() {
|
||||||
|
_instance?.let {
|
||||||
|
_instance = null;
|
||||||
|
it._scope.cancel("PolycentricCache finished");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDataLinkFromUrl(it: String): Protocol.URLInfoDataLink? {
|
||||||
|
val urlData = if (it.startsWith("polycentric://")) {
|
||||||
|
it.substring("polycentric://".length)
|
||||||
|
} else it;
|
||||||
|
|
||||||
|
val urlBytes = urlData.base64UrlToByteArray();
|
||||||
|
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
||||||
|
if (urlInfo.urlType != 4L) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
||||||
|
return dataLink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,16 +39,4 @@ class OffsetDateTimeSerializer : KSerializer<OffsetDateTime> {
|
||||||
return OffsetDateTime.MIN;
|
return OffsetDateTime.MIN;
|
||||||
return OffsetDateTime.of(LocalDateTime.ofEpochSecond(epochSecond, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
return OffsetDateTime.of(LocalDateTime.ofEpochSecond(epochSecond, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
class OffsetDateTimeStringSerializer : KSerializer<OffsetDateTime> {
|
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("OffsetDateTime", PrimitiveKind.STRING)
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: OffsetDateTime) {
|
|
||||||
encoder.encodeString(value.toString());
|
|
||||||
}
|
|
||||||
override fun deserialize(decoder: Decoder): OffsetDateTime {
|
|
||||||
val str = decoder.decodeString();
|
|
||||||
|
|
||||||
return OffsetDateTime.parse(str);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -19,10 +19,10 @@ import kotlinx.serialization.json.jsonPrimitive
|
||||||
class PlatformContentSerializer : JsonContentPolymorphicSerializer<SerializedPlatformContent>(SerializedPlatformContent::class) {
|
class PlatformContentSerializer : JsonContentPolymorphicSerializer<SerializedPlatformContent>(SerializedPlatformContent::class) {
|
||||||
|
|
||||||
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<SerializedPlatformContent> {
|
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<SerializedPlatformContent> {
|
||||||
val obj = element.jsonObject["contentType"] ?: element.jsonObject["ContentType"];
|
val obj = element.jsonObject["contentType"];
|
||||||
|
|
||||||
//TODO: Remove this temporary fallback..at some point
|
//TODO: Remove this temporary fallback..at some point
|
||||||
if(obj == null && (element.jsonObject["isLive"]?.jsonPrimitive?.booleanOrNull ?: element.jsonObject["IsLive"]?.jsonPrimitive?.booleanOrNull) != null)
|
if(obj == null && element.jsonObject["isLive"]?.jsonPrimitive?.booleanOrNull != null)
|
||||||
return SerializedPlatformVideo.serializer();
|
return SerializedPlatformVideo.serializer();
|
||||||
|
|
||||||
if(obj?.jsonPrimitive?.isString != false) {
|
if(obj?.jsonPrimitive?.isString != false) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.structures.DedupContentPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
||||||
import com.futo.platformplayer.stores.db.ManagedDBStore
|
import com.futo.platformplayer.stores.db.ManagedDBStore
|
||||||
|
@ -49,7 +50,14 @@ class StateCache {
|
||||||
val subs = StateSubscriptions.instance.getSubscriptions();
|
val subs = StateSubscriptions.instance.getSubscriptions();
|
||||||
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
|
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
|
||||||
val allUrls = subs
|
val allUrls = subs
|
||||||
.map { it.channel.url }
|
.map {
|
||||||
|
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
|
||||||
|
if(!otherUrls.contains(it.channel.url))
|
||||||
|
return@map listOf(listOf(it.channel.url), otherUrls).flatten();
|
||||||
|
else
|
||||||
|
return@map otherUrls;
|
||||||
|
}
|
||||||
|
.flatten()
|
||||||
.distinct()
|
.distinct()
|
||||||
.filter { StatePlatform.instance.hasEnabledChannelClient(it) };
|
.filter { StatePlatform.instance.hasEnabledChannelClient(it) };
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ class StateMeta {
|
||||||
return when(lastCommentSection.value){
|
return when(lastCommentSection.value){
|
||||||
"Polycentric" -> 0;
|
"Polycentric" -> 0;
|
||||||
"Platform" -> 1;
|
"Platform" -> 1;
|
||||||
else -> 0
|
else -> 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun setLastCommentSection(value: Int) {
|
fun setLastCommentSection(value: Int) {
|
||||||
|
|
|
@ -632,27 +632,6 @@ class StatePlatform {
|
||||||
return pager;
|
return pager;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchChannelsAsContent(query: String): IPager<IPlatformContent> {
|
|
||||||
Logger.i(TAG, "Platform - searchChannels");
|
|
||||||
val pagers = mutableMapOf<IPager<IPlatformContent>, Float>();
|
|
||||||
getSortedEnabledClient().parallelStream().forEach {
|
|
||||||
try {
|
|
||||||
if (it.capabilities.hasChannelSearch)
|
|
||||||
pagers.put(it.searchChannelsAsContent(query), 1f);
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed search channels", ex)
|
|
||||||
UIDialogs.toast("Failed search channels on [${it.name}]\n(${ex.message})");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if(pagers.isEmpty())
|
|
||||||
return EmptyPager<IPlatformContent>();
|
|
||||||
|
|
||||||
val pager = MultiDistributionContentPager(pagers);
|
|
||||||
pager.initialize();
|
|
||||||
return pager;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//Video
|
//Video
|
||||||
fun hasEnabledVideoClient(url: String) : Boolean = getEnabledClients().any { it.isContentDetailsUrl(url) };
|
fun hasEnabledVideoClient(url: String) : Boolean = getEnabledClients().any { it.isContentDetailsUrl(url) };
|
||||||
|
|
|
@ -177,14 +177,11 @@ class StatePlaylists {
|
||||||
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1): Boolean {
|
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1) {
|
||||||
var wasNew = false;
|
|
||||||
synchronized(_watchlistStore) {
|
synchronized(_watchlistStore) {
|
||||||
if(!_watchlistStore.hasItem { it.url == video.url })
|
|
||||||
wasNew = true;
|
|
||||||
_watchlistStore.saveAsync(video);
|
_watchlistStore.saveAsync(video);
|
||||||
if(orderPosition == -1)
|
if(orderPosition == -1)
|
||||||
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray());
|
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values) .toTypedArray());
|
||||||
else {
|
else {
|
||||||
val existing = _watchlistOrderStore.getAllValues().toMutableList();
|
val existing = _watchlistOrderStore.getAllValues().toMutableList();
|
||||||
existing.add(orderPosition, video.url);
|
existing.add(orderPosition, video.url);
|
||||||
|
@ -201,7 +198,6 @@ class StatePlaylists {
|
||||||
}
|
}
|
||||||
|
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
return wasNew;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLastPlayedPlaylist() : Playlist? {
|
fun getLastPlayedPlaylist() : Playlist? {
|
||||||
|
@ -230,20 +226,17 @@ class StatePlaylists {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun getWatchLaterSyncPacket(orderOnly: Boolean = false): SyncWatchLaterPackage{
|
|
||||||
return SyncWatchLaterPackage(
|
|
||||||
if (orderOnly) listOf() else getWatchLater(),
|
|
||||||
if (orderOnly) mapOf() else _watchLaterAdds.all(),
|
|
||||||
if (orderOnly) mapOf() else _watchLaterRemovals.all(),
|
|
||||||
getWatchLaterLastReorderTime().toEpochSecond(),
|
|
||||||
_watchlistOrderStore.values.toList()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
private fun broadcastWatchLater(orderOnly: Boolean = false) {
|
private fun broadcastWatchLater(orderOnly: Boolean = false) {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
StateSync.instance.broadcastJsonData(
|
StateSync.instance.broadcastJsonData(
|
||||||
GJSyncOpcodes.syncWatchLater, getWatchLaterSyncPacket(orderOnly)
|
GJSyncOpcodes.syncWatchLater, SyncWatchLaterPackage(
|
||||||
|
if (orderOnly) listOf() else getWatchLater(),
|
||||||
|
if (orderOnly) mapOf() else _watchLaterAdds.all(),
|
||||||
|
if (orderOnly) mapOf() else _watchLaterRemovals.all(),
|
||||||
|
getWatchLaterLastReorderTime().toEpochSecond(),
|
||||||
|
_watchlistOrderStore.values.toList()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to broadcast watch later", e)
|
Logger.w(TAG, "Failed to broadcast watch later", e)
|
||||||
|
|
|
@ -21,7 +21,9 @@ import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
import com.futo.platformplayer.awaitFirstDeferred
|
import com.futo.platformplayer.awaitFirstDeferred
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
|
@ -31,7 +33,6 @@ import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.ClaimType
|
import com.futo.polycentric.core.ClaimType
|
||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.ContentType
|
||||||
import com.futo.polycentric.core.Opinion
|
import com.futo.polycentric.core.Opinion
|
||||||
import com.futo.polycentric.core.PolycentricProfile
|
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.PublicKey
|
import com.futo.polycentric.core.PublicKey
|
||||||
import com.futo.polycentric.core.SignedEvent
|
import com.futo.polycentric.core.SignedEvent
|
||||||
|
@ -233,7 +234,34 @@ class StatePolycentric {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return Pair(false, listOf(url));
|
return Pair(false, listOf(url));
|
||||||
}
|
}
|
||||||
return Pair(didUpdate, listOf(url));
|
var polycentricProfile: PolycentricProfile? = null;
|
||||||
|
try {
|
||||||
|
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, if(doCacheNull) url else null) }?.profile;
|
||||||
|
didUpdate = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.i("StateSubscriptions", "Get polycentric profile cached");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.w(StateSubscriptions.TAG, "Polycentric getCachedProfile failed for subscriptions", ex);
|
||||||
|
//TODO: Some way to communicate polycentric failing without blocking here
|
||||||
|
}
|
||||||
|
if(polycentricProfile != null) {
|
||||||
|
val urls = polycentricProfile.ownedClaims.groupBy { it.claim.claimType }
|
||||||
|
.mapNotNull { it.value.firstOrNull()?.claim?.resolveChannelUrl() }.toMutableList();
|
||||||
|
if(urls.any { it.equals(url, true) })
|
||||||
|
return Pair(didUpdate, urls);
|
||||||
|
else
|
||||||
|
return Pair(didUpdate, listOf(url) + urls);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return Pair(didUpdate, listOf(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? {
|
fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? {
|
||||||
|
@ -297,7 +325,7 @@ class StatePolycentric {
|
||||||
id = PlatformID("polycentric", author, null, ClaimType.POLYCENTRIC.value.toInt()),
|
id = PlatformID("polycentric", author, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||||
name = systemState.username,
|
name = systemState.username,
|
||||||
url = author,
|
url = author,
|
||||||
thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
|
thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||||
subscribers = null
|
subscribers = null
|
||||||
),
|
),
|
||||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||||
|
@ -321,7 +349,7 @@ class StatePolycentric {
|
||||||
suspend fun getLikesDislikesReplies(reference: Protocol.Reference): LikesDislikesReplies {
|
suspend fun getLikesDislikesReplies(reference: Protocol.Reference): LikesDislikesReplies {
|
||||||
ensureEnabled()
|
ensureEnabled()
|
||||||
|
|
||||||
val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null,
|
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
|
||||||
null,
|
null,
|
||||||
listOf(
|
listOf(
|
||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
||||||
|
@ -354,7 +382,7 @@ class StatePolycentric {
|
||||||
}
|
}
|
||||||
|
|
||||||
val pointer = Protocol.Pointer.parseFrom(reference.reference)
|
val pointer = Protocol.Pointer.parseFrom(reference.reference)
|
||||||
val events = ApiMethods.getEvents(ApiMethods.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
|
val events = ApiMethods.getEvents(PolycentricCache.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
|
||||||
.addRangesForProcesses(Protocol.RangesForProcess.newBuilder()
|
.addRangesForProcesses(Protocol.RangesForProcess.newBuilder()
|
||||||
.setProcess(pointer.process)
|
.setProcess(pointer.process)
|
||||||
.addRanges(Protocol.Range.newBuilder()
|
.addRanges(Protocol.Range.newBuilder()
|
||||||
|
@ -372,11 +400,11 @@ class StatePolycentric {
|
||||||
}
|
}
|
||||||
|
|
||||||
val post = Protocol.Post.parseFrom(ev.content);
|
val post = Protocol.Post.parseFrom(ev.content);
|
||||||
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(ApiMethods.SERVER));
|
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
|
||||||
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
||||||
|
|
||||||
val profileEvents = ApiMethods.getQueryLatest(
|
val profileEvents = ApiMethods.getQueryLatest(
|
||||||
ApiMethods.SERVER,
|
PolycentricCache.SERVER,
|
||||||
ev.system.toProto(),
|
ev.system.toProto(),
|
||||||
listOf(
|
listOf(
|
||||||
ContentType.AVATAR.value,
|
ContentType.AVATAR.value,
|
||||||
|
@ -405,7 +433,7 @@ class StatePolycentric {
|
||||||
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||||
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
||||||
url = systemLinkUrl,
|
url = systemLinkUrl,
|
||||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
|
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||||
subscribers = null
|
subscribers = null
|
||||||
),
|
),
|
||||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||||
|
@ -417,12 +445,12 @@ class StatePolycentric {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCommentPager(contextUrl: String, reference: Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
|
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return EmptyPager()
|
return EmptyPager()
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null,
|
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
|
||||||
Protocol.QueryReferencesRequestEvents.newBuilder()
|
Protocol.QueryReferencesRequestEvents.newBuilder()
|
||||||
.setFromType(ContentType.POST.value)
|
.setFromType(ContentType.POST.value)
|
||||||
.addAllCountLwwElementReferences(arrayListOf(
|
.addAllCountLwwElementReferences(arrayListOf(
|
||||||
|
@ -458,7 +486,7 @@ class StatePolycentric {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun nextPageAsync() {
|
override suspend fun nextPageAsync() {
|
||||||
val nextPageResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, _cursor,
|
val nextPageResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, _cursor,
|
||||||
Protocol.QueryReferencesRequestEvents.newBuilder()
|
Protocol.QueryReferencesRequestEvents.newBuilder()
|
||||||
.setFromType(ContentType.POST.value)
|
.setFromType(ContentType.POST.value)
|
||||||
.addAllCountLwwElementReferences(arrayListOf(
|
.addAllCountLwwElementReferences(arrayListOf(
|
||||||
|
@ -506,7 +534,7 @@ class StatePolycentric {
|
||||||
return@mapNotNull LazyComment(scope.async(_commentPoolDispatcher){
|
return@mapNotNull LazyComment(scope.async(_commentPoolDispatcher){
|
||||||
Logger.i(TAG, "Fetching comment data for [" + ev.system.key.toBase64() + "]");
|
Logger.i(TAG, "Fetching comment data for [" + ev.system.key.toBase64() + "]");
|
||||||
val profileEvents = ApiMethods.getQueryLatest(
|
val profileEvents = ApiMethods.getQueryLatest(
|
||||||
ApiMethods.SERVER,
|
PolycentricCache.SERVER,
|
||||||
ev.system.toProto(),
|
ev.system.toProto(),
|
||||||
listOf(
|
listOf(
|
||||||
ContentType.AVATAR.value,
|
ContentType.AVATAR.value,
|
||||||
|
@ -530,7 +558,7 @@ class StatePolycentric {
|
||||||
|
|
||||||
val unixMilliseconds = ev.unixMilliseconds
|
val unixMilliseconds = ev.unixMilliseconds
|
||||||
//TODO: Don't use single hardcoded sderver here
|
//TODO: Don't use single hardcoded sderver here
|
||||||
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(ApiMethods.SERVER));
|
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
|
||||||
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
||||||
return@async PolycentricPlatformComment(
|
return@async PolycentricPlatformComment(
|
||||||
contextUrl = contextUrl,
|
contextUrl = contextUrl,
|
||||||
|
@ -538,7 +566,7 @@ class StatePolycentric {
|
||||||
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||||
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
||||||
url = systemLinkUrl,
|
url = systemLinkUrl,
|
||||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
|
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||||
subscribers = null
|
subscribers = null
|
||||||
),
|
),
|
||||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||||
|
|
|
@ -1,17 +1,54 @@
|
||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.api.media.structures.*
|
||||||
|
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
||||||
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
|
import com.futo.platformplayer.findNonRuntimeException
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
|
import com.futo.platformplayer.getNowDiffDays
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
|
import com.futo.platformplayer.states.StateHistory.Companion
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringDateMapStorage
|
import com.futo.platformplayer.stores.StringDateMapStorage
|
||||||
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
|
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||||
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
|
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
|
||||||
|
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
|
||||||
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
|
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
|
||||||
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
|
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
|
||||||
import kotlinx.coroutines.Dispatchers
|
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import java.util.concurrent.ExecutionException
|
||||||
|
import java.util.concurrent.ForkJoinPool
|
||||||
|
import java.util.concurrent.ForkJoinTask
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
import kotlin.streams.asSequence
|
||||||
|
import kotlin.streams.toList
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* Used to maintain subscription groups
|
* Used to maintain subscription groups
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
import SubsExchangeClient
|
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
@ -16,10 +15,10 @@ import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImportCache
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringDateMapStorage
|
import com.futo.platformplayer.stores.StringDateMapStorage
|
||||||
import com.futo.platformplayer.stores.StringStorage
|
|
||||||
import com.futo.platformplayer.stores.StringStringMapStorage
|
import com.futo.platformplayer.stores.StringStringMapStorage
|
||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||||
|
@ -69,24 +68,10 @@ class StateSubscriptions {
|
||||||
|
|
||||||
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
||||||
|
|
||||||
private val _subsExchangeServer = "https://exchange.grayjay.app/";
|
|
||||||
private val _subscriptionKey = FragmentedStorage.get<StringStorage>("sub_exchange_key");
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
global.onUpdateProgress.subscribe { progress, total ->
|
global.onUpdateProgress.subscribe { progress, total ->
|
||||||
onFeedProgress.emit(null, progress, total);
|
onFeedProgress.emit(null, progress, total);
|
||||||
}
|
}
|
||||||
if(_subscriptionKey.value.isNullOrBlank())
|
|
||||||
generateNewSubsExchangeKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateNewSubsExchangeKey(){
|
|
||||||
_subscriptionKey.setAndSave(SubsExchangeClient.createPrivateKey());
|
|
||||||
}
|
|
||||||
fun getSubsExchangeClient(): SubsExchangeClient {
|
|
||||||
if(_subscriptionKey.value.isNullOrBlank())
|
|
||||||
throw IllegalStateException("No valid subscription exchange key set");
|
|
||||||
return SubsExchangeClient(_subsExchangeServer, _subscriptionKey.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOldestUpdateTime(): OffsetDateTime {
|
fun getOldestUpdateTime(): OffsetDateTime {
|
||||||
|
@ -350,6 +335,12 @@ class StateSubscriptions {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: This causes issues, because what if the profile is not cached yet when the susbcribe button is loaded for example?
|
||||||
|
val cachedProfile = PolycentricCache.instance.getCachedProfile(urls.first(), true)?.profile;
|
||||||
|
if (cachedProfile != null) {
|
||||||
|
return cachedProfile.ownedClaims.any { c -> _subscriptions.hasItem { s -> c.claim.resolveChannelUrl() == s.channel.url } };
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -375,17 +366,7 @@ class StateSubscriptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null, subGroup: SubscriptionGroup? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null, subGroup: SubscriptionGroup? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
||||||
var exchangeClient: SubsExchangeClient? = null;
|
val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool);
|
||||||
if(Settings.instance.subscriptions.useSubscriptionExchange) {
|
|
||||||
try {
|
|
||||||
exchangeClient = getSubsExchangeClient();
|
|
||||||
}
|
|
||||||
catch(ex: Throwable){
|
|
||||||
Logger.e(TAG, "Failed to get subs exchange client: ${ex.message}", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool, exchangeClient);
|
|
||||||
if(onNewCacheHit != null)
|
if(onNewCacheHit != null)
|
||||||
algo.onNewCacheHit.subscribe(onNewCacheHit)
|
algo.onNewCacheHit.subscribe(onNewCacheHit)
|
||||||
|
|
||||||
|
|
|
@ -6,67 +6,44 @@ import com.futo.platformplayer.LittleEndianDataInputStream
|
||||||
import com.futo.platformplayer.LittleEndianDataOutputStream
|
import com.futo.platformplayer.LittleEndianDataOutputStream
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
|
||||||
import com.futo.platformplayer.activities.SyncShowPairingCodeActivity
|
import com.futo.platformplayer.activities.SyncShowPairingCodeActivity
|
||||||
import com.futo.platformplayer.api.media.Serializer
|
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.encryption.GEncryptionProvider
|
import com.futo.platformplayer.encryption.GEncryptionProvider
|
||||||
import com.futo.platformplayer.generateReadablePassword
|
|
||||||
import com.futo.platformplayer.getConnectedSocket
|
import com.futo.platformplayer.getConnectedSocket
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.mdns.DnsService
|
import com.futo.platformplayer.mdns.DnsService
|
||||||
import com.futo.platformplayer.mdns.ServiceDiscoverer
|
import com.futo.platformplayer.mdns.ServiceDiscoverer
|
||||||
import com.futo.platformplayer.models.HistoryVideo
|
|
||||||
import com.futo.platformplayer.models.Subscription
|
|
||||||
import com.futo.platformplayer.noise.protocol.DHState
|
import com.futo.platformplayer.noise.protocol.DHState
|
||||||
import com.futo.platformplayer.noise.protocol.Noise
|
import com.futo.platformplayer.noise.protocol.Noise
|
||||||
import com.futo.platformplayer.smartMerge
|
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringStringMapStorage
|
import com.futo.platformplayer.stores.StringStringMapStorage
|
||||||
import com.futo.platformplayer.stores.StringArrayStorage
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
import com.futo.platformplayer.stores.StringStorage
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
import com.futo.platformplayer.stores.StringTMapStorage
|
import com.futo.platformplayer.stores.StringTMapStorage
|
||||||
import com.futo.platformplayer.sync.SyncSessionData
|
import com.futo.platformplayer.sync.SyncSessionData
|
||||||
import com.futo.platformplayer.sync.internal.ChannelSocket
|
|
||||||
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
|
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
|
||||||
import com.futo.platformplayer.sync.internal.IAuthorizable
|
|
||||||
import com.futo.platformplayer.sync.internal.IChannel
|
|
||||||
import com.futo.platformplayer.sync.internal.LinkType
|
|
||||||
import com.futo.platformplayer.sync.internal.Opcode
|
|
||||||
import com.futo.platformplayer.sync.internal.SyncDeviceInfo
|
import com.futo.platformplayer.sync.internal.SyncDeviceInfo
|
||||||
import com.futo.platformplayer.sync.internal.SyncKeyPair
|
import com.futo.platformplayer.sync.internal.SyncKeyPair
|
||||||
import com.futo.platformplayer.sync.internal.SyncSession
|
import com.futo.platformplayer.sync.internal.SyncSession
|
||||||
import com.futo.platformplayer.sync.internal.SyncSocketSession
|
import com.futo.platformplayer.sync.internal.SyncSocketSession
|
||||||
import com.futo.platformplayer.sync.models.SendToDevicePackage
|
|
||||||
import com.futo.platformplayer.sync.models.SyncPlaylistsPackage
|
|
||||||
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
|
|
||||||
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
|
|
||||||
import com.futo.platformplayer.sync.models.SyncWatchLaterPackage
|
|
||||||
import com.futo.polycentric.core.base64ToByteArray
|
import com.futo.polycentric.core.base64ToByteArray
|
||||||
import com.futo.polycentric.core.toBase64
|
import com.futo.polycentric.core.toBase64
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.ServerSocket
|
import java.net.ServerSocket
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class StateSync {
|
class StateSync {
|
||||||
private val _authorizedDevices = FragmentedStorage.get<StringArrayStorage>("authorized_devices")
|
private val _authorizedDevices = FragmentedStorage.get<StringArrayStorage>("authorized_devices")
|
||||||
private val _nameStorage = FragmentedStorage.get<StringStringMapStorage>("sync_remembered_name_storage")
|
|
||||||
private val _syncKeyPair = FragmentedStorage.get<StringStorage>("sync_key_pair")
|
private val _syncKeyPair = FragmentedStorage.get<StringStorage>("sync_key_pair")
|
||||||
private val _lastAddressStorage = FragmentedStorage.get<StringStringMapStorage>("sync_last_address_storage")
|
private val _lastAddressStorage = FragmentedStorage.get<StringStringMapStorage>("sync_last_address_storage")
|
||||||
private val _syncSessionData = FragmentedStorage.get<StringTMapStorage<SyncSessionData>>("syncSessionData")
|
private val _syncSessionData = FragmentedStorage.get<StringTMapStorage<SyncSessionData>>("syncSessionData")
|
||||||
|
@ -81,20 +58,13 @@ class StateSync {
|
||||||
//TODO: Should sync mdns and casting mdns be merged?
|
//TODO: Should sync mdns and casting mdns be merged?
|
||||||
//TODO: Decrease interval that devices are updated
|
//TODO: Decrease interval that devices are updated
|
||||||
//TODO: Send less data
|
//TODO: Send less data
|
||||||
private val _serviceDiscoverer = ServiceDiscoverer(arrayOf("_gsync._tcp.local")) { handleServiceUpdated(it) }
|
val _serviceDiscoverer = ServiceDiscoverer(arrayOf("_gsync._tcp.local")) { handleServiceUpdated(it) }
|
||||||
private val _pairingCode: String? = generateReadablePassword(8)
|
|
||||||
val pairingCode: String? get() = _pairingCode
|
|
||||||
private var _relaySession: SyncSocketSession? = null
|
|
||||||
private var _threadRelay: Thread? = null
|
|
||||||
private val _remotePendingStatusUpdate = mutableMapOf<String, (complete: Boolean?, message: String) -> Unit>()
|
|
||||||
|
|
||||||
var keyPair: DHState? = null
|
var keyPair: DHState? = null
|
||||||
var publicKey: String? = null
|
var publicKey: String? = null
|
||||||
val deviceRemoved: Event1<String> = Event1()
|
val deviceRemoved: Event1<String> = Event1()
|
||||||
val deviceUpdatedOrAdded: Event2<String, SyncSession> = Event2()
|
val deviceUpdatedOrAdded: Event2<String, SyncSession> = Event2()
|
||||||
|
|
||||||
//TODO: Should authorize acknowledge be implemented?
|
|
||||||
|
|
||||||
fun hasAuthorizedDevice(): Boolean {
|
fun hasAuthorizedDevice(): Boolean {
|
||||||
synchronized(_sessions) {
|
synchronized(_sessions) {
|
||||||
return _sessions.any{ it.value.connected && it.value.isAuthorized };
|
return _sessions.any{ it.value.connected && it.value.isAuthorized };
|
||||||
|
@ -156,7 +126,10 @@ class StateSync {
|
||||||
|
|
||||||
while (_started) {
|
while (_started) {
|
||||||
val socket = serverSocket.accept()
|
val socket = serverSocket.accept()
|
||||||
val session = createSocketSession(socket, true)
|
val session = createSocketSession(socket, true) { session, socketSession ->
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
session.startAsResponder()
|
session.startAsResponder()
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
@ -190,6 +163,8 @@ class StateSync {
|
||||||
|
|
||||||
for (connectPair in addressesToConnect) {
|
for (connectPair in addressesToConnect) {
|
||||||
try {
|
try {
|
||||||
|
val syncDeviceInfo = SyncDeviceInfo(connectPair.first, arrayOf(connectPair.second), PORT)
|
||||||
|
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val lastConnectTime = synchronized(_lastConnectTimesIp) {
|
val lastConnectTime = synchronized(_lastConnectTimesIp) {
|
||||||
_lastConnectTimesIp[connectPair.first] ?: 0
|
_lastConnectTimesIp[connectPair.first] ?: 0
|
||||||
|
@ -202,7 +177,7 @@ class StateSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "Attempting to connect to authorized device by last known IP '${connectPair.first}' with pkey=${connectPair.first}")
|
Logger.i(TAG, "Attempting to connect to authorized device by last known IP '${connectPair.first}' with pkey=${connectPair.first}")
|
||||||
connect(arrayOf(connectPair.second), PORT, connectPair.first, null)
|
connect(syncDeviceInfo)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.i(TAG, "Failed to connect to " + connectPair.first, e)
|
Logger.i(TAG, "Failed to connect to " + connectPair.first, e)
|
||||||
|
@ -212,125 +187,6 @@ class StateSync {
|
||||||
}
|
}
|
||||||
}.apply { start() }
|
}.apply { start() }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Settings.instance.synchronization.discoverThroughRelay) {
|
|
||||||
_threadRelay = Thread {
|
|
||||||
while (_started) {
|
|
||||||
try {
|
|
||||||
Log.i(TAG, "Starting relay session...")
|
|
||||||
|
|
||||||
var socketClosed = false;
|
|
||||||
val socket = Socket(RELAY_SERVER, 9000)
|
|
||||||
_relaySession = SyncSocketSession(
|
|
||||||
(socket.remoteSocketAddress as InetSocketAddress).address.hostAddress!!,
|
|
||||||
keyPair!!,
|
|
||||||
LittleEndianDataInputStream(socket.getInputStream()),
|
|
||||||
LittleEndianDataOutputStream(socket.getOutputStream()),
|
|
||||||
isHandshakeAllowed = { linkType, syncSocketSession, publicKey, pairingCode -> isHandshakeAllowed(linkType, syncSocketSession, publicKey, pairingCode) },
|
|
||||||
onNewChannel = { _, c ->
|
|
||||||
val remotePublicKey = c.remotePublicKey
|
|
||||||
if (remotePublicKey == null) {
|
|
||||||
Log.e(TAG, "Remote public key should never be null in onNewChannel.")
|
|
||||||
return@SyncSocketSession
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "New channel established from relay (pk: '$remotePublicKey').")
|
|
||||||
|
|
||||||
var session: SyncSession?
|
|
||||||
synchronized(_sessions) {
|
|
||||||
session = _sessions[remotePublicKey]
|
|
||||||
if (session == null) {
|
|
||||||
val remoteDeviceName = synchronized(_nameStorage) {
|
|
||||||
_nameStorage.get(remotePublicKey)
|
|
||||||
}
|
|
||||||
session = createNewSyncSession(remotePublicKey, remoteDeviceName)
|
|
||||||
_sessions[remotePublicKey] = session!!
|
|
||||||
}
|
|
||||||
session!!.addChannel(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.setDataHandler { _, channel, opcode, subOpcode, data ->
|
|
||||||
session?.handlePacket(opcode, subOpcode, data)
|
|
||||||
}
|
|
||||||
c.setCloseHandler { channel ->
|
|
||||||
session?.removeChannel(channel)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onChannelEstablished = { _, channel, isResponder ->
|
|
||||||
handleAuthorization(channel, isResponder)
|
|
||||||
},
|
|
||||||
onClose = { socketClosed = true },
|
|
||||||
onHandshakeComplete = { relaySession ->
|
|
||||||
Thread {
|
|
||||||
try {
|
|
||||||
while (_started && !socketClosed) {
|
|
||||||
val unconnectedAuthorizedDevices = synchronized(_authorizedDevices) {
|
|
||||||
_authorizedDevices.values.filter { !isConnected(it) }.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
relaySession.publishConnectionInformation(unconnectedAuthorizedDevices, PORT, Settings.instance.synchronization.discoverThroughRelay, false, false, Settings.instance.synchronization.discoverThroughRelay && Settings.instance.synchronization.connectThroughRelay)
|
|
||||||
|
|
||||||
val connectionInfos = runBlocking { relaySession.requestBulkConnectionInfo(unconnectedAuthorizedDevices) }
|
|
||||||
|
|
||||||
for ((targetKey, connectionInfo) in connectionInfos) {
|
|
||||||
val potentialLocalAddresses = connectionInfo.ipv4Addresses.union(connectionInfo.ipv6Addresses)
|
|
||||||
.filter { it != connectionInfo.remoteIp }
|
|
||||||
if (connectionInfo.allowLocalDirect) {
|
|
||||||
Thread {
|
|
||||||
try {
|
|
||||||
Log.v(TAG, "Attempting to connect directly, locally to '$targetKey'.")
|
|
||||||
connect(potentialLocalAddresses.map { it }.toTypedArray(), PORT, targetKey, null)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.e(TAG, "Failed to start direct connection using connection info with $targetKey.", e)
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectionInfo.allowRemoteDirect) {
|
|
||||||
// TODO: Implement direct remote connection if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectionInfo.allowRemoteHolePunched) {
|
|
||||||
// TODO: Implement hole punching if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectionInfo.allowRemoteRelayed && Settings.instance.synchronization.connectThroughRelay) {
|
|
||||||
try {
|
|
||||||
Log.v(TAG, "Attempting relayed connection with '$targetKey'.")
|
|
||||||
runBlocking { relaySession.startRelayedChannel(targetKey, null) }
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.e(TAG, "Failed to start relayed channel with $targetKey.", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread.sleep(15000)
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.e(TAG, "Unhandled exception in relay session.", e)
|
|
||||||
relaySession.stop()
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
_relaySession!!.authorizable = object : IAuthorizable {
|
|
||||||
override val isAuthorized: Boolean get() = true
|
|
||||||
}
|
|
||||||
|
|
||||||
_relaySession!!.startAsInitiator(RELAY_PUBLIC_KEY, null)
|
|
||||||
|
|
||||||
Log.i(TAG, "Started relay session.")
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.e(TAG, "Relay session failed.", e)
|
|
||||||
Thread.sleep(5000)
|
|
||||||
} finally {
|
|
||||||
_relaySession?.stop()
|
|
||||||
_relaySession = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.apply { start() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDeviceName(): String {
|
private fun getDeviceName(): String {
|
||||||
|
@ -362,14 +218,14 @@ class StateSync {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun getSessions(): List<SyncSession> {
|
fun getSessions(): List<SyncSession> {
|
||||||
synchronized(_sessions) {
|
return synchronized(_sessions) {
|
||||||
return _sessions.values.toList()
|
return _sessions.values.toList()
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
fun getAuthorizedSessions(): List<SyncSession> {
|
fun getAuthorizedSessions(): List<SyncSession> {
|
||||||
synchronized(_sessions) {
|
return synchronized(_sessions) {
|
||||||
return _sessions.values.filter { it.isAuthorized }.toList()
|
return _sessions.values.filter { it.isAuthorized }.toList()
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSyncSessionData(key: String): SyncSessionData {
|
fun getSyncSessionData(key: String): SyncSessionData {
|
||||||
|
@ -396,7 +252,7 @@ class StateSync {
|
||||||
val urlSafePkey = s.texts.firstOrNull { it.startsWith("pk=") }?.substring("pk=".length) ?: continue
|
val urlSafePkey = s.texts.firstOrNull { it.startsWith("pk=") }?.substring("pk=".length) ?: continue
|
||||||
val pkey = Base64.getEncoder().encodeToString(Base64.getDecoder().decode(urlSafePkey.replace('-', '+').replace('_', '/')))
|
val pkey = Base64.getEncoder().encodeToString(Base64.getDecoder().decode(urlSafePkey.replace('-', '+').replace('_', '/')))
|
||||||
|
|
||||||
val syncDeviceInfo = SyncDeviceInfo(pkey, addresses, port, null)
|
val syncDeviceInfo = SyncDeviceInfo(pkey, addresses, port)
|
||||||
val authorized = isAuthorized(pkey)
|
val authorized = isAuthorized(pkey)
|
||||||
|
|
||||||
if (authorized && !isConnected(pkey)) {
|
if (authorized && !isConnected(pkey)) {
|
||||||
|
@ -431,342 +287,12 @@ class StateSync {
|
||||||
deviceRemoved.emit(remotePublicKey)
|
deviceRemoved.emit(remotePublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createSocketSession(socket: Socket, isResponder: Boolean, onAuthorized: (session: SyncSession, socketSession: SyncSocketSession) -> Unit): SyncSocketSession {
|
||||||
private fun handleSyncSubscriptionPackage(origin: SyncSession, pack: SyncSubscriptionsPackage) {
|
|
||||||
val added = mutableListOf<Subscription>()
|
|
||||||
for(sub in pack.subscriptions) {
|
|
||||||
if(!StateSubscriptions.instance.isSubscribed(sub.channel)) {
|
|
||||||
val removalTime = StateSubscriptions.instance.getSubscriptionRemovalTime(sub.channel.url);
|
|
||||||
if(sub.creationTime > removalTime) {
|
|
||||||
val newSub = StateSubscriptions.instance.addSubscription(sub.channel, sub.creationTime);
|
|
||||||
added.add(newSub);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(added.size > 3)
|
|
||||||
UIDialogs.appToast("${added.size} Subscriptions from ${origin.remotePublicKey.substring(0, Math.min(8, origin.remotePublicKey.length))}");
|
|
||||||
else if(added.size > 0)
|
|
||||||
UIDialogs.appToast("Subscriptions from ${origin.remotePublicKey.substring(0, Math.min(8, origin.remotePublicKey.length))}:\n" +
|
|
||||||
added.map { it.channel.name }.joinToString("\n"));
|
|
||||||
|
|
||||||
|
|
||||||
if(pack.subscriptions.isNotEmpty()) {
|
|
||||||
for (subRemoved in pack.subscriptionRemovals) {
|
|
||||||
val removed = StateSubscriptions.instance.applySubscriptionRemovals(pack.subscriptionRemovals);
|
|
||||||
if(removed.size > 3) {
|
|
||||||
UIDialogs.appToast("Removed ${removed.size} Subscriptions from ${origin.remotePublicKey.substring(0, 8.coerceAtMost(origin.remotePublicKey.length))}");
|
|
||||||
} else if(removed.isNotEmpty()) {
|
|
||||||
UIDialogs.appToast("Subscriptions removed from ${origin.remotePublicKey.substring(0, 8.coerceAtMost(origin.remotePublicKey.length))}:\n" + removed.map { it.channel.name }.joinToString("\n"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleData(session: SyncSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) {
|
|
||||||
val remotePublicKey = session.remotePublicKey
|
|
||||||
when (subOpcode) {
|
|
||||||
GJSyncOpcodes.sendToDevices -> {
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
|
||||||
val context = StateApp.instance.contextOrNull;
|
|
||||||
if (context != null && context is MainActivity) {
|
|
||||||
val dataBody = ByteArray(data.remaining());
|
|
||||||
val remainder = data.remaining();
|
|
||||||
data.get(dataBody, 0, remainder);
|
|
||||||
val json = String(dataBody, Charsets.UTF_8);
|
|
||||||
val obj = Json.decodeFromString<SendToDevicePackage>(json);
|
|
||||||
UIDialogs.appToast("Received url from device [${session.remotePublicKey}]:\n{${obj.url}");
|
|
||||||
context.handleUrl(obj.url, obj.position);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
GJSyncOpcodes.syncStateExchange -> {
|
|
||||||
val dataBody = ByteArray(data.remaining());
|
|
||||||
data.get(dataBody);
|
|
||||||
val json = String(dataBody, Charsets.UTF_8);
|
|
||||||
val syncSessionData = Serializer.json.decodeFromString<SyncSessionData>(json);
|
|
||||||
|
|
||||||
Logger.i(TAG, "Received SyncSessionData from $remotePublicKey");
|
|
||||||
|
|
||||||
|
|
||||||
session.sendData(GJSyncOpcodes.syncSubscriptions, StateSubscriptions.instance.getSyncSubscriptionsPackageString());
|
|
||||||
session.sendData(GJSyncOpcodes.syncSubscriptionGroups, StateSubscriptionGroups.instance.getSyncSubscriptionGroupsPackageString());
|
|
||||||
session.sendData(GJSyncOpcodes.syncPlaylists, StatePlaylists.instance.getSyncPlaylistsPackageString())
|
|
||||||
|
|
||||||
session.sendData(GJSyncOpcodes.syncWatchLater, Json.encodeToString(StatePlaylists.instance.getWatchLaterSyncPacket(false)));
|
|
||||||
|
|
||||||
val recentHistory = StateHistory.instance.getRecentHistory(syncSessionData.lastHistory);
|
|
||||||
if(recentHistory.isNotEmpty())
|
|
||||||
session.sendJsonData(GJSyncOpcodes.syncHistory, recentHistory);
|
|
||||||
}
|
|
||||||
|
|
||||||
GJSyncOpcodes.syncExport -> {
|
|
||||||
val dataBody = ByteArray(data.remaining());
|
|
||||||
val bytesStr = ByteArrayInputStream(data.array(), data.position(), data.remaining());
|
|
||||||
bytesStr.use { bytesStrBytes ->
|
|
||||||
val exportStruct = StateBackup.ExportStructure.fromZipBytes(bytesStrBytes);
|
|
||||||
for (store in exportStruct.stores) {
|
|
||||||
if (store.key.equals("subscriptions", true)) {
|
|
||||||
val subStore =
|
|
||||||
StateSubscriptions.instance.getUnderlyingSubscriptionsStore();
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
|
||||||
val pack = SyncSubscriptionsPackage(
|
|
||||||
store.value.map {
|
|
||||||
subStore.fromReconstruction(it, exportStruct.cache)
|
|
||||||
},
|
|
||||||
StateSubscriptions.instance.getSubscriptionRemovals()
|
|
||||||
);
|
|
||||||
handleSyncSubscriptionPackage(session, pack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GJSyncOpcodes.syncSubscriptions -> {
|
|
||||||
val dataBody = ByteArray(data.remaining());
|
|
||||||
data.get(dataBody);
|
|
||||||
val json = String(dataBody, Charsets.UTF_8);
|
|
||||||
val subPackage = Serializer.json.decodeFromString<SyncSubscriptionsPackage>(json);
|
|
||||||
handleSyncSubscriptionPackage(session, subPackage);
|
|
||||||
|
|
||||||
val newestSub = subPackage.subscriptions.maxOf { it.creationTime };
|
|
||||||
|
|
||||||
val sesData = getSyncSessionData(remotePublicKey);
|
|
||||||
if(newestSub > sesData.lastSubscription) {
|
|
||||||
sesData.lastSubscription = newestSub;
|
|
||||||
saveSyncSessionData(sesData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GJSyncOpcodes.syncSubscriptionGroups -> {
|
|
||||||
val dataBody = ByteArray(data.remaining());
|
|
||||||
data.get(dataBody);
|
|
||||||
val json = String(dataBody, Charsets.UTF_8);
|
|
||||||
val pack = Serializer.json.decodeFromString<SyncSubscriptionGroupsPackage>(json);
|
|
||||||
|
|
||||||
var lastSubgroupChange = OffsetDateTime.MIN;
|
|
||||||
for(group in pack.groups){
|
|
||||||
if(group.lastChange > lastSubgroupChange)
|
|
||||||
lastSubgroupChange = group.lastChange;
|
|
||||||
|
|
||||||
val existing = StateSubscriptionGroups.instance.getSubscriptionGroup(group.id);
|
|
||||||
|
|
||||||
if(existing == null)
|
|
||||||
StateSubscriptionGroups.instance.updateSubscriptionGroup(group, false, true);
|
|
||||||
else if(existing.lastChange < group.lastChange) {
|
|
||||||
existing.name = group.name;
|
|
||||||
existing.urls = group.urls;
|
|
||||||
existing.image = group.image;
|
|
||||||
existing.priority = group.priority;
|
|
||||||
existing.lastChange = group.lastChange;
|
|
||||||
StateSubscriptionGroups.instance.updateSubscriptionGroup(existing, false, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(removal in pack.groupRemovals) {
|
|
||||||
val creation = StateSubscriptionGroups.instance.getSubscriptionGroup(removal.key);
|
|
||||||
val removalTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(removal.value, 0), ZoneOffset.UTC);
|
|
||||||
if(creation != null && creation.creationTime < removalTime)
|
|
||||||
StateSubscriptionGroups.instance.deleteSubscriptionGroup(removal.key, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GJSyncOpcodes.syncPlaylists -> {
|
|
||||||
val dataBody = ByteArray(data.remaining());
|
|
||||||
data.get(dataBody);
|
|
||||||
val json = String(dataBody, Charsets.UTF_8);
|
|
||||||
val pack = Serializer.json.decodeFromString<SyncPlaylistsPackage>(json);
|
|
||||||
|
|
||||||
for(playlist in pack.playlists) {
|
|
||||||
val existing = StatePlaylists.instance.getPlaylist(playlist.id);
|
|
||||||
|
|
||||||
if(existing == null)
|
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist, false);
|
|
||||||
else if(existing.dateUpdate.toLocalDateTime() < playlist.dateUpdate.toLocalDateTime()) {
|
|
||||||
existing.dateUpdate = playlist.dateUpdate;
|
|
||||||
existing.name = playlist.name;
|
|
||||||
existing.videos = playlist.videos;
|
|
||||||
existing.dateCreation = playlist.dateCreation;
|
|
||||||
existing.datePlayed = playlist.datePlayed;
|
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(existing, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(removal in pack.playlistRemovals) {
|
|
||||||
val creation = StatePlaylists.instance.getPlaylist(removal.key);
|
|
||||||
val removalTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(removal.value, 0), ZoneOffset.UTC);
|
|
||||||
if(creation != null && creation.dateCreation < removalTime)
|
|
||||||
StatePlaylists.instance.removePlaylist(creation, false);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GJSyncOpcodes.syncWatchLater -> {
|
|
||||||
val dataBody = ByteArray(data.remaining());
|
|
||||||
data.get(dataBody);
|
|
||||||
val json = String(dataBody, Charsets.UTF_8);
|
|
||||||
val pack = Serializer.json.decodeFromString<SyncWatchLaterPackage>(json);
|
|
||||||
|
|
||||||
Logger.i(TAG, "SyncWatchLater received ${pack.videos.size} (${pack.videoAdds?.size}, ${pack.videoRemovals?.size})");
|
|
||||||
|
|
||||||
val allExisting = StatePlaylists.instance.getWatchLater();
|
|
||||||
for(video in pack.videos) {
|
|
||||||
val existing = allExisting.firstOrNull { it.url == video.url };
|
|
||||||
val time = if(pack.videoAdds != null && pack.videoAdds.containsKey(video.url)) OffsetDateTime.ofInstant(Instant.ofEpochSecond(pack.videoAdds[video.url] ?: 0), ZoneOffset.UTC) else OffsetDateTime.MIN;
|
|
||||||
|
|
||||||
if(existing == null) {
|
|
||||||
StatePlaylists.instance.addToWatchLater(video, false);
|
|
||||||
if(time > OffsetDateTime.MIN)
|
|
||||||
StatePlaylists.instance.setWatchLaterAddTime(video.url, time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(removal in pack.videoRemovals) {
|
|
||||||
val watchLater = allExisting.firstOrNull { it.url == removal.key } ?: continue;
|
|
||||||
val creation = StatePlaylists.instance.getWatchLaterRemovalTime(watchLater.url) ?: OffsetDateTime.MIN;
|
|
||||||
val removalTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(removal.value), ZoneOffset.UTC);
|
|
||||||
if(creation < removalTime)
|
|
||||||
StatePlaylists.instance.removeFromWatchLater(watchLater, false, removalTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
val packReorderTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(pack.reorderTime), ZoneOffset.UTC);
|
|
||||||
val localReorderTime = StatePlaylists.instance.getWatchLaterLastReorderTime();
|
|
||||||
if(localReorderTime < packReorderTime && pack.ordering != null) {
|
|
||||||
StatePlaylists.instance.updateWatchLaterOrdering(smartMerge(pack.ordering!!, StatePlaylists.instance.getWatchLaterOrdering()), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GJSyncOpcodes.syncHistory -> {
|
|
||||||
val dataBody = ByteArray(data.remaining());
|
|
||||||
data.get(dataBody);
|
|
||||||
val json = String(dataBody, Charsets.UTF_8);
|
|
||||||
val history = Serializer.json.decodeFromString<List<HistoryVideo>>(json);
|
|
||||||
Logger.i(TAG, "SyncHistory received ${history.size} videos from ${remotePublicKey}");
|
|
||||||
|
|
||||||
var lastHistory = OffsetDateTime.MIN;
|
|
||||||
for(video in history){
|
|
||||||
val hist = StateHistory.instance.getHistoryByVideo(video.video, true, video.date);
|
|
||||||
if(hist != null)
|
|
||||||
StateHistory.instance.updateHistoryPosition(video.video, hist, true, video.position, video.date)
|
|
||||||
if(lastHistory < video.date)
|
|
||||||
lastHistory = video.date;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(lastHistory != OffsetDateTime.MIN && history.size > 1) {
|
|
||||||
val sesData = getSyncSessionData(remotePublicKey);
|
|
||||||
if (lastHistory > sesData.lastHistory) {
|
|
||||||
sesData.lastHistory = lastHistory;
|
|
||||||
saveSyncSessionData(sesData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onAuthorized(remotePublicKey: String) {
|
|
||||||
synchronized(_remotePendingStatusUpdate) {
|
|
||||||
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(true, "Authorized")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onUnuthorized(remotePublicKey: String) {
|
|
||||||
synchronized(_remotePendingStatusUpdate) {
|
|
||||||
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(false, "Unauthorized")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createNewSyncSession(remotePublicKey: String, remoteDeviceName: String?): SyncSession {
|
|
||||||
return SyncSession(
|
|
||||||
remotePublicKey,
|
|
||||||
onAuthorized = { it, isNewlyAuthorized, isNewSession ->
|
|
||||||
if (!isNewSession) {
|
|
||||||
return@SyncSession
|
|
||||||
}
|
|
||||||
|
|
||||||
it.remoteDeviceName?.let { remoteDeviceName ->
|
|
||||||
synchronized(_nameStorage) {
|
|
||||||
_nameStorage.setAndSave(remotePublicKey, remoteDeviceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "$remotePublicKey authorized (name: ${it.displayName})")
|
|
||||||
onAuthorized(remotePublicKey)
|
|
||||||
_authorizedDevices.addDistinct(remotePublicKey)
|
|
||||||
_authorizedDevices.save()
|
|
||||||
deviceUpdatedOrAdded.emit(it.remotePublicKey, it)
|
|
||||||
|
|
||||||
checkForSync(it);
|
|
||||||
},
|
|
||||||
onUnauthorized = {
|
|
||||||
unauthorize(remotePublicKey)
|
|
||||||
|
|
||||||
Logger.i(TAG, "$remotePublicKey unauthorized (name: ${it.displayName})")
|
|
||||||
onUnuthorized(remotePublicKey)
|
|
||||||
|
|
||||||
synchronized(_sessions) {
|
|
||||||
it.close()
|
|
||||||
_sessions.remove(remotePublicKey)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onConnectedChanged = { it, connected ->
|
|
||||||
Logger.i(TAG, "$remotePublicKey connected: $connected")
|
|
||||||
deviceUpdatedOrAdded.emit(it.remotePublicKey, it)
|
|
||||||
},
|
|
||||||
onClose = {
|
|
||||||
Logger.i(TAG, "$remotePublicKey closed")
|
|
||||||
|
|
||||||
synchronized(_sessions)
|
|
||||||
{
|
|
||||||
_sessions.remove(it.remotePublicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceRemoved.emit(it.remotePublicKey)
|
|
||||||
|
|
||||||
synchronized(_remotePendingStatusUpdate) {
|
|
||||||
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(false, "Connection closed")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dataHandler = { it, opcode, subOpcode, data ->
|
|
||||||
handleData(it, opcode, subOpcode, data)
|
|
||||||
},
|
|
||||||
remoteDeviceName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isHandshakeAllowed(linkType: LinkType, syncSocketSession: SyncSocketSession, publicKey: String, pairingCode: String?): Boolean {
|
|
||||||
Log.v(TAG, "Check if handshake allowed from '$publicKey'.")
|
|
||||||
if (publicKey == RELAY_PUBLIC_KEY)
|
|
||||||
return true
|
|
||||||
|
|
||||||
synchronized(_authorizedDevices) {
|
|
||||||
if (_authorizedDevices.values.contains(publicKey)) {
|
|
||||||
if (linkType == LinkType.Relayed && !Settings.instance.synchronization.connectThroughRelay)
|
|
||||||
return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.v(TAG, "Check if handshake allowed with pairing code '$pairingCode' with active pairing code '$_pairingCode'.")
|
|
||||||
if (_pairingCode == null || pairingCode.isNullOrEmpty())
|
|
||||||
return false
|
|
||||||
|
|
||||||
if (linkType == LinkType.Relayed && !Settings.instance.synchronization.pairThroughRelay)
|
|
||||||
return false
|
|
||||||
|
|
||||||
return _pairingCode == pairingCode
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createSocketSession(socket: Socket, isResponder: Boolean): SyncSocketSession {
|
|
||||||
var session: SyncSession? = null
|
var session: SyncSession? = null
|
||||||
var channelSocket: ChannelSocket? = null
|
return SyncSocketSession((socket.remoteSocketAddress as InetSocketAddress).address.hostAddress!!, keyPair!!, LittleEndianDataInputStream(socket.getInputStream()), LittleEndianDataOutputStream(socket.getOutputStream()),
|
||||||
return SyncSocketSession(
|
|
||||||
(socket.remoteSocketAddress as InetSocketAddress).address.hostAddress!!,
|
|
||||||
keyPair!!,
|
|
||||||
LittleEndianDataInputStream(socket.getInputStream()),
|
|
||||||
LittleEndianDataOutputStream(socket.getOutputStream()),
|
|
||||||
onClose = { s ->
|
onClose = { s ->
|
||||||
if (channelSocket != null)
|
session?.removeSocketSession(s)
|
||||||
session?.removeChannel(channelSocket!!)
|
|
||||||
},
|
},
|
||||||
isHandshakeAllowed = { linkType, syncSocketSession, publicKey, pairingCode -> isHandshakeAllowed(linkType, syncSocketSession, publicKey, pairingCode) },
|
|
||||||
onHandshakeComplete = { s ->
|
onHandshakeComplete = { s ->
|
||||||
val remotePublicKey = s.remotePublicKey
|
val remotePublicKey = s.remotePublicKey
|
||||||
if (remotePublicKey == null) {
|
if (remotePublicKey == null) {
|
||||||
|
@ -776,108 +302,123 @@ class StateSync {
|
||||||
|
|
||||||
Logger.i(TAG, "Handshake complete with (LocalPublicKey = ${s.localPublicKey}, RemotePublicKey = ${s.remotePublicKey})")
|
Logger.i(TAG, "Handshake complete with (LocalPublicKey = ${s.localPublicKey}, RemotePublicKey = ${s.remotePublicKey})")
|
||||||
|
|
||||||
channelSocket = ChannelSocket(s)
|
|
||||||
|
|
||||||
synchronized(_sessions) {
|
synchronized(_sessions) {
|
||||||
session = _sessions[s.remotePublicKey]
|
session = _sessions[s.remotePublicKey]
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
val remoteDeviceName = synchronized(_nameStorage) {
|
session = SyncSession(remotePublicKey, onAuthorized = { it, isNewlyAuthorized, isNewSession ->
|
||||||
_nameStorage.get(remotePublicKey)
|
if (!isNewSession) {
|
||||||
}
|
return@SyncSession
|
||||||
|
}
|
||||||
|
|
||||||
synchronized(_lastAddressStorage) {
|
Logger.i(TAG, "${s.remotePublicKey} authorized")
|
||||||
_lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress)
|
synchronized(_lastAddressStorage) {
|
||||||
}
|
_lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress)
|
||||||
|
}
|
||||||
|
|
||||||
session = createNewSyncSession(remotePublicKey, remoteDeviceName)
|
onAuthorized(it, s)
|
||||||
|
_authorizedDevices.addDistinct(remotePublicKey)
|
||||||
|
_authorizedDevices.save()
|
||||||
|
deviceUpdatedOrAdded.emit(it.remotePublicKey, session!!)
|
||||||
|
|
||||||
|
checkForSync(it);
|
||||||
|
}, onUnauthorized = {
|
||||||
|
unauthorize(remotePublicKey)
|
||||||
|
|
||||||
|
synchronized(_sessions) {
|
||||||
|
session?.close()
|
||||||
|
_sessions.remove(remotePublicKey)
|
||||||
|
}
|
||||||
|
}, onConnectedChanged = { it, connected ->
|
||||||
|
Logger.i(TAG, "${s.remotePublicKey} connected: " + connected)
|
||||||
|
deviceUpdatedOrAdded.emit(it.remotePublicKey, session!!)
|
||||||
|
}, onClose = {
|
||||||
|
Logger.i(TAG, "${s.remotePublicKey} closed")
|
||||||
|
|
||||||
|
synchronized(_sessions)
|
||||||
|
{
|
||||||
|
_sessions.remove(it.remotePublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceRemoved.emit(it.remotePublicKey)
|
||||||
|
|
||||||
|
})
|
||||||
_sessions[remotePublicKey] = session!!
|
_sessions[remotePublicKey] = session!!
|
||||||
}
|
}
|
||||||
session!!.addChannel(channelSocket!!)
|
session!!.addSocketSession(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAuthorization(channelSocket!!, isResponder)
|
if (isResponder) {
|
||||||
},
|
val isAuthorized = synchronized(_authorizedDevices) {
|
||||||
onData = { s, opcode, subOpcode, data ->
|
_authorizedDevices.values.contains(remotePublicKey)
|
||||||
session?.handlePacket(opcode, subOpcode, data)
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleAuthorization(channel: IChannel, isResponder: Boolean) {
|
if (!isAuthorized) {
|
||||||
val syncSession = channel.syncSession!!
|
val scope = StateApp.instance.scopeOrNull
|
||||||
val remotePublicKey = channel.remotePublicKey!!
|
val activity = SyncShowPairingCodeActivity.activity
|
||||||
|
|
||||||
if (isResponder) {
|
if (scope != null && activity != null) {
|
||||||
val isAuthorized = synchronized(_authorizedDevices) {
|
scope.launch(Dispatchers.Main) {
|
||||||
_authorizedDevices.values.contains(remotePublicKey)
|
UIDialogs.showConfirmationDialog(activity, "Allow connection from ${remotePublicKey}?", action = {
|
||||||
}
|
scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
if (!isAuthorized) {
|
session!!.authorize(s)
|
||||||
val scope = StateApp.instance.scopeOrNull
|
Logger.i(TAG, "Connection authorized for $remotePublicKey by confirmation")
|
||||||
val activity = SyncShowPairingCodeActivity.activity
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to send authorize", e)
|
||||||
if (scope != null && activity != null) {
|
}
|
||||||
scope.launch(Dispatchers.Main) {
|
|
||||||
UIDialogs.showConfirmationDialog(activity, "Allow connection from ${remotePublicKey}?",
|
|
||||||
action = {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
syncSession.authorize()
|
|
||||||
Logger.i(TAG, "Connection authorized for $remotePublicKey by confirmation")
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to send authorize", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cancelAction = {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
unauthorize(remotePublicKey)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.w(TAG, "Failed to send unauthorize", e)
|
|
||||||
}
|
}
|
||||||
|
}, cancelAction = {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
unauthorize(remotePublicKey)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to send unauthorize", e)
|
||||||
|
}
|
||||||
|
|
||||||
syncSession.close()
|
synchronized(_sessions) {
|
||||||
synchronized(_sessions) {
|
session?.close()
|
||||||
_sessions.remove(remotePublicKey)
|
_sessions.remove(remotePublicKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
)
|
} else {
|
||||||
|
val publicKey = session!!.remotePublicKey
|
||||||
|
session!!.unauthorize(s)
|
||||||
|
session!!.close()
|
||||||
|
|
||||||
|
synchronized(_sessions) {
|
||||||
|
_sessions.remove(publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "Connection unauthorized for ${remotePublicKey} because not authorized and not on pairing activity to ask")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//Responder does not need to check because already approved
|
||||||
|
session!!.authorize(s)
|
||||||
|
Logger.i(TAG, "Connection authorized for ${remotePublicKey} because already authorized")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val publicKey = syncSession.remotePublicKey
|
//Initiator does not need to check because the manual action of scanning the QR counts as approval
|
||||||
syncSession.unauthorize()
|
session!!.authorize(s)
|
||||||
syncSession.close()
|
Logger.i(TAG, "Connection authorized for ${remotePublicKey} because initiator")
|
||||||
|
|
||||||
synchronized(_sessions) {
|
|
||||||
_sessions.remove(publicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Connection unauthorized for $remotePublicKey because not authorized and not on pairing activity to ask")
|
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
//Responder does not need to check because already approved
|
onData = { s, opcode, subOpcode, data ->
|
||||||
syncSession.authorize()
|
session?.handlePacket(s, opcode, subOpcode, data)
|
||||||
Logger.i(TAG, "Connection authorized for $remotePublicKey because already authorized")
|
})
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//Initiator does not need to check because the manual action of scanning the QR counts as approval
|
|
||||||
syncSession.authorize()
|
|
||||||
Logger.i(TAG, "Connection authorized for $remotePublicKey because initiator")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T> broadcastJsonData(subOpcode: UByte, data: T) {
|
inline fun <reified T> broadcastJsonData(subOpcode: UByte, data: T) {
|
||||||
broadcast(Opcode.DATA.value, subOpcode, Json.encodeToString(data));
|
broadcast(SyncSocketSession.Opcode.DATA.value, subOpcode, Json.encodeToString(data));
|
||||||
}
|
}
|
||||||
fun broadcastData(subOpcode: UByte, data: String) {
|
fun broadcastData(subOpcode: UByte, data: String) {
|
||||||
broadcast(Opcode.DATA.value, subOpcode, ByteBuffer.wrap(data.toByteArray(Charsets.UTF_8)));
|
broadcast(SyncSocketSession.Opcode.DATA.value, subOpcode, data.toByteArray(Charsets.UTF_8));
|
||||||
}
|
}
|
||||||
fun broadcast(opcode: UByte, subOpcode: UByte, data: String) {
|
fun broadcast(opcode: UByte, subOpcode: UByte, data: String) {
|
||||||
broadcast(opcode, subOpcode, ByteBuffer.wrap(data.toByteArray(Charsets.UTF_8)));
|
broadcast(opcode, subOpcode, data.toByteArray(Charsets.UTF_8));
|
||||||
}
|
}
|
||||||
fun broadcast(opcode: UByte, subOpcode: UByte, data: ByteBuffer) {
|
fun broadcast(opcode: UByte, subOpcode: UByte, data: ByteArray) {
|
||||||
for(session in getAuthorizedSessions()) {
|
for(session in getAuthorizedSessions()) {
|
||||||
try {
|
try {
|
||||||
session.send(opcode, subOpcode, data);
|
session.send(opcode, subOpcode, data);
|
||||||
|
@ -904,53 +445,21 @@ class StateSync {
|
||||||
_serverSocket?.close()
|
_serverSocket?.close()
|
||||||
_serverSocket = null
|
_serverSocket = null
|
||||||
|
|
||||||
_thread?.interrupt()
|
//_thread?.join()
|
||||||
_thread = null
|
_thread = null
|
||||||
_connectThread?.interrupt()
|
|
||||||
_connectThread = null
|
_connectThread = null
|
||||||
_threadRelay?.interrupt()
|
|
||||||
_threadRelay = null
|
|
||||||
|
|
||||||
_relaySession?.stop()
|
|
||||||
_relaySession = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun connect(deviceInfo: SyncDeviceInfo, onStatusUpdate: ((complete: Boolean?, message: String) -> Unit)? = null) {
|
fun connect(deviceInfo: SyncDeviceInfo, onStatusUpdate: ((session: SyncSocketSession?, complete: Boolean, message: String) -> Unit)? = null): SyncSocketSession {
|
||||||
try {
|
onStatusUpdate?.invoke(null, false, "Connecting...")
|
||||||
connect(deviceInfo.addresses, deviceInfo.port, deviceInfo.publicKey, deviceInfo.pairingCode, onStatusUpdate)
|
val socket = getConnectedSocket(deviceInfo.addresses.map { InetAddress.getByName(it) }, deviceInfo.port) ?: throw Exception("Failed to connect")
|
||||||
} catch (e: Throwable) {
|
onStatusUpdate?.invoke(null, false, "Handshaking...")
|
||||||
Logger.e(TAG, "Failed to connect directly", e)
|
|
||||||
val relaySession = _relaySession
|
|
||||||
if (relaySession != null && Settings.instance.synchronization.pairThroughRelay) {
|
|
||||||
onStatusUpdate?.invoke(null, "Connecting via relay...")
|
|
||||||
|
|
||||||
runBlocking {
|
val session = createSocketSession(socket, false) { _, ss ->
|
||||||
if (onStatusUpdate != null) {
|
onStatusUpdate?.invoke(ss, true, "Handshake complete")
|
||||||
synchronized(_remotePendingStatusUpdate) {
|
|
||||||
_remotePendingStatusUpdate[deviceInfo.publicKey] = onStatusUpdate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
relaySession.startRelayedChannel(deviceInfo.publicKey, deviceInfo.pairingCode)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun connect(addresses: Array<String>, port: Int, publicKey: String, pairingCode: String?, onStatusUpdate: ((complete: Boolean?, message: String) -> Unit)? = null): SyncSocketSession {
|
|
||||||
onStatusUpdate?.invoke(null, "Connecting directly...")
|
|
||||||
val socket = getConnectedSocket(addresses.map { InetAddress.getByName(it) }, port) ?: throw Exception("Failed to connect")
|
|
||||||
onStatusUpdate?.invoke(null, "Handshaking...")
|
|
||||||
|
|
||||||
val session = createSocketSession(socket, false)
|
|
||||||
if (onStatusUpdate != null) {
|
|
||||||
synchronized(_remotePendingStatusUpdate) {
|
|
||||||
_remotePendingStatusUpdate[publicKey] = onStatusUpdate
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
session.startAsInitiator(publicKey, pairingCode)
|
session.startAsInitiator(deviceInfo.publicKey)
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -960,12 +469,6 @@ class StateSync {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCachedName(publicKey: String): String? {
|
|
||||||
return synchronized(_nameStorage) {
|
|
||||||
_nameStorage.get(publicKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun delete(publicKey: String) {
|
suspend fun delete(publicKey: String) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
@ -1006,8 +509,6 @@ class StateSync {
|
||||||
val hash = "BLAKE2b"
|
val hash = "BLAKE2b"
|
||||||
var protocolName = "Noise_${pattern}_${dh}_${cipher}_${hash}"
|
var protocolName = "Noise_${pattern}_${dh}_${cipher}_${hash}"
|
||||||
val version = 1
|
val version = 1
|
||||||
val RELAY_SERVER = "relay.grayjay.app"
|
|
||||||
val RELAY_PUBLIC_KEY = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw="
|
|
||||||
|
|
||||||
private const val TAG = "StateSync"
|
private const val TAG = "StateSync"
|
||||||
const val PORT = 12315
|
const val PORT = 12315
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.futo.platformplayer.stores
|
||||||
|
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
class CachedPolycentricProfileStorage : FragmentedStorageFileJson() {
|
||||||
|
var map: HashMap<String, PolycentricCache.CachedPolycentricProfile> = hashMapOf();
|
||||||
|
|
||||||
|
override fun encode(): String {
|
||||||
|
val encoded = Json.encodeToString(this);
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(key: String) : PolycentricCache.CachedPolycentricProfile? {
|
||||||
|
return map[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAndSave(key: String, value: PolycentricCache.CachedPolycentricProfile) : PolycentricCache.CachedPolycentricProfile {
|
||||||
|
map[key] = value;
|
||||||
|
save();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAndSaveBlocking(key: String, value: PolycentricCache.CachedPolycentricProfile) : PolycentricCache.CachedPolycentricProfile {
|
||||||
|
map[key] = value;
|
||||||
|
saveBlocking();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,19 +41,4 @@ class StringArrayStorage : FragmentedStorageFileJson() {
|
||||||
return values.toList();
|
return values.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun any(): Boolean {
|
|
||||||
synchronized(values) {
|
|
||||||
return values.any();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun contains(v: String): Boolean {
|
|
||||||
synchronized(values) {
|
|
||||||
return values.contains(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun indexOf(v: String): Int {
|
|
||||||
synchronized(values){
|
|
||||||
return values.indexOf(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package com.futo.platformplayer.subscription
|
package com.futo.platformplayer.subscription
|
||||||
|
|
||||||
import SubsExchangeClient
|
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
@ -16,9 +15,8 @@ class SmartSubscriptionAlgorithm(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
allowFailure: Boolean = false,
|
allowFailure: Boolean = false,
|
||||||
withCacheFallback: Boolean = true,
|
withCacheFallback: Boolean = true,
|
||||||
threadPool: ForkJoinPool? = null,
|
threadPool: ForkJoinPool? = null
|
||||||
subsExchangeClient: SubsExchangeClient? = null
|
): SubscriptionsTaskFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool) {
|
||||||
): SubscriptionsTaskFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool, subsExchangeClient) {
|
|
||||||
override fun getSubscriptionTasks(subs: Map<Subscription, List<String>>): List<SubscriptionTask> {
|
override fun getSubscriptionTasks(subs: Map<Subscription, List<String>>): List<SubscriptionTask> {
|
||||||
val allTasks: List<SubscriptionTask> = subs.flatMap { entry ->
|
val allTasks: List<SubscriptionTask> = subs.flatMap { entry ->
|
||||||
val sub = entry.key;
|
val sub = entry.key;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.futo.platformplayer.subscription
|
package com.futo.platformplayer.subscription
|
||||||
|
|
||||||
import SubsExchangeClient
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
@ -34,11 +33,11 @@ abstract class SubscriptionFetchAlgorithm(
|
||||||
companion object {
|
companion object {
|
||||||
public val TAG = "SubscriptionAlgorithm";
|
public val TAG = "SubscriptionAlgorithm";
|
||||||
|
|
||||||
fun getAlgorithm(algo: SubscriptionFetchAlgorithms, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = false, pool: ForkJoinPool? = null, withExchangeClient: SubsExchangeClient? = null): SubscriptionFetchAlgorithm {
|
fun getAlgorithm(algo: SubscriptionFetchAlgorithms, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = false, pool: ForkJoinPool? = null): SubscriptionFetchAlgorithm {
|
||||||
return when(algo) {
|
return when(algo) {
|
||||||
SubscriptionFetchAlgorithms.CACHE -> CachedSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, 50);
|
SubscriptionFetchAlgorithms.CACHE -> CachedSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, 50);
|
||||||
SubscriptionFetchAlgorithms.SIMPLE -> SimpleSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
|
SubscriptionFetchAlgorithms.SIMPLE -> SimpleSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
|
||||||
SubscriptionFetchAlgorithms.SMART -> SmartSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, withExchangeClient);
|
SubscriptionFetchAlgorithms.SMART -> SmartSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,22 @@
|
||||||
package com.futo.platformplayer.subscription
|
package com.futo.platformplayer.subscription
|
||||||
|
|
||||||
import SubsExchangeClient
|
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.DedupContentPager
|
import com.futo.platformplayer.api.media.structures.DedupContentPager
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
import com.futo.platformplayer.api.media.structures.PlatformContentPager
|
|
||||||
import com.futo.platformplayer.debug.Stopwatch
|
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.findNonRuntimeException
|
import com.futo.platformplayer.findNonRuntimeException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
||||||
import com.futo.platformplayer.getNowDiffMiliseconds
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
@ -30,12 +24,7 @@ import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.subsexchange.ChannelRequest
|
|
||||||
import com.futo.platformplayer.subsexchange.ChannelResolve
|
|
||||||
import com.futo.platformplayer.subsexchange.ExchangeContract
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import java.util.concurrent.ForkJoinPool
|
import java.util.concurrent.ForkJoinPool
|
||||||
|
@ -46,8 +35,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
allowFailure: Boolean = false,
|
allowFailure: Boolean = false,
|
||||||
withCacheFallback: Boolean = true,
|
withCacheFallback: Boolean = true,
|
||||||
_threadPool: ForkJoinPool? = null,
|
_threadPool: ForkJoinPool? = null
|
||||||
private val subsExchangeClient: SubsExchangeClient? = null
|
|
||||||
) : SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, _threadPool) {
|
) : SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, _threadPool) {
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,7 +45,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
|
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
|
||||||
var tasks = getSubscriptionTasks(subs).toMutableList()
|
val tasks = getSubscriptionTasks(subs);
|
||||||
|
|
||||||
val tasksGrouped = tasks.groupBy { it.client }
|
val tasksGrouped = tasks.groupBy { it.client }
|
||||||
|
|
||||||
|
@ -82,46 +70,11 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||||
|
|
||||||
val exs: ArrayList<Throwable> = arrayListOf();
|
val exs: ArrayList<Throwable> = arrayListOf();
|
||||||
|
|
||||||
var contract: ExchangeContract? = null;
|
|
||||||
var providedTasks: MutableList<SubscriptionTask>? = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
val contractingTime = measureTimeMillis {
|
|
||||||
val contractableTasks =
|
|
||||||
tasks.filter { !it.fromPeek && !it.fromCache && (it.type == ResultCapabilities.TYPE_VIDEOS || it.type == ResultCapabilities.TYPE_MIXED) };
|
|
||||||
contract =
|
|
||||||
if (contractableTasks.size > 10) subsExchangeClient?.requestContract(*contractableTasks.map {
|
|
||||||
ChannelRequest(it.url)
|
|
||||||
}.toTypedArray()) else null;
|
|
||||||
if (contract?.provided?.isNotEmpty() == true)
|
|
||||||
Logger.i(TAG, "Received subscription exchange contract (Requires ${contract?.required?.size}, Provides ${contract?.provided?.size}), ID: ${contract?.id}");
|
|
||||||
if (contract != null && contract!!.required.isNotEmpty()) {
|
|
||||||
providedTasks = mutableListOf()
|
|
||||||
for (task in tasks.toList()) {
|
|
||||||
if (!task.fromCache && !task.fromPeek && contract!!.provided.contains(task.url)) {
|
|
||||||
providedTasks!!.add(task);
|
|
||||||
tasks.remove(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(contract != null)
|
|
||||||
Logger.i(TAG, "Subscription Exchange contract received in ${contractingTime}ms");
|
|
||||||
else if(contractingTime > 100)
|
|
||||||
Logger.i(TAG, "Subscription Exchange contract failed to received in${contractingTime}ms");
|
|
||||||
|
|
||||||
}
|
|
||||||
catch(ex: Throwable){
|
|
||||||
Logger.e("SubscriptionsTaskFetchAlgorithm", "Failed to retrieve SubsExchange contract due to: " + ex.message, ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
val failedPlugins = mutableListOf<String>();
|
val failedPlugins = mutableListOf<String>();
|
||||||
val cachedChannels = mutableListOf<String>()
|
val cachedChannels = mutableListOf<String>()
|
||||||
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
||||||
|
|
||||||
val taskResults = arrayListOf<SubscriptionTaskResult>();
|
val taskResults = arrayListOf<SubscriptionTaskResult>();
|
||||||
var resolveCount = 0;
|
|
||||||
var resolveTime = 0L;
|
|
||||||
val timeTotal = measureTimeMillis {
|
val timeTotal = measureTimeMillis {
|
||||||
for(task in forkTasks) {
|
for(task in forkTasks) {
|
||||||
try {
|
try {
|
||||||
|
@ -150,82 +103,14 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//Resolve Subscription Exchange
|
|
||||||
if(contract != null) {
|
|
||||||
fun resolve() {
|
|
||||||
try {
|
|
||||||
resolveTime = measureTimeMillis {
|
|
||||||
val resolves = taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) && contract!!.required.contains(it.task.url) }.map {
|
|
||||||
ChannelResolve(
|
|
||||||
it.task.url,
|
|
||||||
it.pager!!.getResults().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) }
|
|
||||||
)
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
val resolveRequestStart = OffsetDateTime.now();
|
|
||||||
|
|
||||||
val resolve = subsExchangeClient?.resolveContract(
|
|
||||||
contract!!,
|
|
||||||
*resolves
|
|
||||||
);
|
|
||||||
|
|
||||||
Logger.i(TAG, "Subscription Exchange contract resolved request in ${resolveRequestStart.getNowDiffMiliseconds()}ms");
|
|
||||||
|
|
||||||
if (resolve != null) {
|
|
||||||
resolveCount = resolves.size;
|
|
||||||
UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size}")
|
|
||||||
for(result in resolve){
|
|
||||||
val task = providedTasks?.find { it.url == result.channelUrl };
|
|
||||||
if(task != null) {
|
|
||||||
taskResults.add(SubscriptionTaskResult(task, PlatformContentPager(result.content, result.content.size), null));
|
|
||||||
providedTasks?.remove(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (providedTasks != null) {
|
|
||||||
for(task in providedTasks!!) {
|
|
||||||
taskResults.add(SubscriptionTaskResult(task, null, IllegalStateException("No data received from exchange")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Logger.i(TAG, "Subscription Exchange contract resolved in ${resolveTime}ms");
|
|
||||||
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
//TODO: fetch remainder after all?
|
|
||||||
Logger.e(TAG, "Failed to resolve Subscription Exchange contract due to: " + ex.message, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(providedTasks?.size ?: 0 == 0)
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms");
|
|
||||||
if(resolveCount > 0) {
|
|
||||||
val selfFetchTime = timeTotal - resolveTime;
|
|
||||||
val selfFetchCount = tasks.count { !it.fromPeek && !it.fromCache };
|
|
||||||
if(selfFetchCount > 0) {
|
|
||||||
val selfResolvePercentage = resolveCount.toDouble() / selfFetchCount;
|
|
||||||
val estimateSelfFetchTime = selfFetchTime + selfFetchTime * selfResolvePercentage;
|
|
||||||
val selfFetchDelta = timeTotal - estimateSelfFetchTime;
|
|
||||||
if(selfFetchDelta > 0)
|
|
||||||
UIDialogs.appToast("Subscription Exchange lost ${selfFetchDelta}ms (out of ${timeTotal}ms)", true);
|
|
||||||
else
|
|
||||||
UIDialogs.appToast("Subscription Exchange saved ${(selfFetchDelta * -1).toInt()}ms (out of ${timeTotal}ms)", true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms")
|
||||||
|
|
||||||
//Cache pagers grouped by channel
|
//Cache pagers grouped by channel
|
||||||
val groupedPagers = taskResults.groupBy { it.task.sub.channel.url }
|
val groupedPagers = taskResults.groupBy { it.task.sub.channel.url }
|
||||||
.map { entry ->
|
.map { entry ->
|
||||||
val sub = if(!entry.value.isEmpty()) entry.value[0].task.sub else null;
|
val sub = if(!entry.value.isEmpty()) entry.value[0].task.sub else null;
|
||||||
val liveTasks = entry.value.filter { !it.task.fromCache && it.pager != null };
|
val liveTasks = entry.value.filter { !it.task.fromCache };
|
||||||
val cachedTasks = entry.value.filter { it.task.fromCache };
|
val cachedTasks = entry.value.filter { it.task.fromCache };
|
||||||
val livePager = if(liveTasks.isNotEmpty()) StateCache.cachePagerResults(scope, MultiChronoContentPager(liveTasks.map { it.pager!! }, true).apply { this.initialize() }) {
|
val livePager = if(liveTasks.isNotEmpty()) StateCache.cachePagerResults(scope, MultiChronoContentPager(liveTasks.map { it.pager!! }, true).apply { this.initialize() }) {
|
||||||
onNewCacheHit.emit(sub!!, it);
|
onNewCacheHit.emit(sub!!, it);
|
||||||
|
@ -288,8 +173,6 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||||
Logger.e(StateSubscriptions.TAG, "Subscription peek [${task.sub.channel.name}] failed", ex);
|
Logger.e(StateSubscriptions.TAG, "Subscription peek [${task.sub.channel.name}] failed", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Intercepts task.fromCache & task.fromPeek
|
|
||||||
synchronized(cachedChannels) {
|
synchronized(cachedChannels) {
|
||||||
if(task.fromCache || task.fromPeek) {
|
if(task.fromCache || task.fromPeek) {
|
||||||
finished++;
|
finished++;
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
package com.futo.platformplayer.subsexchange
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class ChannelRequest(
|
|
||||||
@SerialName("ChannelUrl")
|
|
||||||
var channelUrl: String
|
|
||||||
);
|
|
|
@ -1,19 +0,0 @@
|
||||||
package com.futo.platformplayer.subsexchange
|
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class ChannelResolve(
|
|
||||||
@SerialName("ChannelUrl")
|
|
||||||
var channelUrl: String,
|
|
||||||
@SerialName("Content")
|
|
||||||
var content: List<SerializedPlatformContent>,
|
|
||||||
@SerialName("Channel")
|
|
||||||
var channel: IPlatformChannel? = null
|
|
||||||
)
|
|
|
@ -1,24 +0,0 @@
|
||||||
package com.futo.platformplayer.subsexchange
|
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
|
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeStringSerializer
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class ChannelResult(
|
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
|
||||||
@SerialName("dateTime")
|
|
||||||
var dateTime: OffsetDateTime,
|
|
||||||
@SerialName("channelUrl")
|
|
||||||
var channelUrl: String,
|
|
||||||
@SerialName("content")
|
|
||||||
var content: List<SerializedPlatformContent>,
|
|
||||||
@SerialName("channel")
|
|
||||||
var channel: IPlatformChannel? = null
|
|
||||||
)
|
|
|
@ -1,27 +0,0 @@
|
||||||
package com.futo.platformplayer.subsexchange
|
|
||||||
|
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeStringSerializer
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.Serializer
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class ExchangeContract(
|
|
||||||
@SerialName("ID")
|
|
||||||
var id: String,
|
|
||||||
@SerialName("Requests")
|
|
||||||
var requests: List<ChannelRequest>,
|
|
||||||
@SerialName("Provided")
|
|
||||||
var provided: List<String> = listOf(),
|
|
||||||
@SerialName("Required")
|
|
||||||
var required: List<String> = listOf(),
|
|
||||||
@SerialName("Expire")
|
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeStringSerializer::class)
|
|
||||||
var expired: OffsetDateTime = OffsetDateTime.MIN,
|
|
||||||
@SerialName("ContractVersion")
|
|
||||||
var contractVersion: Int = 1
|
|
||||||
)
|
|
|
@ -1,14 +0,0 @@
|
||||||
package com.futo.platformplayer.subsexchange
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ExchangeContractResolve(
|
|
||||||
@SerialName("PublicKey")
|
|
||||||
val publicKey: String,
|
|
||||||
@SerialName("Signature")
|
|
||||||
val signature: String,
|
|
||||||
@SerialName("Data")
|
|
||||||
val data: String
|
|
||||||
)
|
|
|
@ -1,169 +0,0 @@
|
||||||
import com.futo.platformplayer.api.media.Serializer
|
|
||||||
import com.futo.platformplayer.getNowDiffMiliseconds
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm.Companion.TAG
|
|
||||||
import com.futo.platformplayer.subsexchange.ChannelRequest
|
|
||||||
import com.futo.platformplayer.subsexchange.ChannelResolve
|
|
||||||
import com.futo.platformplayer.subsexchange.ChannelResult
|
|
||||||
import com.futo.platformplayer.subsexchange.ExchangeContract
|
|
||||||
import com.futo.platformplayer.subsexchange.ExchangeContractResolve
|
|
||||||
import com.futo.platformplayer.toGzip
|
|
||||||
import com.futo.platformplayer.toHumanBytesSize
|
|
||||||
import kotlinx.serialization.*
|
|
||||||
import kotlinx.serialization.json.*
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.URL
|
|
||||||
import java.security.KeyFactory
|
|
||||||
import java.security.PrivateKey
|
|
||||||
import java.security.PublicKey
|
|
||||||
import java.security.Signature
|
|
||||||
import java.security.interfaces.RSAPrivateKey
|
|
||||||
import java.security.interfaces.RSAPublicKey
|
|
||||||
import java.util.Base64
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.io.OutputStreamWriter
|
|
||||||
import java.math.BigInteger
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.security.KeyPairGenerator
|
|
||||||
import java.security.spec.PKCS8EncodedKeySpec
|
|
||||||
import java.security.spec.RSAPublicKeySpec
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
|
|
||||||
|
|
||||||
class SubsExchangeClient(private val server: String, private val privateKey: String, private val contractTimeout: Int = 1000) {
|
|
||||||
|
|
||||||
private val json = Json {
|
|
||||||
ignoreUnknownKeys = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private val publicKey: String = extractPublicKey(privateKey)
|
|
||||||
|
|
||||||
// Endpoints
|
|
||||||
|
|
||||||
// Endpoint: Contract
|
|
||||||
fun requestContract(vararg channels: ChannelRequest): ExchangeContract {
|
|
||||||
val data = post("/api/Channel/Contract", Json.encodeToString(channels).toByteArray(Charsets.UTF_8), "application/json", contractTimeout)
|
|
||||||
return Json.decodeFromString(data)
|
|
||||||
}
|
|
||||||
suspend fun requestContractAsync(vararg channels: ChannelRequest): ExchangeContract {
|
|
||||||
val data = postAsync("/api/Channel/Contract", Json.encodeToString(channels).toByteArray(Charsets.UTF_8), "application/json")
|
|
||||||
return Json.decodeFromString(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Endpoint: Resolve
|
|
||||||
fun resolveContract(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
|
|
||||||
val contractResolve = convertResolves(*resolves)
|
|
||||||
val contractResolveJson = Serializer.json.encodeToString(contractResolve);
|
|
||||||
val contractResolveTimeStart = OffsetDateTime.now();
|
|
||||||
val result = post("/api/Channel/Resolve?contractId=${contract.id}", contractResolveJson.toByteArray(Charsets.UTF_8), "application/json", 0, true)
|
|
||||||
val contractResolveTime = contractResolveTimeStart.getNowDiffMiliseconds();
|
|
||||||
Logger.v("SubsExchangeClient", "Subscription Exchange Resolve Request [${contractResolveTime}ms]:" + result);
|
|
||||||
return Serializer.json.decodeFromString(result)
|
|
||||||
}
|
|
||||||
suspend fun resolveContractAsync(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
|
|
||||||
val contractResolve = convertResolves(*resolves)
|
|
||||||
val result = postAsync("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve).toByteArray(Charsets.UTF_8), "application/json", true)
|
|
||||||
return Serializer.json.decodeFromString(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun convertResolves(vararg resolves: ChannelResolve): ExchangeContractResolve {
|
|
||||||
val data = Serializer.json.encodeToString(resolves)
|
|
||||||
val signature = createSignature(data, privateKey)
|
|
||||||
|
|
||||||
return ExchangeContractResolve(
|
|
||||||
publicKey = publicKey,
|
|
||||||
signature = signature,
|
|
||||||
data = data
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IO methods
|
|
||||||
private fun post(query: String, body: ByteArray, contentType: String, timeout: Int = 0, gzip: Boolean = false): String {
|
|
||||||
val url = URL("${server.trim('/')}$query")
|
|
||||||
with(url.openConnection() as HttpURLConnection) {
|
|
||||||
if(timeout > 0)
|
|
||||||
this.connectTimeout = timeout
|
|
||||||
requestMethod = "POST"
|
|
||||||
setRequestProperty("Content-Type", contentType)
|
|
||||||
doOutput = true
|
|
||||||
|
|
||||||
|
|
||||||
if(gzip) {
|
|
||||||
val gzipData = body.toGzip();
|
|
||||||
setRequestProperty("Content-Encoding", "gzip");
|
|
||||||
outputStream.write(gzipData);
|
|
||||||
Logger.i("SubsExchangeClient", "SubsExchange using gzip (${body.size.toHumanBytesSize()} => ${gzipData.size.toHumanBytesSize()}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
outputStream.write(body);
|
|
||||||
|
|
||||||
val status = responseCode;
|
|
||||||
Logger.i("SubsExchangeClient", "POST [${url}]: ${status}");
|
|
||||||
|
|
||||||
if(status == 200)
|
|
||||||
InputStreamReader(inputStream, StandardCharsets.UTF_8).use {
|
|
||||||
return it.readText()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var errorStr = "";
|
|
||||||
try {
|
|
||||||
errorStr = InputStreamReader(errorStream, StandardCharsets.UTF_8).use {
|
|
||||||
return@use it.readText()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(ex: Throwable){}
|
|
||||||
|
|
||||||
throw Exception("Exchange server resulted in code ${status}:\n" + errorStr);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private suspend fun postAsync(query: String, body: ByteArray, contentType: String, gzip: Boolean = false): String {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
post(query, body, contentType, 0, gzip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crypto methods
|
|
||||||
companion object {
|
|
||||||
fun createPrivateKey(): String {
|
|
||||||
val rsa = KeyFactory.getInstance("RSA")
|
|
||||||
val keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
|
||||||
keyPairGenerator.initialize(2048);
|
|
||||||
val keyPair = keyPairGenerator.generateKeyPair();
|
|
||||||
return Base64.getEncoder().encodeToString(keyPair.private.encoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
fun extractPublicKey(privateKey: String): String {
|
|
||||||
val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))
|
|
||||||
val keyFactory = KeyFactory.getInstance("RSA")
|
|
||||||
val privateKeyObj = keyFactory.generatePrivate(keySpec) as RSAPrivateKey
|
|
||||||
val publicKeyObj: PublicKey? = keyFactory.generatePublic(RSAPublicKeySpec(privateKeyObj.modulus, BigInteger.valueOf(65537)));
|
|
||||||
var publicKeyBase64 = Base64.getEncoder().encodeToString(publicKeyObj?.encoded);
|
|
||||||
var pem = "-----BEGIN PUBLIC KEY-----"
|
|
||||||
while(publicKeyBase64.length > 0) {
|
|
||||||
val length = Math.min(publicKeyBase64.length, 64);
|
|
||||||
pem += "\n" + publicKeyBase64.substring(0, length);
|
|
||||||
publicKeyBase64 = publicKeyBase64.substring(length);
|
|
||||||
}
|
|
||||||
return pem + "\n-----END PUBLIC KEY-----";
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createSignature(data: String, privateKey: String): String {
|
|
||||||
val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))
|
|
||||||
val keyFactory = KeyFactory.getInstance("RSA")
|
|
||||||
val rsaPrivateKey = keyFactory.generatePrivate(keySpec) as RSAPrivateKey
|
|
||||||
|
|
||||||
val signature = Signature.getInstance("SHA256withRSA")
|
|
||||||
signature.initSign(rsaPrivateKey)
|
|
||||||
signature.update(data.toByteArray(Charsets.UTF_8))
|
|
||||||
|
|
||||||
val signatureBytes = signature.sign()
|
|
||||||
return Base64.getEncoder().encodeToString(signatureBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,335 +0,0 @@
|
||||||
package com.futo.platformplayer.sync.internal
|
|
||||||
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.noise.protocol.CipherStatePair
|
|
||||||
import com.futo.platformplayer.noise.protocol.DHState
|
|
||||||
import com.futo.platformplayer.noise.protocol.HandshakeState
|
|
||||||
import com.futo.platformplayer.states.StateSync
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
import java.util.Base64
|
|
||||||
|
|
||||||
interface IChannel : AutoCloseable {
|
|
||||||
val remotePublicKey: String?
|
|
||||||
val remoteVersion: Int?
|
|
||||||
var authorizable: IAuthorizable?
|
|
||||||
var syncSession: SyncSession?
|
|
||||||
fun setDataHandler(onData: ((SyncSocketSession, IChannel, UByte, UByte, ByteBuffer) -> Unit)?)
|
|
||||||
fun send(opcode: UByte, subOpcode: UByte = 0u, data: ByteBuffer? = null)
|
|
||||||
fun setCloseHandler(onClose: ((IChannel) -> Unit)?)
|
|
||||||
val linkType: LinkType
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChannelSocket(private val session: SyncSocketSession) : IChannel {
|
|
||||||
override val remotePublicKey: String? get() = session.remotePublicKey
|
|
||||||
override val remoteVersion: Int? get() = session.remoteVersion
|
|
||||||
private var onData: ((SyncSocketSession, IChannel, UByte, UByte, ByteBuffer) -> Unit)? = null
|
|
||||||
private var onClose: ((IChannel) -> Unit)? = null
|
|
||||||
override val linkType: LinkType get() = LinkType.Direct
|
|
||||||
|
|
||||||
override var authorizable: IAuthorizable?
|
|
||||||
get() = session.authorizable
|
|
||||||
set(value) { session.authorizable = value }
|
|
||||||
override var syncSession: SyncSession? = null
|
|
||||||
|
|
||||||
override fun setDataHandler(onData: ((SyncSocketSession, IChannel, UByte, UByte, ByteBuffer) -> Unit)?) {
|
|
||||||
this.onData = onData
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setCloseHandler(onClose: ((IChannel) -> Unit)?) {
|
|
||||||
this.onClose = onClose
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
session.stop()
|
|
||||||
onClose?.invoke(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invokeDataHandler(opcode: UByte, subOpcode: UByte, data: ByteBuffer) {
|
|
||||||
onData?.invoke(session, this, opcode, subOpcode, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun send(opcode: UByte, subOpcode: UByte, data: ByteBuffer?) {
|
|
||||||
if (data != null) {
|
|
||||||
session.send(opcode, subOpcode, data)
|
|
||||||
} else {
|
|
||||||
session.send(opcode, subOpcode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChannelRelayed(
|
|
||||||
private val session: SyncSocketSession,
|
|
||||||
private val localKeyPair: DHState,
|
|
||||||
private val publicKey: String,
|
|
||||||
private val initiator: Boolean
|
|
||||||
) : IChannel {
|
|
||||||
private val sendLock = Object()
|
|
||||||
private val decryptLock = Object()
|
|
||||||
private var handshakeState: HandshakeState? = if (initiator) {
|
|
||||||
HandshakeState(StateSync.protocolName, HandshakeState.INITIATOR).apply {
|
|
||||||
localKeyPair.copyFrom(this@ChannelRelayed.localKeyPair)
|
|
||||||
remotePublicKey.setPublicKey(Base64.getDecoder().decode(publicKey), 0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
HandshakeState(StateSync.protocolName, HandshakeState.RESPONDER).apply {
|
|
||||||
localKeyPair.copyFrom(this@ChannelRelayed.localKeyPair)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var transport: CipherStatePair? = null
|
|
||||||
override var authorizable: IAuthorizable? = null
|
|
||||||
val isAuthorized: Boolean get() = authorizable?.isAuthorized ?: false
|
|
||||||
var connectionId: Long = 0L
|
|
||||||
override var remotePublicKey: String? = publicKey
|
|
||||||
private set
|
|
||||||
override var remoteVersion: Int? = null
|
|
||||||
private set
|
|
||||||
override var syncSession: SyncSession? = null
|
|
||||||
override val linkType: LinkType get() = LinkType.Relayed
|
|
||||||
|
|
||||||
private var onData: ((SyncSocketSession, IChannel, UByte, UByte, ByteBuffer) -> Unit)? = null
|
|
||||||
private var onClose: ((IChannel) -> Unit)? = null
|
|
||||||
private var disposed = false
|
|
||||||
|
|
||||||
init {
|
|
||||||
handshakeState?.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setDataHandler(onData: ((SyncSocketSession, IChannel, UByte, UByte, ByteBuffer) -> Unit)?) {
|
|
||||||
this.onData = onData
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setCloseHandler(onClose: ((IChannel) -> Unit)?) {
|
|
||||||
this.onClose = onClose
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
disposed = true
|
|
||||||
|
|
||||||
if (connectionId != 0L) {
|
|
||||||
Thread {
|
|
||||||
try {
|
|
||||||
session.sendRelayError(connectionId, SyncErrorCode.ConnectionClosed)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("ChannelRelayed", "Exception while sending relay error", e)
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
transport?.sender?.destroy()
|
|
||||||
transport?.receiver?.destroy()
|
|
||||||
transport = null
|
|
||||||
handshakeState?.destroy()
|
|
||||||
handshakeState = null
|
|
||||||
|
|
||||||
onClose?.invoke(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun throwIfDisposed() {
|
|
||||||
if (disposed) throw IllegalStateException("ChannelRelayed is disposed")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invokeDataHandler(opcode: UByte, subOpcode: UByte, data: ByteBuffer) {
|
|
||||||
onData?.invoke(session, this, opcode, subOpcode, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun completeHandshake(remoteVersion: Int, transport: CipherStatePair) {
|
|
||||||
throwIfDisposed()
|
|
||||||
|
|
||||||
this.remoteVersion = remoteVersion
|
|
||||||
val remoteKeyBytes = ByteArray(handshakeState!!.remotePublicKey.publicKeyLength)
|
|
||||||
handshakeState!!.remotePublicKey.getPublicKey(remoteKeyBytes, 0)
|
|
||||||
this.remotePublicKey = Base64.getEncoder().encodeToString(remoteKeyBytes)
|
|
||||||
handshakeState?.destroy()
|
|
||||||
handshakeState = null
|
|
||||||
this.transport = transport
|
|
||||||
Logger.i("ChannelRelayed", "Completed handshake for connectionId $connectionId")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendPacket(packet: ByteArray) {
|
|
||||||
throwIfDisposed()
|
|
||||||
|
|
||||||
synchronized(sendLock) {
|
|
||||||
val encryptedPayload = ByteArray(packet.size + 16)
|
|
||||||
val encryptedLength = transport!!.sender.encryptWithAd(null, packet, 0, encryptedPayload, 0, packet.size)
|
|
||||||
|
|
||||||
val relayedPacket = ByteArray(8 + encryptedLength)
|
|
||||||
ByteBuffer.wrap(relayedPacket).order(ByteOrder.LITTLE_ENDIAN).apply {
|
|
||||||
putLong(connectionId)
|
|
||||||
put(encryptedPayload, 0, encryptedLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.send(Opcode.RELAY.value, RelayOpcode.DATA.value, ByteBuffer.wrap(relayedPacket).order(ByteOrder.LITTLE_ENDIAN))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendError(errorCode: SyncErrorCode) {
|
|
||||||
throwIfDisposed()
|
|
||||||
|
|
||||||
synchronized(sendLock) {
|
|
||||||
val packet = ByteArray(4)
|
|
||||||
ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).putInt(errorCode.value)
|
|
||||||
|
|
||||||
val encryptedPayload = ByteArray(4 + 16)
|
|
||||||
val encryptedLength = transport!!.sender.encryptWithAd(null, packet, 0, encryptedPayload, 0, packet.size)
|
|
||||||
|
|
||||||
val relayedPacket = ByteArray(8 + encryptedLength)
|
|
||||||
ByteBuffer.wrap(relayedPacket).order(ByteOrder.LITTLE_ENDIAN).apply {
|
|
||||||
putLong(connectionId)
|
|
||||||
put(encryptedPayload, 0, encryptedLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.send(Opcode.RELAY.value, RelayOpcode.ERROR.value, ByteBuffer.wrap(relayedPacket))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun send(opcode: UByte, subOpcode: UByte, data: ByteBuffer?) {
|
|
||||||
throwIfDisposed()
|
|
||||||
|
|
||||||
val actualCount = data?.remaining() ?: 0
|
|
||||||
val ENCRYPTION_OVERHEAD = 16
|
|
||||||
val CONNECTION_ID_SIZE = 8
|
|
||||||
val HEADER_SIZE = 6
|
|
||||||
val MAX_DATA_PER_PACKET = SyncSocketSession.MAXIMUM_PACKET_SIZE - HEADER_SIZE - CONNECTION_ID_SIZE - ENCRYPTION_OVERHEAD - 16
|
|
||||||
|
|
||||||
if (actualCount > MAX_DATA_PER_PACKET && data != null) {
|
|
||||||
val streamId = session.generateStreamId()
|
|
||||||
val totalSize = actualCount
|
|
||||||
var sendOffset = 0
|
|
||||||
|
|
||||||
while (sendOffset < totalSize) {
|
|
||||||
val bytesRemaining = totalSize - sendOffset
|
|
||||||
val bytesToSend = minOf(MAX_DATA_PER_PACKET - 8 - 2, bytesRemaining)
|
|
||||||
|
|
||||||
val streamData: ByteArray
|
|
||||||
val streamOpcode: StreamOpcode
|
|
||||||
if (sendOffset == 0) {
|
|
||||||
streamOpcode = StreamOpcode.START
|
|
||||||
streamData = ByteArray(4 + 4 + 1 + 1 + bytesToSend)
|
|
||||||
ByteBuffer.wrap(streamData).order(ByteOrder.LITTLE_ENDIAN).apply {
|
|
||||||
putInt(streamId)
|
|
||||||
putInt(totalSize)
|
|
||||||
put(opcode.toByte())
|
|
||||||
put(subOpcode.toByte())
|
|
||||||
put(data.array(), data.position() + sendOffset, bytesToSend)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
streamData = ByteArray(4 + 4 + bytesToSend)
|
|
||||||
ByteBuffer.wrap(streamData).order(ByteOrder.LITTLE_ENDIAN).apply {
|
|
||||||
putInt(streamId)
|
|
||||||
putInt(sendOffset)
|
|
||||||
put(data.array(), data.position() + sendOffset, bytesToSend)
|
|
||||||
}
|
|
||||||
streamOpcode = if (bytesToSend < bytesRemaining) StreamOpcode.DATA else StreamOpcode.END
|
|
||||||
}
|
|
||||||
|
|
||||||
val fullPacket = ByteArray(HEADER_SIZE + streamData.size)
|
|
||||||
ByteBuffer.wrap(fullPacket).order(ByteOrder.LITTLE_ENDIAN).apply {
|
|
||||||
putInt(streamData.size + 2)
|
|
||||||
put(Opcode.STREAM.value.toByte())
|
|
||||||
put(streamOpcode.value.toByte())
|
|
||||||
put(streamData)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendPacket(fullPacket)
|
|
||||||
sendOffset += bytesToSend
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val packet = ByteArray(HEADER_SIZE + actualCount)
|
|
||||||
ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).apply {
|
|
||||||
putInt(actualCount + 2)
|
|
||||||
put(opcode.toByte())
|
|
||||||
put(subOpcode.toByte())
|
|
||||||
if (actualCount > 0 && data != null) put(data.array(), data.position(), actualCount)
|
|
||||||
}
|
|
||||||
sendPacket(packet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendRequestTransport(requestId: Int, publicKey: String, pairingCode: String? = null) {
|
|
||||||
throwIfDisposed()
|
|
||||||
|
|
||||||
synchronized(sendLock) {
|
|
||||||
val channelMessage = ByteArray(1024)
|
|
||||||
val channelBytesWritten = handshakeState!!.writeMessage(channelMessage, 0, null, 0, 0)
|
|
||||||
|
|
||||||
val publicKeyBytes = Base64.getDecoder().decode(publicKey)
|
|
||||||
if (publicKeyBytes.size != 32) throw IllegalArgumentException("Public key must be 32 bytes")
|
|
||||||
|
|
||||||
val (pairingMessageLength, pairingMessage) = if (pairingCode != null) {
|
|
||||||
val pairingHandshake = HandshakeState(SyncSocketSession.nProtocolName, HandshakeState.INITIATOR).apply {
|
|
||||||
remotePublicKey.setPublicKey(publicKeyBytes, 0)
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
val pairingCodeBytes = pairingCode.toByteArray(Charsets.UTF_8)
|
|
||||||
if (pairingCodeBytes.size > 32) throw IllegalArgumentException("Pairing code must not exceed 32 bytes")
|
|
||||||
val pairingMessageBuffer = ByteArray(1024)
|
|
||||||
val bytesWritten = pairingHandshake.writeMessage(pairingMessageBuffer, 0, pairingCodeBytes, 0, pairingCodeBytes.size)
|
|
||||||
bytesWritten to pairingMessageBuffer.copyOf(bytesWritten)
|
|
||||||
} else {
|
|
||||||
0 to ByteArray(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
val packetSize = 4 + 32 + 4 + pairingMessageLength + 4 + channelBytesWritten
|
|
||||||
val packet = ByteArray(packetSize)
|
|
||||||
ByteBuffer.wrap(packet).order(ByteOrder.LITTLE_ENDIAN).apply {
|
|
||||||
putInt(requestId)
|
|
||||||
put(publicKeyBytes)
|
|
||||||
putInt(pairingMessageLength)
|
|
||||||
if (pairingMessageLength > 0) put(pairingMessage)
|
|
||||||
putInt(channelBytesWritten)
|
|
||||||
put(channelMessage, 0, channelBytesWritten)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.send(Opcode.REQUEST.value, RequestOpcode.TRANSPORT.value, ByteBuffer.wrap(packet))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendResponseTransport(remoteVersion: Int, requestId: Int, handshakeMessage: ByteArray) {
|
|
||||||
throwIfDisposed()
|
|
||||||
|
|
||||||
synchronized(sendLock) {
|
|
||||||
val message = ByteArray(1024)
|
|
||||||
val plaintext = ByteArray(1024)
|
|
||||||
handshakeState!!.readMessage(handshakeMessage, 0, handshakeMessage.size, plaintext, 0)
|
|
||||||
val bytesWritten = handshakeState!!.writeMessage(message, 0, null, 0, 0)
|
|
||||||
val transport = handshakeState!!.split()
|
|
||||||
|
|
||||||
val responsePacket = ByteArray(20 + bytesWritten)
|
|
||||||
ByteBuffer.wrap(responsePacket).order(ByteOrder.LITTLE_ENDIAN).apply {
|
|
||||||
putInt(0) // Status code
|
|
||||||
putLong(connectionId)
|
|
||||||
putInt(requestId)
|
|
||||||
putInt(bytesWritten)
|
|
||||||
put(message, 0, bytesWritten)
|
|
||||||
}
|
|
||||||
|
|
||||||
completeHandshake(remoteVersion, transport)
|
|
||||||
session.send(Opcode.RESPONSE.value, ResponseOpcode.TRANSPORT.value, ByteBuffer.wrap(responsePacket))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decrypt(encryptedPayload: ByteBuffer): ByteBuffer {
|
|
||||||
throwIfDisposed()
|
|
||||||
|
|
||||||
synchronized(decryptLock) {
|
|
||||||
val encryptedBytes = ByteArray(encryptedPayload.remaining()).also { encryptedPayload.get(it) }
|
|
||||||
val decryptedPayload = ByteArray(encryptedBytes.size - 16)
|
|
||||||
val plen = transport!!.receiver.decryptWithAd(null, encryptedBytes, 0, decryptedPayload, 0, encryptedBytes.size)
|
|
||||||
if (plen != decryptedPayload.size) throw IllegalStateException("Expected decrypted payload length to be $plen")
|
|
||||||
return ByteBuffer.wrap(decryptedPayload).order(ByteOrder.LITTLE_ENDIAN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleTransportRelayed(remoteVersion: Int, connectionId: Long, handshakeMessage: ByteArray) {
|
|
||||||
throwIfDisposed()
|
|
||||||
|
|
||||||
synchronized(decryptLock) {
|
|
||||||
this.connectionId = connectionId
|
|
||||||
val plaintext = ByteArray(1024)
|
|
||||||
val plen = handshakeState!!.readMessage(handshakeMessage, 0, handshakeMessage.size, plaintext, 0)
|
|
||||||
val transport = handshakeState!!.split()
|
|
||||||
completeHandshake(remoteVersion, transport)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue