mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-24 21:44:49 +00:00
Compare commits
167 commits
Author | SHA1 | Date | |
---|---|---|---|
|
daa91986ef | ||
|
63761cfc9a | ||
|
d10026acd1 | ||
|
9347351c37 | ||
|
0ef1f2d40f | ||
|
b460f9915d | ||
|
4e195dfbc3 | ||
|
3c7f7bfca7 | ||
|
05230971b3 | ||
|
dccdf72c73 | ||
|
ca15983a72 | ||
|
4b6a2c9829 | ||
|
1755d03a6b | ||
|
869b1fc15e | ||
|
ce2a2f8582 | ||
|
7b355139fb | ||
|
b14518edb1 | ||
|
7d64003d1c | ||
|
0a59e04f19 | ||
|
b57abb646f | ||
|
dd6bde97a9 | ||
|
b545545712 | ||
|
c1993ffa03 | ||
|
7f7ebafa46 | ||
|
b652597924 | ||
|
258fe77928 | ||
|
5a9fcd6fab | ||
|
3c05521a5b | ||
|
034b8b15ae | ||
|
7bd687331b | ||
|
54d58df4b6 | ||
|
9165a9f7cb | ||
|
b556d1e81d | ||
|
7c25678211 | ||
|
c83a9924e2 | ||
|
bbeb9b83a0 | ||
|
06478f3e36 | ||
|
40f20002b2 | ||
|
442272f517 | ||
|
88dae8e9c4 | ||
|
1bbfa7d39e | ||
|
edc2b3d295 | ||
|
0006da7385 | ||
|
b5ac8b3ec6 | ||
|
78f5169880 | ||
|
3361b77aec | ||
|
8b7c9df286 | ||
|
157d5b4c36 | ||
|
44c8800bec | ||
|
2f0ba1b1f7 | ||
|
36c51f1a0c | ||
|
1dfe18aa6f | ||
|
b9bbfb44c5 | ||
|
83843f192d | ||
|
8839d9f1c6 | ||
|
0630ec1d46 | ||
|
4dce8d6a80 | ||
|
3b62f999bf | ||
|
65ae8610fd | ||
|
c1c2000c98 | ||
|
287c2d82a1 | ||
|
5cde1650f4 | ||
|
a4b90f14ab | ||
|
4826b40136 | ||
|
62618224da | ||
|
49f15e1637 | ||
|
e36047c890 | ||
|
8f1199bd08 | ||
|
d6e045ea4e | ||
|
304e48996b | ||
|
f350dc83b8 | ||
|
ebb7beda8c | ||
|
a01f3da66e | ||
|
72f5b5fbc0 | ||
|
330aa495c8 | ||
|
0b529ae94d | ||
|
83b35183d0 | ||
|
2cd01eb1fe | ||
|
07378f665a | ||
|
bfd5f24f4c | ||
|
3d617187af | ||
|
d040b93ca9 | ||
|
a410e2962a | ||
|
f5aa8f37bb | ||
|
7e932df450 | ||
|
3d4741727e | ||
|
a03b63ef74 | ||
|
15ce3e9f20 | ||
|
da58b72f9d | ||
|
1639bd7af1 | ||
|
d474121f85 | ||
|
978f76ffb6 | ||
|
084bac00f5 | ||
|
94454172dd | ||
|
891d3cf966 | ||
|
561d5ec7ab | ||
|
7ce437d50a | ||
|
4b02d4ce90 | ||
|
3107185869 | ||
|
2e3584a353 | ||
|
e5b1be195c | ||
|
dde30c9d76 | ||
|
3830e65de8 | ||
|
c589cf167e | ||
|
2fde367c82 | ||
|
8fd188268e | ||
|
b65257df42 | ||
|
aaa2d7f08d | ||
|
f73e25ece6 | ||
|
78d427f208 | ||
|
eaeaf3538f | ||
|
85e381a85e | ||
|
1b7ee8231b | ||
|
1b8b8f5738 | ||
|
53df19b477 | ||
|
ccf21b7580 | ||
|
4189d62a57 | ||
|
9a3e3af614 | ||
|
f7187400dc | ||
|
f55a7f0a7b | ||
|
d6d35a645e | ||
|
e719dcc7f5 | ||
|
bc5bc5450c | ||
|
f4bade0c2e | ||
|
9be59c674d | ||
|
a1dec23c20 | ||
|
ed926c4e37 | ||
|
ab360ed6f6 | ||
|
569ba3d651 | ||
|
60fe28c2fe | ||
|
2787e29a07 | ||
|
c77a4d08d6 | ||
|
9b3f90f922 | ||
|
c88d457021 | ||
|
b20b625820 | ||
|
fd95311920 | ||
|
6da5c11731 | ||
|
4e58231308 | ||
|
ef0ecf249a | ||
|
4981617f7a | ||
|
2070bc7007 | ||
|
231d2461b3 | ||
|
3b457f87c4 | ||
|
de3ced4d3c | ||
|
891777e89e | ||
|
287239dd1c | ||
|
7cdded8fd7 | ||
|
8c9d045e1d | ||
|
620f5a0459 | ||
|
178d874ba0 | ||
|
d44f30c8a6 | ||
|
ce66937429 | ||
|
9823337375 | ||
|
11f5f0dfe1 | ||
|
e1882f19e8 | ||
|
6a8b9f06c2 | ||
|
752fc8787d | ||
|
90a1cd8280 | ||
|
aa570ac29d | ||
|
fb7b6363f9 | ||
|
23afe7994c | ||
|
7557e6f6ba | ||
|
86b6938911 | ||
|
8f30a45fa8 | ||
|
7c9e9d5f52 | ||
|
4066ce73a8 | ||
|
b5722dba1a |
263 changed files with 7094 additions and 2736 deletions
44
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
44
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -1,19 +1,19 @@
|
||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
|
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
|
||||||
labels: ["bug", "new"]
|
labels: ["Bug"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
# Thank you for taking the time to fill out this bug report.
|
# Thank you for taking the time to fill out this bug report.
|
||||||
|
|
||||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
||||||
|
|
||||||
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||||
|
|
||||||
## Filing a bug report
|
## Filing a bug report
|
||||||
|
|
||||||
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
|
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
|
||||||
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
|
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
|
||||||
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
|
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
|
||||||
|
|
||||||
|
@ -41,18 +41,21 @@ body:
|
||||||
label: What plugins are you seeing the problem on?
|
label: What plugins are you seeing the problem on?
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- All
|
- "All"
|
||||||
- Youtube
|
- "Youtube"
|
||||||
- BiliBili (CN)
|
- "Odysee"
|
||||||
- Twitch
|
- "Rumble"
|
||||||
- Odysee
|
- "Kick"
|
||||||
- Rumble
|
- "Twitch"
|
||||||
- Kick
|
- "PeerTube"
|
||||||
- PeerTube
|
- "Patreon"
|
||||||
- Patreon
|
- "Nebula"
|
||||||
- Nebula
|
- "BiliBili (CN)"
|
||||||
- SoundCloud
|
- "Bitchute"
|
||||||
- Other
|
- "SoundCloud"
|
||||||
|
- "Dailymotion"
|
||||||
|
- "Apple Podcasts"
|
||||||
|
- "Other"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
@ -72,6 +75,17 @@ body:
|
||||||
- label: While logged out
|
- label: While logged out
|
||||||
- label: N/A
|
- label: N/A
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: vpn
|
||||||
|
attributes:
|
||||||
|
label: Are you using a VPN?
|
||||||
|
multiple: false
|
||||||
|
options:
|
||||||
|
- "No"
|
||||||
|
- "Yes"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: logs
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
name: Documentation Issue
|
name: Documentation Issue
|
||||||
description: Report an issue or suggest a change in the documentation.
|
description: Report an issue or suggest a change in the documentation.
|
||||||
labels: ["documentation", "new"]
|
labels: ["Documentation"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
# Thank you for opening a documentation change request.
|
# Thank you for opening a documentation change request.
|
||||||
|
|
||||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
|
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
|
||||||
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
|
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
|
||||||
|
|
||||||
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||||
|
|
6
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
6
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Suggest a new feature or other enhancement.
|
description: Suggest a new feature or other enhancement.
|
||||||
labels: ["enhancement", "new"]
|
labels: ["Enhancement"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -9,8 +9,6 @@ body:
|
||||||
|
|
||||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
||||||
|
|
||||||
[External Contributions are closed at this time](https://github.com/tom-futo/grayjay-android/blob/master/CONTRIBUTION.md#contributing-to-core)
|
|
||||||
|
|
||||||
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
@ -55,4 +53,4 @@ body:
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
|
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
|
||||||
|
|
||||||
|
|
34
.github/workflows/labeler.yml
vendored
34
.github/workflows/labeler.yml
vendored
|
@ -1,34 +0,0 @@
|
||||||
name: Issue labeler
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [ opened ]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
label-component:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
# required for all workflows
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Parse issue form
|
|
||||||
uses: stefanbuck/github-issue-parser@v3
|
|
||||||
id: issue-parser
|
|
||||||
with:
|
|
||||||
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
|
|
||||||
|
|
||||||
- name: Set labels based on plugin field
|
|
||||||
uses: redhat-plumbers-in-action/advanced-issue-labeler@v2
|
|
||||||
with:
|
|
||||||
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
|
|
||||||
section: plugin
|
|
||||||
block-list: |
|
|
||||||
None
|
|
||||||
Other
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
12
.gitmodules
vendored
12
.gitmodules
vendored
|
@ -82,3 +82,15 @@
|
||||||
[submodule "app/src/stable/assets/sources/dailymotion"]
|
[submodule "app/src/stable/assets/sources/dailymotion"]
|
||||||
path = app/src/stable/assets/sources/dailymotion
|
path = app/src/stable/assets/sources/dailymotion
|
||||||
url = ../plugins/dailymotion.git
|
url = ../plugins/dailymotion.git
|
||||||
|
[submodule "app/src/stable/assets/sources/apple-podcast"]
|
||||||
|
path = app/src/stable/assets/sources/apple-podcasts
|
||||||
|
url = ../plugins/apple-podcasts.git
|
||||||
|
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
|
||||||
|
path = app/src/unstable/assets/sources/apple-podcasts
|
||||||
|
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:5.1'
|
implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS'
|
||||||
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'
|
||||||
|
|
|
@ -0,0 +1,266 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -36,6 +36,12 @@
|
||||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<receiver android:name=".receivers.MediaButtonReceiver" android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service android:name=".services.MediaPlaybackService"
|
<service android:name=".services.MediaPlaybackService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="mediaPlayback" />
|
android:foregroundServiceType="mediaPlayback" />
|
||||||
|
@ -52,7 +58,7 @@
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleInstance"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsPictureInPicture="true">
|
android:supportsPictureInPicture="true">
|
||||||
|
|
||||||
|
@ -150,7 +156,6 @@
|
||||||
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"
|
||||||
|
|
11
app/src/main/assets/scripts/JSDOM.js
Normal file
11
app/src/main/assets/scripts/JSDOM.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -11,7 +11,8 @@ let Type = {
|
||||||
Streams: "STREAMS",
|
Streams: "STREAMS",
|
||||||
Mixed: "MIXED",
|
Mixed: "MIXED",
|
||||||
Live: "LIVE",
|
Live: "LIVE",
|
||||||
Subscriptions: "SUBSCRIPTIONS"
|
Subscriptions: "SUBSCRIPTIONS",
|
||||||
|
Shorts: "SHORTS"
|
||||||
},
|
},
|
||||||
Order: {
|
Order: {
|
||||||
Chronological: "CHRONOLOGICAL"
|
Chronological: "CHRONOLOGICAL"
|
||||||
|
@ -244,6 +245,7 @@ class PlatformVideo extends PlatformContent {
|
||||||
this.viewCount = obj.viewCount ?? -1; //Long
|
this.viewCount = obj.viewCount ?? -1; //Long
|
||||||
|
|
||||||
this.isLive = obj.isLive ?? false; //Boolean
|
this.isLive = obj.isLive ?? false; //Boolean
|
||||||
|
this.isShort = !!obj.isShort ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class PlatformVideoDetails extends PlatformVideo {
|
class PlatformVideoDetails extends PlatformVideo {
|
||||||
|
@ -260,6 +262,11 @@ 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;
|
||||||
|
|
||||||
|
if (obj.getContentRecommendations) {
|
||||||
|
this.getContentRecommendations = obj.getContentRecommendations
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -226,6 +226,25 @@ fun Long.toHumanTime(isMs: Boolean): String {
|
||||||
else
|
else
|
||||||
return "${prefix}${minsStr}:${secsStr}"
|
return "${prefix}${minsStr}:${secsStr}"
|
||||||
}
|
}
|
||||||
|
fun Long.toHumanDuration(isMs: Boolean): String {
|
||||||
|
var scaler = 1;
|
||||||
|
if(isMs)
|
||||||
|
scaler = 1000;
|
||||||
|
val v = Math.abs(this);
|
||||||
|
val hours = Math.max(v/(secondsInHour*scaler), 0);
|
||||||
|
val mins = Math.max((v % (secondsInHour*scaler)) / (secondsInMinute * scaler), 0);
|
||||||
|
val minsStr = mins.toString();
|
||||||
|
val seconds = Math.max(((v % (secondsInHour*scaler)) % (secondsInMinute * scaler))/scaler, 0);
|
||||||
|
val secsStr = seconds.toString().padStart(2, '0');
|
||||||
|
val prefix = if (this < 0) { "-" } else { "" };
|
||||||
|
|
||||||
|
return listOf(
|
||||||
|
if(hours > 0) "${hours}h" else null,
|
||||||
|
if(mins > 0) "${mins}m" else null ,
|
||||||
|
if(seconds > 0) "${seconds}s" else null
|
||||||
|
).filterNotNull().joinToString(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//TODO: Determine if below stuff should have its own proper class, seems a bit too complex for a utility method
|
//TODO: Determine if below stuff should have its own proper class, seems a bit too complex for a utility method
|
||||||
fun String.fixHtmlWhitespace(): Spanned {
|
fun String.fixHtmlWhitespace(): Spanned {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.Inet4Address
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
|
@ -215,9 +216,14 @@ private fun ByteArray.toInetAddress(): InetAddress {
|
||||||
return InetAddress.getByAddress(this);
|
return InetAddress.getByAddress(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
fun getConnectedSocket(attemptAddresses: 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,33 +40,25 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -33,10 +33,10 @@ fun Boolean?.toYesNo(): String {
|
||||||
fun InetAddress?.toUrlAddress(): String {
|
fun InetAddress?.toUrlAddress(): String {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is Inet6Address -> {
|
is Inet6Address -> {
|
||||||
"[${toString()}]"
|
"[${hostAddress}]"
|
||||||
}
|
}
|
||||||
is Inet4Address -> {
|
is Inet4Address -> {
|
||||||
toString()
|
hostAddress
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
throw Exception("Invalid address type")
|
throw Exception("Invalid address type")
|
||||||
|
|
|
@ -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, 5)
|
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 3)
|
||||||
@DropdownFieldOptionsId(R.array.feed_style)
|
@DropdownFieldOptionsId(R.array.feed_style)
|
||||||
var homeFeedStyle: Int = 1;
|
var homeFeedStyle: Int = 1;
|
||||||
|
|
||||||
|
@ -216,6 +216,11 @@ 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;
|
||||||
|
|
||||||
|
@ -254,6 +259,9 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
||||||
var progressBar: Boolean = true;
|
var progressBar: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.hide_hidden_from_search, FieldForm.TOGGLE, R.string.hide_hidden_from_search_description, 7)
|
||||||
|
var hidefromSearch: Boolean = false;
|
||||||
|
|
||||||
|
|
||||||
fun getSearchFeedStyle(): FeedStyle {
|
fun getSearchFeedStyle(): FeedStyle {
|
||||||
if(searchFeedStyle == 0)
|
if(searchFeedStyle == 0)
|
||||||
|
@ -291,6 +299,9 @@ 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;
|
||||||
|
|
||||||
|
@ -353,7 +364,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
var playback = PlaybackSettings();
|
var playback = PlaybackSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class PlaybackSettings {
|
class PlaybackSettings {
|
||||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
|
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -2)
|
||||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||||
var primaryLanguage: Int = 0;
|
var primaryLanguage: Int = 0;
|
||||||
|
|
||||||
|
@ -377,6 +388,8 @@ 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];
|
||||||
|
|
||||||
|
@ -412,15 +425,13 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
var preferredPreviewQuality: Int = 5;
|
var preferredPreviewQuality: Int = 5;
|
||||||
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
||||||
var simplifySources: Boolean = true;
|
var simplifySources: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
|
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
|
||||||
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
|
||||||
var autoRotate: Int = 2;
|
|
||||||
|
|
||||||
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
|
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
|
||||||
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
||||||
var backgroundPlay: Int = 2;
|
var backgroundPlay: Int = 2;
|
||||||
|
|
||||||
|
@ -572,10 +583,15 @@ 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, 1)
|
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
|
||||||
@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)
|
||||||
|
@ -643,6 +659,9 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
@Serializable
|
@Serializable
|
||||||
class Plugins {
|
class Plugins {
|
||||||
|
|
||||||
|
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
|
||||||
|
var checkDisabledPluginsForUpdates: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
||||||
var clearCookiesOnLogout: Boolean = true;
|
var clearCookiesOnLogout: Boolean = true;
|
||||||
|
|
||||||
|
@ -863,11 +882,13 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
class Other {
|
class Other {
|
||||||
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
|
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
|
||||||
var playlistDeleteConfirmation: Boolean = true;
|
var playlistDeleteConfirmation: Boolean = true;
|
||||||
|
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
|
||||||
|
var playlistAllowDups: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 3)
|
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4)
|
||||||
var polycentricEnabled: Boolean = true;
|
var polycentricEnabled: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 4)
|
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5)
|
||||||
var polycentricLocalCache: Boolean = true;
|
var polycentricLocalCache: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -915,6 +936,15 @@ 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)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import androidx.work.Data
|
import androidx.work.Data
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
|
|
@ -5,6 +5,7 @@ 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
|
||||||
|
@ -199,16 +200,21 @@ 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) {
|
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
||||||
|
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;
|
||||||
|
@ -275,6 +281,7 @@ 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) {
|
||||||
|
@ -368,8 +375,8 @@ class UIDialogs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
fun showChangelogDialog(context: Context, lastVersion: Int, changelogs: Map<Int, String>? = null) {
|
||||||
val dialog = ChangelogDialog(context);
|
val dialog = ChangelogDialog(context, changelogs);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
|
|
|
@ -79,6 +79,36 @@ class UISlideOverlays {
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showQueueOptionsOverlay(context: Context, container: ViewGroup) {
|
||||||
|
UISlideOverlays.showOverlay(container, "Queue options", null, {
|
||||||
|
|
||||||
|
}, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, {
|
||||||
|
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
||||||
|
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
||||||
|
addPlaylistOverlay.onOK.subscribe {
|
||||||
|
val text = nameInput.text.trim()
|
||||||
|
if (text.isBlank()) {
|
||||||
|
return@subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
addPlaylistOverlay.hide();
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
StatePlayer.instance.saveQueueAsPlaylist(text);
|
||||||
|
UIDialogs.appToast("Playlist [${text}] created");
|
||||||
|
};
|
||||||
|
|
||||||
|
addPlaylistOverlay.onCancel.subscribe {
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
addPlaylistOverlay.show();
|
||||||
|
nameInput.activate();
|
||||||
|
}, false));
|
||||||
|
}
|
||||||
|
|
||||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
|
|
||||||
|
@ -335,7 +365,9 @@ class UISlideOverlays {
|
||||||
call = {
|
call = {
|
||||||
selectedVideoVariant = it
|
selectedVideoVariant = it
|
||||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
if (audioButtons.isEmpty()){
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
invokeParent = false
|
invokeParent = false
|
||||||
))
|
))
|
||||||
|
@ -370,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, sourceUrl), null)
|
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, 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 {
|
||||||
|
@ -417,7 +449,7 @@ class UISlideOverlays {
|
||||||
}
|
}
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
||||||
listOf(listOf(SlideUpMenuItem(
|
listOf((if (audioSources != null) listOf(SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_movie,
|
R.drawable.ic_movie,
|
||||||
container.context.getString(R.string.none),
|
container.context.getString(R.string.none),
|
||||||
|
@ -430,7 +462,7 @@ class UISlideOverlays {
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
},
|
},
|
||||||
invokeParent = false
|
invokeParent = false
|
||||||
)) +
|
)) else listOf()) +
|
||||||
videoSources
|
videoSources
|
||||||
.filter { it.isDownloadable() }
|
.filter { it.isDownloadable() }
|
||||||
.map {
|
.map {
|
||||||
|
@ -895,7 +927,8 @@ class UISlideOverlays {
|
||||||
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
@ -906,7 +939,7 @@ class UISlideOverlays {
|
||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||||
(listOf(
|
(listOf(
|
||||||
if(!isLimited)
|
if(!isLimited && !video.isLive)
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_download,
|
R.drawable.ic_download,
|
||||||
|
@ -991,7 +1024,8 @@ class UISlideOverlays {
|
||||||
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -1018,7 +1052,8 @@ class UISlideOverlays {
|
||||||
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
@ -1040,7 +1075,10 @@ 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 = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
|
call = {
|
||||||
|
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
||||||
|
UIDialogs.appToast("Added to watch later", false);
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1067,7 +1105,8 @@ class UISlideOverlays {
|
||||||
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -1109,7 +1148,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 }
|
||||||
|
@ -1117,7 +1156,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
|
||||||
))
|
))
|
||||||
|
@ -1125,29 +1164,40 @@ 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,
|
||||||
options.map { SlideUpMenuItem(
|
listOf(
|
||||||
|
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);
|
||||||
} else
|
if(overlayItem != null) {
|
||||||
|
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,14 +27,17 @@ 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.File
|
import java.io.ByteArrayOutputStream
|
||||||
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.nio.ByteOrder
|
import java.security.SecureRandom
|
||||||
|
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 {
|
||||||
|
@ -279,3 +282,46 @@ 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,11 +10,13 @@ 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;
|
||||||
|
@ -54,6 +56,7 @@ 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);
|
||||||
|
@ -81,7 +84,25 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonURL.onClick.subscribe {
|
_buttonURL.onClick.subscribe {
|
||||||
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
val nameInput = SlideUpMenuTextInput(this, "ex. https://yourplugin.com/config.json");
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.media.AudioManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
|
@ -72,6 +74,7 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImportCache
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||||
|
import com.futo.platformplayer.receivers.MediaButtonReceiver
|
||||||
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.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
|
@ -107,6 +110,7 @@ import java.util.LinkedList
|
||||||
import java.util.Queue
|
import java.util.Queue
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
//TODO: Move to dimensions
|
//TODO: Move to dimensions
|
||||||
|
@ -1277,7 +1281,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
if (toast.long)
|
if (toast.long)
|
||||||
delay(5000);
|
delay(5000);
|
||||||
else
|
else
|
||||||
delay(3000);
|
delay(2500);
|
||||||
}
|
}
|
||||||
Logger.i(TAG, "Ending appToast loop");
|
Logger.i(TAG, "Ending appToast loop");
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
|
|
@ -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(PolycentricCache.SERVER);
|
processHandle.addServer(ApiMethods.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(PolycentricCache.SERVER);
|
processHandle.fullyBackfillClient(ApiMethods.SERVER);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
||||||
finish();
|
finish();
|
||||||
|
|
|
@ -21,10 +21,8 @@ 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
|
||||||
|
@ -32,8 +30,10 @@ 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(PolycentricCache.SERVER)
|
processHandle.fullyBackfillClient(ApiMethods.SERVER)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateUI();
|
updateUI();
|
||||||
|
|
|
@ -100,8 +100,10 @@ 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)
|
|
||||||
.setName(publicKey)
|
syncDeviceView.setLinkType(session?.linkType ?: LinkType.None)
|
||||||
|
.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) { session, complete, message ->
|
StateSync.instance.connect(deviceInfo) { complete, message ->
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
if (complete) {
|
if (complete != null && complete) {
|
||||||
_layoutPairingSuccess.visibility = View.VISIBLE
|
_layoutPairingSuccess.visibility = View.VISIBLE
|
||||||
_layoutPairing.visibility = View.GONE
|
_layoutPairing.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
|
@ -122,7 +122,11 @@ class SyncPairActivity : AppCompatActivity() {
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_layoutPairingError.visibility = View.VISIBLE
|
_layoutPairingError.visibility = View.VISIBLE
|
||||||
_textError.text = e.message
|
if(e.message == "Failed to connect") {
|
||||||
|
_textError.text = "Failed to connect.\n\nThis may be due to not being on the same network, due to firewall, or vpn.\nSync currently operates only over local direct connections."
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_textError.text = e.message
|
||||||
_layoutPairing.visibility = View.GONE
|
_layoutPairing.visibility = View.GONE
|
||||||
Logger.e(TAG, "Failed to pair", e)
|
Logger.e(TAG, "Failed to pair", e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,7 @@ class SyncShowPairingCodeActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val ips = getIPs()
|
val ips = getIPs()
|
||||||
val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT)
|
val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT, StateSync.instance.pairingCode)
|
||||||
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}"
|
||||||
|
|
|
@ -5,6 +5,8 @@ import com.futo.platformplayer.SettingsDev
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.ensureNotMainThread
|
import com.futo.platformplayer.ensureNotMainThread
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
@ -63,7 +65,7 @@ open class ManagedHttpClient {
|
||||||
|
|
||||||
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
|
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
|
||||||
_builderTemplate = builder;
|
_builderTemplate = builder;
|
||||||
if(SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
|
if(FragmentedStorage.isInitialized && StateApp.instance.isMainActive && SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
|
||||||
trustAllCertificates(builder);
|
trustAllCertificates(builder);
|
||||||
client = builder.addNetworkInterceptor { chain ->
|
client = builder.addNetworkInterceptor { chain ->
|
||||||
val request = beforeRequest(chain.request());
|
val request = beforeRequest(chain.request());
|
||||||
|
|
|
@ -73,7 +73,7 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
|
||||||
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
||||||
|
|
||||||
current += bytesToSend.toLong()
|
current += bytesToSend.toLong()
|
||||||
if (current >= end) {
|
if (current > end) {
|
||||||
Logger.i(TAG, "Expected amount of bytes sent")
|
Logger.i(TAG, "Expected amount of bytes sent")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
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
|
||||||
|
@ -66,6 +67,11 @@ 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,7 +2,10 @@ 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
|
||||||
|
|
||||||
|
@ -42,4 +45,21 @@ 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
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -30,6 +30,7 @@ class ResultCapabilities(
|
||||||
const val TYPE_POSTS = "POSTS";
|
const val TYPE_POSTS = "POSTS";
|
||||||
const val TYPE_MIXED = "MIXED";
|
const val TYPE_MIXED = "MIXED";
|
||||||
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
|
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
|
||||||
|
const val TYPE_SHORTS = "SHORTS";
|
||||||
|
|
||||||
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ enum class ContentType(val value: Int) {
|
||||||
URL(9),
|
URL(9),
|
||||||
|
|
||||||
NESTED_VIDEO(11),
|
NESTED_VIDEO(11),
|
||||||
|
CHANNEL(60),
|
||||||
|
|
||||||
LOCKED(70),
|
LOCKED(70),
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ 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 LocalVideoMuxedSourceDescriptor(
|
class DownloadedVideoMuxedSourceDescriptor(
|
||||||
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,7 +13,8 @@ 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;
|
||||||
|
|
||||||
|
@ -36,7 +37,9 @@ 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,6 +27,7 @@ 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,4 +8,5 @@ interface IAudioSource {
|
||||||
val language : String;
|
val language : String;
|
||||||
val duration : Long?;
|
val duration : Long?;
|
||||||
val priority: Boolean;
|
val priority: Boolean;
|
||||||
|
val original: Boolean;
|
||||||
}
|
}
|
|
@ -15,6 +15,7 @@ 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;
|
||||||
|
@ -33,13 +34,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource {
|
fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
|
||||||
return LocalAudioSource(
|
return LocalAudioSource(
|
||||||
source.name,
|
source.name,
|
||||||
path,
|
path,
|
||||||
fileSize,
|
fileSize,
|
||||||
source.bitrate,
|
source.bitrate,
|
||||||
source.container,
|
overrideContainer ?: source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.language
|
source.language
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,7 +35,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSource(source: IVideoSource, path: String, fileSize: Long): LocalVideoSource {
|
fun fromSource(source: IVideoSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalVideoSource {
|
||||||
return LocalVideoSource(
|
return LocalVideoSource(
|
||||||
source.name,
|
source.name,
|
||||||
path,
|
path,
|
||||||
|
@ -43,7 +43,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
||||||
source.width,
|
source.width,
|
||||||
source.height,
|
source.height,
|
||||||
source.duration,
|
source.duration,
|
||||||
source.container,
|
overrideContainer ?: source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.bitrate?:0
|
source.bitrate?:0
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,4 +13,6 @@ interface IPlatformVideo : IPlatformContent {
|
||||||
val viewCount: Long;
|
val viewCount: Long;
|
||||||
|
|
||||||
val isLive : Boolean;
|
val isLive : Boolean;
|
||||||
|
|
||||||
|
val isShort: Boolean;
|
||||||
}
|
}
|
|
@ -10,23 +10,26 @@ 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 = "",
|
||||||
|
|
||||||
override val duration: Long,
|
override val duration: Long,
|
||||||
override val viewCount: Long,
|
override val viewCount: Long,
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ -43,6 +46,7 @@ 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,
|
||||||
|
|
|
@ -38,7 +38,8 @@ open class SerializedPlatformVideoDetails(
|
||||||
override val video: ISerializedVideoSourceDescriptor,
|
override val video: ISerializedVideoSourceDescriptor,
|
||||||
override val preview: ISerializedVideoSourceDescriptor?,
|
override val preview: ISerializedVideoSourceDescriptor?,
|
||||||
|
|
||||||
override val subtitles: List<SubtitleRawSource> = listOf()
|
override val subtitles: List<SubtitleRawSource> = listOf(),
|
||||||
|
override val isShort: Boolean = false
|
||||||
) : IPlatformVideo, IPlatformVideoDetails {
|
) : IPlatformVideo, IPlatformVideoDetails {
|
||||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ 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
|
||||||
|
@ -31,6 +32,7 @@ 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
|
||||||
|
@ -361,6 +363,10 @@ 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)")
|
||||||
|
|
|
@ -33,6 +33,7 @@ class SourcePluginConfig(
|
||||||
override val allowEval: Boolean = false,
|
override val allowEval: Boolean = false,
|
||||||
override val allowUrls: List<String> = listOf(),
|
override val allowUrls: List<String> = listOf(),
|
||||||
override val packages: List<String> = listOf(),
|
override val packages: List<String> = listOf(),
|
||||||
|
override val packagesOptional: List<String> = listOf(),
|
||||||
|
|
||||||
val settings: List<Setting> = listOf(),
|
val settings: List<Setting> = listOf(),
|
||||||
|
|
||||||
|
@ -52,6 +53,7 @@ class SourcePluginConfig(
|
||||||
var allowAllHttpHeaderAccess: Boolean = false,
|
var allowAllHttpHeaderAccess: Boolean = false,
|
||||||
var maxDownloadParallelism: Int = 0,
|
var maxDownloadParallelism: Int = 0,
|
||||||
var reduceFunctionsInLimitedVersion: Boolean = false,
|
var reduceFunctionsInLimitedVersion: Boolean = false,
|
||||||
|
var changelog: HashMap<String, List<String>>? = null
|
||||||
) : IV8PluginConfig {
|
) : IV8PluginConfig {
|
||||||
|
|
||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
|
@ -101,6 +103,10 @@ class SourcePluginConfig(
|
||||||
if(!packages.contains(pack))
|
if(!packages.contains(pack))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
for(pack in newConfig.packagesOptional) {
|
||||||
|
if(!packagesOptional.contains(pack))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
//Developer Submit Url should be same or empty
|
//Developer Submit Url should be same or empty
|
||||||
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
||||||
return false;
|
return false;
|
||||||
|
@ -129,7 +135,7 @@ class SourcePluginConfig(
|
||||||
|
|
||||||
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
|
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
|
||||||
if (currentlyInstalledPlugin != null) {
|
if (currentlyInstalledPlugin != null) {
|
||||||
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey) {
|
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey && !currentlyInstalledPlugin.config.scriptPublicKey.isNullOrEmpty()) {
|
||||||
list.add(Pair(
|
list.add(Pair(
|
||||||
"Different Author",
|
"Different Author",
|
||||||
"This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor."));
|
"This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor."));
|
||||||
|
@ -178,6 +184,19 @@ class SourcePluginConfig(
|
||||||
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
|
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getChangelogString(version: String): String?{
|
||||||
|
if(changelog == null || !changelog!!.containsKey(version))
|
||||||
|
return null;
|
||||||
|
val changelog = changelog!![version]!!;
|
||||||
|
if(changelog.size > 1) {
|
||||||
|
return "Changelog (${version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
|
||||||
|
}
|
||||||
|
else if(changelog.size == 1) {
|
||||||
|
return "Changelog (${version})\n" + changelog[0].trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig {
|
fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig {
|
||||||
val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json);
|
val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
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
|
||||||
|
@ -26,6 +27,7 @@ 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,7 +5,6 @@ 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.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
val datetimeInt = _content.getOrDefault<Int>(config, "datetime", contextName, null)?.toLong();
|
||||||
if(datetimeInt == 0.toLong())
|
if(datetimeInt == null || 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,6 +2,7 @@ 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
|
||||||
|
@ -15,4 +16,14 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
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.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||||
|
@ -17,6 +18,7 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||||
final override val viewCount: Long;
|
final override val viewCount: Long;
|
||||||
|
|
||||||
final override val isLive: Boolean;
|
final override val isLive: Boolean;
|
||||||
|
final override val isShort: Boolean;
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||||
val contextName = "PlatformVideo";
|
val contextName = "PlatformVideo";
|
||||||
|
@ -26,5 +28,6 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||||
duration = _content.getOrThrow<Int>(config, "duration", contextName).toLong();
|
duration = _content.getOrThrow<Int>(config, "duration", contextName).toLong();
|
||||||
viewCount = _content.getOrThrow(config, "viewCount", contextName);
|
viewCount = _content.getOrThrow(config, "viewCount", contextName);
|
||||||
isLive = _content.getOrThrow(config, "isLive", contextName);
|
isLive = _content.getOrThrow(config, "isLive", contextName);
|
||||||
|
isShort = _content.getOrDefault(config, "isShort", contextName, false) ?: false;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -21,6 +21,8 @@ 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;
|
||||||
|
@ -35,6 +37,7 @@ 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 {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
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.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
@ -14,13 +16,14 @@ import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.others.Language
|
import com.futo.platformplayer.others.Language
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource {
|
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||||
override val container : String = "application/dash+xml";
|
override val container : String;
|
||||||
override val name : String;
|
override val name : String;
|
||||||
override val codec: String;
|
override val codec: String;
|
||||||
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;
|
||||||
|
|
||||||
|
@ -29,17 +32,21 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||||
|
|
||||||
override val hasGenerate: Boolean;
|
override val hasGenerate: Boolean;
|
||||||
|
|
||||||
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||||
val contextName = "DashRawSource";
|
val contextName = "DashRawSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
|
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||||
manifest = _obj.getOrThrow(config, "manifest", contextName);
|
manifest = _obj.getOrThrow(config, "manifest", contextName);
|
||||||
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
||||||
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,15 +57,28 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||||
throw IllegalStateException("Source object already closed");
|
throw IllegalStateException("Source object already closed");
|
||||||
|
|
||||||
val plugin = _plugin.getUnderlyingPlugin();
|
val plugin = _plugin.getUnderlyingPlugin();
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
if(_plugin is DevJSClient)
|
if(_plugin is DevJSClient)
|
||||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(result != null){
|
||||||
|
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
|
||||||
|
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,6 +6,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
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.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
@ -20,8 +22,8 @@ interface IJSDashManifestRawSource {
|
||||||
var manifest: String?;
|
var manifest: String?;
|
||||||
fun generate(): String?;
|
fun generate(): String?;
|
||||||
}
|
}
|
||||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource {
|
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||||
override val container : String = "application/dash+xml";
|
override val container : String;
|
||||||
override val name : String;
|
override val name : String;
|
||||||
override val width: Int;
|
override val width: Int;
|
||||||
override val height: Int;
|
override val height: Int;
|
||||||
|
@ -36,11 +38,14 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
||||||
override val hasGenerate: Boolean;
|
override val hasGenerate: Boolean;
|
||||||
val canMerge: Boolean;
|
val canMerge: Boolean;
|
||||||
|
|
||||||
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||||
val contextName = "DashRawSource";
|
val contextName = "DashRawSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
|
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||||
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
||||||
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
||||||
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
||||||
|
@ -57,17 +62,30 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
||||||
return manifest;
|
return manifest;
|
||||||
if(_obj.isClosed)
|
if(_obj.isClosed)
|
||||||
throw IllegalStateException("Source object already closed");
|
throw IllegalStateException("Source object already closed");
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
if(_plugin is DevJSClient) {
|
if(_plugin is DevJSClient) {
|
||||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(result != null){
|
||||||
|
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
|
||||||
|
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,12 +118,16 @@ class JSDashManifestMergingRawSource(
|
||||||
if(videoDash == null) return null;
|
if(videoDash == null) return null;
|
||||||
|
|
||||||
//TODO: Temporary simple solution..make more reliable version
|
//TODO: Temporary simple solution..make more reliable version
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
||||||
if(audioAdaptationSet != null) {
|
if(audioAdaptationSet != null) {
|
||||||
return videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
result = videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return videoDash;
|
result = videoDash;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -21,6 +21,7 @@ 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";
|
||||||
|
@ -32,6 +33,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.futo.platformplayer.api.media.platforms.local
|
||||||
|
|
||||||
|
class LocalClient {
|
||||||
|
//TODO
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
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);
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
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> {
|
interface IRefreshPager<T>: IPager<T> {
|
||||||
val onPagerChanged: Event1<IPager<T>>;
|
val onPagerChanged: Event1<IPager<T>>;
|
||||||
val onPagerError: Event1<Throwable>;
|
val onPagerError: Event1<Throwable>;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -9,8 +11,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
|
||||||
*/
|
*/
|
||||||
class ReusablePager<T>: INestedPager<T>, IPager<T> {
|
open class ReusablePager<T>: INestedPager<T>, IReusablePager<T> {
|
||||||
private val _pager: IPager<T>;
|
protected var _pager: IPager<T>;
|
||||||
val previousResults = arrayListOf<T>();
|
val previousResults = arrayListOf<T>();
|
||||||
|
|
||||||
constructor(subPager: IPager<T>) {
|
constructor(subPager: IPager<T>) {
|
||||||
|
@ -44,7 +46,7 @@ class ReusablePager<T>: INestedPager<T>, IPager<T> {
|
||||||
return previousResults;
|
return previousResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getWindow(): Window<T> {
|
override fun getWindow(): Window<T> {
|
||||||
return Window(this);
|
return Window(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,4 +97,118 @@ class ReusablePager<T>: INestedPager<T>, IPager<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,6 +3,7 @@ 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
|
||||||
|
@ -32,6 +33,7 @@ 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
|
||||||
|
@ -90,7 +92,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
|
||||||
private var _lastPongTime = -1L
|
@Volatile private var _lastPongTime = System.currentTimeMillis()
|
||||||
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() {
|
||||||
|
@ -324,9 +326,9 @@ class FCastCastingDevice : CastingDevice {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
localAddress = _socket?.localAddress;
|
localAddress = _socket?.localAddress
|
||||||
connectionState = CastConnectionState.CONNECTED;
|
_lastPongTime = System.currentTimeMillis()
|
||||||
_lastPongTime = -1L
|
connectionState = CastConnectionState.CONNECTED
|
||||||
|
|
||||||
val buffer = ByteArray(4096);
|
val buffer = ByteArray(4096);
|
||||||
|
|
||||||
|
@ -402,36 +404,32 @@ 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) {
|
||||||
try {
|
if (connectionState == CastConnectionState.CONNECTED) {
|
||||||
send(Opcode.Ping)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.w(TAG, "Failed to send ping.")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_socket?.close()
|
send(Opcode.Ping)
|
||||||
_inputStream?.close()
|
if (System.currentTimeMillis() - _lastPongTime > 15000) {
|
||||||
_outputStream?.close()
|
Logger.w(TAG, "Closing socket due to last pong time being larger than 15 seconds.")
|
||||||
|
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 close socket.", e)
|
Log.w(TAG, "Failed to send ping.")
|
||||||
|
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,5 +1,6 @@
|
||||||
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
|
||||||
|
@ -9,6 +10,7 @@ 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
|
||||||
|
@ -64,7 +66,7 @@ class StateCasting {
|
||||||
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
||||||
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
||||||
|
|
||||||
private val _castServer = ManagedHttpServer(9999);
|
private val _castServer = ManagedHttpServer();
|
||||||
private var _started = false;
|
private var _started = false;
|
||||||
|
|
||||||
var devices: HashMap<String, CastingDevice> = hashMapOf();
|
var devices: HashMap<String, CastingDevice> = hashMapOf();
|
||||||
|
@ -239,6 +241,9 @@ 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)
|
||||||
|
@ -272,10 +277,41 @@ 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 -> UIDialogs.toast(it, "Connected to device")
|
CastConnectionState.CONNECTED -> {
|
||||||
CastConnectionState.CONNECTING -> UIDialogs.toast(it, "Connecting to device...")
|
Logger.i(TAG, "Casting connected to [${device.name}]");
|
||||||
CastConnectionState.DISCONNECTED -> UIDialogs.toast(it, "Disconnected from device")
|
UIDialogs.appToast("Connected to 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,37 +1,24 @@
|
||||||
package com.futo.platformplayer.dialogs
|
package com.futo.platformplayer.dialogs
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.app.PendingIntent.*
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.method.ScrollingMovementMethod
|
import android.text.method.ScrollingMovementMethod
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
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.lifecycle.lifecycleScope
|
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.receivers.InstallReceiver
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
|
||||||
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.StateUpdate
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
class ChangelogDialog(context: Context?) : AlertDialog(context) {
|
class ChangelogDialog(context: Context?, val changelogs: Map<Int, String>? = null) : AlertDialog(context) {
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "ChangelogDialog";
|
private val TAG = "ChangelogDialog";
|
||||||
}
|
}
|
||||||
|
@ -48,7 +35,11 @@ class ChangelogDialog(context: Context?) : AlertDialog(context) {
|
||||||
private var _maxVersion: Int = 0;
|
private var _maxVersion: Int = 0;
|
||||||
private var _managedHttpClient = ManagedHttpClient();
|
private var _managedHttpClient = ManagedHttpClient();
|
||||||
|
|
||||||
private val _taskDownloadChangelog = TaskHandler<Int, String?>(StateApp.instance.scopeGetter, { version -> StateUpdate.instance.downloadChangelog(_managedHttpClient, version) })
|
private val _taskDownloadChangelog = TaskHandler<Int, String?>(StateApp.instance.scopeGetter, { version -> if(changelogs == null)
|
||||||
|
StateUpdate.instance.downloadChangelog(_managedHttpClient, version)
|
||||||
|
else
|
||||||
|
changelogs[version]
|
||||||
|
})
|
||||||
.success { setChangelog(it); }
|
.success { setChangelog(it); }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load changelog.", it);
|
Logger.w(TAG, "Failed to load changelog.", it);
|
||||||
|
@ -97,7 +88,7 @@ class ChangelogDialog(context: Context?) : AlertDialog(context) {
|
||||||
setVersion(version);
|
setVersion(version);
|
||||||
|
|
||||||
val currentVersion = BuildConfig.VERSION_CODE;
|
val currentVersion = BuildConfig.VERSION_CODE;
|
||||||
_buttonUpdate.visibility = if (currentVersion == _maxVersion) View.GONE else View.VISIBLE;
|
_buttonUpdate.visibility = if (currentVersion == _maxVersion || changelogs != null) View.GONE else View.VISIBLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setVersion(version: Int) {
|
private fun setVersion(version: Int) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
@ -21,7 +22,6 @@ 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,6 +29,7 @@ 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
|
||||||
|
@ -57,11 +58,21 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
||||||
_editComment = findViewById(R.id.edit_comment);
|
_editComment = findViewById(R.id.edit_comment);
|
||||||
_textCharacterCount = findViewById(R.id.character_count);
|
_textCharacterCount = findViewById(R.id.character_count);
|
||||||
_textCharacterCountMax = findViewById(R.id.character_count_max);
|
_textCharacterCountMax = findViewById(R.id.character_count_max);
|
||||||
|
setCanceledOnTouchOutside(false)
|
||||||
|
setOnKeyListener { _, keyCode, event ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
|
||||||
|
handleCloseAttempt()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_editComment.addTextChangedListener(object : TextWatcher {
|
_editComment.addTextChangedListener(object : TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) = Unit
|
override fun afterTextChanged(s: Editable?) = Unit
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, c: Int) {
|
||||||
|
val count = s?.length ?: 0;
|
||||||
_textCharacterCount.text = count.toString();
|
_textCharacterCount.text = count.toString();
|
||||||
|
|
||||||
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
||||||
|
@ -79,10 +90,13 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
||||||
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
||||||
|
|
||||||
_buttonCancel.setOnClickListener {
|
_buttonCancel.setOnClickListener {
|
||||||
clearFocus();
|
handleCloseAttempt()
|
||||||
dismiss();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setOnCancelListener {
|
||||||
|
handleCloseAttempt()
|
||||||
|
}
|
||||||
|
|
||||||
_buttonCreate.setOnClickListener {
|
_buttonCreate.setOnClickListener {
|
||||||
clearFocus();
|
clearFocus();
|
||||||
|
|
||||||
|
@ -134,6 +148,22 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
||||||
focus();
|
focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleCloseAttempt() {
|
||||||
|
if (_editComment.text.isEmpty()) {
|
||||||
|
clearFocus()
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
UIDialogs.showConfirmationDialog(
|
||||||
|
context,
|
||||||
|
context.resources.getString(R.string.not_empty_close),
|
||||||
|
action = {
|
||||||
|
clearFocus()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun focus() {
|
private fun focus() {
|
||||||
_editComment.requestFocus();
|
_editComment.requestFocus();
|
||||||
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -54,6 +54,7 @@ class PluginUpdateDialog : AlertDialog {
|
||||||
private lateinit var _buttonInstall: LinearLayout;
|
private lateinit var _buttonInstall: LinearLayout;
|
||||||
|
|
||||||
private lateinit var _textPlugin: TextView;
|
private lateinit var _textPlugin: TextView;
|
||||||
|
private lateinit var _textChangelog: TextView;
|
||||||
private lateinit var _textProgres: TextView;
|
private lateinit var _textProgres: TextView;
|
||||||
private lateinit var _textError: TextView;
|
private lateinit var _textError: TextView;
|
||||||
private lateinit var _textResult: TextView;
|
private lateinit var _textResult: TextView;
|
||||||
|
@ -94,6 +95,7 @@ class PluginUpdateDialog : AlertDialog {
|
||||||
_buttonInstall = findViewById(R.id.button_install);
|
_buttonInstall = findViewById(R.id.button_install);
|
||||||
|
|
||||||
_textPlugin = findViewById(R.id.text_plugin);
|
_textPlugin = findViewById(R.id.text_plugin);
|
||||||
|
_textChangelog = findViewById(R.id.text_changelog);
|
||||||
_textProgres = findViewById(R.id.text_progress);
|
_textProgres = findViewById(R.id.text_progress);
|
||||||
_textError = findViewById(R.id.text_error);
|
_textError = findViewById(R.id.text_error);
|
||||||
_textResult = findViewById(R.id.text_result);
|
_textResult = findViewById(R.id.text_result);
|
||||||
|
@ -110,6 +112,27 @@ class PluginUpdateDialog : AlertDialog {
|
||||||
_updateSpinner = findViewById(R.id.update_spinner);
|
_updateSpinner = findViewById(R.id.update_spinner);
|
||||||
_iconPlugin = findViewById(R.id.icon_plugin);
|
_iconPlugin = findViewById(R.id.icon_plugin);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var changelogVersion = _newConfig.version.toString();
|
||||||
|
if (_newConfig.changelog != null && _newConfig.changelog?.containsKey(changelogVersion) == true) {
|
||||||
|
_textChangelog.movementMethod = ScrollingMovementMethod();
|
||||||
|
val changelog = _newConfig.changelog!![changelogVersion]!!;
|
||||||
|
if(changelog.size > 1) {
|
||||||
|
_textChangelog.text = "Changelog (${_newConfig.version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
|
||||||
|
}
|
||||||
|
else if(changelog.size == 1) {
|
||||||
|
_textChangelog.text = "Changelog (${_newConfig.version})\n" + changelog[0].trim();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_textChangelog.visibility = View.GONE;
|
||||||
|
} else
|
||||||
|
_textChangelog.visibility = View.GONE;
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
_textChangelog.visibility = View.GONE;
|
||||||
|
Logger.e(TAG, "Invalid changelog? ", ex);
|
||||||
|
}
|
||||||
|
|
||||||
_buttonCancel1.setOnClickListener {
|
_buttonCancel1.setOnClickListener {
|
||||||
dismiss();
|
dismiss();
|
||||||
};
|
};
|
||||||
|
|
|
@ -100,6 +100,7 @@ class VideoDownload {
|
||||||
|
|
||||||
var requireVideoSource: Boolean = false;
|
var requireVideoSource: Boolean = false;
|
||||||
var requireAudioSource: Boolean = false;
|
var requireAudioSource: Boolean = false;
|
||||||
|
var requiredCheck: Boolean = false;
|
||||||
|
|
||||||
@Contextual
|
@Contextual
|
||||||
@Transient
|
@Transient
|
||||||
|
@ -140,11 +141,17 @@ class VideoDownload {
|
||||||
var error: String? = null;
|
var error: String? = null;
|
||||||
|
|
||||||
var videoFilePath: String? = null;
|
var videoFilePath: String? = null;
|
||||||
var videoFileName: String? = null;
|
var videoFileNameBase: String? = null;
|
||||||
|
var videoFileNameExt: String? = null;
|
||||||
|
val videoFileName: String? get() = if(videoFileNameBase.isNullOrEmpty()) null else videoFileNameBase + (if(!videoFileNameExt.isNullOrEmpty()) "." + videoFileNameExt else "");
|
||||||
|
var videoOverrideContainer: String? = null;
|
||||||
var videoFileSize: Long? = null;
|
var videoFileSize: Long? = null;
|
||||||
|
|
||||||
var audioFilePath: String? = null;
|
var audioFilePath: String? = null;
|
||||||
var audioFileName: String? = null;
|
var audioFileNameBase: String? = null;
|
||||||
|
var audioFileNameExt: String? = null;
|
||||||
|
val audioFileName: String? get() = if(audioFileNameBase.isNullOrEmpty()) null else audioFileNameBase + (if(!audioFileNameExt.isNullOrEmpty()) "." + audioFileNameExt else "");
|
||||||
|
var audioOverrideContainer: String? = null;
|
||||||
var audioFileSize: Long? = null;
|
var audioFileSize: Long? = null;
|
||||||
|
|
||||||
var subtitleFilePath: String? = null;
|
var subtitleFilePath: String? = null;
|
||||||
|
@ -164,7 +171,7 @@ class VideoDownload {
|
||||||
onStateChanged.emit(newState);
|
onStateChanged.emit(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(video: IPlatformVideo, targetPixelCount: Long? = null, targetBitrate: Long? = null) {
|
constructor(video: IPlatformVideo, targetPixelCount: Long? = null, targetBitrate: Long? = null, optionalSources: Boolean = false) {
|
||||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||||
this.videoSource = null;
|
this.videoSource = null;
|
||||||
this.audioSource = null;
|
this.audioSource = null;
|
||||||
|
@ -175,8 +182,9 @@ class VideoDownload {
|
||||||
this.requiresLiveVideoSource = false;
|
this.requiresLiveVideoSource = false;
|
||||||
this.requiresLiveAudioSource = false;
|
this.requiresLiveAudioSource = false;
|
||||||
this.targetVideoName = videoSource?.name;
|
this.targetVideoName = videoSource?.name;
|
||||||
this.requireVideoSource = targetPixelCount != null
|
this.requireVideoSource = targetPixelCount != null;
|
||||||
this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
||||||
|
this.requiredCheck = optionalSources;
|
||||||
}
|
}
|
||||||
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
||||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||||
|
@ -233,11 +241,13 @@ class VideoDownload {
|
||||||
videoDetails = null;
|
videoDetails = null;
|
||||||
videoSource = null;
|
videoSource = null;
|
||||||
videoSourceLive = null;
|
videoSourceLive = null;
|
||||||
|
videoOverrideContainer = null;
|
||||||
}
|
}
|
||||||
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
||||||
videoDetails = null;
|
videoDetails = null;
|
||||||
audioSource = null;
|
audioSource = null;
|
||||||
videoSourceLive = null;
|
videoSourceLive = null;
|
||||||
|
audioOverrideContainer = null;
|
||||||
}
|
}
|
||||||
if(video == null && videoDetails == null)
|
if(video == null && videoDetails == null)
|
||||||
throw IllegalStateException("Missing information for download to complete");
|
throw IllegalStateException("Missing information for download to complete");
|
||||||
|
@ -250,6 +260,30 @@ class VideoDownload {
|
||||||
if(original !is IPlatformVideoDetails)
|
if(original !is IPlatformVideoDetails)
|
||||||
throw IllegalStateException("Original content is not media?");
|
throw IllegalStateException("Original content is not media?");
|
||||||
|
|
||||||
|
if(requiredCheck) {
|
||||||
|
if(original.video is VideoUnMuxedSourceDescriptor) {
|
||||||
|
if(requireVideoSource) {
|
||||||
|
if((original.video as VideoUnMuxedSourceDescriptor).audioSources.any() && !original.video.videoSources.any()) {
|
||||||
|
requireVideoSource = false;
|
||||||
|
targetPixelCount = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(requireAudioSource) {
|
||||||
|
if(!(original.video as VideoUnMuxedSourceDescriptor).audioSources.any() && original.video.videoSources.any()) {
|
||||||
|
requireAudioSource = false;
|
||||||
|
targetBitrate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(requireAudioSource) {
|
||||||
|
requireAudioSource = false;
|
||||||
|
targetBitrate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requiredCheck = false;
|
||||||
|
}
|
||||||
|
|
||||||
if(original.video.hasAnySource() && !original.isDownloadable()) {
|
if(original.video.hasAnySource() && !original.isDownloadable()) {
|
||||||
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
|
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
|
||||||
throw DownloadException("Unsupported video for downloading", false);
|
throw DownloadException("Unsupported video for downloading", false);
|
||||||
|
@ -284,6 +318,10 @@ class VideoDownload {
|
||||||
if(vsource == null)
|
if(vsource == null)
|
||||||
vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
||||||
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
||||||
|
if(vsource is JSSource) {
|
||||||
|
this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || vsource.hasRequestExecutor;
|
||||||
|
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (vsource is JSDashManifestRawSource && vsource.hasGenerate);
|
||||||
|
}
|
||||||
|
|
||||||
if(vsource == null) {
|
if(vsource == null) {
|
||||||
videoSource = null;
|
videoSource = null;
|
||||||
|
@ -335,6 +373,12 @@ class VideoDownload {
|
||||||
asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
||||||
?: if(videoSource != null ) null
|
?: if(videoSource != null ) null
|
||||||
else throw DownloadException("Could not find a valid video or audio source for download")
|
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||||
|
|
||||||
|
if(asource is JSSource) {
|
||||||
|
this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor;
|
||||||
|
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
||||||
|
}
|
||||||
|
|
||||||
if(asource == null) {
|
if(asource == null) {
|
||||||
audioSource = null;
|
audioSource = null;
|
||||||
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
|
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
|
||||||
|
@ -374,11 +418,13 @@ class VideoDownload {
|
||||||
else audioSource;
|
else audioSource;
|
||||||
|
|
||||||
if(actualVideoSource != null) {
|
if(actualVideoSource != null) {
|
||||||
videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource!!.container)}".sanitizeFileName();
|
videoFileNameBase = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}]".sanitizeFileName();
|
||||||
|
videoFileNameExt = videoContainerToExtension(actualVideoSource!!.container);
|
||||||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(actualAudioSource != null) {
|
if(actualAudioSource != null) {
|
||||||
audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName();
|
audioFileNameBase = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}]".sanitizeFileName();
|
||||||
|
audioFileNameExt = audioContainerToExtension(actualAudioSource!!.container);
|
||||||
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(subtitleSource != null) {
|
if(subtitleSource != null) {
|
||||||
|
@ -1026,8 +1072,8 @@ class VideoDownload {
|
||||||
fun complete() {
|
fun complete() {
|
||||||
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
||||||
val existing = StateDownloads.instance.getCachedVideo(id);
|
val existing = StateDownloads.instance.getCachedVideo(id);
|
||||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0) };
|
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0, videoOverrideContainer) };
|
||||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0) };
|
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0, audioOverrideContainer) };
|
||||||
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
||||||
|
|
||||||
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
||||||
|
@ -1056,7 +1102,7 @@ class VideoDownload {
|
||||||
StateDownloads.instance.updateCachedVideo(existing);
|
StateDownloads.instance.updateCachedVideo(existing);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
val newVideo = VideoLocal(videoDetails!!);
|
val newVideo = VideoLocal(videoDetails!!, OffsetDateTime.now());
|
||||||
if(localVideoSource != null)
|
if(localVideoSource != null)
|
||||||
newVideo.videoSource.add(localVideoSource);
|
newVideo.videoSource.add(localVideoSource);
|
||||||
if(localAudioSource != null)
|
if(localAudioSource != null)
|
||||||
|
@ -1108,7 +1154,7 @@ class VideoDownload {
|
||||||
else if (container.contains("video/x-matroska"))
|
else if (container.contains("video/x-matroska"))
|
||||||
return "mkv";
|
return "mkv";
|
||||||
else
|
else
|
||||||
return "video";
|
return "video";//throw IllegalStateException("Unknown container: " + container)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun audioContainerToExtension(container: String): String {
|
fun audioContainerToExtension(container: String): String {
|
||||||
|
@ -1119,11 +1165,11 @@ class VideoDownload {
|
||||||
else if (container.contains("audio/mp3"))
|
else if (container.contains("audio/mp3"))
|
||||||
return "mp3";
|
return "mp3";
|
||||||
else if (container.contains("audio/webm"))
|
else if (container.contains("audio/webm"))
|
||||||
return "webma";
|
return "webm";
|
||||||
else if (container == "application/vnd.apple.mpegurl")
|
else if (container == "application/vnd.apple.mpegurl")
|
||||||
return "mp4";
|
return "mp4a";
|
||||||
else
|
else
|
||||||
return "audio";
|
return "audio";// throw IllegalStateException("Unknown container: " + container)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subtitleContainerToExtension(container: String?): String {
|
fun subtitleContainerToExtension(container: String?): String {
|
||||||
|
|
|
@ -39,7 +39,7 @@ class VideoExport {
|
||||||
this.subtitleSource = subtitleSource;
|
this.subtitleSource = subtitleSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
|
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null, documentRoot: DocumentFile? = null): DocumentFile = coroutineScope {
|
||||||
val v = videoSource;
|
val v = videoSource;
|
||||||
val a = audioSource;
|
val a = audioSource;
|
||||||
val s = subtitleSource;
|
val s = subtitleSource;
|
||||||
|
@ -50,7 +50,7 @@ class VideoExport {
|
||||||
if (s != null) sourceCount++;
|
if (s != null) sourceCount++;
|
||||||
|
|
||||||
val outputFile: DocumentFile?;
|
val outputFile: DocumentFile?;
|
||||||
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
val downloadRoot = documentRoot ?: StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
||||||
if (sourceCount > 1) {
|
if (sourceCount > 1) {
|
||||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
||||||
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
||||||
|
|
|
@ -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.LocalVideoMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor
|
||||||
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
|
||||||
|
@ -23,6 +23,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.platformplayer.stores.v2.IStoreItem
|
import com.futo.platformplayer.stores.v2.IStoreItem
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
@ -56,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
|
||||||
LocalVideoMuxedSourceDescriptor(this);
|
DownloadedVideoMuxedSourceDescriptor(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;
|
||||||
|
@ -70,14 +71,21 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
||||||
|
|
||||||
override val isLive: Boolean get() = videoSerialized.isLive;
|
override val isLive: Boolean get() = videoSerialized.isLive;
|
||||||
|
|
||||||
|
override val isShort: Boolean get() = videoSerialized.isShort;
|
||||||
|
|
||||||
//TODO: Offline subtitles
|
//TODO: Offline subtitles
|
||||||
override val subtitles: List<ISubtitleSource> = listOf();
|
override val subtitles: List<ISubtitleSource> = listOf();
|
||||||
|
|
||||||
constructor(video: SerializedPlatformVideoDetails) {
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
var downloadDate: OffsetDateTime? = null;
|
||||||
|
|
||||||
|
constructor(video: SerializedPlatformVideoDetails, downloadDate: OffsetDateTime? = null) {
|
||||||
this.videoSerialized = video;
|
this.videoSerialized = video;
|
||||||
|
this.downloadDate = downloadDate;
|
||||||
}
|
}
|
||||||
constructor(video: IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) {
|
constructor(video: IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) {
|
||||||
this.videoSerialized = SerializedPlatformVideoDetails.fromVideo(video, subtitleSources);
|
this.videoSerialized = SerializedPlatformVideoDetails.fromVideo(video, subtitleSources);
|
||||||
|
downloadDate = OffsetDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||||
|
|
|
@ -32,6 +32,7 @@ import com.futo.platformplayer.engine.internal.V8Converter
|
||||||
import com.futo.platformplayer.engine.packages.PackageBridge
|
import com.futo.platformplayer.engine.packages.PackageBridge
|
||||||
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
||||||
import com.futo.platformplayer.engine.packages.PackageHttp
|
import com.futo.platformplayer.engine.packages.PackageHttp
|
||||||
|
import com.futo.platformplayer.engine.packages.PackageJSDOM
|
||||||
import com.futo.platformplayer.engine.packages.PackageUtilities
|
import com.futo.platformplayer.engine.packages.PackageUtilities
|
||||||
import com.futo.platformplayer.engine.packages.V8Package
|
import com.futo.platformplayer.engine.packages.V8Package
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
@ -94,7 +95,11 @@ class V8Plugin {
|
||||||
withDependency(PackageBridge(this, config));
|
withDependency(PackageBridge(this, config));
|
||||||
|
|
||||||
for(pack in config.packages)
|
for(pack in config.packages)
|
||||||
withDependency(getPackage(pack));
|
withDependency(getPackage(pack)!!);
|
||||||
|
for(pack in config.packagesOptional)
|
||||||
|
getPackage(pack, true)?.let {
|
||||||
|
withDependency(it);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeAllowDevSubmit(isAllowed: Boolean) {
|
fun changeAllowDevSubmit(isAllowed: Boolean) {
|
||||||
|
@ -254,13 +259,14 @@ class V8Plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPackage(packageName: String): V8Package {
|
private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? {
|
||||||
//TODO: Auto get all package types?
|
//TODO: Auto get all package types?
|
||||||
return when(packageName) {
|
return when(packageName) {
|
||||||
"DOMParser" -> PackageDOMParser(this)
|
"DOMParser" -> PackageDOMParser(this)
|
||||||
"Http" -> PackageHttp(this, config)
|
"Http" -> PackageHttp(this, config)
|
||||||
"Utilities" -> PackageUtilities(this, config)
|
"Utilities" -> PackageUtilities(this, config)
|
||||||
else -> throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
"JSDOM" -> PackageJSDOM(this, config)
|
||||||
|
else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ interface IV8PluginConfig {
|
||||||
val allowEval: Boolean;
|
val allowEval: Boolean;
|
||||||
val allowUrls: List<String>;
|
val allowUrls: List<String>;
|
||||||
val packages: List<String>;
|
val packages: List<String>;
|
||||||
|
val packagesOptional: List<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
|
@ -13,17 +14,20 @@ class V8PluginConfig : IV8PluginConfig {
|
||||||
override val allowEval: Boolean;
|
override val allowEval: Boolean;
|
||||||
override val allowUrls: List<String>;
|
override val allowUrls: List<String>;
|
||||||
override val packages: List<String>;
|
override val packages: List<String>;
|
||||||
|
override val packagesOptional: List<String>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
name = "Unknown";
|
name = "Unknown";
|
||||||
allowEval = false;
|
allowEval = false;
|
||||||
allowUrls = listOf();
|
allowUrls = listOf();
|
||||||
packages = listOf();
|
packages = listOf();
|
||||||
|
packagesOptional = listOf();
|
||||||
}
|
}
|
||||||
constructor(name: String, allowEval: Boolean, allowUrls: List<String>, packages: List<String> = listOf()) {
|
constructor(name: String, allowEval: Boolean, allowUrls: List<String>, packages: List<String> = listOf(), packagesOptional: List<String> = listOf()) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.allowEval = allowEval;
|
this.allowEval = allowEval;
|
||||||
this.allowUrls = allowUrls;
|
this.allowUrls = allowUrls;
|
||||||
this.packages = packages;
|
this.packages = packages;
|
||||||
|
this.packagesOptional = packagesOptional;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,12 @@
|
||||||
package com.futo.platformplayer.engine.packages
|
package com.futo.platformplayer.engine.packages
|
||||||
|
|
||||||
|
import android.media.MediaCodec
|
||||||
|
import android.media.MediaCodecList
|
||||||
import com.caoccao.javet.annotations.V8Function
|
import com.caoccao.javet.annotations.V8Function
|
||||||
import com.caoccao.javet.annotations.V8Property
|
import com.caoccao.javet.annotations.V8Property
|
||||||
|
import com.caoccao.javet.utils.JavetResourceUtils
|
||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueFunction
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
@ -16,6 +20,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
@ -37,6 +42,18 @@ class PackageBridge : V8Package {
|
||||||
_config = config;
|
_config = config;
|
||||||
_client = plugin.httpClient;
|
_client = plugin.httpClient;
|
||||||
_clientAuth = plugin.httpClientAuth;
|
_clientAuth = plugin.httpClientAuth;
|
||||||
|
|
||||||
|
withScript("""
|
||||||
|
function setTimeout(func, delay) {
|
||||||
|
let args = Array.prototype.slice.call(arguments, 2);
|
||||||
|
return bridge.setTimeout(func.bind(globalThis, ...args), delay || 0);
|
||||||
|
}
|
||||||
|
""".trimIndent());
|
||||||
|
withScript("""
|
||||||
|
function clearTimeout(id) {
|
||||||
|
bridge.clearTimeout(id);
|
||||||
|
}
|
||||||
|
""".trimIndent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,6 +79,48 @@ class PackageBridge : V8Package {
|
||||||
value.close();
|
value.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var timeoutCounter = 0;
|
||||||
|
var timeoutMap = HashSet<Int>();
|
||||||
|
@V8Function
|
||||||
|
fun setTimeout(func: V8ValueFunction, timeout: Long): Int {
|
||||||
|
val id = timeoutCounter++;
|
||||||
|
|
||||||
|
val funcClone = func.toClone<V8ValueFunction>()
|
||||||
|
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
delay(timeout);
|
||||||
|
synchronized(timeoutMap) {
|
||||||
|
if(!timeoutMap.contains(id)) {
|
||||||
|
JavetResourceUtils.safeClose(funcClone);
|
||||||
|
return@launch;
|
||||||
|
}
|
||||||
|
timeoutMap.remove(id);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
_plugin.whenNotBusy {
|
||||||
|
funcClone.callVoid(null, arrayOf<Any>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed timeout callback", ex);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
JavetResourceUtils.safeClose(funcClone);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
synchronized(timeoutMap) {
|
||||||
|
timeoutMap.add(id);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun clearTimeout(id: Int) {
|
||||||
|
synchronized(timeoutMap) {
|
||||||
|
if(timeoutMap.contains(id))
|
||||||
|
timeoutMap.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun toast(str: String) {
|
fun toast(str: String) {
|
||||||
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
|
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
|
||||||
|
@ -130,7 +189,44 @@ class PackageBridge : V8Package {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun getHardwareCodecs(): List<String>{
|
||||||
|
return getSupportedHardwareMediaCodecs();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PackageBridge";
|
private const val TAG = "PackageBridge";
|
||||||
|
|
||||||
|
private var _mediaCodecList: MutableList<String> = mutableListOf();
|
||||||
|
private var _mediaCodecListHardware: MutableList<String> = mutableListOf();
|
||||||
|
|
||||||
|
fun getSupportedMediaCodecs(): List<String>{
|
||||||
|
synchronized(_mediaCodecList) {
|
||||||
|
if(_mediaCodecList.size <= 0)
|
||||||
|
updateMediaCodecList();
|
||||||
|
return _mediaCodecList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getSupportedHardwareMediaCodecs(): List<String>{
|
||||||
|
synchronized(_mediaCodecList) {
|
||||||
|
if(_mediaCodecList.size <= 0)
|
||||||
|
updateMediaCodecList();
|
||||||
|
return _mediaCodecListHardware;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun updateMediaCodecList() {
|
||||||
|
_mediaCodecList.clear();
|
||||||
|
_mediaCodecListHardware.clear();
|
||||||
|
for(codec in MediaCodecList(MediaCodecList.ALL_CODECS).codecInfos) {
|
||||||
|
if(!codec.isEncoder) {
|
||||||
|
_mediaCodecList.add(codec.canonicalName);
|
||||||
|
if (codec.isHardwareAccelerated)
|
||||||
|
_mediaCodecListHardware.add(codec.canonicalName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -21,9 +21,13 @@ import com.futo.platformplayer.engine.internal.IV8Convertable
|
||||||
import com.futo.platformplayer.engine.internal.V8BindObject
|
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||||
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 kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
|
import java.util.concurrent.ForkJoinPool
|
||||||
|
import java.util.concurrent.ForkJoinTask
|
||||||
|
import kotlin.concurrent.thread
|
||||||
import kotlin.streams.asSequence
|
import kotlin.streams.asSequence
|
||||||
|
|
||||||
class PackageHttp: V8Package {
|
class PackageHttp: V8Package {
|
||||||
|
@ -42,6 +46,9 @@ class PackageHttp: V8Package {
|
||||||
override val name: String get() = "Http";
|
override val name: String get() = "Http";
|
||||||
override val variableName: String get() = "http";
|
override val variableName: String get() = "http";
|
||||||
|
|
||||||
|
private var _batchPoolLock: Any = Any();
|
||||||
|
private var _batchPool: ForkJoinPool? = null;
|
||||||
|
|
||||||
|
|
||||||
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
|
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
|
||||||
_config = config;
|
_config = config;
|
||||||
|
@ -51,6 +58,37 @@ class PackageHttp: V8Package {
|
||||||
_packageClientAuth = PackageHttpClient(this, _clientAuth);
|
_packageClientAuth = PackageHttpClient(this, _clientAuth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Automatically adjusting threadpool dedicated per PackageHttp for batch requests.
|
||||||
|
*/
|
||||||
|
private fun <T, R> autoParallelPool(data: List<T>, parallelism: Int, handle: (T)->R): List<Pair<R?, Throwable?>> {
|
||||||
|
synchronized(_batchPoolLock) {
|
||||||
|
val threadsToUse = if (parallelism <= 0) data.size else Math.min(parallelism, data.size);
|
||||||
|
if(_batchPool == null)
|
||||||
|
_batchPool = ForkJoinPool(threadsToUse);
|
||||||
|
var pool = _batchPool ?: return listOf();
|
||||||
|
if(pool.poolSize < threadsToUse) { //Resize pool
|
||||||
|
pool.shutdown();
|
||||||
|
_batchPool = ForkJoinPool(threadsToUse);
|
||||||
|
pool = _batchPool ?: return listOf();
|
||||||
|
}
|
||||||
|
|
||||||
|
val resultTasks = mutableListOf<ForkJoinTask<Pair<R?, Throwable?>>>();
|
||||||
|
for(item in data){
|
||||||
|
resultTasks.add(pool.submit<Pair<R?, Throwable?>> {
|
||||||
|
try {
|
||||||
|
return@submit Pair<R?, Throwable?>(handle(item), null);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
return@submit Pair<R?, Throwable?>(null, ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return resultTasks.map { it.join() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun newClient(withAuth: Boolean): PackageHttpClient {
|
fun newClient(withAuth: Boolean): PackageHttpClient {
|
||||||
val httpClient = if(withAuth) _clientAuth.clone() else _client.clone();
|
val httpClient = if(withAuth) _clientAuth.clone() else _client.clone();
|
||||||
|
@ -176,8 +214,6 @@ class PackageHttp: V8Package {
|
||||||
obj.set("url", url);
|
obj.set("url", url);
|
||||||
obj.set("code", code);
|
obj.set("code", code);
|
||||||
if(body != null) {
|
if(body != null) {
|
||||||
val buffer = runtime.createV8ValueArrayBuffer(body.size);
|
|
||||||
buffer.fromBytes(body);
|
|
||||||
obj.set("body", body);
|
obj.set("body", body);
|
||||||
}
|
}
|
||||||
obj.set("headers", headers);
|
obj.set("headers", headers);
|
||||||
|
@ -236,16 +272,19 @@ class PackageHttp: V8Package {
|
||||||
//Finalizer
|
//Finalizer
|
||||||
@V8Function
|
@V8Function
|
||||||
fun execute(): List<IBridgeHttpResponse?> {
|
fun execute(): List<IBridgeHttpResponse?> {
|
||||||
return _reqs.parallelStream().map {
|
return _package.autoParallelPool(_reqs, -1) {
|
||||||
if(it.second.method == "DUMMY")
|
if(it.second.method == "DUMMY")
|
||||||
return@map null;
|
return@autoParallelPool null;
|
||||||
if(it.second.body != null)
|
if(it.second.body != null)
|
||||||
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType);
|
return@autoParallelPool it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType);
|
||||||
else
|
else
|
||||||
return@map it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
|
return@autoParallelPool it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
|
||||||
}
|
}.map {
|
||||||
.asSequence()
|
if(it.second != null)
|
||||||
.toList();
|
throw it.second!!;
|
||||||
|
else
|
||||||
|
return@map it.first;
|
||||||
|
}.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,11 +478,8 @@ class PackageHttp: V8Package {
|
||||||
else {
|
else {
|
||||||
headers?.forEach { (header, values) ->
|
headers?.forEach { (header, values) ->
|
||||||
val lowerCaseHeader = header.lowercase()
|
val lowerCaseHeader = header.lowercase()
|
||||||
if(lowerCaseHeader == "set-cookie") {
|
if(lowerCaseHeader == "set-cookie" && !values.any { it.lowercase().contains("httponly") })
|
||||||
result[lowerCaseHeader] = values.filter{
|
result[lowerCaseHeader] = values;
|
||||||
!it.lowercase().contains("httponly")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
result[lowerCaseHeader] = values;
|
result[lowerCaseHeader] = values;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.futo.platformplayer.engine.packages
|
||||||
|
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
|
||||||
|
|
||||||
|
class PackageJSDOM : V8Package {
|
||||||
|
@Transient
|
||||||
|
private val _config: IV8PluginConfig;
|
||||||
|
|
||||||
|
override val name: String get() = "JSDOM";
|
||||||
|
override val variableName: String get() = "packageJSDOM";
|
||||||
|
|
||||||
|
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
|
||||||
|
_config = config;
|
||||||
|
plugin.withDependency(StateApp.instance.contextOrNull ?: return, "scripts/JSDOM.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -13,7 +13,6 @@ 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
|
||||||
|
@ -21,6 +20,7 @@ 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,9 +134,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!map.containsKey("Harbor"))
|
if(!map.containsKey("Harbor"))
|
||||||
this.context?.let {
|
map.set("Harbor", polycentricProfile.getHarborUrl());
|
||||||
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 ?: "")
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
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.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
|
@ -15,7 +16,6 @@ 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.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.models.video.IPlatformVideo
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
|
||||||
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
@ -29,7 +29,6 @@ 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
|
||||||
|
@ -39,12 +38,14 @@ 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
|
||||||
|
|
||||||
class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
class ChannelContentsFragment(private val subType: String? = null) : Fragment(), IChannelTabFragment {
|
||||||
private var _recyclerResults: RecyclerView? = null;
|
private var _recyclerResults: RecyclerView? = null;
|
||||||
private var _llmVideo: LinearLayoutManager? = null;
|
private var _glmVideo: GridLayoutManager? = null;
|
||||||
private var _loading = false;
|
private var _loading = false;
|
||||||
private var _pager_parent: IPager<IPlatformContent>? = null;
|
private var _pager_parent: IPager<IPlatformContent>? = null;
|
||||||
private var _pager: IPager<IPlatformContent>? = null;
|
private var _pager: IPager<IPlatformContent>? = null;
|
||||||
|
@ -72,9 +73,12 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||||
if (lastPolycentricProfile != null)
|
if (lastPolycentricProfile != null)
|
||||||
pager= StatePolycentric.instance.getChannelContent(lifecycleScope, lastPolycentricProfile);
|
pager= StatePolycentric.instance.getChannelContent(lifecycleScope, lastPolycentricProfile);
|
||||||
|
|
||||||
if(pager == null)
|
if(pager == null) {
|
||||||
pager = StatePlatform.instance.getChannelContent(channel.url);
|
if(subType != null)
|
||||||
|
pager = StatePlatform.instance.getChannelContent(channel.url, subType);
|
||||||
|
else
|
||||||
|
pager = StatePlatform.instance.getChannelContent(channel.url);
|
||||||
|
}
|
||||||
return pager;
|
return pager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +122,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||||
super.onScrolled(recyclerView, dx, dy);
|
super.onScrolled(recyclerView, dx, dy);
|
||||||
|
|
||||||
val recyclerResults = _recyclerResults ?: return;
|
val recyclerResults = _recyclerResults ?: return;
|
||||||
val llmVideo = _llmVideo ?: return;
|
val llmVideo = _glmVideo ?: return;
|
||||||
|
|
||||||
val visibleItemCount = recyclerResults.childCount;
|
val visibleItemCount = recyclerResults.childCount;
|
||||||
val firstVisibleItem = llmVideo.findFirstVisibleItemPosition();
|
val firstVisibleItem = llmVideo.findFirstVisibleItemPosition();
|
||||||
|
@ -163,9 +167,10 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||||
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
||||||
}
|
}
|
||||||
|
|
||||||
_llmVideo = LinearLayoutManager(view.context);
|
val numColumns = max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
_glmVideo = GridLayoutManager(view.context, numColumns);
|
||||||
_recyclerResults?.adapter = _adapterResults;
|
_recyclerResults?.adapter = _adapterResults;
|
||||||
_recyclerResults?.layoutManager = _llmVideo;
|
_recyclerResults?.layoutManager = _glmVideo;
|
||||||
_recyclerResults?.addOnScrollListener(_scrollListener);
|
_recyclerResults?.addOnScrollListener(_scrollListener);
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
|
@ -181,6 +186,13 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||||
_nextPageHandler.cancel();
|
_nextPageHandler.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
_glmVideo?.spanCount =
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
private fun setPager(pager: IPager<IPlatformContent>, cache: FeedFragment.ItemCache<IPlatformContent>? = null) {
|
private fun setPager(pager: IPager<IPlatformContent>, cache: FeedFragment.ItemCache<IPlatformContent>? = null) {
|
||||||
if (_pager_parent != null && _pager_parent is IRefreshPager<*>) {
|
if (_pager_parent != null && _pager_parent is IRefreshPager<*>) {
|
||||||
|
@ -358,6 +370,6 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "VideoListFragment";
|
val TAG = "VideoListFragment";
|
||||||
fun newInstance() = ChannelContentsFragment().apply { }
|
fun newInstance(subType: String? = null) = ChannelContentsFragment(subType).apply { }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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,12 +1,13 @@
|
||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
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.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
|
@ -36,10 +37,11 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
||||||
private var _recyclerResults: RecyclerView? = null
|
private var _recyclerResults: RecyclerView? = null
|
||||||
private var _llmPlaylist: LinearLayoutManager? = null
|
private var _glmPlaylist: GridLayoutManager? = null
|
||||||
private var _loading = false
|
private var _loading = false
|
||||||
private var _pagerParent: IPager<IPlatformPlaylist>? = null
|
private var _pagerParent: IPager<IPlatformPlaylist>? = null
|
||||||
private var _pager: IPager<IPlatformPlaylist>? = null
|
private var _pager: IPager<IPlatformPlaylist>? = null
|
||||||
|
@ -109,7 +111,7 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
|
||||||
val recyclerResults = _recyclerResults ?: return
|
val recyclerResults = _recyclerResults ?: return
|
||||||
val llmPlaylist = _llmPlaylist ?: return
|
val llmPlaylist = _glmPlaylist ?: return
|
||||||
|
|
||||||
val visibleItemCount = recyclerResults.childCount
|
val visibleItemCount = recyclerResults.childCount
|
||||||
val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
|
val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
|
||||||
|
@ -158,9 +160,10 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
||||||
this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
|
this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
|
||||||
}
|
}
|
||||||
|
|
||||||
_llmPlaylist = LinearLayoutManager(view.context)
|
val numColumns = max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
_glmPlaylist = GridLayoutManager(view.context, numColumns)
|
||||||
_recyclerResults?.adapter = _adapterResults
|
_recyclerResults?.adapter = _adapterResults
|
||||||
_recyclerResults?.layoutManager = _llmPlaylist
|
_recyclerResults?.layoutManager = _glmPlaylist
|
||||||
_recyclerResults?.addOnScrollListener(_scrollListener)
|
_recyclerResults?.addOnScrollListener(_scrollListener)
|
||||||
|
|
||||||
return view
|
return view
|
||||||
|
@ -176,6 +179,13 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
||||||
_nextPageHandler.cancel()
|
_nextPageHandler.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
_glmPlaylist?.spanCount =
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setPager(
|
private fun setPager(
|
||||||
pager: IPager<IPlatformPlaylist>
|
pager: IPager<IPlatformPlaylist>
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
interface IChannelTabFragment {
|
interface IChannelTabFragment {
|
||||||
fun setChannel(channel: IPlatformChannel)
|
fun setChannel(channel: IPlatformChannel)
|
||||||
|
|
|
@ -90,7 +90,7 @@ class BuyFragment : MainFragment() {
|
||||||
try {
|
try {
|
||||||
val currencies = StatePayment.instance.getAvailableCurrencies("grayjay");
|
val currencies = StatePayment.instance.getAvailableCurrencies("grayjay");
|
||||||
val prices = StatePayment.instance.getAvailableCurrencyPrices("grayjay");
|
val prices = StatePayment.instance.getAvailableCurrencyPrices("grayjay");
|
||||||
val country = StatePayment.instance.getPaymentCountryFromIP()?.let { c -> PaymentConfigurations.COUNTRIES.find { it.id.equals(c, ignoreCase = true) } };
|
val country = StatePayment.instance.getPaymentCountryFromIP(true)?.let { c -> PaymentConfigurations.COUNTRIES.find { it.id.equals(c, ignoreCase = true) } };
|
||||||
val currency = country?.let { c -> PaymentConfigurations.CURRENCIES.find { it.id == c.defaultCurrencyId && (currencies.contains(it.id)) } };
|
val currency = country?.let { c -> PaymentConfigurations.CURRENCIES.find { it.id == c.defaultCurrencyId && (currencies.contains(it.id)) } };
|
||||||
|
|
||||||
if(currency != null && prices.containsKey(currency.id)) {
|
if(currency != null && prices.containsKey(currency.id)) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import com.futo.platformplayer.UISlideOverlays
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
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.channels.IPlatformChannel
|
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.channels.SerializedChannel
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
@ -41,7 +42,6 @@ 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,29 +54,14 @@ 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.OwnedClaim
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.PublicKey
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
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
|
||||||
|
@ -143,15 +128,14 @@ class ChannelFragment : MainFragment() {
|
||||||
|
|
||||||
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
|
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>
|
private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricProfile?>
|
||||||
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 =
|
_taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>({ fragment.lifecycleScope },
|
||||||
TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>({ fragment.lifecycleScope },
|
|
||||||
{ id ->
|
{ id ->
|
||||||
return@TaskHandler PolycentricCache.instance.getProfileAsync(id)
|
return@TaskHandler ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, id.claimFieldType.toLong(), id.claimType.toLong(), id.value!!)
|
||||||
}).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)
|
||||||
}
|
}
|
||||||
|
@ -237,8 +221,8 @@ class ChannelFragment : MainFragment() {
|
||||||
}
|
}
|
||||||
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||||
if (content is IPlatformVideo) {
|
if (content is IPlatformVideo) {
|
||||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)
|
if(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 ->
|
||||||
|
@ -327,7 +311,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, parameter.url)
|
loadPolycentricProfile(parameter.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
_url = parameter.url
|
_url = parameter.url
|
||||||
|
@ -341,7 +325,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, parameter.channel.url)
|
loadPolycentricProfile(parameter.channel.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
_url = parameter.channel.url
|
_url = parameter.channel.url
|
||||||
|
@ -358,16 +342,8 @@ class ChannelFragment : MainFragment() {
|
||||||
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
|
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadPolycentricProfile(id: PlatformID, url: String) {
|
private fun loadPolycentricProfile(id: PlatformID) {
|
||||||
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true)
|
_taskLoadPolycentricProfile.run(id)
|
||||||
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) {
|
||||||
|
@ -457,6 +433,12 @@ class ChannelFragment : MainFragment() {
|
||||||
|
|
||||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons)
|
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons)
|
||||||
}
|
}
|
||||||
|
if(plugin != null && plugin.capabilities.hasGetChannelCapabilities) {
|
||||||
|
if(plugin.getChannelCapabilities()?.types?.contains(ResultCapabilities.TYPE_SHORTS) ?: false &&
|
||||||
|
!(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(ChannelTab.SHORTS.ordinal.toLong())) {
|
||||||
|
(_viewPager.adapter as ChannelViewPagerAdapter).insert(1, ChannelTab.SHORTS);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -469,8 +451,13 @@ class ChannelFragment : MainFragment() {
|
||||||
R.string.subscribers
|
R.string.subscribers
|
||||||
).lowercase() else ""
|
).lowercase() else ""
|
||||||
|
|
||||||
val supportsPlaylists =
|
var supportsPlaylists = false;
|
||||||
StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists
|
try {
|
||||||
|
supportsPlaylists = StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
//Ignore error
|
||||||
|
Logger.e(TAG, "Failed to check if supports playlists", ex);
|
||||||
|
}
|
||||||
val playlistPosition = 1
|
val playlistPosition = 1
|
||||||
if (supportsPlaylists && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
|
if (supportsPlaylists && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
|
||||||
ChannelTab.PLAYLISTS.ordinal.toLong()
|
ChannelTab.PLAYLISTS.ordinal.toLong()
|
||||||
|
@ -521,20 +508,13 @@ 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(
|
||||||
cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean
|
profile: PolycentricProfile?, 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,7 +23,6 @@ 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
|
||||||
|
@ -32,6 +31,7 @@ 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
|
||||||
|
|
|
@ -33,6 +33,7 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
import com.futo.platformplayer.withTimestamp
|
import com.futo.platformplayer.withTimestamp
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
|
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
|
||||||
private var _exoPlayer: PlayerManager? = null;
|
private var _exoPlayer: PlayerManager? = null;
|
||||||
|
@ -81,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) {
|
||||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
if(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) {
|
||||||
|
@ -168,7 +169,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||||
val glmResults =
|
val glmResults =
|
||||||
GridLayoutManager(
|
GridLayoutManager(
|
||||||
context,
|
context,
|
||||||
(resources.configuration.screenWidthDp / resources.getDimension(R.dimen.landscape_threshold)).toInt() + 1
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
);
|
);
|
||||||
return glmResults
|
return glmResults
|
||||||
}
|
}
|
||||||
|
@ -200,11 +201,12 @@ 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,7 +9,9 @@ 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
|
||||||
|
@ -17,6 +19,8 @@ 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.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -82,6 +86,7 @@ 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
|
||||||
|
@ -93,7 +98,13 @@ 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 {
|
||||||
StatePlatform.instance.searchRefresh(fragment.lifecycleScope, query, null, _sortBy, _filterValues, _enabledClientIds)
|
when (_searchType)
|
||||||
|
{
|
||||||
|
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> { }
|
||||||
|
@ -114,6 +125,7 @@ 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) {
|
||||||
|
@ -159,8 +171,14 @@ 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 {
|
||||||
navigate<VideoDetailFragment>(it);
|
val url = it;
|
||||||
|
activity?.let {
|
||||||
|
close()
|
||||||
|
if(it is MainActivity)
|
||||||
|
it.navigate(it.getFragment<VideoDetailFragment>(), url);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
setQuery(it, true);
|
setQuery(it, true);
|
||||||
|
@ -222,6 +240,12 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||||
setSortByOptions(null);
|
setSortByOptions(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
|
if(Settings.instance.search.hidefromSearch)
|
||||||
|
return super.filterResults(results.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) });
|
||||||
|
return super.filterResults(results)
|
||||||
|
}
|
||||||
|
|
||||||
override fun reload() {
|
override fun reload() {
|
||||||
loadResults();
|
loadResults();
|
||||||
}
|
}
|
||||||
|
@ -244,6 +268,15 @@ 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;
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,9 @@ import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -25,13 +27,28 @@ class CreatorsFragment : MainFragment() {
|
||||||
private var _overlayContainer: FrameLayout? = null;
|
private var _overlayContainer: FrameLayout? = null;
|
||||||
private var _containerSearch: FrameLayout? = null;
|
private var _containerSearch: FrameLayout? = null;
|
||||||
private var _editSearch: EditText? = null;
|
private var _editSearch: EditText? = null;
|
||||||
|
private var _textMeta: TextView? = null;
|
||||||
|
private var _buttonClearSearch: ImageButton? = null
|
||||||
|
|
||||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val view = inflater.inflate(R.layout.fragment_creators, container, false);
|
val view = inflater.inflate(R.layout.fragment_creators, container, false);
|
||||||
_containerSearch = view.findViewById(R.id.container_search);
|
_containerSearch = view.findViewById(R.id.container_search);
|
||||||
_editSearch = view.findViewById(R.id.edit_search);
|
val editSearch: EditText = view.findViewById(R.id.edit_search);
|
||||||
|
val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
|
||||||
|
_editSearch = editSearch
|
||||||
|
_textMeta = view.findViewById(R.id.text_meta);
|
||||||
|
_buttonClearSearch = buttonClearSearch
|
||||||
|
buttonClearSearch.setOnClickListener {
|
||||||
|
editSearch.text.clear()
|
||||||
|
editSearch.requestFocus()
|
||||||
|
_buttonClearSearch?.visibility = View.INVISIBLE;
|
||||||
|
}
|
||||||
|
|
||||||
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
|
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)) { subs ->
|
||||||
|
_textMeta?.let {
|
||||||
|
it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}";
|
||||||
|
}
|
||||||
|
};
|
||||||
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
||||||
adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } }
|
adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } }
|
||||||
|
|
||||||
|
@ -51,7 +68,12 @@ class CreatorsFragment : MainFragment() {
|
||||||
_spinnerSortBy = spinnerSortBy;
|
_spinnerSortBy = spinnerSortBy;
|
||||||
|
|
||||||
_editSearch?.addTextChangedListener {
|
_editSearch?.addTextChangedListener {
|
||||||
adapter.query = it.toString();
|
adapter.query = it.toString()
|
||||||
|
if (it?.isEmpty() == true) {
|
||||||
|
_buttonClearSearch?.visibility = View.INVISIBLE
|
||||||
|
} else {
|
||||||
|
_buttonClearSearch?.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_subscriptions);
|
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_subscriptions);
|
||||||
|
|
|
@ -4,8 +4,13 @@ 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 android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Spinner
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
@ -16,7 +21,10 @@ 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.views.AnyInsertedAdapterView
|
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||||
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
||||||
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
|
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
|
||||||
|
@ -25,6 +33,7 @@ import com.futo.platformplayer.views.items.PlaylistDownloadItem
|
||||||
import com.futo.platformplayer.views.others.ProgressBar
|
import com.futo.platformplayer.views.others.ProgressBar
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class DownloadsFragment : MainFragment() {
|
class DownloadsFragment : MainFragment() {
|
||||||
private val TAG = "DownloadsFragment";
|
private val TAG = "DownloadsFragment";
|
||||||
|
@ -92,18 +101,26 @@ class DownloadsFragment : MainFragment() {
|
||||||
|
|
||||||
private val _listDownloadedHeader: LinearLayout;
|
private val _listDownloadedHeader: LinearLayout;
|
||||||
private val _listDownloadedMeta: TextView;
|
private val _listDownloadedMeta: TextView;
|
||||||
|
private val _listDownloadSearch: EditText;
|
||||||
private val _listDownloaded: AnyInsertedAdapterView<VideoLocal, VideoDownloadViewHolder>;
|
private val _listDownloaded: AnyInsertedAdapterView<VideoLocal, VideoDownloadViewHolder>;
|
||||||
|
|
||||||
|
private var lastDownloads: List<VideoLocal>? = null;
|
||||||
|
private var ordering = FragmentedStorage.get<StringStorage>("downloads_ordering")
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
_listActiveDownloadsContainer = findViewById(R.id.downloads_active_downloads_container);
|
_listActiveDownloadsContainer = findViewById(R.id.downloads_active_downloads_container);
|
||||||
_listActiveDownloadsMeta = findViewById(R.id.downloads_active_downloads_meta);
|
_listActiveDownloadsMeta = findViewById(R.id.downloads_active_downloads_meta);
|
||||||
|
_listDownloadSearch = findViewById(R.id.downloads_search);
|
||||||
_listActiveDownloads = findViewById(R.id.downloads_active_downloads_list);
|
_listActiveDownloads = findViewById(R.id.downloads_active_downloads_list);
|
||||||
|
|
||||||
_listPlaylistsContainer = findViewById(R.id.downloads_playlist_container);
|
_listPlaylistsContainer = findViewById(R.id.downloads_playlist_container);
|
||||||
|
@ -113,6 +130,31 @@ class DownloadsFragment : MainFragment() {
|
||||||
_listDownloadedHeader = findViewById(R.id.downloads_videos_header);
|
_listDownloadedHeader = findViewById(R.id.downloads_videos_header);
|
||||||
_listDownloadedMeta = findViewById(R.id.downloads_videos_meta);
|
_listDownloadedMeta = findViewById(R.id.downloads_videos_meta);
|
||||||
|
|
||||||
|
_listDownloadSearch.addTextChangedListener {
|
||||||
|
updateContentFilters();
|
||||||
|
}
|
||||||
|
val spinnerSortBy: Spinner = findViewById(R.id.spinner_sortby);
|
||||||
|
spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.downloads_sortby_array)).also {
|
||||||
|
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
||||||
|
};
|
||||||
|
val options = listOf("nameAsc", "nameDesc", "downloadDateAsc", "downloadDateDesc", "releasedAsc", "releasedDesc");
|
||||||
|
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("downloadDateAsc")
|
||||||
|
3 -> ordering.setAndSave("downloadDateDesc")
|
||||||
|
4 -> ordering.setAndSave("releasedAsc")
|
||||||
|
5 -> ordering.setAndSave("releasedDesc")
|
||||||
|
else -> ordering.setAndSave("")
|
||||||
|
}
|
||||||
|
updateContentFilters()
|
||||||
|
}
|
||||||
|
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)) {
|
||||||
it.onClick.subscribe {
|
it.onClick.subscribe {
|
||||||
|
@ -125,7 +167,6 @@ class DownloadsFragment : MainFragment() {
|
||||||
reloadUI();
|
reloadUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun reloadUI() {
|
fun reloadUI() {
|
||||||
val usage = StateDownloads.instance.getTotalUsage(true);
|
val usage = StateDownloads.instance.getTotalUsage(true);
|
||||||
_usageUsed.text = "${usage.usage.toHumanBytesSize()} " + context.getString(R.string.used);
|
_usageUsed.text = "${usage.usage.toHumanBytesSize()} " + context.getString(R.string.used);
|
||||||
|
@ -181,10 +222,32 @@ class DownloadsFragment : MainFragment() {
|
||||||
_listDownloadedHeader.visibility = GONE;
|
_listDownloadedHeader.visibility = GONE;
|
||||||
} else {
|
} else {
|
||||||
_listDownloadedHeader.visibility = VISIBLE;
|
_listDownloadedHeader.visibility = VISIBLE;
|
||||||
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()})";
|
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()}${if(downloaded.size > 0) ", ${downloaded.sumOf { it.duration }.toHumanDuration(false)}" else ""})";
|
||||||
}
|
}
|
||||||
|
|
||||||
_listDownloaded.setData(downloaded);
|
lastDownloads = downloaded;
|
||||||
|
_listDownloaded.setData(filterDownloads(downloaded));
|
||||||
|
}
|
||||||
|
fun updateContentFilters(){
|
||||||
|
val toFilter = lastDownloads ?: return;
|
||||||
|
_listDownloaded.setData(filterDownloads(toFilter));
|
||||||
|
}
|
||||||
|
fun filterDownloads(vids: List<VideoLocal>): List<VideoLocal>{
|
||||||
|
var vidsToReturn = vids;
|
||||||
|
if(!_listDownloadSearch.text.isNullOrEmpty())
|
||||||
|
vidsToReturn = vids.filter { it.name.contains(_listDownloadSearch.text, true) || it.author.name.contains(_listDownloadSearch.text, true) };
|
||||||
|
if(!ordering.value.isNullOrEmpty()) {
|
||||||
|
vidsToReturn = when(ordering.value){
|
||||||
|
"downloadDateAsc" -> vidsToReturn.sortedBy { it.downloadDate ?: OffsetDateTime.MAX };
|
||||||
|
"downloadDateDesc" -> vidsToReturn.sortedByDescending { it.downloadDate ?: OffsetDateTime.MIN };
|
||||||
|
"nameAsc" -> vidsToReturn.sortedBy { it.name.lowercase() }
|
||||||
|
"nameDesc" -> vidsToReturn.sortedByDescending { it.name.lowercase() }
|
||||||
|
"releasedAsc" -> vidsToReturn.sortedBy { it.datetime ?: OffsetDateTime.MAX }
|
||||||
|
"releasedDesc" -> vidsToReturn.sortedByDescending { it.datetime ?: OffsetDateTime.MIN }
|
||||||
|
else -> vidsToReturn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vidsToReturn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,12 +3,15 @@ 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
|
||||||
|
@ -20,6 +23,7 @@ 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
|
||||||
|
@ -28,8 +32,11 @@ 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
|
||||||
|
|
||||||
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
|
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
|
||||||
protected val _recyclerResults: RecyclerView;
|
protected val _recyclerResults: RecyclerView;
|
||||||
|
@ -67,6 +74,7 @@ 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;
|
||||||
|
@ -181,29 +189,61 @@ 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;
|
||||||
if (firstVisibleItemPosition != RecyclerView.NO_POSITION) {
|
val lastVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition();
|
||||||
val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition)
|
val lastVisibleItemView = if(lastVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(lastVisibleItemPosition) else null;
|
||||||
val itemHeight = firstVisibleView?.height ?: 0
|
val rows = if(recyclerData.layoutManager is GridLayoutManager) Math.max(1, recyclerData.results.size / recyclerData.layoutManager.spanCount) else 1;
|
||||||
val occupiedSpace = recyclerData.results.size / recyclerData.layoutManager.spanCount * itemHeight
|
val rowsHeight = (firstVisibleItemView?.height ?: 0) * rows;
|
||||||
val recyclerViewHeight = _recyclerResults.height
|
if(lastVisibleItemView != null && lastVisibleItemPosition == (recyclerData.results.size - 1)) {
|
||||||
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage occupiedSpace=$occupiedSpace recyclerViewHeight=$recyclerViewHeight")
|
false;
|
||||||
occupiedSpace >= recyclerViewHeight
|
}
|
||||||
|
else if (firstVisibleItemView != null && height != null && rowsHeight < height) {
|
||||||
|
false;
|
||||||
} else {
|
} else {
|
||||||
false
|
true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 <= 4)
|
if(_automaticNextPageCounter < _automaticBackoff.size) {
|
||||||
loadNextPage()
|
if(_automaticNextPageCounter > 0) {
|
||||||
|
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;
|
||||||
|
@ -234,7 +274,8 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun updateSpanCount() {
|
open fun updateSpanCount() {
|
||||||
recyclerData.layoutManager.spanCount = (resources.configuration.screenWidthDp / resources.getDimension(R.dimen.landscape_threshold)).toInt() + 1
|
recyclerData.layoutManager.spanCount =
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||||
|
|
|
@ -5,28 +5,38 @@ 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
|
||||||
|
@ -38,6 +48,12 @@ 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()
|
||||||
|
@ -63,7 +79,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);
|
val view = HomeView(this, inflater, _cachedRecyclerData, _cachedLastPager);
|
||||||
_view = view;
|
_view = view;
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
@ -81,6 +97,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -90,18 +107,32 @@ 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
|
||||||
|
|
||||||
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
var lastPager: IReusablePager<IPlatformContent>? = null;
|
||||||
|
|
||||||
|
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 { loadedResult(it); }
|
.success {
|
||||||
|
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);
|
||||||
|
@ -127,6 +158,8 @@ class HomeFragment : MainFragment() {
|
||||||
}, fragment);
|
}, fragment);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
initializeToolbarContent();
|
||||||
|
|
||||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||||
showAnnouncementView()
|
showAnnouncementView()
|
||||||
}
|
}
|
||||||
|
@ -201,13 +234,119 @@ class HomeFragment : MainFragment() {
|
||||||
loadResults();
|
loadResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
private val _filterLock = Object();
|
||||||
return results.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) };
|
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()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadResults() {
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
|
return results.filter {
|
||||||
|
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) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
_taskGetPager.run(true);
|
_taskGetPager.run(withRefetch);
|
||||||
}
|
}
|
||||||
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
||||||
if (pager is EmptyPager<IPlatformContent>) {
|
if (pager is EmptyPager<IPlatformContent>) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import android.view.ViewGroup
|
||||||
import androidx.core.app.ShareCompat
|
import androidx.core.app.ShareCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
@ -78,6 +79,14 @@ class PlaylistFragment : MainFragment() {
|
||||||
val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name));
|
val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name));
|
||||||
val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, context.getString(R.string.edit_playlist), context.getString(R.string.ok), false, nameInput);
|
val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, context.getString(R.string.edit_playlist), context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
||||||
|
_buttonExport.setOnClickListener {
|
||||||
|
_playlist?.let {
|
||||||
|
val context = StateApp.instance.contextOrNull ?: return@let;
|
||||||
|
if(context is IWithResultLauncher)
|
||||||
|
StateDownloads.instance.exportPlaylist(context, it.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_buttonDownload.visibility = View.VISIBLE;
|
_buttonDownload.visibility = View.VISIBLE;
|
||||||
editPlaylistOverlay.onOK.subscribe {
|
editPlaylistOverlay.onOK.subscribe {
|
||||||
val text = nameInput.text;
|
val text = nameInput.text;
|
||||||
|
@ -146,7 +155,7 @@ class PlaylistFragment : MainFragment() {
|
||||||
setName(it.name);
|
setName(it.name);
|
||||||
//TODO: Implement support for pagination
|
//TODO: Implement support for pagination
|
||||||
setVideos(it.videos, false);
|
setVideos(it.videos, false);
|
||||||
setVideoCount(it.videos.size);
|
setMetadata(it.videos.size, it.videos.sumOf { it.duration });
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
|
@ -174,8 +183,9 @@ class PlaylistFragment : MainFragment() {
|
||||||
if (parameter != null) {
|
if (parameter != null) {
|
||||||
setName(parameter.name)
|
setName(parameter.name)
|
||||||
setVideos(parameter.videos, true)
|
setVideos(parameter.videos, true)
|
||||||
setVideoCount(parameter.videos.size)
|
setMetadata(parameter.videos.size, parameter.videos.sumOf { it.duration })
|
||||||
setButtonDownloadVisible(true)
|
setButtonDownloadVisible(true)
|
||||||
|
setButtonExportVisible(false)
|
||||||
setButtonEditVisible(true)
|
setButtonEditVisible(true)
|
||||||
|
|
||||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
||||||
|
@ -187,7 +197,7 @@ class PlaylistFragment : MainFragment() {
|
||||||
} else {
|
} else {
|
||||||
setName(null)
|
setName(null)
|
||||||
setVideos(null, false)
|
setVideos(null, false)
|
||||||
setVideoCount(-1)
|
setMetadata(-1, -1);
|
||||||
setButtonDownloadVisible(false)
|
setButtonDownloadVisible(false)
|
||||||
setButtonEditVisible(false)
|
setButtonEditVisible(false)
|
||||||
}
|
}
|
||||||
|
@ -195,7 +205,7 @@ class PlaylistFragment : MainFragment() {
|
||||||
_playlist = null
|
_playlist = null
|
||||||
_url = parameter.url
|
_url = parameter.url
|
||||||
|
|
||||||
setVideoCount(parameter.videoCount)
|
setMetadata(parameter.videoCount, -1);
|
||||||
setName(parameter.name)
|
setName(parameter.name)
|
||||||
setVideos(null, false)
|
setVideos(null, false)
|
||||||
setButtonDownloadVisible(false)
|
setButtonDownloadVisible(false)
|
||||||
|
@ -208,7 +218,7 @@ class PlaylistFragment : MainFragment() {
|
||||||
|
|
||||||
setName(null)
|
setName(null)
|
||||||
setVideos(null, false)
|
setVideos(null, false)
|
||||||
setVideoCount(-1)
|
setMetadata(-1, -1);
|
||||||
setButtonDownloadVisible(false)
|
setButtonDownloadVisible(false)
|
||||||
setButtonEditVisible(false)
|
setButtonEditVisible(false)
|
||||||
|
|
||||||
|
@ -316,6 +326,10 @@ 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,12 +6,17 @@ 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
|
||||||
|
@ -21,11 +26,15 @@ 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() {
|
||||||
|
@ -65,6 +74,7 @@ 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;
|
||||||
|
@ -72,12 +82,20 @@ 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);
|
||||||
|
|
||||||
|
@ -105,6 +123,7 @@ 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);
|
||||||
|
|
||||||
|
@ -120,6 +139,35 @@ 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) {
|
||||||
|
@ -134,10 +182,12 @@ class PlaylistsFragment : MainFragment() {
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun onShown() {
|
fun onShown() {
|
||||||
|
allPlaylists.clear();
|
||||||
playlists.clear()
|
playlists.clear()
|
||||||
playlists.addAll(
|
allPlaylists.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();
|
||||||
|
@ -157,6 +207,32 @@ 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()) {
|
||||||
|
@ -164,7 +240,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, 230.0f, resources.displayMetrics).toInt();
|
layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 315.0f, resources.displayMetrics).toInt();
|
||||||
appBar.layoutParams = layoutParams;
|
appBar.layoutParams = layoutParams;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -172,7 +248,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, 25.0f, resources.displayMetrics).toInt();
|
layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 110.0f, resources.displayMetrics).toInt();
|
||||||
appBar.layoutParams = layoutParams;
|
appBar.layoutParams = layoutParams;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,10 +33,8 @@ 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
|
||||||
|
@ -47,7 +45,6 @@ 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
|
||||||
|
@ -57,6 +54,8 @@ 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
|
||||||
|
@ -112,7 +111,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: PolycentricCache.CachedPolycentricProfile? = null;
|
private var _polycentricProfile: PolycentricProfile? = 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;
|
||||||
|
@ -169,7 +168,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, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(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!!) })
|
||||||
.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);
|
||||||
|
@ -274,7 +273,7 @@ class PostDetailFragment : MainFragment {
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonStore.setOnClickListener {
|
_buttonStore.setOnClickListener {
|
||||||
_polycentricProfile?.profile?.systemState?.store?.let {
|
_polycentricProfile?.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);
|
||||||
|
@ -334,7 +333,7 @@ class PostDetailFragment : MainFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
|
val queryReferencesResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, ref, null,null,
|
||||||
arrayListOf(
|
arrayListOf(
|
||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
||||||
ContentType.OPINION.value).setValue(
|
ContentType.OPINION.value).setValue(
|
||||||
|
@ -604,16 +603,8 @@ 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?) {
|
||||||
|
@ -639,17 +630,18 @@ class PostDetailFragment : MainFragment {
|
||||||
_repliesOverlay.cleanup();
|
_repliesOverlay.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
private fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) {
|
||||||
_polycentricProfile = cachedPolycentricProfile;
|
_polycentricProfile = polycentricProfile;
|
||||||
|
|
||||||
if (cachedPolycentricProfile?.profile == null) {
|
val pp = _polycentricProfile;
|
||||||
|
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, cachedPolycentricProfile.profile.system.toProto());
|
_creatorThumbnail.setHarborAvailable(true, animate, pp.system.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchPost() {
|
private fun fetchPost() {
|
||||||
|
|
|
@ -237,7 +237,19 @@ class SourceDetailFragment : MainFragment() {
|
||||||
BigButtonGroup(c, context.getString(R.string.update),
|
BigButtonGroup(c, context.getString(R.string.update),
|
||||||
BigButton(c, context.getString(R.string.check_for_updates), context.getString(R.string.checks_for_new_versions_of_the_source), R.drawable.ic_update) {
|
BigButton(c, context.getString(R.string.check_for_updates), context.getString(R.string.checks_for_new_versions_of_the_source), R.drawable.ic_update) {
|
||||||
checkForUpdatesSource();
|
checkForUpdatesSource();
|
||||||
}
|
},
|
||||||
|
if(config.changelog?.any() == true)
|
||||||
|
BigButton(c, context.getString(R.string.changelog), context.getString(R.string.changelog_plugin_description), R.drawable.ic_list) {
|
||||||
|
UIDialogs.showChangelogDialog(context, config.version, config.changelog!!.filterKeys { it.toIntOrNull() != null }
|
||||||
|
.mapKeys { it.key.toInt() }
|
||||||
|
.mapValues { config.getChangelogString(it.key.toString()) ?: "" });
|
||||||
|
}.apply {
|
||||||
|
this.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
null
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -256,6 +256,8 @@ 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,6 +18,7 @@ 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
|
||||||
|
@ -56,6 +57,9 @@ 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();
|
||||||
|
@ -184,8 +188,6 @@ 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;
|
||||||
|
@ -204,8 +206,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
val feed = StateSubscriptions.instance.getFeed(group?.id);
|
val feed = StateSubscriptions.instance.getFeed(group?.id);
|
||||||
|
|
||||||
val currentExs = feed?.exceptions ?: listOf();
|
val currentExs = feed?.exceptions ?: listOf();
|
||||||
if(currentExs != _lastExceptions && currentExs.any())
|
if(currentExs != _lastExceptions && currentExs.any()) {
|
||||||
handleExceptions(currentExs);
|
handleExceptions(currentExs)
|
||||||
|
feed?.exceptions = listOf()
|
||||||
|
}
|
||||||
|
|
||||||
return@TaskHandler resp;
|
return@TaskHandler resp;
|
||||||
})
|
})
|
||||||
|
@ -282,13 +286,18 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
fragment.navigate<SubscriptionGroupFragment>(g);
|
fragment.navigate<SubscriptionGroupFragment>(g);
|
||||||
};
|
};
|
||||||
|
|
||||||
synchronized(_filterLock) {
|
synchronized(fragment._filterLock) {
|
||||||
_subscriptionBar?.setToggles(
|
_subscriptionBar?.setToggles(
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.videos), _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); },
|
SubscriptionBar.Toggle(context.getString(R.string.videos), fragment._filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { view, active ->
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); },
|
toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), active); },
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); },
|
SubscriptionBar.Toggle(context.getString(R.string.posts), fragment._filterSettings.allowContentTypes.contains(ContentType.POST)) { view, active ->
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); },
|
toggleFilterContentType(ContentType.POST, active); },
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.watched), _filterSettings.allowWatched) { _filterSettings.allowWatched = it; _filterSettings.save(); loadResults(false); }
|
SubscriptionBar.Toggle(context.getString(R.string.live), fragment._filterSettings.allowLive) { view, active ->
|
||||||
|
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); }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,13 +308,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(_filterLock) {
|
synchronized(fragment._filterLock) {
|
||||||
if(!isTrue) {
|
if(!isTrue) {
|
||||||
_filterSettings.allowContentTypes.remove(contentType);
|
fragment._filterSettings.allowContentTypes.remove(contentType);
|
||||||
} else if(!_filterSettings.allowContentTypes.contains(contentType)) {
|
} else if(!fragment._filterSettings.allowContentTypes.contains(contentType)) {
|
||||||
_filterSettings.allowContentTypes.add(contentType)
|
fragment._filterSettings.allowContentTypes.add(contentType)
|
||||||
}
|
}
|
||||||
_filterSettings.save();
|
fragment._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);
|
||||||
|
@ -318,9 +327,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 = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
|
val allowedContentType = fragment._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 && !_filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration))
|
if(it is IPlatformVideo && it.duration > 0 && !fragment._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
|
||||||
|
@ -329,11 +338,11 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
|
|
||||||
|
|
||||||
if(it.datetime?.isAfter(nowSoon) == true) {
|
if(it.datetime?.isAfter(nowSoon) == true) {
|
||||||
if(!_filterSettings.allowPlanned)
|
if(!fragment._filterSettings.allowPlanned)
|
||||||
return@filter false;
|
return@filter false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_filterSettings.allowLive) { //If allowLive, always show live
|
if(fragment._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,6 +9,7 @@ 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
|
||||||
|
@ -17,6 +18,8 @@ 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);
|
||||||
|
|
||||||
|
@ -27,6 +30,7 @@ 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;
|
||||||
|
@ -48,14 +52,7 @@ 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);
|
||||||
|
@ -79,6 +76,15 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -109,25 +115,27 @@ 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 (_searchType == SearchType.CREATOR) {
|
if(it.isHttpUrl()) {
|
||||||
navigate<CreatorSearchResultsFragment>(it);
|
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
|
||||||
} else if (_searchType == SearchType.PLAYLIST) {
|
navigate<RemotePlaylistFragment>(it);
|
||||||
navigate<PlaylistSearchResultsFragment>(it);
|
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||||
} else {
|
navigate<ChannelFragment>(it);
|
||||||
if(it.isHttpUrl()) {
|
else {
|
||||||
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
|
val url = it;
|
||||||
navigate<RemotePlaylistFragment>(it);
|
activity?.let {
|
||||||
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
close()
|
||||||
navigate<ChannelFragment>(it);
|
if(it is MainActivity)
|
||||||
else
|
it.navigate(it.getFragment<VideoDetailFragment>(), url);
|
||||||
navigate<VideoDetailFragment>(it);
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, _searchType, _channelUrl));
|
||||||
};
|
};
|
||||||
|
|
||||||
onTextChange.subscribe(this) {
|
onTextChange.subscribe(this) {
|
||||||
|
@ -189,6 +197,7 @@ 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() {
|
||||||
|
|
|
@ -151,6 +151,7 @@ class TutorialFragment : MainFragment() {
|
||||||
override val rating: IRating = RatingLikes(-1)
|
override val rating: IRating = RatingLikes(-1)
|
||||||
override val viewCount: Long = -1
|
override val viewCount: Long = -1
|
||||||
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration, width, height)
|
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration, width, height)
|
||||||
|
override val isShort: Boolean = false;
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
|
||||||
return EmptyPager()
|
return EmptyPager()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.database.ContentObserver
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.OrientationEventListener
|
||||||
|
import android.view.Surface
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import android.view.WindowInsetsController
|
import android.view.WindowInsetsController
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
|
import androidx.core.view.ViewCompat.getDisplay
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
@ -28,13 +35,18 @@ import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
||||||
import kotlin.math.min
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
//region Fragment
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class VideoDetailFragment : MainFragment {
|
class VideoDetailFragment() : MainFragment() {
|
||||||
override val isMainView : Boolean = false;
|
override val isMainView: Boolean = false;
|
||||||
override val hasBottomBar: Boolean = true;
|
override val hasBottomBar: Boolean = true;
|
||||||
override val isOverlay : Boolean = true;
|
override val isOverlay: Boolean = true;
|
||||||
override val isHistory: Boolean = false;
|
override val isHistory: Boolean = false;
|
||||||
|
|
||||||
private var _isActive: Boolean = false;
|
private var _isActive: Boolean = false;
|
||||||
|
@ -76,8 +88,9 @@ class VideoDetailFragment : MainFragment {
|
||||||
private var _loadUrlOnCreate: UrlVideoWithTime? = null;
|
private var _loadUrlOnCreate: UrlVideoWithTime? = null;
|
||||||
private var _leavingPiP = false;
|
private var _leavingPiP = false;
|
||||||
|
|
||||||
//region Fragment
|
private var _landscapeOrientationListener: LandscapeOrientationListener? = null
|
||||||
constructor() : super()
|
private var _portraitOrientationListener: PortraitOrientationListener? = null
|
||||||
|
private var _autoRotateObserver: AutoRotateObserver? = null
|
||||||
|
|
||||||
fun nextVideo() {
|
fun nextVideo() {
|
||||||
_viewDetail?.nextVideo(true, true, true);
|
_viewDetail?.nextVideo(true, true, true);
|
||||||
|
@ -88,23 +101,27 @@ class VideoDetailFragment : MainFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isSmallWindow(): Boolean {
|
private fun isSmallWindow(): Boolean {
|
||||||
return min(
|
return resources.configuration.smallestScreenWidthDp < resources.getInteger(R.integer.column_width_dp) * 2
|
||||||
resources.configuration.screenWidthDp,
|
}
|
||||||
resources.configuration.screenHeightDp
|
|
||||||
) < resources.getDimension(R.dimen.landscape_threshold)
|
private fun isAutoRotateEnabled(): Boolean {
|
||||||
|
return android.provider.Settings.System.getInt(
|
||||||
|
context?.contentResolver,
|
||||||
|
android.provider.Settings.System.ACCELEROMETER_ROTATION, 0
|
||||||
|
) == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: false
|
val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: false
|
||||||
|
|
||||||
val isSmallWindow = isSmallWindow()
|
val isSmallWindow = isSmallWindow()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isSmallWindow
|
isSmallWindow
|
||||||
&& newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
|
&& newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
&& !isFullscreen
|
&& !isFullscreen
|
||||||
|
&& !isInPictureInPicture
|
||||||
&& state == State.MAXIMIZED
|
&& state == State.MAXIMIZED
|
||||||
) {
|
) {
|
||||||
_viewDetail?.setFullscreen(true)
|
_viewDetail?.setFullscreen(true)
|
||||||
|
@ -141,49 +158,93 @@ class VideoDetailFragment : MainFragment {
|
||||||
) {
|
) {
|
||||||
_viewDetail?.setFullscreen(true)
|
_viewDetail?.setFullscreen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateOrientation()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SourceLockedOrientationActivity")
|
|
||||||
fun updateOrientation() {
|
fun updateOrientation() {
|
||||||
val a = activity ?: return
|
val a = activity ?: return
|
||||||
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait
|
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait
|
||||||
val isReversePortraitAllowed = Settings.instance.playback.reversePortrait
|
val isReversePortraitAllowed = Settings.instance.playback.reversePortrait
|
||||||
val rotationLock = StatePlayer.instance.rotationLock
|
val rotationLock = StatePlayer.instance.rotationLock
|
||||||
|
val alwaysAllowReverseLandscapeAutoRotate = Settings.instance.playback.alwaysAllowReverseLandscapeAutoRotate
|
||||||
|
|
||||||
val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: false
|
val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: true
|
||||||
|
|
||||||
val isSmallWindow = isSmallWindow()
|
val isSmallWindow = isSmallWindow()
|
||||||
|
val autoRotateEnabled = isAutoRotateEnabled()
|
||||||
|
|
||||||
// For small windows if the device isn't landscape right now and full screen portrait isn't allowed then we should force landscape
|
// For small windows if the device isn't landscape right now and full screen portrait isn't allowed then we should force landscape
|
||||||
if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT && !rotationLock && isLandscapeVideo) {
|
if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT && !rotationLock && isLandscapeVideo) {
|
||||||
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
if (alwaysAllowReverseLandscapeAutoRotate){
|
||||||
|
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||||
|
} else {
|
||||||
|
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
||||||
|
}
|
||||||
|
if (autoRotateEnabled
|
||||||
|
) {
|
||||||
|
// start listening for the device to rotate to landscape
|
||||||
|
// at which point we'll be able to set requestedOrientation to back to UNSPECIFIED
|
||||||
|
_landscapeOrientationListener?.enableListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For small windows if always all reverse landscape then we'll lock the orientation to landscape when system auto-rotate is off to make sure that locking
|
||||||
|
// and unlockiung in the player settings keep orientation in landscape
|
||||||
|
else if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && alwaysAllowReverseLandscapeAutoRotate && !rotationLock && isLandscapeVideo && !autoRotateEnabled) {
|
||||||
|
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||||
}
|
}
|
||||||
// For small windows if the device isn't in a portrait orientation and we're in the maximized state then we should force portrait
|
// For small windows if the device isn't in a portrait orientation and we're in the maximized state then we should force portrait
|
||||||
|
// only do this if auto-rotate is on portrait is forced when leaving full screen for autorotate off
|
||||||
else if (isSmallWindow && !isMinimizingFromFullScreen && !isFullscreen && state == State.MAXIMIZED && resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
else if (isSmallWindow && !isMinimizingFromFullScreen && !isFullscreen && state == State.MAXIMIZED && resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
|
@SuppressLint("SourceLockedOrientationActivity")
|
||||||
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||||
|
if (autoRotateEnabled
|
||||||
|
) {
|
||||||
|
// start listening for the device to rotate to portrait
|
||||||
|
// at which point we'll be able to set requestedOrientation to back to UNSPECIFIED
|
||||||
|
_portraitOrientationListener?.enableListener()
|
||||||
|
}
|
||||||
} else if (rotationLock) {
|
} else if (rotationLock) {
|
||||||
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
_portraitOrientationListener?.disableListener()
|
||||||
|
_landscapeOrientationListener?.disableListener()
|
||||||
|
val display = getDisplay(_viewDetail!!)
|
||||||
|
val rotation = display!!.rotation
|
||||||
|
val orientation = resources.configuration.orientation
|
||||||
|
|
||||||
|
a.requestedOrientation = when (orientation) {
|
||||||
|
Configuration.ORIENTATION_PORTRAIT -> {
|
||||||
|
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
|
||||||
|
if (rotation == Surface.ROTATION_0) {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||||
|
if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
|
||||||
|
if (rotation == Surface.ROTATION_90) {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
when (Settings.instance.playback.autoRotate) {
|
_portraitOrientationListener?.disableListener()
|
||||||
0 -> {
|
_landscapeOrientationListener?.disableListener()
|
||||||
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
a.requestedOrientation = if (isReversePortraitAllowed) {
|
||||||
}
|
ActivityInfo.SCREEN_ORIENTATION_FULL_USER
|
||||||
|
} else {
|
||||||
1 -> {
|
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
a.requestedOrientation = if (isReversePortraitAllowed) {
|
|
||||||
ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
|
|
||||||
} else {
|
|
||||||
ActivityInfo.SCREEN_ORIENTATION_SENSOR
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
2 -> {
|
|
||||||
a.requestedOrientation = if (isReversePortraitAllowed) {
|
|
||||||
ActivityInfo.SCREEN_ORIENTATION_FULL_USER
|
|
||||||
} else {
|
|
||||||
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -354,6 +415,30 @@ class VideoDetailFragment : MainFragment {
|
||||||
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
|
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
|
||||||
updateOrientation()
|
updateOrientation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val delayBeforeRemoveRotationLock = 800L
|
||||||
|
|
||||||
|
_landscapeOrientationListener = LandscapeOrientationListener(requireContext())
|
||||||
|
{
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
// delay to make sure that the system auto rotate updates
|
||||||
|
delay(delayBeforeRemoveRotationLock)
|
||||||
|
updateOrientation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_portraitOrientationListener = PortraitOrientationListener(requireContext())
|
||||||
|
{
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
// delay to make sure that the system auto rotate updates
|
||||||
|
delay(delayBeforeRemoveRotationLock)
|
||||||
|
updateOrientation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_autoRotateObserver = AutoRotateObserver(requireContext(), Handler(Looper.getMainLooper())) {
|
||||||
|
updateOrientation()
|
||||||
|
}
|
||||||
|
_autoRotateObserver?.startObserving()
|
||||||
|
|
||||||
return _view!!;
|
return _view!!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -455,6 +540,10 @@ class VideoDetailFragment : MainFragment {
|
||||||
SettingsActivity.settingsActivityClosed.remove(this)
|
SettingsActivity.settingsActivityClosed.remove(this)
|
||||||
StatePlayer.instance.onRotationLockChanged.remove(this)
|
StatePlayer.instance.onRotationLockChanged.remove(this)
|
||||||
|
|
||||||
|
_landscapeOrientationListener?.disableListener()
|
||||||
|
_portraitOrientationListener?.disableListener()
|
||||||
|
_autoRotateObserver?.stopObserving()
|
||||||
|
|
||||||
_viewDetail?.let {
|
_viewDetail?.let {
|
||||||
_viewDetail = null;
|
_viewDetail = null;
|
||||||
it.onDestroy();
|
it.onDestroy();
|
||||||
|
@ -526,6 +615,11 @@ class VideoDetailFragment : MainFragment {
|
||||||
showSystemUI()
|
showSystemUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// temporarily force the device to portrait if auto-rotate is disabled to prevent landscape when exiting full screen on a small device
|
||||||
|
// @SuppressLint("SourceLockedOrientationActivity")
|
||||||
|
// if (!isFullscreen && isSmallWindow() && !isAutoRotateEnabled() && !isMinimizingFromFullScreen) {
|
||||||
|
// activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||||
|
// }
|
||||||
updateOrientation();
|
updateOrientation();
|
||||||
_view?.allowMotion = !fullscreen;
|
_view?.allowMotion = !fullscreen;
|
||||||
}
|
}
|
||||||
|
@ -547,4 +641,88 @@ class VideoDetailFragment : MainFragment {
|
||||||
//region View
|
//region View
|
||||||
//TODO: Determine if encapsulated would be readable enough
|
//TODO: Determine if encapsulated would be readable enough
|
||||||
//endregion
|
//endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LandscapeOrientationListener(
|
||||||
|
context: Context,
|
||||||
|
private val onLandscapeDetected: () -> Unit
|
||||||
|
) : OrientationEventListener(context) {
|
||||||
|
|
||||||
|
private var isListening = false
|
||||||
|
|
||||||
|
override fun onOrientationChanged(orientation: Int) {
|
||||||
|
if (!isListening) return
|
||||||
|
|
||||||
|
if (orientation in 60..120 || orientation in 240..300) {
|
||||||
|
onLandscapeDetected()
|
||||||
|
disableListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enableListener() {
|
||||||
|
if (!isListening) {
|
||||||
|
isListening = true
|
||||||
|
enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disableListener() {
|
||||||
|
if (isListening) {
|
||||||
|
isListening = false
|
||||||
|
disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PortraitOrientationListener(
|
||||||
|
context: Context,
|
||||||
|
private val onPortraitDetected: () -> Unit
|
||||||
|
) : OrientationEventListener(context) {
|
||||||
|
|
||||||
|
private var isListening = false
|
||||||
|
|
||||||
|
override fun onOrientationChanged(orientation: Int) {
|
||||||
|
if (!isListening) return
|
||||||
|
|
||||||
|
if (orientation in 0..30 || orientation in 330..360 || orientation in 150..210) {
|
||||||
|
onPortraitDetected()
|
||||||
|
disableListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enableListener() {
|
||||||
|
if (!isListening) {
|
||||||
|
isListening = true
|
||||||
|
enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disableListener() {
|
||||||
|
if (isListening) {
|
||||||
|
isListening = false
|
||||||
|
disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutoRotateObserver(context: Context, handler: Handler, private val onAutoRotateChanged: () -> Unit) : ContentObserver(handler) {
|
||||||
|
private val contentResolver = context.contentResolver
|
||||||
|
|
||||||
|
override fun onChange(selfChange: Boolean) {
|
||||||
|
super.onChange(selfChange)
|
||||||
|
|
||||||
|
onAutoRotateChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startObserving() {
|
||||||
|
contentResolver.registerContentObserver(
|
||||||
|
android.provider.Settings.System.getUriFor(android.provider.Settings.System.ACCELEROMETER_ROTATION),
|
||||||
|
false,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopObserving() {
|
||||||
|
contentResolver.unregisterContentObserver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue