Compare commits

..

1 Commits

Author SHA1 Message Date
Stefan 3775e589f6 docs: update file Authentication.md link to packageHttp 2024-07-08 21:26:42 +01:00
383 changed files with 2617 additions and 25523 deletions
-12
View File
@@ -70,15 +70,3 @@
[submodule "app/src/unstable/assets/sources/spotify"]
path = app/src/unstable/assets/sources/spotify
url = ../plugins/spotify.git
[submodule "app/src/stable/assets/sources/bitchute"]
path = app/src/stable/assets/sources/bitchute
url = ../plugins/bitchute.git
[submodule "app/src/unstable/assets/sources/bitchute"]
path = app/src/unstable/assets/sources/bitchute
url = ../plugins/bitchute.git
[submodule "app/src/unstable/assets/sources/dailymotion"]
path = app/src/unstable/assets/sources/dailymotion
url = ../plugins/dailymotion.git
[submodule "app/src/stable/assets/sources/dailymotion"]
path = app/src/stable/assets/sources/dailymotion
url = ../plugins/dailymotion.git
+2 -16
View File
@@ -49,23 +49,9 @@ We encourage developers to write their own plugins. Please refer to the "Getting
## Contributing to Core
**We are currently not accepting contributions to the core.**
### License
The core is currently licensed under the [Source First License 1.1](./LICENSE.md). All contributors have to sign FUTO Individual Contributor License Agreement before contributions can be accepted. You can read more about it at [https://cla.futo.org/](https://cla.futo.org/).
### How to Contribute
1. Fork the core repository.
2. Clone your fork.
3. Make your changes.
4. Commit and push your changes.
5. Open a pull request.
### Guidelines
- Ensure your code adheres to the existing style.
- Include documentation and unit tests (where applicable).
The core is currently licensed under the FUTO Temporary License (FTL). The licensing and ownership of contributions to the core are complex topics that we are still working on. We'll update these guidelines when we have more clarity.
---
+2 -2
View File
@@ -1,4 +1,4 @@
# Source First License 1.1
# Grayjay Core License 1.0
## Acceptance
By using the software, you agree to all of the terms and conditions below.
@@ -16,7 +16,7 @@ Notwithstanding the above, you may not remove or obscure any functionality in th
You may not alter, remove, or obscure any licensing, copyright, or other notices of the Licensor in the software. Any use of the Licensors trademarks is subject to applicable law.
## Patents
If you make any written claim that the software infringes or contributes to infringement of any patent, your license for the software granted under these terms ends immediately. If your company makes such a claim, your license ends immediately for work on behalf of your company.
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
## Notices
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. If you modify the software, you must include in any modified copies of the software a prominent notice stating that you have modified the software, such as but not limited to, a statement in a readme file or an in-application about section.
+16 -19
View File
@@ -9,8 +9,8 @@ technologies that frustrate centralization and industry consolidation.
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/video.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/video-details.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/video.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/video-details.jpg" height="700" /></b></td>
</tr>
<tr>
<td>Video</td>
@@ -24,10 +24,12 @@ The FUTO media app is a player that exposes multiple video websites as sources i
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/source.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/sources.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/sources-disabled.jpg" height="700" /></b></td>
</tr>
<tr>
<td>Sources</td>
<td>Sources (all enabled)</td>
<td>Sources (one disabled)</td>
</tr>
</table>
@@ -36,7 +38,7 @@ Additional sources can also be installed. These sources are JavaScript sources,
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/source-install.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/source-settings.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/source-settings.jpg" height="700" /></b></td>
</tr>
<tr>
<td>Install a new source</td>
@@ -52,8 +54,8 @@ When a user enters a search term into the search bar, the query is posted to th
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/search-list.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/search-preview.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/search-list.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/search-preview.jpg" height="700" /></b></td>
</tr>
<tr>
<td>Search (list)</td>
@@ -69,7 +71,7 @@ Creators are able to configure their profile using NeoPass.
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/channel.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/channel.jpg" height="700" /></b></td>
</tr>
<tr>
<td>Channel</td>
@@ -110,7 +112,7 @@ The app offers a lot of settings customizing how the app looks and feels. An exa
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/settings.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/settings.jpg" height="700" /></b></td>
</tr>
<tr>
<td>Settings</td>
@@ -123,8 +125,8 @@ Playlists allow you to make a collection of videos that you can create and custo
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/playlists.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/playlist.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/playlists.jpg" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/playlist.jpg" height="700" /></b></td>
</tr>
<tr>
<td>Playlists</td>
@@ -140,7 +142,7 @@ Both individual videos and playlists can be downloaded for local, offline playba
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/downloads.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/downloads.jpg" height="700" /></b></td>
</tr>
<tr>
<td>Downloads</td>
@@ -155,7 +157,7 @@ For more information about casting please click [here](./docs/casting.md).
<table border="0">
<tr>
<td><b style="font-size:30px"><img src="images/casting.png" height="700" /></b></td>
<td><b style="font-size:30px"><img src="images/casting.jpg" height="700" /></b></td>
</tr>
<tr>
<td>Casting</td>
@@ -180,12 +182,6 @@ In the future we hope to offer users the choice of their desired recommendation
1. Download a copy of the repository.
2. Open the project in Android Studio: Once the repository is cloned, you can open it in Android Studio by selecting "Open an Existing Project" from the welcome screen and navigating to the directory where you cloned the repository.
3. Open the terminal in Android Studio by clicking on the terminal icon on bottom left and run the following command:
```sh
git submodule update --init --recursive
```
3. Build the project: With the project open in Android Studio, you can build it by selecting "Build > Make Project" from the main menu. This will compile the code and generate an APK file that you can install on your device or emulator.
4. Run the project: To run the project, select "Run > Run 'app'" from the main menu. This will launch the app on your device or emulator, allowing you to test it and make any necessary changes.
@@ -203,6 +199,7 @@ Create a tag on the master branch, incrementing the last version number by 1 (fo
Click on the CI/CD tab, you should now see the tests and build are in progress. If the build succeeds the last step will become available. The last step is a manual action which can be triggered by clicking the run button on the action. This action will deploy the build to all users using the app through auto-update.
## Documentation
The documentation can be found [here](https://gitlab.futo.org/videostreaming/documents/-/wikis/API-Overview).
+2 -11
View File
@@ -2,7 +2,7 @@ plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
id 'org.ajoberstar.grgit' version '5.2.2'
id 'org.ajoberstar.grgit' version '1.7.2'
id 'com.google.protobuf'
id 'kotlin-parcelize'
id 'com.google.devtools.ksp'
@@ -144,19 +144,9 @@ android {
buildFeatures {
buildConfig true
}
sourceSets {
main {
assets {
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
}
}
}
}
dependencies {
implementation 'com.google.dagger:dagger:2.48'
implementation 'androidx.test:monitor:1.7.2'
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
//Core
implementation 'androidx.core:core-ktx:1.12.0'
@@ -194,6 +184,7 @@ dependencies {
implementation 'androidx.media:media:1.7.0'
//Other
implementation 'org.jmdns:jmdns:3.5.1'
implementation 'org.jsoup:jsoup:1.15.3'
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
+21 -35
View File
@@ -11,7 +11,6 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<!--<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
@@ -36,12 +35,6 @@
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
</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"
android:enabled="true"
android:foregroundServiceType="mediaPlayback" />
@@ -57,8 +50,9 @@
android:name=".activities.MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar"
android:launchMode="singleInstance"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:supportsPictureInPicture="true">
@@ -151,31 +145,34 @@
<data android:scheme="polycentric" />
</intent-filter>
</activity>
<activity
android:name=".activities.TestActivity"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SettingsActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.DeveloperActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.ExceptionActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.CaptchaActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.LoginActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.AddSourceActivity"
android:screenOrientation="portrait"
android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBar">
<intent-filter>
@@ -189,55 +186,44 @@
</activity>
<activity
android:name=".activities.AddSourceOptionsActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.PolycentricHomeActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.PolycentricBackupActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.PolycentricCreateProfileActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.PolycentricProfileActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.PolycentricWhyActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.PolycentricImportProfileActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.ManageTabsActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.QRCaptureActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.FCastGuideActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SyncHomeActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SyncPairActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SyncShowPairingCodeActivity"
android:screenOrientation="sensorPortrait"
android:screenOrientation="portrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
</application>
</manifest>
+3 -207
View File
@@ -201,7 +201,7 @@ class PlatformContent {
obj = obj ?? {};
this.id = obj.id ?? PlatformID(); //PlatformID
this.name = obj.name ?? ""; //string
this.thumbnails = obj.thumbnails ?? new Thumbnails([]); //Thumbnail[]
this.thumbnails = obj.thumbnails; //Thumbnail[]
this.author = obj.author; //PlatformAuthorLink
this.datetime = obj.datetime ?? obj.uploadDate ?? 0; //OffsetDateTime (Long)
this.url = obj.url ?? ""; //String
@@ -278,49 +278,12 @@ class PlatformPostDetails extends PlatformPost {
super(obj);
obj = obj ?? {};
this.plugin_type = "PlatformPostDetails";
this.rating = obj.rating ?? new RatingLikes(-1);
this.rating = obj.rating ?? RatingLikes(-1);
this.textType = obj.textType ?? 0;
this.content = obj.content ?? "";
}
}
class PlatformArticleDetails extends PlatformContent {
constructor(obj) {
super(obj, 3);
obj = obj ?? {};
this.plugin_type = "PlatformArticleDetails";
this.rating = obj.rating ?? new RatingLikes(-1);
this.summary = obj.summary ?? "";
this.segments = obj.segments ?? [];
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
}
}
class ArticleSegment {
constructor(type) {
this.type = type;
}
}
class ArticleTextSegment extends ArticleSegment {
constructor(content, textType) {
super(1);
this.textType = textType;
this.content = content;
}
}
class ArticleImagesSegment extends ArticleSegment {
constructor(images) {
super(2);
this.images = images;
}
}
class ArticleNestedSegment extends ArticleSegment {
constructor(nested) {
super(9);
this.nested = nested;
}
}
//Sources
class VideoSourceDescriptor {
constructor(obj) {
@@ -367,16 +330,6 @@ class VideoUrlSource {
this.requestModifier = obj.requestModifier;
}
}
class VideoUrlWidevineSource extends VideoUrlSource {
constructor(obj) {
super(obj);
this.plugin_type = "VideoUrlWidevineSource";
this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
}
}
class VideoUrlRangeSource extends VideoUrlSource {
constructor(obj) {
super(obj);
@@ -409,26 +362,8 @@ class AudioUrlWidevineSource extends AudioUrlSource {
super(obj);
this.plugin_type = "AudioUrlWidevineSource";
this.bearerToken = obj.bearerToken;
this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
// deprecated api conversion
if(obj.bearerToken) {
this.getLicenseRequestExecutor = () => {
return {
executeRequest: (url, _headers, _method, license_request_data) => {
return http.POST(
url,
license_request_data,
{ Authorization: `Bearer ${obj.bearerToken}` },
false,
true
).body
}
}
}
}
}
}
class AudioUrlRangeSource extends AudioUrlSource {
@@ -471,49 +406,6 @@ class DashSource {
this.requestModifier = obj.requestModifier;
}
}
class DashWidevineSource extends DashSource {
constructor(obj) {
super(obj);
this.plugin_type = "DashWidevineSource";
this.licenseUri = obj.licenseUri;
if(obj.getLicenseRequestExecutor)
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
}
}
class DashManifestRawSource {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "DashRawSource";
this.name = obj.name ?? "";
this.bitrate = obj.bitrate ?? 0;
this.container = obj.container ?? "";
this.codec = obj.codec ?? "";
this.duration = obj.duration ?? 0;
this.url = obj.url;
this.language = obj.language ?? Language.UNKNOWN;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class DashManifestRawAudioSource {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "DashRawAudioSource";
this.name = obj.name ?? "";
this.bitrate = obj.bitrate ?? 0;
this.container = obj.container ?? "";
this.codec = obj.codec ?? "";
this.duration = obj.duration ?? 0;
this.url = obj.url;
this.language = obj.language ?? Language.UNKNOWN;
this.manifest = obj.manifest ?? null;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class RequestModifier {
constructor(obj) {
@@ -870,99 +762,3 @@ class URLSearchParams {
return searchString;
}
}
var __REGEX_SPACE_CHARACTERS = /<%= spaceCharacters %>/g;
var __btoa_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function btoa(input) {
input = String(input);
if (/[^\0-\xFF]/.test(input)) {
// Note: no need to special-case astral symbols here, as surrogates are
// matched, and the input is supposed to only contain ASCII anyway.
error(
'The string to be encoded contains characters outside of the ' +
'Latin1 range.'
);
}
var padding = input.length % 3;
var output = '';
var position = -1;
var a;
var b;
var c;
var buffer;
// Make sure any padding is handled outside of the loop.
var length = input.length - padding;
while (++position < length) {
// Read three bytes, i.e. 24 bits.
a = input.charCodeAt(position) << 16;
b = input.charCodeAt(++position) << 8;
c = input.charCodeAt(++position);
buffer = a + b + c;
// Turn the 24 bits into four chunks of 6 bits each, and append the
// matching character for each of them to the output.
output += (
__btoa_TABLE.charAt(buffer >> 18 & 0x3F) +
__btoa_TABLE.charAt(buffer >> 12 & 0x3F) +
__btoa_TABLE.charAt(buffer >> 6 & 0x3F) +
__btoa_TABLE.charAt(buffer & 0x3F)
);
}
if (padding == 2) {
a = input.charCodeAt(position) << 8;
b = input.charCodeAt(++position);
buffer = a + b;
output += (
__btoa_TABLE.charAt(buffer >> 10) +
__btoa_TABLE.charAt((buffer >> 4) & 0x3F) +
__btoa_TABLE.charAt((buffer << 2) & 0x3F) +
'='
);
} else if (padding == 1) {
buffer = input.charCodeAt(position);
output += (
__btoa_TABLE.charAt(buffer >> 2) +
__btoa_TABLE.charAt((buffer << 4) & 0x3F) +
'=='
);
}
return output;
};
function atob(input) {
input = String(input)
.replace(__REGEX_SPACE_CHARACTERS, '');
var length = input.length;
if (length % 4 == 0) {
input = input.replace(/==?$/, '');
length = input.length;
}
if (
length % 4 == 1 ||
// http://whatwg.org/C#alphanumeric-ascii-characters
/[^+a-zA-Z0-9/]/.test(input)
) {
error(
'Invalid character: the string to be decoded is not correctly encoded.'
);
}
var bitCounter = 0;
var bitStorage;
var buffer;
var output = '';
var position = -1;
while (++position < length) {
buffer = __btoa_TABLE.indexOf(input.charAt(position));
bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
// Unless this is the first of a group of 4 characters…
if (bitCounter++ % 4) {
// …convert the first 8 bits to a single ASCII character.
output += String.fromCharCode(
0xFF & bitStorage >> (-2 * bitCounter & 6)
);
}
}
return output;
};
@@ -18,10 +18,7 @@ fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
@UnstableApi
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
val requestModifier = getRequestModifier();
val requestExecutor = getRequestExecutor();
return if (requestExecutor != null) {
JSHttpDataSource.Factory().setRequestExecutor(requestExecutor);
} else if (requestModifier != null) {
return if (requestModifier != null) {
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
} else {
DefaultHttpDataSource.Factory();
File diff suppressed because one or more lines are too long
@@ -1,9 +1,6 @@
package com.futo.platformplayer
import android.net.Uri
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.net.URI
import java.net.URISyntaxException
import java.net.URLEncoder
@@ -28,18 +25,4 @@ fun String?.yesNoToBoolean(): Boolean {
fun Boolean?.toYesNo(): String {
return if (this == true) "YES" else "NO"
}
fun InetAddress?.toUrlAddress(): String {
return when (this) {
is Inet6Address -> {
"[${toString()}]"
}
is Inet4Address -> {
toString()
}
else -> {
throw Exception("Invalid address type")
}
}
}
@@ -1,192 +0,0 @@
package com.futo.platformplayer
import com.google.common.base.Preconditions
import com.google.common.io.ByteStreams
import com.google.common.primitives.Ints
import com.google.common.primitives.Longs
import java.io.DataInput
import java.io.DataInputStream
import java.io.EOFException
import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
class LittleEndianDataInputStream
/**
* Creates a `LittleEndianDataInputStream` that wraps the given stream.
*
* @param in the stream to delegate to
*/
(`in`: InputStream?) : FilterInputStream(Preconditions.checkNotNull(`in`)), DataInput {
/** This method will throw an [UnsupportedOperationException]. */
override fun readLine(): String {
throw UnsupportedOperationException("readLine is not supported")
}
@Throws(IOException::class)
override fun readFully(b: ByteArray) {
ByteStreams.readFully(this, b)
}
@Throws(IOException::class)
override fun readFully(b: ByteArray, off: Int, len: Int) {
ByteStreams.readFully(this, b, off, len)
}
@Throws(IOException::class)
override fun skipBytes(n: Int): Int {
return `in`.skip(n.toLong()).toInt()
}
@Throws(IOException::class)
override fun readUnsignedByte(): Int {
val b1 = `in`.read()
if (0 > b1) {
throw EOFException()
}
return b1
}
/**
* Reads an unsigned `short` as specified by [DataInputStream.readUnsignedShort],
* except using little-endian byte order.
*
* @return the next two bytes of the input stream, interpreted as an unsigned 16-bit integer in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readUnsignedShort(): Int {
val b1 = readAndCheckByte()
val b2 = readAndCheckByte()
return Ints.fromBytes(0.toByte(), 0.toByte(), b2, b1)
}
/**
* Reads an integer as specified by [DataInputStream.readInt], except using little-endian
* byte order.
*
* @return the next four bytes of the input stream, interpreted as an `int` in little-endian
* byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readInt(): Int {
val b1 = readAndCheckByte()
val b2 = readAndCheckByte()
val b3 = readAndCheckByte()
val b4 = readAndCheckByte()
return Ints.fromBytes(b4, b3, b2, b1)
}
/**
* Reads a `long` as specified by [DataInputStream.readLong], except using
* little-endian byte order.
*
* @return the next eight bytes of the input stream, interpreted as a `long` in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readLong(): Long {
val b1 = readAndCheckByte()
val b2 = readAndCheckByte()
val b3 = readAndCheckByte()
val b4 = readAndCheckByte()
val b5 = readAndCheckByte()
val b6 = readAndCheckByte()
val b7 = readAndCheckByte()
val b8 = readAndCheckByte()
return Longs.fromBytes(b8, b7, b6, b5, b4, b3, b2, b1)
}
/**
* Reads a `float` as specified by [DataInputStream.readFloat], except using
* little-endian byte order.
*
* @return the next four bytes of the input stream, interpreted as a `float` in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readFloat(): Float {
return java.lang.Float.intBitsToFloat(readInt())
}
/**
* Reads a `double` as specified by [DataInputStream.readDouble], except using
* little-endian byte order.
*
* @return the next eight bytes of the input stream, interpreted as a `double` in
* little-endian byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readDouble(): Double {
return java.lang.Double.longBitsToDouble(readLong())
}
@Throws(IOException::class)
override fun readUTF(): String {
return DataInputStream(`in`).readUTF()
}
/**
* Reads a `short` as specified by [DataInputStream.readShort], except using
* little-endian byte order.
*
* @return the next two bytes of the input stream, interpreted as a `short` in little-endian
* byte order.
* @throws IOException if an I/O error occurs.
*/
@Throws(IOException::class)
override fun readShort(): Short {
return readUnsignedShort().toShort()
}
/**
* Reads a char as specified by [DataInputStream.readChar], except using little-endian
* byte order.
*
* @return the next two bytes of the input stream, interpreted as a `char` in little-endian
* byte order
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun readChar(): Char {
return readUnsignedShort().toChar()
}
@Throws(IOException::class)
override fun readByte(): Byte {
return readUnsignedByte().toByte()
}
@Throws(IOException::class)
override fun readBoolean(): Boolean {
return readUnsignedByte() != 0
}
/**
* Reads a byte from the input stream checking that the end of file (EOF) has not been
* encountered.
*
* @return byte read from input
* @throws IOException if an error is encountered while reading
* @throws EOFException if the end of file (EOF) is encountered.
*/
@Throws(IOException::class, EOFException::class)
private fun readAndCheckByte(): Byte {
val b1 = `in`.read()
if (-1 == b1) {
throw EOFException()
}
return b1.toByte()
}
}
@@ -1,144 +0,0 @@
package com.futo.platformplayer
import com.google.common.base.Preconditions
import com.google.common.primitives.Longs
import java.io.*
class LittleEndianDataOutputStream
/**
* Creates a `LittleEndianDataOutputStream` that wraps the given stream.
*
* @param out the stream to delegate to
*/
(out: OutputStream?) : FilterOutputStream(DataOutputStream(Preconditions.checkNotNull(out))),
DataOutput {
@Throws(IOException::class)
override fun write(b: ByteArray, off: Int, len: Int) {
// Override slow FilterOutputStream impl
out.write(b, off, len)
}
@Throws(IOException::class)
override fun writeBoolean(v: Boolean) {
(out as DataOutputStream).writeBoolean(v)
}
@Throws(IOException::class)
override fun writeByte(v: Int) {
(out as DataOutputStream).writeByte(v)
}
@Deprecated(
"""The semantics of {@code writeBytes(String s)} are considered dangerous. Please use
{@link #writeUTF(String s)}, {@link #writeChars(String s)} or another write method instead."""
)
@Throws(
IOException::class
)
override fun writeBytes(s: String) {
(out as DataOutputStream).writeBytes(s)
}
/**
* Writes a char as specified by [DataOutputStream.writeChar], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeChar(v: Int) {
writeShort(v)
}
/**
* Writes a `String` as specified by [DataOutputStream.writeChars], except
* each character is written using little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeChars(s: String) {
for (i in 0 until s.length) {
writeChar(s[i].code)
}
}
/**
* Writes a `double` as specified by [DataOutputStream.writeDouble], except
* using little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeDouble(v: Double) {
writeLong(java.lang.Double.doubleToLongBits(v))
}
/**
* Writes a `float` as specified by [DataOutputStream.writeFloat], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeFloat(v: Float) {
writeInt(java.lang.Float.floatToIntBits(v))
}
/**
* Writes an `int` as specified by [DataOutputStream.writeInt], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeInt(v: Int) {
val bytes = byteArrayOf(
(0xFF and v).toByte(),
(0xFF and (v shr 8)).toByte(),
(0xFF and (v shr 16)).toByte(),
(0xFF and (v shr 24)).toByte()
)
out.write(bytes)
}
/**
* Writes a `long` as specified by [DataOutputStream.writeLong], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeLong(v: Long) {
val bytes = Longs.toByteArray(java.lang.Long.reverseBytes(v))
write(bytes, 0, bytes.size)
}
/**
* Writes a `short` as specified by [DataOutputStream.writeShort], except using
* little-endian byte order.
*
* @throws IOException if an I/O error occurs
*/
@Throws(IOException::class)
override fun writeShort(v: Int) {
val bytes = byteArrayOf(
(0xFF and v).toByte(),
(0xFF and (v shr 8)).toByte()
)
out.write(bytes)
}
@Throws(IOException::class)
override fun writeUTF(str: String) {
(out as DataOutputStream).writeUTF(str)
}
// Overriding close() because FilterOutputStream's close() method pre-JDK8 has bad behavior:
// it silently ignores any exception thrown by flush(). Instead, just close the delegate stream.
// It should flush itself if necessary.
@Throws(IOException::class)
override fun close() {
out.close()
}
}
@@ -11,7 +11,6 @@ import com.futo.platformplayer.activities.ManageTabsActivity
import com.futo.platformplayer.activities.PolycentricHomeActivity
import com.futo.platformplayer.activities.PolycentricProfileActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.activities.SyncHomeActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
@@ -33,6 +32,7 @@ import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldButton
import com.futo.platformplayer.views.fields.FormFieldWarning
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -44,7 +44,6 @@ import kotlinx.serialization.json.Json
import java.io.File
import java.time.OffsetDateTime
@Serializable
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
@@ -58,16 +57,7 @@ class Settings : FragmentedStorageFileJson() {
@Transient
val onTabsChanged = Event0();
@FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
@FormFieldButton(R.drawable.ic_update)
fun syncGrayjay() {
SettingsActivity.getActivity()?.let {
it.startActivity(Intent(it, SyncHomeActivity::class.java))
}
}
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -6)
@FormFieldButton(R.drawable.ic_person)
fun managePolycentricIdentity() {
SettingsActivity.getActivity()?.let {
@@ -83,7 +73,7 @@ class Settings : FragmentedStorageFileJson() {
}
}
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -6)
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -5)
@FormFieldButton(R.drawable.ic_quiz)
fun openFAQ() {
try {
@@ -93,7 +83,7 @@ class Settings : FragmentedStorageFileJson() {
//Ignored
}
}
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -5)
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -4)
@FormFieldButton(R.drawable.ic_data_alert)
fun openIssues() {
try {
@@ -125,7 +115,7 @@ class Settings : FragmentedStorageFileJson() {
}
}*/
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -4)
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -3)
@FormFieldButton(R.drawable.ic_tabs)
fun manageTabs() {
try {
@@ -139,15 +129,16 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -2)
@FormFieldButton(R.drawable.ic_move_up)
fun import() {
val act = SettingsActivity.getActivity() ?: return;
val intent = MainActivity.getImportOptionsIntent(act);
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK;
act.startActivity(intent);
}
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -2)
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -1)
@FormFieldButton(R.drawable.ic_link)
fun manageLinks() {
try {
@@ -157,24 +148,6 @@ class Settings : FragmentedStorageFileJson() {
}
}
/*@FormField(R.string.disable_battery_optimization, FieldForm.BUTTON, R.string.click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions, -1)
@FormFieldButton(R.drawable.battery_full_24px)
fun ignoreBatteryOptimization() {
SettingsActivity.getActivity()?.let {
val intent = Intent()
val packageName = it.packageName
val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
intent.setAction(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.setData(Uri.parse("package:$packageName"))
it.startActivity(intent)
UIDialogs.toast(it, "Please ignore battery optimizations for Grayjay")
} else {
UIDialogs.toast(it, "Battery optimizations already disabled for Grayjay")
}
}
}*/
@FormField(R.string.language, "group", -1, 0)
var language = LanguageSettings();
@Serializable
@@ -353,7 +326,7 @@ class Settings : FragmentedStorageFileJson() {
var playback = PlaybackSettings();
@Serializable
class PlaybackSettings {
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0)
@DropdownFieldOptionsId(R.array.audio_languages)
var primaryLanguage: Int = 0;
@@ -380,7 +353,7 @@ class Settings : FragmentedStorageFileJson() {
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 0)
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1)
@DropdownFieldOptionsId(R.array.playback_speeds)
var defaultPlaybackSpeed: Int = 3;
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
@@ -396,29 +369,37 @@ class Settings : FragmentedStorageFileJson() {
else -> 1.0f;
};
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 1)
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 2)
@DropdownFieldOptionsId(R.array.preferred_quality_array)
var preferredQuality: Int = 0;
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 2)
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array)
var preferredMeteredQuality: Int = 0;
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 3)
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 4)
@DropdownFieldOptionsId(R.array.preferred_quality_array)
var preferredPreviewQuality: Int = 5;
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
var simplifySources: Boolean = true;
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
var autoRotate: Int = 2;
@FormField(R.string.force_enable_auto_rotate_in_full_screen, FieldForm.TOGGLE, R.string.force_enable_auto_rotate_in_full_screen_description, 5)
var forceAllowFullScreenRotation: Boolean = true
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
@FormField(R.string.auto_rotate_dead_zone, FieldForm.DROPDOWN, R.string.this_prevents_the_device_from_rotating_within_the_given_amount_of_degrees, 6)
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
var autoRotateDeadZone: Int = 0;
fun getAutoRotateDeadZoneDegrees(): Int {
return autoRotateDeadZone * 5;
}
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
@DropdownFieldOptionsId(R.array.player_background_behavior)
var backgroundPlay: Int = 2;
@@ -469,44 +450,18 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
var fullscreenPortrait: Boolean = false;
@FormField(R.string.reverse_portrait, FieldForm.TOGGLE, R.string.reverse_portrait_description, 14)
var reversePortrait: Boolean = false;
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 18)
var preferWebmVideo: Boolean = false;
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 19)
var preferWebmAudio: Boolean = false;
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 20)
var allowVideoToGoUnderCutout: Boolean = true;
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
var autoplay: Boolean = false;
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
var deleteFromWatchLaterAuto: Boolean = true;
}
@FormField(R.string.comments, "group", R.string.comments_description, 6)
var comments = CommentSettings();
@Serializable
class CommentSettings {
var didAskPolycentricDefault: Boolean = false;
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
@DropdownFieldOptionsId(R.array.comment_sections)
var defaultCommentSection: Int = 2;
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
var recommendationsDefault: Boolean = false;
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
var hideRecommendations: Boolean = false;
var defaultCommentSection: Int = 0;
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
var badReputationCommentsFading: Boolean = true;
}
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7)
@@ -555,7 +510,7 @@ class Settings : FragmentedStorageFileJson() {
class Browsing {
@FormField(R.string.enable_video_cache, FieldForm.TOGGLE, R.string.cache_to_quickly_load_previously_fetched_videos, 0)
@Serializable(with = FlexibleBooleanSerializer::class)
var videoCache: Boolean = false; //Temporary default disabled to prevent ui freeze?
var videoCache: Boolean = true;
}
@FormField(R.string.casting, "group", R.string.configure_casting, 9)
@@ -570,10 +525,6 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true;
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 1)
@Serializable(with = FlexibleBooleanSerializer::class)
var alwaysProxyRequests: Boolean = false;
/*TODO: Should we have a different casting quality?
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array)
@@ -824,10 +775,10 @@ class Settings : FragmentedStorageFileJson() {
fun export() {
val activity = SettingsActivity.getActivity() ?: return;
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", null, {
StateBackup.shareExternalBackup();
}),
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", tag = null, call = {
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", null, {
StateBackup.saveExternalBackup(activity);
})
)
@@ -843,14 +794,10 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
fun clearPayment() {
SettingsActivity.getActivity()?.let { context ->
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
StatePayment.instance.clearLicenses();
SettingsActivity.getActivity()?.let {
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
it.reloadSettings();
}
})
StatePayment.instance.clearLicenses();
SettingsActivity.getActivity()?.let {
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
it.reloadSettings();
}
}
}
@@ -859,14 +806,12 @@ class Settings : FragmentedStorageFileJson() {
var other = Other();
@Serializable
class Other {
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
var playlistDeleteConfirmation: Boolean = true;
@FormField(R.string.bypass_rotation_prevention, FieldForm.TOGGLE, R.string.bypass_rotation_prevention_description, 1)
@FormFieldWarning(R.string.bypass_rotation_prevention_warning)
var bypassRotationPrevention: Boolean = false;
@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, 1)
var polycentricEnabled: Boolean = true;
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 4)
var polycentricLocalCache: Boolean = true;
}
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
@@ -898,24 +843,7 @@ class Settings : FragmentedStorageFileJson() {
var pan: Boolean = true;
}
@FormField(R.string.synchronization, FieldForm.GROUP, -1, 20)
var synchronization = Synchronization();
@Serializable
class Synchronization {
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
var enabled: Boolean = true;
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
var broadcast: Boolean = false;
@FormField(R.string.connect_discovered, FieldForm.TOGGLE, R.string.connect_discovered_description, 2)
var connectDiscovered: Boolean = true;
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
var connectLast: Boolean = true;
}
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
@FormField(R.string.info, FieldForm.GROUP, -1, 20)
var info = Info();
@Serializable
class Info {
@@ -8,7 +8,6 @@ import androidx.work.WorkManager
import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString
import com.futo.platformplayer.activities.DeveloperActivity
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
@@ -235,17 +234,13 @@ class SettingsDev : FragmentedStorageFileJson() {
R.string.test_background_worker_description, 4)
fun triggerBackgroundUpdate() {
val act = SettingsActivity.getActivity()!!;
try {
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
val wm = WorkManager.getInstance(act);
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
.setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
.build();
wm.enqueue(req);
} catch (e: Throwable) {
UIDialogs.showGeneralErrorDialog(act, "Failed to trigger background update", e)
}
val wm = WorkManager.getInstance(act);
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
.setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
.build();
wm.enqueue(req);
}
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
R.string.test_background_worker_description, 4)
@@ -496,13 +491,6 @@ class SettingsDev : FragmentedStorageFileJson() {
}
}
}
@FormField(R.string.test_playback, FieldForm.BUTTON,
R.string.test_playback, 1)
fun testPlayback(context: Context) {
context.startActivity(MainActivity.getActionIntent(context, "TEST_PLAYBACK"));
}
}
@@ -6,8 +6,6 @@ import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.text.Layout
import android.text.method.ScrollingMovementMethod
import android.util.TypedValue
import android.view.Gravity
import android.view.LayoutInflater
@@ -199,6 +197,7 @@ class UIDialogs {
dialog.show();
}
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
val builder = AlertDialog.Builder(context);
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
@@ -214,31 +213,28 @@ class UIDialogs {
this.text = text;
};
view.findViewById<TextView>(R.id.dialog_text_details).apply {
if (textDetails == null)
if(textDetails == null)
this.visibility = View.GONE;
else {
else
this.text = textDetails;
}
};
view.findViewById<TextView>(R.id.dialog_text_code).apply {
if (code == null) this.visibility = View.GONE;
if(code == null)
this.visibility = View.GONE;
else {
this.text = code;
this.movementMethod = ScrollingMovementMethod.getInstance();
this.visibility = View.VISIBLE;
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
}
};
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
val center = actions.any { it?.center == true };
val buttons = actions.map<Action, TextView> { act ->
val buttonView = TextView(context);
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt();
val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt();
val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics).toInt();
buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
this.marginStart = if(actions.size >= 2) dp14 / 2 else dp28 / 2;
this.marginEnd = if(actions.size >= 2) dp14 / 2 else dp28 / 2;
if(actions.size > 1)
this.marginEnd = if(actions.size > 2) dp14 else dp28;
};
buttonView.setTextColor(Color.WHITE);
buttonView.textSize = 14f;
@@ -260,7 +256,7 @@ class UIDialogs {
return@map buttonView;
};
if(actions.size <= 1 || center)
if(actions.size <= 1)
this.gravity = Gravity.CENTER;
else
this.gravity = Gravity.END;
@@ -349,13 +345,6 @@ class UIDialogs {
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
}
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null) {
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
val doNotAskAgain = Action(context.getString(R.string.do_not_ask_again), doNotAskAgainAction ?: {}, ActionStyle.NONE)
showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction)
}
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
val dialog = AutoUpdateDialog(context);
registerDialogOpened(dialog);
@@ -518,13 +507,11 @@ class UIDialogs {
val text: String;
val action: ()->Unit;
val style: ActionStyle;
var center: Boolean;
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE) {
this.text = text;
this.action = action;
this.style = style;
this.center = center;
}
}
enum class ActionStyle {
@@ -15,19 +15,14 @@ import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
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.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
import com.futo.platformplayer.helpers.VideoHelper
@@ -39,12 +34,12 @@ import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.LoaderView
@@ -96,17 +91,9 @@ class UISlideOverlays {
withContext(Dispatchers.Main) {
items.addAll(listOf(
SlideUpMenuItem(
container.context,
R.drawable.ic_notifications,
"Notifications",
"",
tag = "notifications",
call = {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
},
invokeParent = false
),
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
}, false),
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
SlideUpMenuGroup(container.context, "Subscription Groups",
"You can select which groups this subscription is part of.",
@@ -141,62 +128,22 @@ class UISlideOverlays {
SlideUpMenuGroup(container.context, "Fetch Settings",
"Depending on the platform you might not need to enable a type for it to be available.",
-1, listOf()),
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
container.context,
R.drawable.ic_live_tv,
"Livestreams",
"Check for livestreams",
tag = "fetchLive",
call = {
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
},
invokeParent = false
) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Streams",
"Check for streams",
tag = "fetchStreams",
call = {
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
},
invokeParent = false
) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
}, false) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for streams", "fetchStreams", {
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
}, false) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Videos",
"Check for videos",
tag = "fetchVideos",
call = {
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
},
invokeParent = false
) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
SlideUpMenuItem(
container.context,
R.drawable.ic_play,
"Content",
"Check for content",
tag = "fetchVideos",
call = {
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
},
invokeParent = false
) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
container.context,
R.drawable.ic_chat,
"Posts",
"Check for posts",
tag = "fetchPosts",
call = {
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
},
invokeParent = false
) else null/*,,
SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
}, false) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
SlideUpMenuItem(container.context, R.drawable.ic_play, "Content", "Check for content", "fetchVideos", {
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
}, false) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
}, false) else null/*,,
SlideUpMenuGroup(container.context, "Actions",
"Various things you can do with this subscription",
@@ -295,23 +242,11 @@ class UISlideOverlays {
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
masterPlaylist.getAudioSources().forEach { it ->
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
audioButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_music,
it.name,
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudioVariant = it
slideUpMenuOverlay.selectOption(audioButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
audioButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
selectedAudioVariant = it
slideUpMenuOverlay.selectOption(audioButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}, false))
}
/*masterPlaylist.getSubtitleSources().forEach { it ->
@@ -323,22 +258,11 @@ class UISlideOverlays {
}*/
masterPlaylist.getVideoSources().forEach {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
videoButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
it.name,
"${it.width}x${it.height}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedVideoVariant = it
slideUpMenuOverlay.selectOption(videoButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
videoButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
selectedVideoVariant = it
slideUpMenuOverlay.selectOption(videoButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}, false))
}
val newItems = arrayListOf<View>()
@@ -397,8 +321,8 @@ class UISlideOverlays {
val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor;
var selectedVideo: IVideoSource? = null;
var selectedAudio: IAudioSource? = null;
var selectedVideo: IVideoUrlSource? = null;
var selectedAudio: IAudioUrlSource? = null;
var selectedSubtitle: ISubtitleSource? = null;
val videoSources = descriptor.videoSources;
@@ -417,93 +341,45 @@ class UISlideOverlays {
}
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
listOf(listOf(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
container.context.getString(R.string.none),
container.context.getString(R.string.audio_only),
tag = "none",
call = {
selectedVideo = null;
menu?.selectOption(videoSources, "none");
if(selectedAudio != null || !requiresAudio)
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)) +
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.none), container.context.getString(R.string.audio_only), "none", {
selectedVideo = null;
menu?.selectOption(videoSources, "none");
if(selectedAudio != null || !requiresAudio)
menu?.setOk(container.context.getString(R.string.download));
}, false)) +
videoSources
.filter { it.isDownloadable() }
.map {
when (it) {
is IVideoUrlSource -> {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
it.name,
"${it.width}x${it.height}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedVideo = it
menu?.selectOption(videoSources, it);
if(selectedAudio != null || !requiresAudio)
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)
}
is JSDashManifestRawSource -> {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
it.name,
"${it.width}x${it.height}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedVideo = it
menu?.selectOption(videoSources, it);
if(selectedAudio != null || !requiresAudio)
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
selectedVideo = it
menu?.selectOption(videoSources, it);
if(selectedAudio != null || !requiresAudio)
menu?.setOk(container.context.getString(R.string.download));
}, false)
}
is IHLSManifestSource -> {
SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
it.name,
"HLS",
tag = it,
call = {
showHlsPicker(video, it, it.url, container)
},
invokeParent = false
)
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS", it, {
showHlsPicker(video, it, it.url, container)
}, false)
}
else -> {
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
null;//throw Exception("Unhandled source type")
throw Exception("Unhandled source type")
}
}
}.filterNotNull()).flatten().toList()
}).flatten().toList()
));
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
//TODO: Add HLS support here
selectedVideo = VideoHelper.selectBestVideoSource(
videoSources.filter { it is IVideoSource && it.isDownloadable() }.asIterable(),
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
Settings.instance.downloads.getDefaultVideoQualityPixels(),
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
) as IVideoSource?;
) as IVideoUrlSource?;
}
if (audioSources != null) {
@@ -512,90 +388,43 @@ class UISlideOverlays {
.map {
when (it) {
is IAudioUrlSource -> {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
SlideUpMenuItem(
container.context,
R.drawable.ic_music,
it.name,
"${it.bitrate}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudio = it
menu?.selectOption(audioSources, it);
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
);
}
is JSDashManifestRawAudioSource -> {
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
SlideUpMenuItem(
container.context,
R.drawable.ic_music,
it.name,
"${it.bitrate}",
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudio = it
menu?.selectOption(audioSources, it);
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
);
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
selectedAudio = it
menu?.selectOption(audioSources, it);
menu?.setOk(container.context.getString(R.string.download));
}, false);
}
is IHLSManifestAudioSource -> {
SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
it.name,
"HLS Audio",
tag = it,
call = {
showHlsPicker(video, it, it.url, container)
},
invokeParent = false
)
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS Audio", it, {
showHlsPicker(video, it, it.url, container)
}, false)
}
else -> {
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
null;//throw Exception("Unhandled source type")
throw Exception("Unhandled source type")
}
}
}.filterNotNull()));
}));
//TODO: Add HLS support here
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioSource && it.isDownloadable() }.asIterable(),
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource && it.isDownloadable() }.asIterable(),
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
Settings.instance.playback.getPrimaryLanguage(container.context),
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioSource?;
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
}
if(contentResolver != null && subtitleSources.isNotEmpty()) {
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources.map {
SlideUpMenuItem(
container.context,
R.drawable.ic_edit,
it.name,
"",
tag = it,
call = {
if (selectedSubtitle == it) {
selectedSubtitle = null;
menu?.selectOption(subtitleSources, null);
} else {
selectedSubtitle = it;
menu?.selectOption(subtitleSources, it);
}
},
invokeParent = false
);
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
if (selectedSubtitle == it) {
selectedSubtitle = null;
menu?.selectOption(subtitleSources, null);
} else {
selectedSubtitle = it;
menu?.selectOption(subtitleSources, it);
}
}, false);
})
);
}
@@ -613,18 +442,6 @@ class UISlideOverlays {
}
menu.onOK.subscribe {
val sv = selectedVideo
if (sv is IHLSManifestSource) {
showHlsPicker(video, sv, sv.url, container)
return@subscribe
}
val sa = selectedAudio
if (sa is IHLSManifestAudioSource) {
showHlsPicker(video, sa, sa.url, container)
return@subscribe
}
menu.hide();
val subtitleToDownload = selectedSubtitle;
if(selectedAudio != null || !requiresAudio) {
@@ -681,9 +498,8 @@ class UISlideOverlays {
}
}
catch(ex: Throwable) {
Logger.e(TAG, "Fetching details for download failed due to: " + ex.message, ex);
withContext(Dispatchers.Main) {
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download) + "\n" + ex.message);
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download));
handleUnknownDownload();
loader.hide(true);
}
@@ -720,47 +536,23 @@ class UISlideOverlays {
);
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_resolution), "Video", resolutions.map {
SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
it.first,
it.second,
tag = it.third,
call = {
targetPxSize = it.third;
menu?.selectOption("Video", it.third);
},
invokeParent = false
)
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.first, it.second, it.third, {
targetPxSize = it.third;
menu?.selectOption("Video", it.third);
}, false)
}));
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_bitrate), "Bitrate", listOf(
SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
container.context.getString(R.string.low_bitrate),
"",
tag = 1,
call = {
targetBitrate = 1;
menu?.selectOption("Bitrate", 1);
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
),
SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
container.context.getString(R.string.high_bitrate),
"",
tag = 9999999,
call = {
targetBitrate = 9999999;
menu?.selectOption("Bitrate", 9999999);
menu?.setOk(container.context.getString(R.string.download));
},
invokeParent = false
)
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.low_bitrate), "", 1, {
targetBitrate = 1;
menu?.selectOption("Bitrate", 1);
menu?.setOk(container.context.getString(R.string.download));
}, false),
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.high_bitrate), "", 9999999, {
targetBitrate = 9999999;
menu?.selectOption("Bitrate", 9999999);
menu?.setOk(container.context.getString(R.string.download));
}, false)
)));
@@ -880,21 +672,11 @@ class UISlideOverlays {
val items = arrayListOf<View>();
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
val isLimited = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
if (it is JSClient)
return@let it.config.reduceFunctionsInLimitedVersion && BuildConfig.IS_PLAYSTORE_BUILD
else false;
} ?: false;
if (lastUpdated != null) {
items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
SlideUpMenuItem(container.context,
R.drawable.ic_playlist_add,
lastUpdated.name,
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
{
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
StateDownloads.instance.checkForOutdatedPlaylists();
}))
@@ -906,91 +688,42 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater();
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
(listOf(
if(!isLimited)
SlideUpMenuItem(
container.context,
R.drawable.ic_download,
container.context.getString(R.string.download),
container.context.getString(R.string.download_the_video),
tag = "download",
call = {
showDownloadVideoOverlay(video, container, true);
},
invokeParent = false
) else null,
SlideUpMenuItem(
container.context,
R.drawable.ic_share,
container.context.getString(R.string.share),
"Share the video",
tag = "share",
call = {
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
container.context.startActivity(Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND;
putExtra(Intent.EXTRA_TEXT, url);
type = "text/plain";
}, null));
},
invokeParent = false
),
SlideUpMenuItem(
container.context,
R.drawable.ic_visibility_off,
container.context.getString(R.string.hide_creator_from_home),
"",
tag = "hide_creator",
call = {
StateMeta.instance.addHiddenCreator(video.author.url);
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
}))
+ actions).filterNotNull()
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
showDownloadVideoOverlay(video, container, true);
}, false),
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
container.context.startActivity(Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND;
putExtra(Intent.EXTRA_TEXT, url);
type = "text/plain";
}, null));
}, false),
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
StateMeta.instance.addHiddenCreator(video.author.url);
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
}))
+ actions)
));
items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
SlideUpMenuItem(container.context,
R.drawable.ic_queue_add,
container.context.getString(R.string.add_to_queue),
"${queue.size} " + container.context.getString(R.string.videos),
tag = "queue",
call = { StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem(container.context,
R.drawable.ic_watchlist_add,
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
"${watchLater.size} " + container.context.getString(R.string.videos),
tag = "watch later",
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
SlideUpMenuItem(container.context,
R.drawable.ic_history,
container.context.getString(R.string.add_to_history),
"Mark as watched",
tag = "history",
call = { StateHistory.instance.markAsWatched(video); }),
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.add_to_queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
{ StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, "${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "", "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); })
));
val playlistItems = arrayListOf<SlideUpMenuItem>();
playlistItems.add(SlideUpMenuItem(
container.context,
R.drawable.ic_playlist_add,
container.context.getString(R.string.new_playlist),
container.context.getString(R.string.add_to_new_playlist),
tag = "add_to_new_playlist",
call = {
showCreatePlaylistOverlay(container) {
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
};
},
invokeParent = false
))
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
showCreatePlaylistOverlay(container) {
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
};
}, false))
for (playlist in allPlaylists) {
playlistItems.add(SlideUpMenuItem(container.context,
R.drawable.ic_playlist_add,
"${container.context.getString(R.string.add_to)} " + playlist.name + "",
"${playlist.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, "${container.context.getString(R.string.add_to)} " + playlist.name + "", "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
{
StatePlaylists.instance.addToPlaylist(playlist.id, video);
StateDownloads.instance.checkForOutdatedPlaylists();
}));
@@ -1012,12 +745,8 @@ class UISlideOverlays {
if (lastUpdated != null) {
items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
SlideUpMenuItem(container.context,
R.drawable.ic_playlist_add,
lastUpdated.name,
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
{
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
StateDownloads.instance.checkForOutdatedPlaylists();
}))
@@ -1029,44 +758,25 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater();
items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
SlideUpMenuItem(container.context,
R.drawable.ic_queue_add,
container.context.getString(R.string.queue),
"${queue.size} " + container.context.getString(R.string.videos),
tag = "queue",
call = { StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem(container.context,
R.drawable.ic_watchlist_add,
StatePlayer.TYPE_WATCHLATER,
"${watchLater.size} " + container.context.getString(R.string.videos),
tag = "watch later",
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
)
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
{ StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download),
{ showDownloadVideoOverlay(video, container, true); }, false))
);
val playlistItems = arrayListOf<SlideUpMenuItem>();
playlistItems.add(SlideUpMenuItem(
container.context,
R.drawable.ic_playlist_add,
container.context.getString(R.string.new_playlist),
container.context.getString(R.string.add_to_new_playlist),
tag = "add_to_new_playlist",
call = {
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
});
},
invokeParent = false
))
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
});
}, false))
for (playlist in allPlaylists) {
playlistItems.add(SlideUpMenuItem(container.context,
R.drawable.ic_playlist_add,
playlist.name,
"${playlist.videos.size} " + container.context.getString(R.string.videos),
tag = "",
call = {
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
{
StatePlaylists.instance.addToPlaylist(playlist.id, video);
StateDownloads.instance.checkForOutdatedPlaylists();
}));
@@ -1091,36 +801,20 @@ class UISlideOverlays {
val views = arrayOf(
hidden
.map { btn -> SlideUpMenuItem(
container.context,
btn.iconResource,
btn.text.text.toString(),
"",
tag = "",
call = {
btn.handler?.invoke(btn);
},
invokeParent = invokeParents
) as View }.toTypedArray(),
arrayOf(SlideUpMenuItem(
container.context,
R.drawable.ic_pin,
container.context.getString(R.string.change_pins),
container.context.getString(R.string.decide_which_buttons_should_be_pinned),
tag = "",
call = {
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
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
.filter { it != null }
.map { it!! }
.toList();
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
btn.handler?.invoke(btn);
}, invokeParents) as View }.toTypedArray(),
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", {
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
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
.filter { it != null }
.map { it!! }
.toList();
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
}
},
invokeParent = false
))
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
}
}, false))
).flatten().toTypedArray();
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
@@ -1132,21 +826,14 @@ class UISlideOverlays {
var overlay: SlideUpMenuOverlay? = null;
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
options.map { SlideUpMenuItem(
container.context,
R.drawable.ic_move_up,
it.first,
"",
tag = it.second,
call = {
options.map { SlideUpMenuItem(container.context, R.drawable.ic_move_up, it.first, "", it.second, {
if(overlay!!.selectOption(null, it.second, true, true)) {
if(!selection.contains(it.second))
selection.add(it.second);
} else
}
else
selection.remove(it.second);
},
invokeParent = false
)
}, false)
});
overlay.onOK.subscribe {
onOrdered.invoke(selection);
@@ -13,7 +13,6 @@ import android.os.OperationCanceledException
import android.util.TypedValue
import android.view.View
import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
@@ -26,13 +25,10 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.others.PlatformLinkMovementMethod
import java.io.ByteArrayInputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.*
import java.util.concurrent.ThreadLocalRandom
@@ -233,49 +229,4 @@ fun String.decodeUnicode(): String {
i++
}
return sb.toString()
}
fun <T> smartMerge(targetArr: List<T>, toMerge: List<T>) : List<T>{
val missingToMerge = toMerge.filter { !targetArr.contains(it) }.toList();
val newArrResult = targetArr.toMutableList();
for(missing in missingToMerge) {
val newIndex = findNewIndex(toMerge, newArrResult, missing);
newArrResult.add(newIndex, missing);
}
return newArrResult;
}
fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
var originalIndex = originalArr.indexOf(item);
var newIndex = -1;
for(i in originalIndex-1 downTo 0) {
val previousItem = originalArr[i];
val indexInNewArr = newArr.indexOfFirst { it == previousItem };
if(indexInNewArr >= 0) {
newIndex = indexInNewArr + 1;
break;
}
}
if(newIndex < 0) {
for(i in originalIndex+1 until originalArr.size) {
val previousItem = originalArr[i];
val indexInNewArr = newArr.indexOfFirst { it == previousItem };
if(indexInNewArr >= 0) {
newIndex = indexInNewArr - 1;
break;
}
}
}
if(newIndex < 0)
return originalArr.size;
else
return newIndex;
}
fun ByteBuffer.toUtf8String(): String {
val remainingBytes = ByteArray(remaining())
get(remainingBytes)
return String(remainingBytes, Charsets.UTF_8)
}
}
File diff suppressed because it is too large Load Diff
@@ -3,7 +3,6 @@ package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
@@ -18,7 +17,6 @@ import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.LoaderView
import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store
import kotlinx.coroutines.Dispatchers
@@ -29,7 +27,6 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
private lateinit var _buttonHelp: ImageButton;
private lateinit var _profileName: EditText;
private lateinit var _buttonCreate: LinearLayout;
private lateinit var _loader: LoaderView;
private val TAG = "PolycentricCreateProfileActivity";
private var _creating = false;
@@ -46,7 +43,6 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
_buttonHelp = findViewById(R.id.button_help);
_profileName = findViewById(R.id.edit_profile_name);
_buttonCreate = findViewById(R.id.button_create_profile);
_loader = findViewById(R.id.loader);
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish();
};
@@ -69,49 +65,35 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
return@setOnClickListener;
}
_profileName.isEnabled = false;
_buttonCreate.visibility = View.GONE;
_loader.start();
_loader.visibility = View.VISIBLE;
lifecycleScope.launch(Dispatchers.IO) {
val processHandle: ProcessHandle;
try {
try {
processHandle = ProcessHandle.create();
Store.instance.addProcessSecret(processHandle.processSecret);
try {
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
processHandle.addServer(PolycentricCache.SERVER);
processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
} catch (e: Throwable) {
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
return@launch;
} finally {
_creating = false;
}
processHandle = ProcessHandle.create();
Store.instance.addProcessSecret(processHandle.processSecret);
try {
Logger.i(TAG, "Started backfill");
processHandle.fullyBackfillServersAnnounceExceptions();
Logger.i(TAG, "Finished backfill");
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
} catch (e: Throwable) {
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
processHandle.addServer(PolycentricCache.SERVER);
processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
} catch (e: Throwable) {
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
return@launch;
} finally {
_creating = false;
}
finally {
withContext(Dispatchers.Main) {
_profileName.isEnabled = true;
_buttonCreate.visibility = View.VISIBLE;
_loader.stop();
_loader.visibility = View.GONE;
}
try {
Logger.i(TAG, "Started backfill");
processHandle.fullyBackfillServersAnnounceExceptions();
Logger.i(TAG, "Finished backfill");
} catch (e: Throwable) {
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
}
withContext(Dispatchers.Main) {
@@ -8,7 +8,6 @@ import android.os.Bundle
import android.util.TypedValue
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.ScrollView
import androidx.appcompat.app.AppCompatActivity
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
@@ -29,7 +28,6 @@ class PolycentricHomeActivity : AppCompatActivity() {
private lateinit var _buttonNewProfile: BigButton;
private lateinit var _buttonImportProfile: BigButton;
private lateinit var _layoutButtons: LinearLayout;
private lateinit var _scroll: ScrollView;
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@@ -44,7 +42,6 @@ class PolycentricHomeActivity : AppCompatActivity() {
_buttonNewProfile = findViewById(R.id.button_new_profile);
_buttonImportProfile = findViewById(R.id.button_import_profile);
_layoutButtons = findViewById(R.id.layout_buttons);
_scroll = findViewById(R.id.scroll);
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish();
};
@@ -81,7 +78,6 @@ class PolycentricHomeActivity : AppCompatActivity() {
_layoutButtons.addView(profileButton, 0);
}
_scroll.invalidate();
_buttonHelp.setOnClickListener {
startActivity(Intent(this, PolycentricWhyActivity::class.java));
@@ -25,7 +25,6 @@ import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
@@ -34,7 +33,6 @@ import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.toBase64Url
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.github.dhaval2404.imagepicker.ImagePicker
@@ -49,7 +47,6 @@ class PolycentricProfileActivity : AppCompatActivity() {
private lateinit var _buttonHelp: ImageButton;
private lateinit var _editName: EditText;
private lateinit var _buttonExport: BigButton;
private lateinit var _buttonOpenHarborProfile: BigButton;
private lateinit var _buttonLogout: BigButton;
private lateinit var _buttonDelete: BigButton;
private lateinit var _username: String;
@@ -71,14 +68,10 @@ class PolycentricProfileActivity : AppCompatActivity() {
_imagePolycentric = findViewById(R.id.image_polycentric);
_editName = findViewById(R.id.edit_profile_name);
_buttonExport = findViewById(R.id.button_export);
_buttonOpenHarborProfile = findViewById(R.id.button_open_harbor_profile);
_buttonLogout = findViewById(R.id.button_logout);
_buttonDelete = findViewById(R.id.button_delete);
_loaderOverlay = findViewById(R.id.loader_overlay);
_textSystem = findViewById(R.id.text_system)
findViewById<TextView>(R.id.text_cta2).setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://harbor.social")))
}
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
saveIfRequired();
finish();
@@ -99,16 +92,6 @@ class PolycentricProfileActivity : AppCompatActivity() {
startActivity(Intent(this, PolycentricBackupActivity::class.java));
};
_buttonOpenHarborProfile.onClick.subscribe {
val processHandle = StatePolycentric.instance.processHandle!!;
processHandle?.let {
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(it.system));
val url = it.system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
val navUrl = "https://harbor.social/" + url.substring("polycentric://".length)
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
}
}
_buttonLogout.onClick.subscribe {
StatePolycentric.instance.setProcessHandle(null);
startActivity(Intent(this, PolycentricHomeActivity::class.java));
@@ -125,7 +108,6 @@ class PolycentricProfileActivity : AppCompatActivity() {
StatePolycentric.instance.setProcessHandle(null);
Store.instance.removeProcessSecret(processHandle.system);
PolycentricStorage.instance.removeProcessSecret(processHandle.system);
startActivity(Intent(this, PolycentricHomeActivity::class.java));
finish();
});
@@ -18,7 +18,6 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.*
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.LoaderView
@@ -185,19 +184,12 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
resultLauncher.launch(intent);
}
override fun onDestroy() {
super.onDestroy()
settingsActivityClosed.emit()
}
companion object {
//TODO: Temporary for solving Settings issues
@SuppressLint("StaticFieldLeak")
private var _lastActivity: SettingsActivity? = null;
val settingsActivityClosed = Event0()
fun getActivity(): SettingsActivity? {
val act = _lastActivity;
if(act != null && !act._isFinished)
@@ -1,138 +0,0 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.sync.internal.LinkType
import com.futo.platformplayer.sync.internal.SyncSession
import com.futo.platformplayer.views.sync.SyncDeviceView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class SyncHomeActivity : AppCompatActivity() {
private lateinit var _layoutDevices: LinearLayout
private lateinit var _layoutEmpty: LinearLayout
private val _viewMap: MutableMap<String, SyncDeviceView> = mutableMapOf()
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sync_home)
setNavigationBarColorAndIcons()
_layoutDevices = findViewById(R.id.layout_devices)
_layoutEmpty = findViewById(R.id.layout_empty)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish()
}
findViewById<LinearLayout>(R.id.button_link_new_device).setOnClickListener {
startActivity(Intent(this@SyncHomeActivity, SyncPairActivity::class.java))
}
findViewById<LinearLayout>(R.id.button_show_pairing_code).setOnClickListener {
startActivity(Intent(this@SyncHomeActivity, SyncShowPairingCodeActivity::class.java))
}
initializeDevices()
StateSync.instance.deviceUpdatedOrAdded.subscribe(this) { publicKey, session ->
lifecycleScope.launch(Dispatchers.Main) {
val view = _viewMap[publicKey]
if (!session.isAuthorized) {
if (view != null) {
_layoutDevices.removeView(view)
_viewMap.remove(publicKey)
}
return@launch
}
if (view == null) {
val syncDeviceView = SyncDeviceView(this@SyncHomeActivity)
syncDeviceView.onRemove.subscribe {
StateApp.instance.scopeOrNull?.launch {
StateSync.instance.delete(publicKey)
}
}
val v = updateDeviceView(syncDeviceView, publicKey, session)
_layoutDevices.addView(v, 0)
_viewMap[publicKey] = v
} else {
updateDeviceView(view, publicKey, session)
}
updateEmptyVisibility()
}
}
StateSync.instance.deviceRemoved.subscribe(this) {
lifecycleScope.launch(Dispatchers.Main) {
val view = _viewMap[it]
if (view != null) {
_layoutDevices.removeView(view)
_viewMap.remove(it)
}
updateEmptyVisibility()
}
}
}
override fun onDestroy() {
super.onDestroy()
StateSync.instance.deviceUpdatedOrAdded.remove(this)
StateSync.instance.deviceRemoved.remove(this)
}
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
val connected = session?.connected ?: false
syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None)
.setName(publicKey)
.setStatus(if (connected) "Connected" else "Disconnected")
return syncDeviceView
}
private fun updateEmptyVisibility() {
if (_viewMap.isNotEmpty()) {
_layoutEmpty.visibility = View.GONE
} else {
_layoutEmpty.visibility = View.VISIBLE
}
}
private fun initializeDevices() {
_layoutDevices.removeAllViews()
for (publicKey in StateSync.instance.getAll()) {
val syncDeviceView = SyncDeviceView(this)
syncDeviceView.onRemove.subscribe {
StateApp.instance.scopeOrNull?.launch {
StateSync.instance.delete(publicKey)
}
}
val view = updateDeviceView(syncDeviceView, publicKey, StateSync.instance.getSession(publicKey))
_layoutDevices.addView(view)
_viewMap[publicKey] = view
}
updateEmptyVisibility()
}
companion object {
private const val TAG = "SyncHomeActivity"
}
}
@@ -1,144 +0,0 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.os.Bundle
import android.util.Base64
import android.view.View
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.sync.internal.SyncDeviceInfo
import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
class SyncPairActivity : AppCompatActivity() {
private lateinit var _editCode: EditText
private lateinit var _layoutPairing: LinearLayout
private lateinit var _textPairingStatus: TextView
private lateinit var _layoutPairingSuccess: LinearLayout
private lateinit var _layoutPairingError: LinearLayout
private lateinit var _textError: TextView
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
scanResult?.let {
if (it.contents != null) {
_editCode.text.clear()
_editCode.text.append(it.contents)
pair(it.contents)
}
}
}
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sync_pair)
setNavigationBarColorAndIcons()
_editCode = findViewById(R.id.edit_code)
_layoutPairing = findViewById(R.id.layout_pairing)
_textPairingStatus = findViewById(R.id.text_pairing_status)
_layoutPairingSuccess = findViewById(R.id.layout_pairing_success)
_layoutPairingError = findViewById(R.id.layout_pairing_error)
_textError = findViewById(R.id.text_error)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish()
}
findViewById<LinearLayout>(R.id.button_scan_qr).setOnClickListener {
val integrator = IntentIntegrator(this)
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
integrator.setPrompt(getString(R.string.scan_a_qr_code))
integrator.setOrientationLocked(true);
integrator.setCameraId(0)
integrator.setBeepEnabled(false)
integrator.setBarcodeImageEnabled(true)
integrator.setCaptureActivity(QRCaptureActivity::class.java);
_qrCodeResultLauncher.launch(integrator.createScanIntent())
}
findViewById<LinearLayout>(R.id.button_link_new_device).setOnClickListener {
pair(_editCode.text.toString())
}
_layoutPairingSuccess.setOnClickListener {
_layoutPairingSuccess.visibility = View.GONE
}
_layoutPairingError.setOnClickListener {
_layoutPairingError.visibility = View.GONE
}
_layoutPairingSuccess.visibility = View.GONE
_layoutPairingError.visibility = View.GONE
}
fun pair(url: String) {
try {
_layoutPairing.visibility = View.VISIBLE
_textPairingStatus.text = "Parsing text..."
if (!url.startsWith("grayjay://sync/")) {
throw Exception("Not a valid URL: $url")
}
val deviceInfo: SyncDeviceInfo = Json.decodeFromString<SyncDeviceInfo>(Base64.decode(url.substring("grayjay://sync/".length), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP).decodeToString())
if (StateSync.instance.isAuthorized(deviceInfo.publicKey)) {
throw Exception("This device is already paired")
}
_textPairingStatus.text = "Connecting..."
lifecycleScope.launch(Dispatchers.IO) {
try {
StateSync.instance.connect(deviceInfo) { session, complete, message ->
lifecycleScope.launch(Dispatchers.Main) {
if (complete) {
_layoutPairingSuccess.visibility = View.VISIBLE
_layoutPairing.visibility = View.GONE
} else {
_textPairingStatus.text = message
}
}
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
_layoutPairingError.visibility = View.VISIBLE
_textError.text = e.message
_layoutPairing.visibility = View.GONE
Logger.e(TAG, "Failed to pair", e)
}
}
}
} catch(e: Throwable) {
_layoutPairingError.visibility = View.VISIBLE
_textError.text = e.message
_layoutPairing.visibility = View.GONE
Logger.e(TAG, "Failed to pair", e)
} finally {
_layoutPairing.visibility = View.GONE
}
}
companion object {
private const val TAG = "SyncPairActivity"
}
}
@@ -1,142 +0,0 @@
package com.futo.platformplayer.activities
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.util.Base64
import android.util.TypedValue
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.sync.internal.SyncDeviceInfo
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.net.NetworkInterface
class SyncShowPairingCodeActivity : AppCompatActivity() {
private lateinit var _textCode: TextView
private lateinit var _imageQR: ImageView
private lateinit var _textQR: TextView
private var _code: String? = null
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onDestroy() {
super.onDestroy()
activity = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity = this
setContentView(R.layout.activity_sync_show_pairing_code)
setNavigationBarColorAndIcons()
_textCode = findViewById(R.id.text_code)
_imageQR = findViewById(R.id.image_qr)
_textQR = findViewById(R.id.text_scan_qr)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish()
}
findViewById<LinearLayout>(R.id.button_copy).setOnClickListener {
val code = _code ?: return@setOnClickListener
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager;
val clip = ClipData.newPlainText(getString(R.string.copied_text), code);
clipboard.setPrimaryClip(clip);
UIDialogs.toast(this, "Copied to clipboard")
}
val ips = getIPs()
val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT)
val json = Json.encodeToString(selfDeviceInfo)
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
val url = "grayjay://sync/${base64}"
setCode(url)
}
fun setCode(code: String?) {
_code = code
_textCode.text = code
if (code == null) {
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
return
}
try {
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt()
val qrCodeBitmap = generateQRCode(code, dimension, dimension)
_imageQR.setImageBitmap(qrCodeBitmap)
_imageQR.visibility = View.VISIBLE
_textQR.visibility = View.VISIBLE
} catch (e: Exception) {
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
}
}
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
return bitMatrixToBitmap(bitMatrix);
}
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
val width = matrix.width;
val height = matrix.height;
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
for (x in 0 until width) {
for (y in 0 until height) {
bmp.setPixel(x, y, if (matrix[x, y]) Color.BLACK else Color.WHITE);
}
}
return bmp;
}
private fun getIPs(): List<String> {
val ips = arrayListOf<String>()
for (intf in NetworkInterface.getNetworkInterfaces()) {
for (addr in intf.inetAddresses) {
if (addr.isLoopbackAddress) {
continue
}
if (addr.address.size != 4) {
continue
}
addr.hostAddress?.let { ips.add(it) }
}
}
return ips
}
companion object {
private const val TAG = "SyncShowPairingCodeActivity"
var activity: SyncShowPairingCodeActivity? = null
private set
}
}
@@ -17,14 +17,13 @@ import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.time.Duration
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
import kotlin.system.measureTimeMillis
open class ManagedHttpClient {
protected var _builderTemplate: OkHttpClient.Builder;
protected val _builderTemplate: OkHttpClient.Builder;
private var client: OkHttpClient;
@@ -33,15 +32,6 @@ open class ManagedHttpClient {
var user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
fun setTimeout(timeout: Long) {
rebuildClient {
it.callTimeout(Duration.ofMillis(client.callTimeoutMillis.toLong()))
.writeTimeout(Duration.ofMillis(client.writeTimeoutMillis.toLong()))
.readTimeout(Duration.ofMillis(client.readTimeoutMillis.toLong()))
.connectTimeout(Duration.ofMillis(timeout));
}
}
private val trustAllCerts = arrayOf<TrustManager>(
object: X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
@@ -72,15 +62,6 @@ open class ManagedHttpClient {
}.build();
}
fun rebuildClient(modify: (OkHttpClient.Builder) -> OkHttpClient.Builder) {
_builderTemplate = modify(_builderTemplate);
client = _builderTemplate.addNetworkInterceptor { chain ->
val request = beforeRequest(chain.request());
val response = afterRequest(chain.proceed(request));
return@addNetworkInterceptor response;
}.build();
}
open fun clone(): ManagedHttpClient {
val clonedClient = ManagedHttpClient(_builderTemplate);
clonedClient.user_agent = user_agent;
@@ -210,20 +210,6 @@ class HttpContext : AutoCloseable {
}
}
}
fun respondBytes(status: Int, headers: HttpHeaders, body: ByteArray? = null) {
if(headers.get("content-length").isNullOrEmpty()) {
if (body != null) {
headers.put("content-length", body.size.toString());
} else {
headers.put("content-length", "0")
}
}
respond(status, headers) { responseStream ->
if(body != null) {
responseStream.write(body);
}
}
}
fun respond(status: Int, headers: HttpHeaders, writing: (OutputStream)->Unit) {
val responseStream = _responseStream ?: throw IllegalStateException("No response stream set");
@@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
import com.futo.platformplayer.logging.Logger
@@ -208,20 +208,20 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
for(getMethod in getMethods)
if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1)
addHandler(HttpFunctionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
if(!getMethod.second.contentType.isEmpty())
this.withContentType(getMethod.second.contentType);
}.withContentType(getMethod.second.contentType);
for(postMethod in postMethods)
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
addHandler(HttpFunctionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
addHandler(HttpFuntionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
if(!postMethod.second.contentType.isEmpty())
this.withContentType(postMethod.second.contentType);
}.withContentType(postMethod.second.contentType);
for(getField in getFields) {
getField.first.isAccessible = true;
addHandler(HttpFunctionHandler("GET", getField.second.path) {
addHandler(HttpFuntionHandler("GET", getField.second.path) {
val value = getField.first.get(obj) as String?;
if(value != null) {
val headers = HttpHeaders(
@@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server.handlers
import com.futo.platformplayer.api.http.server.HttpContext
class HttpFunctionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
class HttpFuntionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
override fun handle(httpContext: HttpContext) {
httpContext.setResponseHeaders(this.headers);
handler(httpContext);
@@ -13,15 +13,13 @@ class PlatformClientPool {
private val _pool: HashMap<JSClient, Int> = hashMapOf();
private var _poolCounter = 0;
private val _poolName: String?;
private val _privatePool: Boolean;
var isDead: Boolean = false
private set;
val onDead = Event2<JSClient, PlatformClientPool>();
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
constructor(parentClient: IPlatformClient, name: String? = null) {
_poolName = name;
_privatePool = privatePool;
if(parentClient !is JSClient)
throw IllegalArgumentException("Pooling only supported for JSClients right now");
Logger.i(TAG, "Pool for ${parentClient.name} was started");
@@ -53,7 +51,7 @@ class PlatformClientPool {
reserved = _pool.keys.find { !it.isBusy };
if(reserved == null && _pool.size < capacity) {
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
reserved = _parent.getCopy(_privatePool);
reserved = _parent.getCopy();
reserved?.onCaptchaException?.subscribe { client, ex ->
StateApp.instance.handleCaptchaException(client, ex);
@@ -6,14 +6,12 @@ class PlatformMultiClientPool {
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
private var _isFake = false;
private var _privatePool = false;
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false) {
constructor(name: String, maxCap: Int = -1) {
_name = name;
_maxCap = if(maxCap > 0)
maxCap
else 99;
_privatePool = isPrivatePool;
}
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
@@ -21,7 +19,7 @@ class PlatformMultiClientPool {
return parentClient;
val pool = synchronized(_clientPools) {
if(!_clientPools.containsKey(parentClient))
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply {
this.onDead.subscribe { _, pool ->
synchronized(_clientPools) {
if(_clientPools[parentClient] == pool)
@@ -4,6 +4,6 @@ import kotlinx.serialization.json.Json
class Serializer {
companion object {
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; coerceInputValues = true };
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; };
}
}
@@ -27,8 +27,6 @@ open class PlatformAuthorLink {
}
companion object {
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
if(value.has("membershipUrl"))
return PlatformAuthorMembershipLink.fromV8(config, value);
@@ -23,7 +23,7 @@ enum class ChapterType(val value: Int) {
companion object {
fun fromInt(value: Int): ChapterType
{
val result = ChapterType.entries.firstOrNull { it.value == value };
val result = ChapterType.values().firstOrNull { it.value == value };
if(result == null)
throw UnknownPlatformException(value.toString());
return result;
@@ -1,63 +0,0 @@
package com.futo.platformplayer.api.media.models.comments
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
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.ratings.RatingType
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.Deferred
import java.time.OffsetDateTime
class LazyComment: IPlatformComment {
private var _commentDeferred: Deferred<IPlatformComment>;
private var _commentLoaded: IPlatformComment? = null;
private var _commentException: Throwable? = null;
override val contextUrl: String
get() = _commentLoaded?.contextUrl ?: "";
override val author: PlatformAuthorLink
get() = _commentLoaded?.author ?: PlatformAuthorLink.UNKNOWN;
override val message: String
get() = _commentLoaded?.message ?: "";
override val rating: IRating
get() = _commentLoaded?.rating ?: RatingLikes(0);
override val date: OffsetDateTime?
get() = _commentLoaded?.date ?: OffsetDateTime.MIN;
override val replyCount: Int?
get() = _commentLoaded?.replyCount ?: 0;
val isAvailable: Boolean get() = _commentLoaded != null;
private var _uiHandler: ((LazyComment)->Unit)? = null;
constructor(commentDeferred: Deferred<IPlatformComment>) {
_commentDeferred = commentDeferred;
_commentDeferred.invokeOnCompletion {
if(it == null) {
_commentLoaded = commentDeferred.getCompleted();
Logger.i("LazyComment", "Resolved comment");
}
else {
_commentException = it;
Logger.e("LazyComment", "Resolving comment failed: ${it.message}", it);
}
_uiHandler?.invoke(this);
}
}
fun getUnderlyingComment(): IPlatformComment? {
return _commentLoaded;
}
fun setUIHandler(handler: (LazyComment)->Unit){
_uiHandler = handler;
}
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment>? {
return _commentLoaded?.getReplies(client);
}
}
@@ -21,7 +21,7 @@ enum class ContentType(val value: Int) {
companion object {
fun fromInt(value: Int): ContentType
{
val result = ContentType.entries.firstOrNull { it.value == value };
val result = ContentType.values().firstOrNull { it.value == value };
if(result == null)
throw UnknownPlatformException(value.toString());
return result;
@@ -10,7 +10,7 @@ enum class LiveEventType(val value : Int) {
companion object{
fun fromInt(value : Int) : LiveEventType{
return LiveEventType.entries.first { it.value == value };
return LiveEventType.values().first { it.value == value };
}
}
}
@@ -10,7 +10,7 @@ enum class TextType(val value: Int) {
companion object {
fun fromInt(value: Int): TextType
{
val result = TextType.entries.firstOrNull { it.value == value };
val result = TextType.values().firstOrNull { it.value == value };
if(result == null)
throw IllegalArgumentException("Unknown Texttype: $value");
return result;
@@ -8,7 +8,7 @@ enum class RatingType(val value : Int) {
companion object{
fun fromInt(value : Int) : RatingType{
return RatingType.entries.first { it.value == value };
return RatingType.values().first { it.value == value };
}
}
}
@@ -1,3 +1,6 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IAudioUrlWidevineSource : IAudioUrlSource, IWidevineSource
interface IAudioUrlWidevineSource : IAudioUrlSource {
val bearerToken: String
val licenseUri: String
}
@@ -1,5 +0,0 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IDashManifestWidevineSource : IWidevineSource {
val url: String
}
@@ -1,3 +0,0 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IVideoUrlWidevineSource : IVideoUrlSource, IWidevineSource
@@ -1,9 +0,0 @@
package com.futo.platformplayer.api.media.models.streams.sources
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
interface IWidevineSource {
val licenseUri: String
val hasLicenseRequestExecutor: Boolean
fun getLicenseRequestExecutor(): JSRequestExecutor?
}
@@ -19,9 +19,9 @@ open class SerializedPlatformVideo(
override val thumbnails: Thumbnails,
override val author: PlatformAuthorLink,
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override val datetime: OffsetDateTime? = null,
override val datetime: OffsetDateTime?,
override val url: String,
override val shareUrl: String = "",
override val shareUrl: String,
override val duration: Long,
override val viewCount: Long,
@@ -54,8 +54,8 @@ class DevJSClient : JSClient {
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
}
override fun getCopy(privateCopy: Boolean): JSClient {
return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID);
override fun getCopy(): JSClient {
return DevJSClient(_context, descriptor, _script, _auth, _captcha, saveState(), devID);
}
override fun initialize() {
@@ -164,16 +164,13 @@ open class JSClient : IPlatformClient {
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
}
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) {
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
this._context = context;
this.config = descriptor.config;
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
this.descriptor = descriptor;
_injectedSaveState = saveState;
if(!withoutCredentials)
_auth = descriptor.getAuth();
else
_auth = null;
_auth = descriptor.getAuth();
_captcha = descriptor.getCaptchaData();
flags = descriptor.flags.toTypedArray();
@@ -193,8 +190,8 @@ open class JSClient : IPlatformClient {
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
}
open fun getCopy(withoutCredentials: Boolean = false): JSClient {
return JSClient(_context, descriptor, saveState(), _script, withoutCredentials);
open fun getCopy(): JSClient {
return JSClient(_context, descriptor, saveState(), _script);
}
fun getUnderlyingPlugin(): V8Plugin {
@@ -237,8 +234,7 @@ open class JSClient : IPlatformClient {
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false
);
try {
@@ -1,7 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js
class JSClientConstants {
companion object {
val PLUGIN_SPEC_VERSION = 2;
}
}
@@ -4,9 +4,7 @@ import android.net.Uri
import com.futo.platformplayer.SignatureProvider
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.Contextual
import java.net.URL
import java.util.UUID
@@ -50,8 +48,6 @@ class SourcePluginConfig(
var primaryClaimFieldType: Int? = null,
var developerSubmitUrl: String? = null,
var allowAllHttpHeaderAccess: Boolean = false,
var maxDownloadParallelism: Int = 0,
var reduceFunctionsInLimitedVersion: Boolean = false,
) : IV8PluginConfig {
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
@@ -81,8 +77,7 @@ class SourcePluginConfig(
private var _allowUrlsLowerVal: List<String>? = null;
private val _allowUrlsLower: List<String> get() {
if(_allowUrlsLowerVal == null)
_allowUrlsLowerVal = allowUrls.map { it.lowercase() }
.filter { it.length > 0 };
_allowUrlsLowerVal = allowUrls.map { it.lowercase() };
return _allowUrlsLowerVal!!;
};
@@ -175,7 +170,7 @@ class SourcePluginConfig(
return true;
val uri = Uri.parse(url);
val host = uri.host?.lowercase() ?: "";
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
return _allowUrlsLower.any { it == host };
}
companion object {
@@ -38,7 +38,7 @@ class JSHttpClient : ManagedHttpClient {
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super(
//Temporary ugly solution for DevPortal proxy support
(if((jsClient?.config?.id == StateDeveloper.DEV_ID || jsClient == null) && StateDeveloper.instance.devProxy != null)
(if(jsClient?.config?.id == StateDeveloper.DEV_ID && StateDeveloper.instance.devProxy != null)
OkHttpClient.Builder().proxy(Proxy(Proxy.Type.HTTP,
InetSocketAddress(StateDeveloper.instance.devProxy!!.url, StateDeveloper.instance.devProxy!!.port)
))
@@ -16,7 +16,6 @@ interface IJSContentDetails: IPlatformContent {
return when(ContentType.fromInt(type)) {
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
ContentType.POST -> JSPostDetails(plugin.config, obj);
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
else -> throw NotImplementedError("Unknown content type ${type}");
}
}
@@ -1,162 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
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.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
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.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails {
final override val contentType: ContentType get() = ContentType.ARTICLE;
private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean;
val rating: IRating;
val summary: String;
val thumbnails: Thumbnails?;
val segments: List<IJSArticleSegment>;
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
val contextName = "PlatformPost";
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
summary = _content.getOrThrow(client.config, "summary", contextName);
if(_content.has("thumbnails"))
thumbnails = Thumbnails.fromV8(client.config, _content.getOrThrow(client.config, "thumbnails", contextName));
else
thumbnails = null;
segments = (obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", contextName)
?.map { fromV8Segment(client, it) }
?.filterNotNull() ?: listOf());
_hasGetComments = _content.has("getComments");
_hasGetContentRecommendations = _content.has("getContentRecommendations");
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
if(!_hasGetComments || _content.isClosed)
return null;
if(client is DevJSClient)
return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.getComments()") {
return@handleDevCall getCommentsJS(client);
}
else if(client is JSClient)
return getCommentsJS(client);
return null;
}
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
if(!_hasGetContentRecommendations || _content.isClosed)
return null;
if(client is DevJSClient)
return StateDeveloper.instance.handleDevCall(client.devID, "postDetail.getContentRecommendations()") {
return@handleDevCall getContentRecommendationsJS(client);
}
else if(client is JSClient)
return getContentRecommendationsJS(client);
return null;
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager);
}
private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager);
}
companion object {
fun fromV8Segment(client: JSClient, obj: V8ValueObject): IJSArticleSegment? {
if(!obj.has("type"))
throw IllegalArgumentException("Object missing type field");
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
SegmentType.TEXT -> JSTextSegment(client, obj);
SegmentType.IMAGES -> JSImagesSegment(client, obj);
SegmentType.NESTED -> JSNestedSegment(client, obj);
else -> null;
}
}
}
}
enum class SegmentType(val value: Int) {
UNKNOWN(0),
TEXT(1),
IMAGES(2),
NESTED(9);
companion object {
fun fromInt(value: Int): SegmentType
{
val result = SegmentType.entries.firstOrNull { it.value == value };
if(result == null)
throw IllegalArgumentException("Unknown Texttype: $value");
return result;
}
}
}
interface IJSArticleSegment {
val type: SegmentType;
}
class JSTextSegment: IJSArticleSegment {
override val type = SegmentType.TEXT;
val textType: TextType;
val content: String;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSTextSegment";
textType = TextType.fromInt((obj.getOrDefault<Int>(client.config, "textType", contextName, null) ?: 0));
content = obj.getOrDefault(client.config, "content", contextName, "") ?: "";
}
}
class JSImagesSegment: IJSArticleSegment {
override val type = SegmentType.IMAGES;
val images: List<String>;
val caption: String;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSTextSegment";
images = obj.getOrThrowNullableList<String>(client.config, "images", contextName) ?: listOf();
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
}
}
class JSNestedSegment: IJSArticleSegment {
override val type = SegmentType.NESTED;
val nested: IPlatformContent;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSNestedSegment";
val nestedObj = obj.getOrThrow<V8ValueObject>(client.config, "nested", contextName, false);
nested = IJSContent.fromV8(client, nestedObj);
}
}
@@ -42,12 +42,7 @@ open class JSContent : IPlatformContent, IPluginSourced {
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
val authorObj = _content.getOrDefault<V8ValueObject>(config, "author", contextName, null);
if(authorObj != null)
author = PlatformAuthorLink.fromV8(_pluginConfig, authorObj);
else
author = PlatformAuthorLink.UNKNOWN;
author = PlatformAuthorLink.fromV8(_pluginConfig, _content.getOrThrow(config, "author", contextName));
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
if(datetimeInt == 0.toLong())
@@ -59,8 +54,4 @@ open class JSContent : IPlatformContent, IPluginSourced {
_hasGetDetails = _content.has("getDetails");
}
fun getUnderlyingObject(): V8ValueObject? {
return _content;
}
}
@@ -71,8 +71,6 @@ abstract class JSPager<T> : IPager<T> {
warnIfMainThread("JSPager.getResults");
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
if(items.v8Runtime.isDead || items.v8Runtime.isClosed)
throw IllegalStateException("Runtime closed");
val newResults = items.toArray()
.map { convertResult(it as V8ValueObject) }
.toList();
@@ -10,7 +10,6 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.ReusablePager
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.models.Playlist
import java.util.UUID
class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
override val contents: IPager<IPlatformVideo>;
@@ -38,6 +37,6 @@ class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
onProgress?.invoke(videos.size);
}
return Playlist(UUID.randomUUID().toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
return Playlist(id.toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
}
}
@@ -1,134 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.primitive.V8ValueUndefined
import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValueTypedArray
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.serialization.Serializable
import java.util.Base64
class JSRequestExecutor {
private val _plugin: JSClient;
private val _config: IV8PluginConfig;
private var _executor: V8ValueObject;
val urlPrefix: String?;
private val hasCleanup: Boolean;
constructor(plugin: JSClient, executor: V8ValueObject) {
this._plugin = plugin;
this._executor = executor;
this._config = plugin.config;
val config = plugin.config;
urlPrefix = executor.getOrDefault(config, "urlPrefix", "RequestExecutor", null);
if(!executor.has("executeRequest"))
throw ScriptImplementationException(config, "RequestExecutor is missing executeRequest", null);
hasCleanup = executor.has("cleanup");
}
//TODO: Executor properties?
@Throws(ScriptException::class)
open fun executeRequest(method: String, url: String, body: ByteArray?, headers: Map<String, String>): ByteArray {
if (_executor.isClosed)
throw IllegalStateException("Executor object is closed");
val result = if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invoke("executeRequest", url, headers, method, body);
} as V8Value;
}
else V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invoke("executeRequest", url, headers, method, body);
} as V8Value;
try {
if(result is V8ValueString) {
val base64Result = Base64.getDecoder().decode(result.value);
return base64Result;
}
if(result is V8ValueTypedArray) {
val buffer = result.buffer;
val byteBuffer = buffer.byteBuffer;
val bytesResult = ByteArray(result.byteLength);
byteBuffer.get(bytesResult, 0, result.byteLength);
buffer.close();
return bytesResult;
}
if(result is V8ValueObject && result.has("type")) {
val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier");
when(type) {
//TODO: Buffer type?
}
}
if(result is V8ValueUndefined) {
if(_plugin is DevJSClient)
StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined");
throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null);
}
throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name);
}
finally {
result.close();
}
}
open fun cleanup() {
if (!hasCleanup || _executor.isClosed)
return;
if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invokeVoid("cleanup", null);
};
}
else V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invokeVoid("cleanup", null);
};
}
protected fun finalize() {
cleanup();
}
}
//TODO: are these available..?
@Serializable
class ExecutorParameters {
var rangeStart: Int = -1;
var rangeEnd: Int = -1;
var segment: Int = -1;
}
@@ -3,39 +3,22 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
override val bearerToken: String
override val licenseUri: String
override val hasLicenseRequestExecutor: Boolean
@Suppress("ConvertSecondaryConstructorToPrimary")
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
val contextName = "JSAudioUrlWidevineSource"
val config = plugin.config
bearerToken = _obj.getOrThrow(config, "bearerToken", contextName)
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
}
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
if (!hasLicenseRequestExecutor || _obj.isClosed)
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)
return null
return JSRequestExecutor(_plugin, result)
}
override fun toString(): String {
val url = getAudioUrl()
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, hasLicenseRequestExecutor=${hasLicenseRequestExecutor}, licenseUri=$licenseUri)"
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, bearerToken=$bearerToken, licenseUri=$licenseUri)"
}
}
@@ -35,9 +35,4 @@ class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource {
indexEnd = _obj.getOrDefault(config, "indexEnd", contextName, null);
audioChannels = _obj.getOrDefault(config, "audioChannels", contextName, 2) ?: 2;
}
override fun toString(): String {
return "RangeSource(url=[${getAudioUrl()}], itagId=[${itagId}], initStart=[${initStart}], initEnd=[${initEnd}], indexStart=[${indexStart}], indexEnd=[${indexEnd}]))";
return super.toString()
}
}
@@ -1,64 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
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.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.others.Language
import com.futo.platformplayer.states.StateDeveloper
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource {
override val container : String = "application/dash+xml";
override val name : String;
override val codec: String;
override val bitrate: Int;
override val duration: Long;
override val priority: Boolean;
override val language: String;
val url: String;
override var manifest: String?;
override val hasGenerate: Boolean;
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
val contextName = "DashRawSource";
val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", contextName);
manifest = _obj.getOrThrow(config, "manifest", contextName);
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
hasGenerate = _obj.has("generate");
}
override fun generate(): String? {
if(!hasGenerate)
return manifest;
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
val plugin = _plugin.getUnderlyingPlugin();
if(_plugin is DevJSClient)
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_obj.invokeString("generate");
}
}
else
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_obj.invokeString("generate");
}
}
}
@@ -1,114 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueString
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.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.states.StateDeveloper
interface IJSDashManifestRawSource {
val hasGenerate: Boolean;
var manifest: String?;
fun generate(): String?;
}
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource {
override val container : String = "application/dash+xml";
override val name : String;
override val width: Int;
override val height: Int;
override val codec: String;
override val bitrate: Int?;
override val duration: Long;
override val priority: Boolean;
var url: String?;
override var manifest: String?;
override val hasGenerate: Boolean;
val canMerge: Boolean;
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
val contextName = "DashRawSource";
val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", contextName);
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
canMerge = _obj.getOrDefault(config, "canMerge", contextName, false) ?: false;
hasGenerate = _obj.has("generate");
}
override open fun generate(): String? {
if(!hasGenerate)
return manifest;
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
if(_plugin is DevJSClient) {
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_obj.invokeString("generate");
});
}
}
else
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_obj.invokeString("generate");
});
}
}
class JSDashManifestMergingRawSource(
val video: JSDashManifestRawSource,
val audio: JSDashManifestRawAudioSource): JSDashManifestRawSource(video.getUnderlyingPlugin()!!, video.getUnderlyingObject()!!), IVideoSource {
override val name: String
get() = video.name;
override val bitrate: Int
get() = (video.bitrate ?: 0) + audio.bitrate;
override val codec: String
get() = video.codec
override val container: String
get() = video.container
override val duration: Long
get() = video.duration;
override val height: Int
get() = video.height;
override val width: Int
get() = video.width;
override val priority: Boolean
get() = video.priority;
override fun generate(): String? {
val videoDash = video.generate();
val audioDash = audio.generate();
if(videoDash != null && audioDash == null) return videoDash;
if(audioDash != null && videoDash == null) return audioDash;
if(videoDash == null) return null;
//TODO: Temporary simple solution..make more reliable version
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
if(audioAdaptationSet != null) {
return videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
}
else
return videoDash;
}
companion object {
private val adaptationSetRegex = Regex("<AdaptationSet.*?>.*?<\\/AdaptationSet>", RegexOption.DOT_MATCHES_ALL);
}
}
@@ -1,60 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
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.IDashManifestWidevineSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
IDashManifestWidevineSource, JSSource {
override val width: Int = 0
override val height: Int = 0
override val container: String = "application/dash+xml"
override val codec: String = "Dash"
override val name: String
override val bitrate: Int? = null
override val url: String
override val duration: Long
override var priority: Boolean = false
override val licenseUri: String
override val hasLicenseRequestExecutor: Boolean
@Suppress("ConvertSecondaryConstructorToPrimary")
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) {
val contextName = "DashWidevineSource"
val config = plugin.config
name = _obj.getOrThrow(config, "name", contextName)
url = _obj.getOrThrow(config, "url", contextName)
duration = _obj.getOrThrow(config, "duration", contextName)
priority = obj.getOrNull(config, "priority", contextName) ?: false
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
}
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
if (!hasLicenseRequestExecutor || _obj.isClosed)
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)
return null
return JSRequestExecutor(_plugin, result)
}
override fun getVideoUrl(): String {
return url
}
}
@@ -10,12 +10,10 @@ import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
@@ -23,17 +21,9 @@ abstract class JSSource {
protected val _plugin: JSClient;
protected val _config: IV8PluginConfig;
protected val _obj: V8ValueObject;
val hasRequestModifier: Boolean;
private val _requestModifier: JSRequest?;
val hasRequestExecutor: Boolean;
private val _requestExecutor: JSRequest?;
val requiresCustomDatasource: Boolean get() {
return hasRequestModifier || hasRequestExecutor;
}
val type : String;
constructor(type: String, plugin: JSClient, obj: V8ValueObject) {
@@ -46,11 +36,6 @@ abstract class JSSource {
JSRequest(plugin, it, null, null, true);
}
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
_requestExecutor = obj.getOrDefault<V8ValueObject>(_config, "requestExecutor", "JSSource.requestExecutor", null)?.let {
JSRequest(plugin, it, null, null, true);
}
hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor");
}
fun getRequestModifier(): IRequestModifier? {
@@ -59,38 +44,20 @@ abstract class JSSource {
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
};
if (!hasRequestModifier || _obj.isClosed)
if (!hasRequestModifier || _obj.isClosed) {
return null;
}
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") {
_obj.invoke("getRequestModifier", arrayOf<Any>());
};
if (result !is V8ValueObject)
if (result !is V8ValueObject) {
return null;
}
return JSRequestModifier(_plugin, result)
}
open fun getRequestExecutor(): JSRequestExecutor? {
if (!hasRequestExecutor || _obj.isClosed)
return null;
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") {
_obj.invoke("getRequestExecutor", arrayOf<Any>());
};
if (result !is V8ValueObject)
return null;
return JSRequestExecutor(_plugin, result)
}
fun getUnderlyingPlugin(): JSClient? {
return _plugin;
}
fun getUnderlyingObject(): V8ValueObject? {
return _obj;
}
companion object {
const val TYPE_AUDIOURL = "AudioUrlSource";
@@ -98,49 +65,33 @@ abstract class JSSource {
const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource";
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
const val TYPE_DASH = "DashSource";
const val TYPE_DASH_WIDEVINE = "DashWidevineSource";
const val TYPE_DASH_RAW = "DashRawSource";
const val TYPE_DASH_RAW_AUDIO = "DashRawAudioSource";
const val TYPE_HLS = "HLSSource";
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
const val TYPE_VIDEOURL_WIDEVINE = "VideoUrlWidevineSource"
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? {
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource {
val type = obj.getString("plugin_type");
return when(type) {
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
TYPE_VIDEOURL_WIDEVINE -> JSVideoUrlWidevineSource(plugin, obj);
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
TYPE_HLS -> fromV8HLS(plugin, obj);
TYPE_DASH_WIDEVINE -> JSDashManifestWidevineSource(plugin, obj)
TYPE_DASH -> fromV8Dash(plugin, obj);
TYPE_DASH_RAW -> fromV8DashRaw(plugin, obj);
else -> {
Logger.w("JSSource", "Unknown video type ${type}");
null;
};
else -> throw NotImplementedError("Unknown type ${type}");
}
}
fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj);
fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource = JSDashManifestRawSource(plugin, obj);
fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource = JSDashManifestRawAudioSource(plugin, obj);
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj);
fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource? {
fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource {
val type = obj.getString("plugin_type");
return when(type) {
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
TYPE_DASH_RAW_AUDIO -> fromV8DashRawAudio(plugin, obj);
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj);
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
else -> {
Logger.w("JSSource", "Unknown audio type ${type}");
null;
};
else -> throw NotImplementedError("Unknown type ${type}");
}
}
}
@@ -23,11 +23,9 @@ class JSUnMuxVideoSourceDescriptor: VideoUnMuxedSourceDescriptor {
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
.filterNotNull()
.toTypedArray();
this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray()
.map { JSSource.fromV8Audio(plugin, it as V8ValueObject) }
.filterNotNull()
.toTypedArray();
}
}
@@ -21,7 +21,6 @@ class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
.filterNotNull()
.toTypedArray();
}
@@ -1,41 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlWidevineSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow
class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
override val licenseUri: String
override val hasLicenseRequestExecutor: Boolean
@Suppress("ConvertSecondaryConstructorToPrimary")
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
val contextName = "JSAudioUrlWidevineSource"
val config = plugin.config
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
}
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
if (!hasLicenseRequestExecutor || _obj.isClosed)
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)
return null
return JSRequestExecutor(_plugin, result)
}
override fun toString(): String {
val url = getVideoUrl()
return "(width=$width, height=$height, container=$container, codec=$codec, name=$name, bitrate=$bitrate, duration=$duration, url=$url, hasLicenseRequestExecutor=$hasLicenseRequestExecutor, licenseUri=$licenseUri)"
}
}
@@ -33,9 +33,4 @@ class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource {
indexStart = _obj.getOrDefault(config, "indexStart", contextName, null);
indexEnd = _obj.getOrDefault(config, "indexEnd", contextName, null);
}
override fun toString(): String {
return "RangeSource(url=[${getVideoUrl()}], itagId=[${itagId}], initStart=[${initStart}], initEnd=[${initEnd}], indexStart=[${indexStart}], indexEnd=[${indexEnd}]))";
return super.toString()
}
}
@@ -88,8 +88,7 @@ class DashBuilder : XMLBuilder {
fun withRepresentationOnDemand(id: String, subtitleSource: ISubtitleSource, subtitleUrl: String) {
withRepresentation(id, mapOf(
Pair("mimeType", subtitleSource.format ?: "text/vtt"),
Pair("default", "true"),
Pair("lang", "en"),
Pair("startWithSAP", "1"),
Pair("bandwidth", "1000")
)) {
it.withBaseURL(subtitleUrl)
@@ -152,7 +151,7 @@ class DashBuilder : XMLBuilder {
)
) {
//TODO: Verify if & really should be replaced like this?
it.withRepresentationOnDemand("caption_en", subtitleSource, subtitleUrl.replace("&", "&amp;"))
it.withRepresentationOnDemand("1", subtitleSource, subtitleUrl.replace("&", "&amp;"))
}
}
//Video
@@ -165,7 +164,7 @@ class DashBuilder : XMLBuilder {
Pair("subsegmentStartsWithSAP", "1")
)
) {
it.withRepresentationOnDemand("2", vidSource, vidUrl.replace("&", "&amp;"));
it.withRepresentationOnDemand("1", vidSource, vidUrl.replace("&", "&amp;"));
}
}
@@ -6,17 +6,13 @@ import android.net.Uri
import android.os.Looper
import android.util.Base64
import android.util.Log
import android.util.Xml
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import com.futo.platformplayer.Settings
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.http.server.HttpHeaders
import com.futo.platformplayer.api.http.server.ManagedHttpServer
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
import com.futo.platformplayer.api.http.server.handlers.HttpFileHandler
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
@@ -29,23 +25,16 @@ import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSou
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
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.js.models.JSRequestExecutor
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestMergingRawSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
import com.futo.platformplayer.builders.DashBuilder
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.exceptions.UnsupportedCastException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.mdns.DnsService
import com.futo.platformplayer.mdns.ServiceDiscoverer
import com.futo.platformplayer.models.CastingDeviceInfo
import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.CastingDeviceInfoStorage
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.toUrlAddress
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
@@ -53,15 +42,17 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.ByteArrayInputStream
import java.net.InetAddress
import java.net.URLDecoder
import java.net.URLEncoder
import java.util.UUID
import javax.jmdns.JmDNS
import javax.jmdns.ServiceEvent
import javax.jmdns.ServiceListener
import javax.jmdns.ServiceTypeListener
class StateCasting {
private val _scopeIO = CoroutineScope(Dispatchers.IO);
private val _scopeMain = CoroutineScope(Dispatchers.Main);
private var _jmDNS: JmDNS? = null;
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
private val _castServer = ManagedHttpServer(9999);
@@ -78,51 +69,105 @@ class StateCasting {
val onActiveDeviceDurationChanged = Event1<Double>();
val onActiveDeviceVolumeChanged = Event1<Double>();
var activeDevice: CastingDevice? = null;
private var _videoExecutor: JSRequestExecutor? = null
private var _audioExecutor: JSRequestExecutor? = null
private val _client = ManagedHttpClient();
var _resumeCastingDevice: CastingDeviceInfo? = null;
val _serviceDiscoverer = ServiceDiscoverer(arrayOf(
"_googlecast._tcp.local",
"_airplay._tcp.local",
"_fastcast._tcp.local",
"_fcast._tcp.local"
)) { handleServiceUpdated(it) }
val isCasting: Boolean get() = activeDevice != null;
private fun handleServiceUpdated(services: List<DnsService>) {
for (s in services) {
//TODO: Addresses IPv4 only?
val addresses = s.addresses.toTypedArray()
val port = s.port.toInt()
var name = s.texts.firstOrNull { it.startsWith("md=") }?.substring("md=".length)
if (s.name.endsWith("._googlecast._tcp.local")) {
if (name == null) {
name = s.name.substring(0, s.name.length - "._googlecast._tcp.local".length)
}
private val _chromecastServiceListener = object : ServiceListener {
override fun serviceAdded(event: ServiceEvent) {
Logger.i(TAG, "ChromeCast service added: " + event.info);
addOrUpdateDevice(event);
}
addOrUpdateChromeCastDevice(name, addresses, port)
} else if (s.name.endsWith("._airplay._tcp.local")) {
if (name == null) {
name = s.name.substring(0, s.name.length - "._airplay._tcp.local".length)
override fun serviceRemoved(event: ServiceEvent) {
Logger.i(TAG, "ChromeCast service removed: " + event.info);
synchronized(devices) {
val device = devices[event.info.name];
if (device != null) {
onDeviceRemoved.emit(device);
}
addOrUpdateAirPlayDevice(name, addresses, port)
} else if (s.name.endsWith("._fastcast._tcp.local")) {
if (name == null) {
name = s.name.substring(0, s.name.length - "._fastcast._tcp.local".length)
}
addOrUpdateFastCastDevice(name, addresses, port)
} else if (s.name.endsWith("._fcast._tcp.local")) {
if (name == null) {
name = s.name.substring(0, s.name.length - "._fcast._tcp.local".length)
}
addOrUpdateFastCastDevice(name, addresses, port)
}
}
override fun serviceResolved(event: ServiceEvent) {
Logger.v(TAG, "ChromeCast service resolved: " + event.info);
addOrUpdateDevice(event);
}
fun addOrUpdateDevice(event: ServiceEvent) {
addOrUpdateChromeCastDevice(event.info.name, event.info.inetAddresses, event.info.port);
}
}
private val _airPlayServiceListener = object : ServiceListener {
override fun serviceAdded(event: ServiceEvent) {
Logger.i(TAG, "AirPlay service added: " + event.info);
addOrUpdateDevice(event);
}
override fun serviceRemoved(event: ServiceEvent) {
Logger.i(TAG, "AirPlay service removed: " + event.info);
synchronized(devices) {
val device = devices[event.info.name];
if (device != null) {
onDeviceRemoved.emit(device);
}
}
}
override fun serviceResolved(event: ServiceEvent) {
Logger.i(TAG, "AirPlay service resolved: " + event.info);
addOrUpdateDevice(event);
}
fun addOrUpdateDevice(event: ServiceEvent) {
addOrUpdateAirPlayDevice(event.info.name, event.info.inetAddresses, event.info.port);
}
}
private val _fastCastServiceListener = object : ServiceListener {
override fun serviceAdded(event: ServiceEvent) {
Logger.i(TAG, "FastCast service added: " + event.info);
addOrUpdateDevice(event);
}
override fun serviceRemoved(event: ServiceEvent) {
Logger.i(TAG, "FastCast service removed: " + event.info);
synchronized(devices) {
val device = devices[event.info.name];
if (device != null) {
onDeviceRemoved.emit(device);
}
}
}
override fun serviceResolved(event: ServiceEvent) {
Logger.i(TAG, "FastCast service resolved: " + event.info);
addOrUpdateDevice(event);
}
fun addOrUpdateDevice(event: ServiceEvent) {
addOrUpdateFastCastDevice(event.info.name, event.info.inetAddresses, event.info.port);
}
}
private val _serviceTypeListener = object : ServiceTypeListener {
override fun serviceTypeAdded(event: ServiceEvent?) {
if (event == null) {
return;
}
Logger.i(TAG, "Service type added (name: ${event.name}, type: ${event.type})");
}
override fun subTypeForServiceTypeAdded(event: ServiceEvent?) {
if (event == null) {
return;
}
Logger.i(TAG, "Sub type for service type added (name: ${event.name}, type: ${event.type})");
}
}
fun handleUrl(context: Context, url: String) {
@@ -191,30 +236,29 @@ class StateCasting {
rememberedDevices.clear();
rememberedDevices.addAll(_storage.deviceInfos.map { deviceFromCastingDeviceInfo(it) });
_scopeIO.launch {
try {
val jmDNS = JmDNS.create(InetAddress.getLocalHost());
jmDNS.addServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
jmDNS.addServiceListener("_airplay._tcp.local.", _airPlayServiceListener);
jmDNS.addServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
jmDNS.addServiceListener("_fcast._tcp.local.", _fastCastServiceListener);
if (BuildConfig.DEBUG) {
jmDNS.addServiceTypeListener(_serviceTypeListener);
}
_jmDNS = jmDNS;
} catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting service.", e);
}
}
_castServer.start();
enableDeveloper(true);
Logger.i(TAG, "CastingService started.");
}
@Synchronized
fun startDiscovering() {
try {
_serviceDiscoverer.start()
} catch (e: Throwable) {
Logger.i(TAG, "Failed to start ServiceDiscoverer", e)
}
}
@Synchronized
fun stopDiscovering() {
try {
_serviceDiscoverer.stop()
} catch (e: Throwable) {
Logger.i(TAG, "Failed to stop ServiceDiscoverer", e)
}
}
@Synchronized
fun stop() {
if (!_started)
@@ -224,7 +268,25 @@ class StateCasting {
Logger.i(TAG, "CastingService stopping.")
stopDiscovering()
val jmDNS = _jmDNS;
if (jmDNS != null) {
_scopeIO.launch {
try {
jmDNS.removeServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
jmDNS.removeServiceListener("_airplay._tcp", _airPlayServiceListener);
jmDNS.removeServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
if (BuildConfig.DEBUG) {
jmDNS.removeServiceTypeListener(_serviceTypeListener);
}
jmDNS.close();
} catch (e: Throwable) {
Logger.e(TAG, "Failed to stop mDNS.", e);
}
}
}
_scopeIO.cancel();
_scopeMain.cancel();
@@ -374,26 +436,15 @@ class StateCasting {
} else {
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource
if (isRawDash) {
Logger.i(TAG, "Casting as raw DASH");
try {
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource} audioSource=${audioSource}.", e);
}
if (ad is FCastCastingDevice) {
Logger.i(TAG, "Casting as DASH direct");
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} else if (ad is AirPlayCastingDevice) {
Logger.i(TAG, "Casting as HLS indirect");
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} else {
if (ad is FCastCastingDevice) {
Logger.i(TAG, "Casting as DASH direct");
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} else if (ad is AirPlayCastingDevice) {
Logger.i(TAG, "Casting as HLS indirect");
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} else {
Logger.i(TAG, "Casting as DASH indirect");
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
}
Logger.i(TAG, "Casting as DASH indirect");
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
@@ -401,22 +452,14 @@ class StateCasting {
}
}
} else {
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID();
if (videoSource is IVideoUrlSource) {
val videoPath = "/video-${id}"
val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl();
Logger.i(TAG, "Casting as singular video");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed);
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble(), speed);
} else if (audioSource is IAudioUrlSource) {
val audioPath = "/audio-${id}"
val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl();
Logger.i(TAG, "Casting as singular audio");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed);
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble(), speed);
} else if(videoSource is IHLSManifestSource) {
if (proxyStreams || ad is ChromecastCastingDevice) {
if (ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied HLS");
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
} else {
@@ -424,7 +467,7 @@ class StateCasting {
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed);
}
} else if(audioSource is IHLSManifestAudioSource) {
if (proxyStreams || ad is ChromecastCastingDevice) {
if (ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied audio HLS");
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
} else {
@@ -437,26 +480,6 @@ class StateCasting {
} else if (audioSource is LocalAudioSource) {
Logger.i(TAG, "Casting as local audio");
castLocalAudio(video, audioSource, resumePosition, speed);
} else if (videoSource is JSDashManifestRawSource) {
Logger.i(TAG, "Casting as JSDashManifestRawSource video");
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource}.", e);
}
}
} else if (audioSource is JSDashManifestRawAudioSource) {
Logger.i(TAG, "Casting as JSDashManifestRawSource audio");
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting DASH raw audioSource=${audioSource}.", e);
}
}
} else {
var str = listOf(
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
@@ -497,7 +520,7 @@ class StateCasting {
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf();
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID();
val videoPath = "/video-${id}"
val videoUrl = url + videoPath;
@@ -516,7 +539,7 @@ class StateCasting {
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf();
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID();
val audioPath = "/audio-${id}"
val audioUrl = url + audioPath;
@@ -535,7 +558,7 @@ class StateCasting {
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
val ad = activeDevice ?: return listOf()
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}"
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"
val id = UUID.randomUUID()
val hlsPath = "/hls-${id}"
@@ -631,7 +654,7 @@ class StateCasting {
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf();
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID();
val dashPath = "/dash-${id}"
@@ -644,11 +667,8 @@ class StateCasting {
val audioUrl = url + audioPath;
val subtitleUrl = url + subtitlePath;
val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl);
Logger.v(TAG) { "Dash manifest: $dashContent" };
_castServer.addHandlerWithAllowAllOptions(
HttpConstantHandler("GET", dashPath, dashContent,
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl),
"application/dash+xml")
.withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast");
@@ -679,17 +699,13 @@ class StateCasting {
private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf();
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID();
val subtitlePath = "/subtitle-${id}";
val videoPath = "/video-${id}"
val audioPath = "/audio-${id}"
val subtitlePath = "/subtitle-${id}"
val videoUrl = if(proxyStreams) url + videoPath else videoSource?.getVideoUrl();
val audioUrl = if(proxyStreams) url + audioPath else audioSource?.getAudioUrl();
val videoUrl = videoSource?.getVideoUrl();
val audioUrl = audioSource?.getAudioUrl();
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
return@withContext subtitleSource.getSubtitlesURI();
@@ -718,42 +734,26 @@ class StateCasting {
}
}
if (videoSource != null) {
_castServer.addHandlerWithAllowAllOptions(
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
.withInjectedHost()
.withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast");
}
if (audioSource != null) {
_castServer.addHandlerWithAllowAllOptions(
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
.withInjectedHost()
.withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast");
}
val content = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
Logger.i(TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl).");
Logger.v(TAG) { "Dash manifest: $content" };
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed);
return listOf(videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "");
}
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
_castServer.removeAllHandlers("castProxiedHlsMaster")
val ad = activeDevice ?: return listOf();
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID();
val hlsPath = "/hls-${id}"
val hlsUrl = url + hlsPath
Logger.i(TAG, "HLS url: $hlsUrl");
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", hlsPath) { masterContext ->
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", hlsPath) { masterContext ->
_castServer.removeAllHandlers("castProxiedHlsVariant")
val headers = masterContext.headers.clone()
@@ -780,7 +780,7 @@ class StateCasting {
val proxiedVariantPlaylist = proxyVariantPlaylist(url, id, variantPlaylist, video.isLive)
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
return@HttpFunctionHandler
return@HttpFuntionHandler
} else {
throw e
}
@@ -797,7 +797,7 @@ class StateCasting {
val newPlaylistPath = "/hls-playlist-${playlistId}"
val newPlaylistUrl = url + newPlaylistPath;
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", newPlaylistPath) { vpContext ->
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
val vpHeaders = vpContext.headers.clone()
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
@@ -827,7 +827,7 @@ class StateCasting {
val newPlaylistPath = "/hls-playlist-${playlistId}"
newPlaylistUrl = url + newPlaylistPath
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", newPlaylistPath) { vpContext ->
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
val vpHeaders = vpContext.headers.clone()
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
@@ -916,7 +916,7 @@ class StateCasting {
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf();
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID();
val hlsPath = "/hls-${id}"
@@ -1044,9 +1044,9 @@ class StateCasting {
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf();
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
val proxyStreams = ad !is FCastCastingDevice;
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID();
val dashPath = "/dash-${id}"
@@ -1090,11 +1090,8 @@ class StateCasting {
}
}
val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
Logger.v(TAG) { "Dash manifest: $dashContent" };
_castServer.addHandlerWithAllowAllOptions(
HttpConstantHandler("GET", dashPath, dashContent,
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl),
"application/dash+xml")
.withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast");
@@ -1120,166 +1117,6 @@ class StateCasting {
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
}
private fun cleanExecutors() {
if (_videoExecutor != null) {
_videoExecutor?.cleanup()
_videoExecutor = null
}
if (_audioExecutor != null) {
_audioExecutor?.cleanup()
_audioExecutor = null
}
}
@OptIn(UnstableApi::class)
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf();
cleanExecutors()
_castServer.removeAllHandlers("castDashRaw")
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID();
val dashPath = "/dash-${id}"
val videoPath = "/video-${id}"
val audioPath = "/audio-${id}"
val subtitlePath = "/subtitle-${id}"
val dashUrl = url + dashPath;
Logger.i(TAG, "DASH url: $dashUrl");
val videoUrl = url + videoPath
val audioUrl = url + audioPath
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
return@withContext subtitleSource.getSubtitlesURI();
} else null;
var subtitlesUrl: String? = null;
if (subtitlesUri != null) {
if(subtitlesUri.scheme == "file") {
var content: String? = null;
val inputStream = contentResolver.openInputStream(subtitlesUri);
inputStream?.use { stream ->
val reader = stream.bufferedReader();
content = reader.use { it.readText() };
}
if (content != null) {
_castServer.addHandlerWithAllowAllOptions(
HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt")
.withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast");
}
subtitlesUrl = url + subtitlePath;
} else {
subtitlesUrl = subtitlesUri.toString();
}
}
var dashContent = withContext(Dispatchers.IO) {
//TODO: Include subtitlesURl in the future
return@withContext if (audioSource != null && videoSource != null) {
JSDashManifestMergingRawSource(videoSource, audioSource).generate()
} else if (audioSource != null) {
audioSource.generate()
} else if (videoSource != null) {
videoSource.generate()
} else {
Logger.e(TAG, "Expected at least audio or video to be set")
null
}
} ?: throw Exception("Dash is null")
for (representation in representationRegex.findAll(dashContent)) {
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
dashContent = mediaInitializationRegex.replace(dashContent) {
if (it.range.first < representation.range.first || it.range.last > representation.range.last) {
return@replace it.value
}
if (mediaType.startsWith("video/")) {
return@replace "${it.groups[1]!!.value}=\"${videoUrl}?url=${URLEncoder.encode(it.groups[2]!!.value, "UTF-8").replace("%24Number%24", "\$Number\$")}&amp;mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\""
} else if (mediaType.startsWith("audio/")) {
return@replace "${it.groups[1]!!.value}=\"${audioUrl}?url=${URLEncoder.encode(it.groups[2]!!.value, "UTF-8").replace("%24Number%24", "\$Number\$")}&amp;mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\""
} else {
throw Exception("Expected audio or video")
}
}
}
if (videoSource != null && !videoSource.hasRequestExecutor) {
throw Exception("Video source without request executor not supported")
}
if (audioSource != null && !audioSource.hasRequestExecutor) {
throw Exception("Audio source without request executor not supported")
}
if (audioSource != null && audioSource.hasRequestExecutor) {
_audioExecutor = audioSource.getRequestExecutor()
}
if (videoSource != null && videoSource.hasRequestExecutor) {
_videoExecutor = videoSource.getRequestExecutor()
}
//TOOD: Else also handle the non request executor case, perhaps add ?url=$originalUrl to the query parameters, ... propagate this for all other flows also
Logger.v(TAG) { "Dash manifest: $dashContent" };
_castServer.addHandlerWithAllowAllOptions(
HttpConstantHandler("GET", dashPath, dashContent,
"application/dash+xml")
.withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("castDashRaw");
if (videoSource != null) {
_castServer.addHandlerWithAllowAllOptions(
HttpFunctionHandler("GET", videoPath) { httpContext ->
val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
val mediaType = httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
val videoExecutor = _videoExecutor;
if (videoExecutor != null) {
val data = videoExecutor.executeRequest("GET", originalUrl, null, httpContext.headers)
httpContext.respondBytes(200, HttpHeaders().apply {
put("Content-Type", mediaType)
}, data);
} else {
throw NotImplementedError()
}
}.withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("castDashRaw");
}
if (audioSource != null) {
_castServer.addHandlerWithAllowAllOptions(
HttpFunctionHandler("GET", audioPath) { httpContext ->
val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
val mediaType = httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
val audioExecutor = _audioExecutor;
if (audioExecutor != null) {
val data = audioExecutor.executeRequest("GET", originalUrl, null, httpContext.headers)
httpContext.respondBytes(200, HttpHeaders().apply {
put("Content-Type", mediaType)
}, data);
} else {
throw NotImplementedError()
}
}.withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("castDashRaw");
}
Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath).");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), speed);
return listOf()
}
private fun deviceFromCastingDeviceInfo(deviceInfo: CastingDeviceInfo): CastingDevice {
return when (deviceInfo.type) {
CastProtocolType.CHROMECAST -> {
@@ -1374,7 +1211,7 @@ class StateCasting {
}
} else {
val newDevice = deviceFactory();
this.devices[name] = newDevice;
devices[name] = newDevice;
invokeEvents = {
onDeviceAdded.emit(newDevice);
@@ -1388,7 +1225,7 @@ class StateCasting {
fun enableDeveloper(enableDev: Boolean){
_castServer.removeAllHandlers("dev");
if(enableDev) {
_castServer.addHandler(HttpFunctionHandler("GET", "/dashPlayer") { context ->
_castServer.addHandler(HttpFuntionHandler("GET", "/dashPlayer") { context ->
if (context.query.containsKey("dashUrl")) {
val dashUrl = context.query["dashUrl"];
val html = "<div>\n" +
@@ -1428,9 +1265,6 @@ class StateCasting {
companion object {
val instance: StateCasting = StateCasting();
private val representationRegex = Regex("<Representation .*?mimeType=\"(.*?)\".*?>(.*?)<\\/Representation>", RegexOption.DOT_MATCHES_ALL)
private val mediaInitializationRegex = Regex("(media|initiali[sz]ation)=\"([^\"]+)\"", RegexOption.DOT_MATCHES_ALL);
private val TAG = "StateCasting";
}
}
@@ -25,8 +25,10 @@ import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.states.StatePlatform
import com.google.gson.ExclusionStrategy
import com.google.gson.FieldAttributes
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -571,7 +573,7 @@ class DeveloperEndpoints(private val context: Context) {
val resp = _client.get(body.url!!, body.headers);
context.respondCode(200,
Json.encodeToString(PackageHttp.BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string())),
Json.encodeToString(PackageHttp.BridgeHttpResponse(resp.url, resp.code, resp.body?.string())),
context.query.getOrDefault("CT", "text/plain"));
}
catch(ex: Exception) {
@@ -22,7 +22,6 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
private lateinit var _buttonCancel: ImageButton;
private lateinit var _editPassword: EditText;
private lateinit var _editPassword2: EditText;
private lateinit var _inputMethodManager: InputMethodManager;
@@ -35,7 +34,6 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
_buttonStop = findViewById(R.id.button_stop);
_buttonStart = findViewById(R.id.button_start);
_editPassword = findViewById(R.id.edit_password);
_editPassword2 = findViewById(R.id.edit_password2);
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
@@ -54,13 +52,6 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
}
_buttonStart.setOnClickListener {
val p1 = _editPassword.text.toString();
val p2 = _editPassword2.text.toString();
if(!(p1?.equals(p2) ?: false)) {
UIDialogs.toast(context, "Password fields do not match, confirm that you typed it correctly.");
return@setOnClickListener;
}
val pbytes = _editPassword.text.toString().toByteArray();
if(pbytes.size < 4 || pbytes.size > 32) {
UIDialogs.toast(context, "Password needs to be atleast 4 bytes long and smaller than 32 bytes", false);
@@ -6,7 +6,6 @@ import android.graphics.Color
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
@@ -58,21 +57,11 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
_editComment = findViewById(R.id.edit_comment);
_textCharacterCount = findViewById(R.id.character_count);
_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 {
override fun afterTextChanged(s: Editable?) = Unit
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, c: Int) {
val count = s?.length ?: 0;
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
_textCharacterCount.text = count.toString();
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
@@ -90,13 +79,10 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
_buttonCancel.setOnClickListener {
handleCloseAttempt()
clearFocus();
dismiss();
};
setOnCancelListener {
handleCloseAttempt()
}
_buttonCreate.setOnClickListener {
clearFocus();
@@ -148,22 +134,6 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
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() {
_editComment.requestFocus();
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
@@ -104,8 +104,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
super.show();
Logger.i(TAG, "Dialog shown.");
StateCasting.instance.startDiscovering()
(_imageLoader.drawable as Animatable?)?.start();
_devices.clear();
@@ -171,7 +169,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
(_imageLoader.drawable as Animatable?)?.stop();
StateCasting.instance.stopDiscovering()
StateCasting.instance.onDeviceAdded.remove(this);
StateCasting.instance.onDeviceChanged.remove(this);
StateCasting.instance.onDeviceRemoved.remove(this);
@@ -12,8 +12,6 @@ import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescri
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
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.streams.sources.IVideoUrlSource
@@ -27,14 +25,6 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.api.media.platforms.js.models.JSVideo
import com.futo.platformplayer.api.media.platforms.js.models.sources.IJSDashManifestRawSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.exceptions.DownloadException
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
@@ -44,7 +34,6 @@ import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.toHumanBitrate
import com.futo.platformplayer.toHumanBytesSpeed
import hasAnySource
@@ -57,12 +46,9 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.serialization.Contextual
import kotlinx.serialization.Transient
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.lang.Thread.sleep
import java.time.OffsetDateTime
import java.util.UUID
import java.util.concurrent.Executors
@@ -70,7 +56,6 @@ import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask
import java.util.concurrent.ThreadLocalRandom
import kotlin.coroutines.resumeWithException
import kotlin.time.times
@kotlinx.serialization.Serializable
class VideoDownload {
@@ -86,51 +71,12 @@ class VideoDownload {
var targetPixelCount: Long? = null;
var targetBitrate: Long? = null;
var targetVideoName: String? = null;
var targetAudioName: String? = null;
var videoSource: VideoUrlSource?;
var audioSource: AudioUrlSource?;
@Contextual
@Transient
val videoSourceToUse: IVideoSource? get () = if(requiresLiveVideoSource) videoSourceLive as IVideoSource? else videoSource as IVideoSource?;
@Contextual
@Transient
val audioSourceToUse: IAudioSource? get () = if(requiresLiveAudioSource) audioSourceLive as IAudioSource? else audioSource as IAudioSource?;
var requireVideoSource: Boolean = false;
var requireAudioSource: Boolean = false;
var requiredCheck: Boolean = false;
@Contextual
@Transient
val isVideoDownloadReady: Boolean get() = !requireVideoSource ||
((requiresLiveVideoSource && isLiveVideoSourceValid) || (!requiresLiveVideoSource && videoSource != null));
@Contextual
@Transient
val isAudioDownloadReady: Boolean get() = !requireAudioSource ||
((requiresLiveAudioSource && isLiveAudioSourceValid) || (!requiresLiveAudioSource && audioSource != null));
var subtitleSource: SubtitleRawSource?;
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
var prepareTime: OffsetDateTime? = null;
var requiresLiveVideoSource: Boolean = false;
@Contextual
@kotlinx.serialization.Transient
var videoSourceLive: JSSource? = null;
val isLiveVideoSourceValid get() = videoSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
var requiresLiveAudioSource: Boolean = false;
@Contextual
@kotlinx.serialization.Transient
var audioSourceLive: JSSource? = null;
val isLiveAudioSourceValid get() = audioSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
var hasVideoRequestExecutor: Boolean = false;
var hasAudioRequestExecutor: Boolean = false;
var progress: Double = 0.0;
var isCancelled = false;
@@ -165,40 +111,21 @@ class VideoDownload {
onStateChanged.emit(newState);
}
constructor(video: IPlatformVideo, targetPixelCount: Long? = null, targetBitrate: Long? = null, optionalSources: Boolean = false) {
constructor(video: IPlatformVideo, targetPixelCount: Long? = null, targetBitrate: Long? = null) {
this.video = SerializedPlatformVideo.fromVideo(video);
this.videoSource = null;
this.audioSource = null;
this.subtitleSource = null;
this.targetPixelCount = targetPixelCount;
this.targetBitrate = targetBitrate;
this.hasVideoRequestExecutor = video is JSSource && video.hasRequestExecutor;
this.requiresLiveVideoSource = false;
this.requiresLiveAudioSource = false;
this.targetVideoName = videoSource?.name;
this.requireVideoSource = targetPixelCount != null;
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: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: SubtitleRawSource?) {
this.video = SerializedPlatformVideo.fromVideo(video);
this.videoDetails = SerializedPlatformVideoDetails.fromVideo(video, if (subtitleSource != null) listOf(subtitleSource) else listOf());
this.videoSource = if(videoSource is IVideoUrlSource) VideoUrlSource.fromUrlSource(videoSource) else null;
this.audioSource = if(audioSource is IAudioUrlSource) AudioUrlSource.fromUrlSource(audioSource) else null;
this.videoSourceLive = if(videoSource is JSSource) videoSource else null;
this.audioSourceLive = if(audioSource is JSSource) audioSource else null;
this.videoSource = VideoUrlSource.fromUrlSource(videoSource);
this.audioSource = AudioUrlSource.fromUrlSource(audioSource);
this.subtitleSource = subtitleSource;
this.prepareTime = OffsetDateTime.now();
this.hasVideoRequestExecutor = videoSource is JSSource && videoSource.hasRequestExecutor;
this.hasAudioRequestExecutor = audioSource is JSSource && audioSource.hasRequestExecutor;
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (videoSource is JSDashManifestRawSource && videoSource.hasGenerate);
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (audioSource is JSDashManifestRawAudioSource && audioSource.hasGenerate);
this.targetVideoName = videoSource?.name;
this.targetAudioName = audioSource?.name;
this.targetPixelCount = if(videoSource != null) (videoSource.width * videoSource.height).toLong() else null;
this.targetBitrate = if(audioSource != null) audioSource.bitrate.toLong() else null;
this.requireVideoSource = videoSource != null;
this.requireAudioSource = audioSource != null;
}
fun withGroup(groupType: String, groupID: String): VideoDownload {
@@ -229,21 +156,9 @@ class VideoDownload {
suspend fun prepare(client: ManagedHttpClient) {
Logger.i(TAG, "VideoDownload Prepare [${name}]");
//If live sources are required, ensure a live object is present
if(requiresLiveVideoSource && !isLiveVideoSourceValid) {
videoDetails = null;
videoSource = null;
videoSourceLive = null;
}
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
videoDetails = null;
audioSource = null;
videoSourceLive = null;
}
if(video == null && videoDetails == null)
throw IllegalStateException("Missing information for download to complete");
if(targetPixelCount == null && targetBitrate == null && videoSource == null && audioSource == null && targetVideoName == null && targetAudioName == null)
if(targetPixelCount == null && targetBitrate == null && videoSource == null && audioSource == null)
throw IllegalStateException("No sources or query values set");
//Fetch full video object and determine source
@@ -252,30 +167,6 @@ class VideoDownload {
if(original !is IPlatformVideoDetails)
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()) {
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
throw DownloadException("Unsupported video for downloading", false);
@@ -301,35 +192,23 @@ class VideoDownload {
videoSources.add(source)
}
}
var vsource: IVideoSource? = null;
if(targetVideoName != null)
vsource = videoSources.find { x -> x.isDownloadable() && x.name == targetVideoName };
if(vsource == null && targetPixelCount == null)
throw IllegalStateException("Could not find comparable downloadable video stream (No target pixel count)");
if(vsource == null)
vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
val vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
// ?: throw IllegalStateException("Could not find a valid video source for video");
if(vsource == null) {
videoSource = null;
if(original.video.videoSources.size == 0)
requireVideoSource = false;
if(vsource != null) {
if (vsource is IVideoUrlSource)
videoSource = VideoUrlSource.fromUrlSource(vsource)
else
throw DownloadException("Video source is not supported for downloading (yet)", false);
}
else if(vsource is IVideoUrlSource)
videoSource = VideoUrlSource.fromUrlSource(vsource)
else if(vsource is JSSource && requiresLiveVideoSource)
videoSourceLive = vsource;
else
throw DownloadException("Video source is not supported for downloading (yet) [" + vsource?.javaClass?.name + "]", false);
}
if(audioSource == null && targetBitrate != null) {
var audioSources = mutableListOf<IAudioSource>()
val audioSources = arrayListOf<IAudioSource>()
val video = original.video
if (video is VideoUnMuxedSourceDescriptor) {
for (source in video.audioSources) {
if (source is IHLSManifestAudioSource) {
if (source is IHLSManifestSource) {
try {
val playlistResponse = client.get(source.url)
if (playlistResponse.isOk) {
@@ -347,43 +226,25 @@ class VideoDownload {
}
}
var asource: IAudioSource? = null;
if(targetAudioName != null) {
val filteredAudioSources = audioSources.filter { x -> x.isDownloadable() && x.name == targetAudioName }.toTypedArray();
if(filteredAudioSources.size == 1)
asource = filteredAudioSources.first();
else if(filteredAudioSources.size > 1)
audioSources = filteredAudioSources.toMutableList();
}
if(asource == null && targetBitrate == null)
throw IllegalStateException("Could not find comparable downloadable video stream (No target bitrate)");
val asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
?: if(videoSource != null ) null
else throw DownloadException("Could not find a valid video or audio source for download")
if(asource == null)
asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
?: if(videoSource != null ) null
else throw DownloadException("Could not find a valid video or audio source for download")
if(asource == null) {
audioSource = null;
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
requireVideoSource = false;
}
else if(asource is IAudioUrlSource)
audioSource = AudioUrlSource.fromUrlSource(asource)
else if(asource is JSSource && requiresLiveAudioSource)
audioSourceLive = asource;
else
throw DownloadException("Audio source is not supported for downloading (yet) [" + asource?.javaClass?.name + "]", false);
throw DownloadException("Audio source is not supported for downloading (yet)", false);
}
if(!isVideoDownloadReady)
throw DownloadException("No valid sources found for video");
if(!isAudioDownloadReady)
throw DownloadException("No valid sources found for audio");
if(videoSource == null && audioSource == null)
throw DownloadException("No valid sources found for video/audio");
}
}
suspend fun download(context: Context, client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
Logger.i(TAG, "VideoDownload Download [${name}]");
if(videoDetails == null || (videoSourceToUse == null && audioSourceToUse == null))
if(videoDetails == null || (videoSource == null && audioSource == null))
throw IllegalStateException("Missing information for download to complete");
val downloadDir = StateDownloads.instance.getDownloadsDirectory();
@@ -392,19 +253,12 @@ class VideoDownload {
if(isCancelled) throw CancellationException("Download got cancelled");
val actualVideoSource = if(requiresLiveVideoSource && videoSourceLive is IVideoSource)
videoSourceLive as IVideoSource?;
else videoSource;
val actualAudioSource = if(requiresLiveAudioSource && audioSourceLive is IAudioSource)
audioSourceLive as IAudioSource?;
else audioSource;
if(actualVideoSource != null) {
videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource!!.container)}".sanitizeFileName();
if(videoSource != null) {
videoFileName = "${videoDetails!!.id.value!!} [${videoSource!!.width}x${videoSource!!.height}].${videoContainerToExtension(videoSource!!.container)}".sanitizeFileName();
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
}
if(actualAudioSource != null) {
audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName();
if(audioSource != null) {
audioFileName = "${videoDetails!!.id.value!!} [${audioSource!!.language}-${audioSource!!.bitrate}].${audioContainerToExtension(audioSource!!.container)}".sanitizeFileName();
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
}
if(subtitleSource != null) {
@@ -419,11 +273,10 @@ class VideoDownload {
var lastAudioLength: Long = 0;
var lastAudioRead: Long = 0;
if(actualVideoSource != null) {
if(videoSource != null) {
sourcesToDownload.add(async {
Logger.i(TAG, "Started downloading video");
var lastEmit = 0L;
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
synchronized(progressLock) {
lastVideoLength = length;
@@ -436,34 +289,23 @@ class VideoDownload {
val total = lastVideoRead + lastAudioRead;
if(totalLength > 0) {
val percentage = (total / totalLength.toDouble());
onProgress?.invoke(percentage);
progress = percentage;
val now = System.currentTimeMillis();
if(now - lastEmit > 200) {
lastEmit = System.currentTimeMillis();
onProgress?.invoke(percentage);
onProgressChanged.emit(percentage);
}
onProgressChanged.emit(percentage);
}
}
}
if(actualVideoSource is IVideoUrlSource)
videoFileSize = when (videoSource!!.container) {
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
else -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
}
else if(actualVideoSource is JSDashManifestRawSource) {
videoFileSize = downloadDashFileSource("Video", client, actualVideoSource, File(downloadDir, videoFileName!!), progressCallback);
videoFileSize = when (videoSource!!.container) {
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
else -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
}
else throw NotImplementedError("NotImplemented video download: " + actualVideoSource.javaClass.name);
});
}
if(actualAudioSource != null) {
if(audioSource != null) {
sourcesToDownload.add(async {
Logger.i(TAG, "Started downloading audio");
var lastEmit = 0L;
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
synchronized(progressLock) {
lastAudioLength = length;
@@ -476,27 +318,17 @@ class VideoDownload {
val total = lastVideoRead + lastAudioRead;
if(totalLength > 0) {
val percentage = (total / totalLength.toDouble());
onProgress?.invoke(percentage);
progress = percentage;
val now = System.currentTimeMillis();
if(now - lastEmit > 200) {
lastEmit = System.currentTimeMillis();
onProgress?.invoke(percentage);
onProgressChanged.emit(percentage);
}
onProgressChanged.emit(percentage);
}
}
}
if(actualAudioSource is IAudioUrlSource)
audioFileSize = when (audioSource!!.container) {
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
else -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
}
else if(actualAudioSource is JSDashManifestRawAudioSource) {
audioFileSize = downloadDashFileSource("Audio", client, actualAudioSource, File(downloadDir, audioFileName!!), progressCallback);
audioFileSize = when (audioSource!!.container) {
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
else -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
}
else throw NotImplementedError("NotImplemented audio download: " + actualAudioSource.javaClass.name);
});
}
if (subtitleSource != null) {
@@ -566,20 +398,15 @@ class VideoDownload {
Logger.i(TAG, "Download '$name' segment $index Sequential");
val segmentFile = File(context.cacheDir, "segment-${UUID.randomUUID()}")
val outputStream = segmentFile.outputStream()
try {
segmentFiles.add(segmentFile)
segmentFiles.add(segmentFile)
val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri) { segmentLength, totalRead, lastSpeed ->
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
}
downloadedTotalLength += segmentLength
} finally {
outputStream.close()
val segmentLength = downloadSource_Sequential(client, segmentFile.outputStream(), segment.uri) { segmentLength, totalRead, lastSpeed ->
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
}
downloadedTotalLength += segmentLength
}
Logger.i(TAG, "Combining segments into $targetFile");
@@ -646,86 +473,6 @@ class VideoDownload {
}
}
private fun downloadDashFileSource(name: String, client: ManagedHttpClient, source: IJSDashManifestRawSource, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
if(targetFile.exists())
targetFile.delete();
targetFile.createNewFile();
val sourceLength: Long?;
val fileStream = FileOutputStream(targetFile);
try{
var manifest = source.manifest;
if(source.hasGenerate)
manifest = source.generate();
if(manifest == null)
throw IllegalStateException("No manifest after generation");
//TODO: Temporary naive assume single-sourced dash
val foundTemplate = REGEX_DASH_TEMPLATE.find(manifest);
if(foundTemplate == null || foundTemplate.groupValues.size != 3)
throw IllegalStateException("No SegmentTemplate found in manifest (unsupported dash?)");
val foundTemplateUrl = foundTemplate.groupValues[1];
val foundCues = REGEX_DASH_CUE.findAll(foundTemplate.groupValues[2]);
if(foundCues.count() <= 0)
throw IllegalStateException("No Cues found in manifest (unsupported dash?)");
val executor = if(source is JSSource && source.hasRequestExecutor)
source.getRequestExecutor();
else
null;
val speedTracker = SpeedTracker(1000);
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
var written = 0;
var indexCounter = 0;
onProgress(foundCues.count().toLong(), 0, 0);
for(cue in foundCues) {
val t = cue.groupValues[1];
val d = cue.groupValues[2];
val url = foundTemplateUrl.replace("\$Number\$", indexCounter.toString());
val data = if(executor != null)
executor.executeRequest("GET", url, null, mapOf());
else {
val resp = client.get(url, mutableMapOf());
if(!resp.isOk)
throw IllegalStateException("Dash request failed for index " + indexCounter.toString() + ", with code: " + resp.code.toString());
resp.body!!.bytes()
}
fileStream.write(data, 0, data.size);
speedTracker.addWork(data.size.toLong());
written += data.size;
onProgress(foundCues.count().toLong(), indexCounter.toLong(), speedTracker.lastSpeed);
indexCounter++;
}
sourceLength = written.toLong();
Logger.i(TAG, "$name downloadSource Finished");
}
catch(ioex: IOException) {
if(targetFile.exists() ?: false)
targetFile.delete();
if(ioex.message?.contains("ENOSPC") ?: false)
throw Exception("Not enough space on device", ioex);
else
throw ioex;
}
catch(ex: Throwable) {
if(targetFile.exists() ?: false)
targetFile.delete();
throw ex;
}
finally {
fileStream.close();
}
return sourceLength!!;
}
private fun downloadFileSource(name: String, client: ManagedHttpClient, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
if(targetFile.exists())
targetFile.delete();
@@ -737,25 +484,17 @@ class VideoDownload {
try{
val head = client.tryHead(videoUrl);
val relatedPlugin = (video?.url ?: videoDetails?.url)?.let { StatePlatform.instance.getContentClient(it) }?.let { if(it is JSClient) it else null };
if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length"))
{
val maxParallel = if(relatedPlugin != null && relatedPlugin.config.maxDownloadParallelism > 0)
relatedPlugin.config.maxDownloadParallelism else 99;
val concurrency = Math.min(maxParallel, Settings.instance.downloads.getByteRangeThreadCount());
Logger.i(TAG, "Download $name ByteRange Parallel (${concurrency}): " + videoUrl);
val concurrency = Settings.instance.downloads.getByteRangeThreadCount();
Logger.i(TAG, "Download $name ByteRange Parallel (${concurrency})");
sourceLength = head["content-length"]!!.toLong();
onProgress(sourceLength, 0, 0);
downloadSource_Ranges(name, client, fileStream, videoUrl, sourceLength, 1024*512, concurrency, onProgress);
}
else {
Logger.i(TAG, "Download $name Sequential");
try {
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, onProgress);
} catch (e: Throwable) {
Logger.w(TAG, "Failed to download sequentially (url = $videoUrl)")
throw e
}
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, onProgress);
}
Logger.i(TAG, "$name downloadSource Finished");
@@ -779,19 +518,17 @@ class VideoDownload {
return sourceLength!!;
}
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
val progressRate: Int = 4096 * 5;
val progressRate: Int = 4096 * 25;
var lastProgressCount: Int = 0;
val speedRate: Int = 4096 * 5;
val speedRate: Int = 4096 * 25;
var readSinceLastSpeedTest: Long = 0;
var timeSinceLastSpeedTest: Long = System.currentTimeMillis();
var lastSpeed: Long = 0;
val result = client.get(url);
if (!result.isOk) {
result.body?.close()
if (!result.isOk)
throw IllegalStateException("Failed to download source. Web[${result.code}] Error");
}
if (result.body == null)
throw IllegalStateException("Failed to download source. Web[${result.code}] No response");
@@ -799,114 +536,41 @@ class VideoDownload {
val sourceStream = result.body.byteStream();
var totalRead: Long = 0;
try {
var read: Int;
val buffer = ByteArray(4096);
var read: Int;
do {
read = sourceStream.read(buffer);
if (read < 0)
break;
val buffer = ByteArray(4096);
fileStream.write(buffer, 0, read);
do {
read = sourceStream.read(buffer);
if (read < 0)
break;
totalRead += read;
fileStream.write(buffer, 0, read);
readSinceLastSpeedTest += read;
if (totalRead.toDouble() / progressRate > lastProgressCount) {
onProgress(sourceLength, totalRead, lastSpeed);
lastProgressCount++;
}
if (readSinceLastSpeedTest > speedRate) {
val lastSpeedTime = timeSinceLastSpeedTest;
timeSinceLastSpeedTest = System.currentTimeMillis();
val timeSince = timeSinceLastSpeedTest - lastSpeedTime;
if (timeSince > 0)
lastSpeed = (readSinceLastSpeedTest / (timeSince / 1000.0)).toLong();
readSinceLastSpeedTest = 0;
}
totalRead += read;
if (isCancelled)
throw CancellationException("Cancelled");
} while (read > 0);
} finally {
sourceStream.close()
result.body.close()
}
readSinceLastSpeedTest += read;
if (totalRead / progressRate > lastProgressCount) {
onProgress(sourceLength, totalRead, lastSpeed);
lastProgressCount++;
}
if (readSinceLastSpeedTest > speedRate) {
val lastSpeedTime = timeSinceLastSpeedTest;
timeSinceLastSpeedTest = System.currentTimeMillis();
val timeSince = timeSinceLastSpeedTest - lastSpeedTime;
if (timeSince > 0)
lastSpeed = (readSinceLastSpeedTest / (timeSince / 1000.0)).toLong();
readSinceLastSpeedTest = 0;
}
if (isCancelled)
throw CancellationException("Cancelled");
} while (read > 0);
lastSpeed = 0;
onProgress(sourceLength, totalRead, 0);
return sourceLength;
}
/*private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
val progressRate: Int = 4096 * 25
var lastProgressCount: Int = 0
val speedRate: Int = 4096 * 25
var readSinceLastSpeedTest: Long = 0
var timeSinceLastSpeedTest: Long = System.currentTimeMillis()
var lastSpeed: Long = 0
var totalRead: Long = 0
var sourceLength: Long
val buffer = ByteArray(4096)
var isPartialDownload = false
var result: ManagedHttpClient.Response? = null
do {
result = client.get(url, if (isPartialDownload) hashMapOf("Range" to "bytes=$totalRead-") else hashMapOf())
if (isPartialDownload) {
if (result.code != 206)
throw IllegalStateException("Failed to download source, byte range fallback failed. Web[${result.code}] Error")
} else {
if (!result.isOk)
throw IllegalStateException("Failed to download source. Web[${result.code}] Error")
}
if (result.body == null)
throw IllegalStateException("Failed to download source. Web[${result.code}] No response")
isPartialDownload = true
sourceLength = result.body!!.contentLength()
val sourceStream = result.body!!.byteStream()
try {
while (true) {
val read = sourceStream.read(buffer)
if (read <= 0) {
break
}
fileStream.write(buffer, 0, read)
totalRead += read
readSinceLastSpeedTest += read
if (totalRead / progressRate > lastProgressCount) {
onProgress(sourceLength, totalRead, lastSpeed)
lastProgressCount++
}
if (readSinceLastSpeedTest > speedRate) {
val lastSpeedTime = timeSinceLastSpeedTest
timeSinceLastSpeedTest = System.currentTimeMillis()
val timeSince = timeSinceLastSpeedTest - lastSpeedTime
if (timeSince > 0)
lastSpeed = (readSinceLastSpeedTest / (timeSince / 1000.0)).toLong()
readSinceLastSpeedTest = 0
}
if (isCancelled)
throw CancellationException("Cancelled")
}
} catch (e: Throwable) {
Logger.w(TAG, "Sequential download was interrupted, trying to fallback to byte ranges", e)
} finally {
sourceStream.close()
result.body?.close()
}
} while (totalRead < sourceLength)
onProgress(sourceLength, totalRead, 0)
return sourceLength
}*/
private fun downloadSource_Ranges(name: String, client: ManagedHttpClient, fileStream: FileOutputStream, url: String, sourceLength: Long, rangeSize: Int, concurrency: Int = 1, onProgress: (Long, Long, Long) -> Unit) {
val progressRate: Int = 4096 * 5;
var lastProgressCount: Int = 0;
@@ -979,47 +643,23 @@ class VideoDownload {
return tasks.map { it.get() };
}
private fun requestByteRange(client: ManagedHttpClient, url: String, rangeStart: Long, rangeEnd: Long): Triple<ByteArray, Long, Long> {
var retryCount = 0
var lastException: Throwable? = null
val toRead = rangeEnd - rangeStart;
val req = client.get(url, mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}")));
if(!req.isOk)
throw IllegalStateException("Range request failed Code [${req.code}] due to: ${req.message}");
if(req.body == null)
throw IllegalStateException("Range request failed, No body");
val read = req.body.contentLength();
while (retryCount <= 3) {
try {
val toRead = rangeEnd - rangeStart;
val req = client.get(url, mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}")));
if (!req.isOk) {
val bodyString = req.body?.string()
req.body?.close()
throw IllegalStateException("Range request failed Code [${req.code}] due to: ${req.message}");
}
if (req.body == null)
throw IllegalStateException("Range request failed, No body");
val read = req.body.contentLength();
if(read < toRead)
throw IllegalStateException("Byte-Range request attempted to provide less (${read} < ${toRead})");
if (read < toRead)
throw IllegalStateException("Byte-Range request attempted to provide less (${read} < ${toRead})");
return Triple(req.body.bytes(), rangeStart, rangeEnd);
} catch (e: Throwable) {
Logger.w(TAG, "Failed to download range (url=${url} bytes=${rangeStart}-${rangeEnd})", e)
retryCount++
lastException = e
sleep(when (retryCount) {
1 -> 1000 + ((Math.random() * 300.0).toLong() - 150)
2 -> 2000 + ((Math.random() * 300.0).toLong() - 150)
3 -> 4000 + ((Math.random() * 300.0).toLong() - 150)
else -> 1000 + ((Math.random() * 300.0).toLong() - 150)
})
}
}
throw lastException!!
return Triple(req.body.bytes(), rangeStart, rangeEnd);
}
fun validate() {
Logger.i(TAG, "VideoDownload Validate [${name}]");
if(videoSourceToUse != null) {
if(videoSource != null) {
if(videoFilePath == null)
throw IllegalStateException("Missing video file name after download");
val expectedFile = File(videoFilePath!!);
@@ -1030,7 +670,7 @@ class VideoDownload {
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
}
}
if(audioSourceToUse != null) {
if(audioSource != null) {
if(audioFilePath == null)
throw IllegalStateException("Missing audio file name after download");
val expectedFile = File(audioFilePath!!);
@@ -1052,15 +692,15 @@ class VideoDownload {
fun complete() {
Logger.i(TAG, "VideoDownload Complete [${name}]");
val existing = StateDownloads.instance.getCachedVideo(id);
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0) };
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0) };
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSource!!, it, videoFileSize ?: 0) };
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSource!!, it, audioFileSize ?: 0) };
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
localVideoSource.streamMetaData = (videoSourceToUse as IStreamMetaDataSource).streamMetaData;
if(localVideoSource != null && videoSource != null && videoSource is IStreamMetaDataSource)
localVideoSource.streamMetaData = (videoSource as IStreamMetaDataSource).streamMetaData;
if(localAudioSource != null && audioSourceToUse != null && audioSourceToUse is IStreamMetaDataSource)
localAudioSource.streamMetaData = (audioSourceToUse as IStreamMetaDataSource).streamMetaData;
if(localAudioSource != null && audioSource != null && audioSource is IStreamMetaDataSource)
localAudioSource.streamMetaData = (audioSource as IStreamMetaDataSource).streamMetaData;
if(existing != null) {
existing.videoSerialized = videoDetails!!;
@@ -1117,9 +757,6 @@ class VideoDownload {
const val GROUP_PLAYLIST = "Playlist";
const val GROUP_WATCHLATER= "WatchLater";
val REGEX_DASH_TEMPLATE = Regex("<SegmentTemplate .*?media=\"(.*?)\".*?>(.*?)<\\/SegmentTemplate>", RegexOption.DOT_MATCHES_ALL);
val REGEX_DASH_CUE = Regex("<S .*?t=\"([0-9]*?)\".*?d=\"([0-9]*?)\".*?\\/>", RegexOption.DOT_MATCHES_ALL);
fun videoContainerToExtension(container: String): String? {
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
return "mp4";
@@ -1166,27 +803,4 @@ class VideoDownload {
return "subtitle";
}
}
class SpeedTracker {
private val segmentStart: Long;
private val intervalMs: Long;
private var workDone: Long;
var lastSpeed: Long;
constructor(intervalMs: Long) {
segmentStart = System.currentTimeMillis();
this.intervalMs = intervalMs;
this.workDone = 0;
this.lastSpeed = 0;
}
fun addWork(work: Long) {
val now = System.currentTimeMillis();
if((now - segmentStart) > intervalMs)
{
lastSpeed = workDone;
workDone = 0;
}
workDone += work;
}
}
}
@@ -6,8 +6,6 @@ import com.caoccao.javet.exceptions.JavetException
import com.caoccao.javet.exceptions.JavetExecutionException
import com.caoccao.javet.interop.V8Host
import com.caoccao.javet.interop.V8Runtime
import com.caoccao.javet.interop.options.V8Flags
import com.caoccao.javet.interop.options.V8RuntimeOptions
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueBoolean
import com.caoccao.javet.values.primitive.V8ValueInteger
@@ -135,10 +133,9 @@ class V8Plugin {
synchronized(_runtimeLock) {
if (_runtime != null)
return;
//V8RuntimeOptions.V8_FLAGS.setUseStrict(true);
val host = V8Host.getV8Instance();
val options = host.jsRuntimeType.getRuntimeOptions();
_runtime = host.createV8Runtime(options);
if (!host.isIsolateCreated)
throw IllegalStateException("Isolate not created");
@@ -2,14 +2,12 @@ package com.futo.platformplayer.engine.packages
import com.caoccao.javet.annotations.V8Function
import com.caoccao.javet.annotations.V8Property
import com.caoccao.javet.values.V8Value
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.JSClientConstants
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.engine.IV8PluginConfig
@@ -51,20 +49,9 @@ class PackageBridge : V8Package {
fun buildFlavor(): String {
return BuildConfig.FLAVOR;
}
@V8Property
fun buildSpecVersion(): Int {
return JSClientConstants.PLUGIN_SPEC_VERSION;
}
@V8Function
fun dispose(value: V8Value) {
Logger.e(TAG, "Manual dispose: " + value.javaClass.name);
value.close();
}
@V8Function
fun toast(str: String) {
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
try {
UIDialogs.toast(str);
@@ -68,10 +68,6 @@ class PackageDOMParser : V8Package {
return result;
}
@V8Property
fun parentElement(): DOMNode? {
return parentNode();
}
@V8Property
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
@V8Property
fun innerHTML(): String = _element.html();
@@ -80,8 +76,6 @@ class PackageDOMParser : V8Package {
@V8Property
fun textContent(): String = _element.text();
@V8Property
fun tagName(): String = _element.tagName().uppercase();
@V8Property
fun text(): String = _element.text().ifEmpty { data() };
@V8Property
fun data(): String = _element.data();
@@ -7,11 +7,7 @@ import com.caoccao.javet.enums.V8ConversionMode
import com.caoccao.javet.enums.V8ProxyMode
import com.caoccao.javet.interop.V8Runtime
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueArrayBuffer
import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValueSharedArrayBuffer
import com.caoccao.javet.values.reference.V8ValueTypedArray
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
@@ -20,9 +16,6 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.internal.IV8Convertable
import com.futo.platformplayer.engine.internal.V8BindObject
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.net.SocketTimeoutException
import kotlin.streams.asSequence
@@ -71,44 +64,33 @@ class PackageHttp: V8Package {
}
@V8Function
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
return if(useAuth)
_packageClientAuth.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
_packageClientAuth.request(method, url, headers)
else
_packageClient.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
_packageClient.request(method, url, headers);
}
@V8Function
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
return if(useAuth)
_packageClientAuth.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
_packageClientAuth.requestWithBody(method, url, body, headers)
else
_packageClient.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
_packageClient.requestWithBody(method, url, body, headers);
}
@V8Function
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
return if(useAuth)
_packageClientAuth.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING)
_packageClientAuth.GET(url, headers)
else
_packageClient.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
_packageClient.GET(url, headers);
}
@V8Function
fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
val client = if(useAuth) _packageClientAuth else _packageClient;
if(body is V8ValueString)
return client.POST(url, body.value, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else if(body is String)
return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else if(body is V8ValueTypedArray)
return client.POST(url, body.toBytes(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else if(body is ByteArray)
return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else if(body is ArrayList<*>) //Avoid this case, used purely for testing
return client.POST(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
return if(useAuth)
_packageClientAuth.POST(url, body, headers)
else
throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
_packageClient.POST(url, body, headers);
}
@V8Function
@@ -129,19 +111,8 @@ class PackageHttp: V8Package {
}
}
interface IBridgeHttpResponse {
val url: String;
val code: Int;
val headers: Map<String, List<String>>?;
}
@kotlinx.serialization.Serializable
class BridgeHttpStringResponse(
override val url: String,
override val code: Int, val
body: String?,
override val headers: Map<String, List<String>>? = null) : IV8Convertable, IBridgeHttpResponse {
class BridgeHttpResponse(val url: String, val code: Int, val body: String?, val headers: Map<String, List<String>>? = null) : IV8Convertable {
val isOk = code >= 200 && code < 300;
override fun toV8(runtime: V8Runtime): V8Value? {
@@ -154,37 +125,6 @@ class PackageHttp: V8Package {
return obj;
}
}
@kotlinx.serialization.Serializable
class BridgeHttpBytesResponse: IV8Convertable, IBridgeHttpResponse {
override val url: String;
override val code: Int;
val body: ByteArray?;
override val headers: Map<String, List<String>>?;
val isOk: Boolean;
constructor(url: String, code: Int, body: ByteArray? = null, headers: Map<String, List<String>>? = null) {
this.url = url;
this.code = code;
this.body = body;
this.headers = headers;
this.isOk = code >= 200 && code < 300;
}
override fun toV8(runtime: V8Runtime): V8Value? {
val obj = runtime.createV8ValueObject();
obj.set("url", url);
obj.set("code", code);
if(body != null) {
val buffer = runtime.createV8ValueArrayBuffer(body.size);
buffer.fromBytes(body);
obj.set("body", body);
}
obj.set("headers", headers);
obj.set("isOk", isOk);
return obj;
}
}
//TODO: This object is currently re-wrapped each modification, this is due to an issue passing the same object back and forth, should be fixed in future.
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
@@ -207,12 +147,6 @@ class PackageHttp: V8Package {
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
= clientPOST(_package.getDefaultClient(useAuth), url, body, headers);
@V8Function
fun DUMMY(): BatchBuilder {
_reqs.add(Pair(_package.getDefaultClient(false), RequestDescriptor("DUMMY", "", mutableMapOf())));
return BatchBuilder(_package, _reqs);
}
//Client-specific
@V8Function
@@ -235,14 +169,12 @@ class PackageHttp: V8Package {
//Finalizer
@V8Function
fun execute(): List<IBridgeHttpResponse?> {
fun execute(): List<BridgeHttpResponse> {
return _reqs.parallelStream().map {
if(it.second.method == "DUMMY")
return@map 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@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers);
else
return@map it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
return@map it.first.request(it.second.method, it.second.url, it.second.headers);
}
.asSequence()
.toList();
@@ -298,116 +230,65 @@ class PackageHttp: V8Package {
if(_client is JSHttpClient)
_client.doAllowNewCookies = allow;
}
@V8Function
fun setTimeout(timeoutMs: Int) {
if(_client is JSHttpClient) {
_client.setTimeout(timeoutMs.toLong());
}
}
@V8Function
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
applyDefaultHeaders(headers);
return logExceptions {
return@logExceptions catchHttp {
val client = _client;
//logRequest(method, url, headers, null);
val resp = client.requestMethod(method, url, headers);
val responseBody = resp.body?.string();
//logResponse(method, url, resp.code, resp.headers, responseBody);
return@catchHttp when(returnType) {
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
}
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
}
};
}
@V8Function
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
applyDefaultHeaders(headers);
return logExceptions {
catchHttp {
val client = _client;
//logRequest(method, url, headers, body);
val resp = client.requestMethod(method, url, body, headers);
val responseBody = resp.body?.string();
//logResponse(method, url, resp.code, resp.headers, responseBody);
return@catchHttp when(returnType) {
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
}
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
}
};
}
@V8Function
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
fun GET(url: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
applyDefaultHeaders(headers);
return logExceptions {
catchHttp {
val client = _client;
//logRequest("GET", url, headers, null);
val resp = client.get(url, headers);
//val responseBody = resp.body?.string();
val responseBody = resp.body?.string();
//logResponse("GET", url, resp.code, resp.headers, responseBody);
return@catchHttp when(returnType) {
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
}
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
}
};
}
@V8Function
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
applyDefaultHeaders(headers);
return logExceptions {
catchHttp {
val client = _client;
//logRequest("POST", url, headers, body);
val resp = client.post(url, body, headers);
//val responseBody = resp.body?.string();
val responseBody = resp.body?.string();
//logResponse("POST", url, resp.code, resp.headers, responseBody);
return@catchHttp when(returnType) {
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
}
}
};
}
@V8Function
fun POST(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers);
return logExceptions {
catchHttp {
val client = _client;
//logRequest("POST", url, headers, body);
val resp = client.post(url, body, headers);
//val responseBody = resp.body?.string();
//logResponse("POST", url, resp.code, resp.headers, responseBody);
return@catchHttp when(returnType) {
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
}
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
}
};
}
@@ -507,13 +388,13 @@ class PackageHttp: V8Package {
}
}
private fun catchHttp(handle: ()->IBridgeHttpResponse): IBridgeHttpResponse {
private fun catchHttp(handle: ()->BridgeHttpResponse): BridgeHttpResponse {
try{
return handle();
}
//Forward timeouts
catch(ex: SocketTimeoutException) {
return BridgeHttpStringResponse("", 408, null);
return BridgeHttpResponse("", 408, null);
}
}
}
@@ -633,25 +514,20 @@ class PackageHttp: V8Package {
val url: String,
val headers: MutableMap<String, String>,
val body: String? = null,
val contentType: String? = null,
val respType: ReturnType = ReturnType.STRING
val contentType: String? = null
)
private fun catchHttp(handle: ()->BridgeHttpStringResponse): BridgeHttpStringResponse {
private fun catchHttp(handle: ()->BridgeHttpResponse): BridgeHttpResponse {
try{
return handle();
}
//Forward timeouts
catch(ex: SocketTimeoutException) {
return BridgeHttpStringResponse("", 408, null);
return BridgeHttpResponse("", 408, null);
}
}
enum class ReturnType(val value: Int) {
STRING(0),
BYTES(1);
}
companion object {
private const val TAG = "PackageHttp";
@@ -133,10 +133,6 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
Logger.w(TAG, "Failed to parse claim=$c", e)
}
}
if(!map.containsKey("Harbor"))
this.context?.let {
map.set("Harbor", polycentricProfile.getHarborUrl(it));
}
if (map.isNotEmpty())
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
@@ -1,13 +1,12 @@
package com.futo.platformplayer.fragment.channel.tab
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
@@ -16,6 +15,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
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.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.structures.IAsyncPager
import com.futo.platformplayer.api.media.structures.IPager
@@ -41,11 +41,10 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.max
class ChannelContentsFragment : Fragment(), IChannelTabFragment {
private var _recyclerResults: RecyclerView? = null;
private var _glmVideo: GridLayoutManager? = null;
private var _llmVideo: LinearLayoutManager? = null;
private var _loading = false;
private var _pager_parent: IPager<IPlatformContent>? = null;
private var _pager: IPager<IPlatformContent>? = null;
@@ -119,7 +118,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
super.onScrolled(recyclerView, dx, dy);
val recyclerResults = _recyclerResults ?: return;
val llmVideo = _glmVideo ?: return;
val llmVideo = _llmVideo ?: return;
val visibleItemCount = recyclerResults.childCount;
val firstVisibleItem = llmVideo.findFirstVisibleItemPosition();
@@ -164,10 +163,9 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
}
val numColumns = max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
_glmVideo = GridLayoutManager(view.context, numColumns);
_llmVideo = LinearLayoutManager(view.context);
_recyclerResults?.adapter = _adapterResults;
_recyclerResults?.layoutManager = _glmVideo;
_recyclerResults?.layoutManager = _llmVideo;
_recyclerResults?.addOnScrollListener(_scrollListener);
return view;
@@ -183,13 +181,6 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
_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) {
if (_pager_parent != null && _pager_parent is IRefreshPager<*>) {
@@ -1,13 +1,12 @@
package com.futo.platformplayer.fragment.channel.tab
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
@@ -37,11 +36,10 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.max
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
private var _recyclerResults: RecyclerView? = null
private var _glmPlaylist: GridLayoutManager? = null
private var _llmPlaylist: LinearLayoutManager? = null
private var _loading = false
private var _pagerParent: IPager<IPlatformPlaylist>? = null
private var _pager: IPager<IPlatformPlaylist>? = null
@@ -111,7 +109,7 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
super.onScrolled(recyclerView, dx, dy)
val recyclerResults = _recyclerResults ?: return
val llmPlaylist = _glmPlaylist ?: return
val llmPlaylist = _llmPlaylist ?: return
val visibleItemCount = recyclerResults.childCount
val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
@@ -160,10 +158,9 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
}
val numColumns = max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
_glmPlaylist = GridLayoutManager(view.context, numColumns)
_llmPlaylist = LinearLayoutManager(view.context)
_recyclerResults?.adapter = _adapterResults
_recyclerResults?.layoutManager = _glmPlaylist
_recyclerResults?.layoutManager = _llmPlaylist
_recyclerResults?.addOnScrollListener(_scrollListener)
return view
@@ -179,13 +176,6 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
_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(
pager: IPager<IPlatformPlaylist>
) {
@@ -7,7 +7,6 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -17,7 +16,6 @@ import androidx.core.animation.doOnEnd
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.dp
@@ -35,7 +33,7 @@ import kotlin.math.roundToInt
class MenuBottomBarFragment : MainActivityFragment() {
private var _view: MenuBottomBarView? = null;
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = MenuBottomBarView(this, inflater);
_view = view;
return view;
@@ -57,13 +55,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
return _view?.onBackPressed() ?: false;
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
_view?.updateAllButtonVisibility()
}
@SuppressLint("ViewConstructor")
@SuppressLint("ViewConstructor")
class MenuBottomBarView : LinearLayout {
private val _fragment: MenuBottomBarFragment;
private val _inflater: LayoutInflater;
@@ -83,7 +75,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
private var _buttonsVisible = 0;
private var _subscriptionsVisible = true;
private var currentButtonDefinitions: List<ButtonDefinition>? = null;
var currentButtonDefinitions: List<ButtonDefinition>? = null;
constructor(fragment: MenuBottomBarFragment, inflater: LayoutInflater) : super(inflater.context) {
_fragment = fragment;
@@ -139,7 +131,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
val staggerFactor = 3.0f
if (visible) {
moreOverlay.visibility = VISIBLE
moreOverlay.visibility = LinearLayout.VISIBLE
val animations = arrayListOf<Animator>()
animations.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 0.0f, 1.0f).setDuration(duration))
@@ -168,7 +160,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
animatorSet.doOnEnd {
_moreVisibleAnimating = false
_moreVisible = false
moreOverlay.visibility = INVISIBLE
moreOverlay.visibility = LinearLayout.INVISIBLE
}
animatorSet.playTogether(animations)
animatorSet.start()
@@ -185,7 +177,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
_layoutBottomBarButtons.removeAllViews();
_layoutBottomBarButtons.addView(Space(context).apply {
layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
})
for ((index, button) in buttons.withIndex()) {
@@ -199,7 +191,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
_layoutBottomBarButtons.addView(menuButton)
if (index < buttonDefinitions.size - 1) {
_layoutBottomBarButtons.addView(Space(context).apply {
layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
})
}
@@ -207,7 +199,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
}
_layoutBottomBarButtons.addView(Space(context).apply {
layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
})
}
@@ -216,30 +208,19 @@ class MenuBottomBarFragment : MainActivityFragment() {
_moreButtons.clear();
_layoutMoreButtons.removeAllViews();
var insertedButtons = 0;
//Force buy to be on top for more buttons
val buyIndex = buttons.indexOfFirst { b -> b.id == 98 };
if (buyIndex != -1) {
val button = buttons[buyIndex]
buttons.removeAt(buyIndex)
buttons.add(0, button)
insertedButtons++;
}
//Force faq to be second
val faqIndex = buttons.indexOfFirst { b -> b.id == 97 };
if (faqIndex != -1) {
val button = buttons[faqIndex]
buttons.removeAt(faqIndex)
buttons.add(if (insertedButtons == 1) 1 else 0, button)
insertedButtons++;
}
//Force privacy to be third
val privacyIndex = buttons.indexOfFirst { b -> b.id == 96 };
if (privacyIndex != -1) {
val button = buttons[privacyIndex]
buttons.removeAt(privacyIndex)
buttons.add(if (insertedButtons == 2) 2 else (if(insertedButtons == 1) 1 else 0), button)
insertedButtons++;
buttons.add(if (buttons.size == 1) 1 else 0, button)
}
for (data in buttons) {
@@ -262,20 +243,9 @@ class MenuBottomBarFragment : MainActivityFragment() {
button.updateActive(_fragment);
}
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
updateAllButtonVisibility()
}
fun updateAllButtonVisibility() {
// if the more fly-out menu is open the we should close it
if(_moreVisible) {
setMoreVisible(false)
}
val defs = currentButtonDefinitions?.toMutableList() ?: return
val metrics = resources.displayMetrics
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
if (_buttonsVisible >= defs.size) {
updateBottomMenuButtons(defs.toMutableList(), false);
@@ -332,6 +302,9 @@ class MenuBottomBarFragment : MainActivityFragment() {
if (!StatePayment.instance.hasPaid) {
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
}
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, {
it.navigate<BrowserFragment>(Settings.URL_FAQ);
}))
//Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated
@@ -377,15 +350,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
//Add configurable buttons here
var buttonDefinitions = listOf(
ButtonDefinition(0, R.drawable.ic_home, R.drawable.ic_home_filled, R.string.home, canToggle = true, { it.currentMain is HomeFragment }, {
val currentMain = it.currentMain
if (currentMain is HomeFragment) {
currentMain.scrollToTop(false)
currentMain.reloadFeed()
} else {
it.navigate<HomeFragment>()
}
}),
ButtonDefinition(0, R.drawable.ic_home, R.drawable.ic_home_filled, R.string.home, canToggle = true, { it.currentMain is HomeFragment }, { it.navigate<HomeFragment>() }),
ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate<SubscriptionsFeedFragment>() }),
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>() }),
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>() }),
@@ -404,22 +369,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
if (c is Activity) {
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
}
}),
ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = true, { false }, {
UIDialogs.showDialog(it.context ?: return@ButtonDefinition, R.drawable.ic_disabled_visible_purple, "Privacy Mode",
"All requests will be processed anonymously (any logins will be disabled except for the personalized home page), local playback and history tracking will also be disabled.\n\nTap the icon to disable.", null, 0,
UIDialogs.Action("Cancel", {
StateApp.instance.setPrivacyMode(false);
}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action("Enable", {
StateApp.instance.setPrivacyMode(true);
}, UIDialogs.ActionStyle.PRIMARY));
}),
ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = true, { false }, {
it.navigate<BrowserFragment>(Settings.URL_FAQ);
})
//96 is reserved for privacy button
//98 is reserved for buy button
//98 is reversed for buy button
//99 is reserved for more button
);
}
@@ -10,7 +10,6 @@ import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import com.futo.futopay.PaymentConfigurations
import com.futo.futopay.PaymentManager
import com.futo.futopay.formatMoney
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
@@ -90,13 +89,14 @@ class BuyFragment : MainFragment() {
try {
val currencies = StatePayment.instance.getAvailableCurrencies("grayjay");
val prices = StatePayment.instance.getAvailableCurrencyPrices("grayjay");
val country = StatePayment.instance.getPaymentCountryFromIP(true)?.let { c -> PaymentConfigurations.COUNTRIES.find { it.id.equals(c, ignoreCase = true) } };
val country = StatePayment.instance.getPaymentCountryFromIP()?.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)) } };
if(currency != null && prices.containsKey(currency.id)) {
val price = prices[currency.id]!!;
val priceDecimal = (price.toDouble() / 100);
withContext(Dispatchers.Main) {
_buttonBuyText.text = formatMoney(country.id, currency.id, price) + context.getString(R.string.plus_tax);
_buttonBuyText.text = currency.symbol + String.format("%.2f", priceDecimal) + context.getString(R.string.plus_tax);
}
}
}
@@ -1,10 +1,7 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Animatable
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -15,7 +12,6 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2
import com.bumptech.glide.Glide
@@ -56,9 +52,7 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.subscriptions.SubscribeButton
import com.futo.polycentric.core.OwnedClaim
import com.futo.polycentric.core.PublicKey
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
@@ -70,13 +64,7 @@ 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() {
override val isMainView: Boolean = true
@@ -237,7 +225,11 @@ class ChannelFragment : MainFragment() {
}
adapter.onAddToWatchLaterClicked.subscribe { content ->
if (content is IPlatformVideo) {
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)
StatePlaylists.instance.addToWatchLater(
SerializedPlatformVideo.fromVideo(
content
)
)
UIDialogs.toast("Added to watch later\n[${content.name}]")
}
}
@@ -221,8 +221,8 @@ class CommentsFragment : MainFragment() {
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
//val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
_fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
//_fragment.navigate<BrowserFragment>(navUrl);
}
@@ -4,7 +4,7 @@ import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
@@ -33,7 +33,6 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.withTimestamp
import kotlin.math.floor
import kotlin.math.max
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
private var _exoPlayer: PlayerManager? = null;
@@ -46,7 +45,9 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
private var _videoOptionsOverlay: SlideUpMenuOverlay? = null;
protected open val shouldShowTimeBar: Boolean get() = true
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData)
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
}
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
return results;
@@ -54,10 +55,16 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<IPlatformContent>): InsertedViewAdapterWithLoader<ContentPreviewViewHolder> {
val player = StatePlayer.instance.getThumbnailPlayerOrCreate(context);
player.modifyState("ThumbnailPlayer") { state -> state.muted = true };
player.modifyState("ThumbnailPlayer", { state -> state.muted = true });
_exoPlayer = player;
return PreviewContentListAdapter(context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
val v = LinearLayout(context).apply {
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
orientation = LinearLayout.VERTICAL;
};
headerView = v;
return PreviewContentListAdapter(context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(v), arrayListOf(), shouldShowTimeBar).apply {
attachAdapterEvents(this);
}
}
@@ -82,7 +89,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
};
adapter.onAddToWatchLaterClicked.subscribe(this) {
if(it is IPlatformVideo) {
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it));
UIDialogs.toast("Added to watch later\n[${it.name}]");
}
};
@@ -111,13 +118,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
private fun showVideoOptionsOverlay(content: IPlatformVideo) {
_overlayContainer.let {
_videoOptionsOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it, SlideUpMenuItem(
context,
R.drawable.ic_visibility_off,
context.getString(R.string.hide),
context.getString(R.string.hide_from_home),
tag = "hide",
call = { StateMeta.instance.addHiddenVideo(content.url);
_videoOptionsOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it, SlideUpMenuItem(context, R.drawable.ic_visibility_off, context.getString(R.string.hide), context.getString(R.string.hide_from_home), "hide",
{ StateMeta.instance.addHiddenVideo(content.url);
if (fragment is HomeFragment) {
val removeIndex = recyclerData.results.indexOf(content);
if (removeIndex >= 0) {
@@ -126,19 +128,12 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
}
}
}),
SlideUpMenuItem(context,
R.drawable.ic_playlist,
context.getString(R.string.play_feed_as_queue),
context.getString(R.string.play_entire_feed),
tag = "playFeed",
call = {
SlideUpMenuItem(context, R.drawable.ic_playlist, context.getString(R.string.play_feed_as_queue), context.getString(R.string.play_entire_feed), "playFeed",
{
val newQueue = listOf(content) + recyclerData.results
.filterIsInstance<IPlatformVideo>()
.filter { it != content };
StatePlayer.instance.setQueue(newQueue, StatePlayer.TYPE_QUEUE, "Feed Queue",
focus = true,
shuffle = false
);
StatePlayer.instance.setQueue(newQueue, StatePlayer.TYPE_QUEUE, "Feed Queue", true, false);
})
);
}
@@ -156,22 +151,21 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
adapter.onLongPress.remove(this);
}
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
super.onRestoreCachedData(cachedData)
val v = LinearLayout(context).apply {
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
orientation = LinearLayout.VERTICAL;
};
headerView = v;
cachedData.adapter.viewsToPrepend.add(v);
(cachedData.adapter as PreviewContentListAdapter?)?.let { attachAdapterEvents(it) };
}
override fun createLayoutManager(
recyclerResults: RecyclerView,
context: Context
): GridLayoutManager {
val glmResults =
GridLayoutManager(
context,
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
);
return glmResults
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): LinearLayoutManager {
val llmResults = LinearLayoutManager(context);
llmResults.orientation = LinearLayoutManager.VERTICAL;
return llmResults;
}
override fun onScrollStateChanged(newState: Int) {
@@ -214,11 +208,11 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
}
private fun playPreview() {
if(feedStyle == FeedStyle.THUMBNAIL || recyclerData.layoutManager.spanCount > 1)
if(feedStyle == FeedStyle.THUMBNAIL)
return;
val firstVisible = recyclerData.layoutManager.findFirstVisibleItemPosition()
val lastVisible = recyclerData.layoutManager.findLastVisibleItemPosition()
val firstVisible = recyclerData.layoutManager.findFirstVisibleItemPosition();
val lastVisible = recyclerData.layoutManager.findLastVisibleItemPosition();
val itemsVisible = lastVisible - firstVisible + 1;
val autoPlayIndex = (firstVisible + floor(itemsVisible / 2.0 + 0.49).toInt()).coerceAtLeast(0).coerceAtMost((recyclerData.results.size - 1));
@@ -238,7 +232,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
(recyclerData.adapter as PreviewContentListAdapter?)?.preview(viewHolder.childViewHolder)
}
private fun stopVideo() {
fun stopVideo() {
//TODO: Is this still necessary?
(recyclerData.adapter as PreviewContentListAdapter?)?.stopPreview();
}
@@ -266,6 +260,6 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
}
companion object {
private const val TAG = "ContentFeedView";
private val TAG = "ContentFeedView";
}
}
@@ -3,9 +3,13 @@ package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Context
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.ViewGroup.MarginLayoutParams
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.structures.*
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.adapters.*
@@ -14,7 +18,9 @@ import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder
abstract class CreatorFeedView<TFragment> : FeedView<TFragment, PlatformAuthorLink, PlatformAuthorLink, IPager<PlatformAuthorLink>, CreatorViewHolder> where TFragment : MainFragment {
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
constructor(fragment: TFragment, inflater: LayoutInflater) : super(fragment, inflater)
constructor(fragment: TFragment, inflater: LayoutInflater) : super(fragment, inflater) {
}
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<PlatformAuthorLink>): InsertedViewAdapterWithLoader<CreatorViewHolder> {
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
@@ -28,31 +34,18 @@ abstract class CreatorFeedView<TFragment> : FeedView<TFragment, PlatformAuthorLi
);
}
/*
* An empty override to remove the inherited span count update functionality
*/
override fun updateSpanCount(){
}
override fun createLayoutManager(
recyclerResults: RecyclerView,
context: Context
): GridLayoutManager {
val glmResults = GridLayoutManager(context, 2)
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): LinearLayoutManager {
val glmResults = GridLayoutManager(context, 2);
glmResults.orientation = LinearLayoutManager.VERTICAL;
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
rightMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
8.0f,
context.resources.displayMetrics
).toInt()
}
rightMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8.0f, context.resources.displayMetrics).toInt();
};
return glmResults
return glmResults;
}
companion object {
private const val TAG = "CreatorFeedView";
private val TAG = "CreatorFeedView";
}
}
@@ -8,7 +8,6 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.Spinner
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager
@@ -26,20 +25,11 @@ class CreatorsFragment : MainFragment() {
private var _overlayContainer: FrameLayout? = null;
private var _containerSearch: FrameLayout? = null;
private var _editSearch: EditText? = null;
private var _buttonClearSearch: ImageButton? = null
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_creators, container, false);
_containerSearch = view.findViewById(R.id.container_search);
val editSearch: EditText = view.findViewById(R.id.edit_search);
val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
_editSearch = editSearch
_buttonClearSearch = buttonClearSearch
buttonClearSearch.setOnClickListener {
editSearch.text.clear()
editSearch.requestFocus()
_buttonClearSearch?.visibility = View.INVISIBLE;
}
_editSearch = view.findViewById(R.id.edit_search);
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
@@ -61,12 +51,7 @@ class CreatorsFragment : MainFragment() {
_spinnerSortBy = spinnerSortBy;
_editSearch?.addTextChangedListener {
adapter.query = it.toString()
if (it?.isEmpty() == true) {
_buttonClearSearch?.visibility = View.INVISIBLE
} else {
_buttonClearSearch?.visibility = View.VISIBLE
}
adapter.query = it.toString();
}
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_subscriptions);
@@ -1,14 +1,13 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@@ -25,21 +24,18 @@ import com.futo.platformplayer.views.others.ProgressBar
import com.futo.platformplayer.views.others.TagsView
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.announcements.AnnouncementView
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
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 {
protected val _recyclerResults: RecyclerView;
protected val _overlayContainer: FrameLayout;
protected val _swipeRefresh: SwipeRefreshLayout;
private val _progressBar: ProgressBar;
private val _progress_bar: ProgressBar;
private val _spinnerSortBy: Spinner;
private val _containerSortBy: LinearLayout;
private val _announcementView: AnnouncementView;
private val _tagsView: TagsView;
private val _textCentered: TextView;
private val _emptyPagerContainer: FrameLayout;
@@ -48,7 +44,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
private var _loading: Boolean = true;
private val _pagerLock = Object();
private val _pager_lock = Object();
private var _cache: ItemCache<TResult>? = null;
open val visibleThreshold = 15;
@@ -62,22 +58,21 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
private var _activeTags: List<String>? = null;
private var _nextPageHandler: TaskHandler<TPager, List<TResult>>;
val recyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>;
val recyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>;
val fragment: TFragment;
private val _scrollListener: RecyclerView.OnScrollListener;
private var _automaticNextPageCounter = 0;
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>, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>? = null) : super(inflater.context) {
this.fragment = fragment;
inflater.inflate(R.layout.fragment_feed, this);
_textCentered = findViewById(R.id.text_centered);
_emptyPagerContainer = findViewById(R.id.empty_pager_container);
_progressBar = findViewById(R.id.progress_bar);
_announcementView = findViewById(R.id.announcement_view)
_progressBar.inactiveColor = Color.TRANSPARENT;
_progress_bar = findViewById(R.id.progress_bar);
_progress_bar.inactiveColor = Color.TRANSPARENT;
_swipeRefresh = findViewById(R.id.swipe_refresh);
val recyclerResults: RecyclerView = findViewById(R.id.list_results);
@@ -163,7 +158,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
super.onScrolled(recyclerView, dx, dy);
val visibleItemCount = _recyclerResults.childCount;
val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPosition()
val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPosition();
//Logger.i(TAG, "onScrolled loadNextPage visibleItemCount=$visibleItemCount firstVisibleItem=$visibleItemCount")
if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= recyclerData.results.size && firstVisibleItem > 0) {
@@ -176,10 +171,6 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
_recyclerResults.addOnScrollListener(_scrollListener);
}
protected fun showAnnouncementView() {
_announcementView.visibility = View.VISIBLE
}
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
val canScroll = if (recyclerData.results.isEmpty()) false else {
val layoutManager = recyclerData.layoutManager
@@ -188,13 +179,14 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
if (firstVisibleItemPosition != RecyclerView.NO_POSITION) {
val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition)
val itemHeight = firstVisibleView?.height ?: 0
val occupiedSpace = recyclerData.results.size / recyclerData.layoutManager.spanCount * itemHeight
val occupiedSpace = recyclerData.results.size * itemHeight
val recyclerViewHeight = _recyclerResults.height
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage occupiedSpace=$occupiedSpace recyclerViewHeight=$recyclerViewHeight")
occupiedSpace >= recyclerViewHeight
} else {
false
}
}
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter")
if (!canScroll || filteredResults.isEmpty()) {
@@ -234,20 +226,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
}
}
open fun updateSpanCount() {
recyclerData.layoutManager.spanCount =
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
}
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
updateSpanCount()
}
fun onResume() {
updateSpanCount()
//Reload the pager if the plugin was killed
val pager = recyclerData.pager;
if((pager is MultiPager<*> && pager.findPager { it is JSPager<*> && !it.isAvailable } != null) ||
@@ -273,7 +252,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
protected open fun setActiveTags(activeTags: List<String>?) {
_activeTags = activeTags;
if (!activeTags.isNullOrEmpty()) {
if (activeTags != null && activeTags.isNotEmpty()) {
_tagsView.setTags(activeTags);
_tagsView.visibility = View.VISIBLE;
} else {
@@ -283,7 +262,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
protected open fun setSortByOptions(options: List<String>?) {
_sortByOptions = options;
if (!options.isNullOrEmpty()) {
if (options != null && options.isNotEmpty()) {
val allOptions = arrayListOf<String>();
allOptions.add("Default");
allOptions.addAll(options);
@@ -298,19 +277,19 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
}
}
protected abstract fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<TConverted>): InsertedViewAdapterWithLoader<TViewHolder>;
protected abstract fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager;
protected open fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>) {}
protected abstract fun createLayoutManager(recyclerResults: RecyclerView, context: Context): LinearLayoutManager;
protected open fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>) {}
protected fun setProgress(fin: Int, total: Int) {
val progress = (fin.toFloat() / total);
_progressBar.progress = progress;
_progress_bar.progress = progress;
if(progress > 0 && progress < 1)
{
if(_progressBar.height == 0)
_progressBar.layoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5);
if(_progress_bar.height == 0)
_progress_bar.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5);
}
else if(_progressBar.height > 0) {
_progressBar.layoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
else if(_progress_bar.height > 0) {
_progress_bar.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
}
}
@@ -366,7 +345,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
//insertPagerResults(_cache!!.cachePager.getResults(), false);
}
fun setPager(pager: TPager, cache: ItemCache<TResult>? = null) {
synchronized(_pagerLock) {
synchronized(_pager_lock) {
detachParentPagerEvents();
detachPagerEvents();
@@ -446,7 +425,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
val p = recyclerData.pager;
if(p is IReplacerPager<*>) {
p.onReplaced.subscribe(this) { _, newItem ->
synchronized(_pagerLock) {
synchronized(_pager_lock) {
val filtered = filterResults(listOf(newItem as TResult));
if(filtered.isEmpty())
return@subscribe;
@@ -464,7 +443,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
var _lastNextPage = false;
private fun loadNextPage() {
synchronized(_pagerLock) {
synchronized(_pager_lock) {
val pager: TPager = recyclerData.pager ?: return;
val hasMorePages = pager.hasMorePages();
Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages, page size=${pager.getResults().size}");
@@ -489,7 +468,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
}
companion object {
private const val TAG = "FeedView";
private val TAG = "FeedView";
}
abstract class ItemCache<TResult>(val cachePager: IPager<TResult>) {
@@ -6,7 +6,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.futo.platformplayer.*
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
@@ -18,9 +18,13 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.NoResultsView
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
@@ -28,8 +32,11 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.announcements.AnnouncementView
import com.futo.platformplayer.views.buttons.BigButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.time.OffsetDateTime
import java.util.UUID
class HomeFragment : MainFragment() {
override val isMainView : Boolean = true;
@@ -37,15 +44,7 @@ class HomeFragment : MainFragment() {
override val hasBottomBar: Boolean get() = true;
private var _view: HomeView? = null;
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
fun reloadFeed() {
_view?.reloadFeed()
}
fun scrollToTop(smooth: Boolean) {
_view?.scrollToTop(smooth)
}
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
@@ -94,10 +93,16 @@ class HomeFragment : MainFragment() {
class HomeView : ContentFeedView<HomeFragment> {
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
private var _announcementsView: AnnouncementView;
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
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) {
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
_announcementsView = AnnouncementView(context, null).apply {
headerView.addView(this);
};
_taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({ fragment.lifecycleScope }, {
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
})
@@ -128,18 +133,22 @@ class HomeFragment : MainFragment() {
};
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
showAnnouncementView()
}
fun onShown() {
val lastClients = recyclerData.lastClients;
val clients = StatePlatform.instance.getSortedEnabledClient().filter { if (it is JSClient) it.enableInHome else true };
val feedstyleChanged = recyclerData.loadedFeedStyle != feedStyle;
val clientsChanged = lastClients == null || lastClients.size != clients.size || !lastClients.containsAll(clients);
Logger.i(TAG, "onShown (recyclerData.loadedFeedStyle=${recyclerData.loadedFeedStyle}, recyclerData.lastLoad=${recyclerData.lastLoad}, feedstyleChanged=$feedstyleChanged, clientsChanged=$clientsChanged)")
val outdated = recyclerData.lastLoad.getNowDiffSeconds() > 60;
Logger.i(TAG, "onShown (recyclerData.loadedFeedStyle=${recyclerData.loadedFeedStyle}, recyclerData.lastLoad=${recyclerData.lastLoad}, feedstyleChanged=$feedstyleChanged, clientsChanged=$clientsChanged, outdated=$outdated)")
if(feedstyleChanged || clientsChanged) {
reloadFeed()
if(feedstyleChanged || outdated || clientsChanged) {
recyclerData.lastLoad = OffsetDateTime.now();
recyclerData.loadedFeedStyle = feedStyle;
recyclerData.lastClients = clients;
loadResults();
} else {
setLoading(false);
}
@@ -147,22 +156,7 @@ class HomeFragment : MainFragment() {
finishRefreshLayoutLoader();
}
fun scrollToTop(smooth: Boolean) {
if (smooth) {
_recyclerResults.smoothScrollToPosition(0)
} else {
_recyclerResults.scrollToPosition(0)
}
}
fun reloadFeed() {
recyclerData.lastLoad = OffsetDateTime.now();
recyclerData.loadedFeedStyle = feedStyle;
recyclerData.lastClients = StatePlatform.instance.getSortedEnabledClient().filter { if (it is JSClient) it.enableInHome else true };
loadResults();
}
override fun getEmptyPagerView(): View {
override fun getEmptyPagerView(): View? {
val dp10 = 10.dp(resources);
val dp30 = 30.dp(resources);
@@ -194,7 +188,8 @@ class HomeFragment : MainFragment() {
listOf(BigButton(context, "Sources", "Go to the sources tab", R.drawable.ic_creators) {
fragment.navigate<SourcesFragment>();
}.withMargin(dp10, dp30))
)
);
return null;
}
override fun reload() {
@@ -214,7 +209,7 @@ class HomeFragment : MainFragment() {
//StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), context.getString(R.string.no_home_available), context.getString(R.string.no_home_page_is_available_please_check_if_you_are_connected_to_the_internet_and_refresh), AnnouncementType.SESSION);
}
Logger.i(TAG, "Got new home pager $pager");
Logger.i(TAG, "Got new home pager ${pager}");
finishRefreshLayoutLoader();
setLoading(false);
setPager(pager);
@@ -224,7 +219,7 @@ class HomeFragment : MainFragment() {
}
companion object {
const val TAG = "HomeFragment";
val TAG = "HomeFragment";
fun newInstance() = HomeFragment().apply {}
}
@@ -8,6 +8,7 @@ import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.fragment.mainactivity.MainActivityFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.TopFragment
import com.futo.platformplayer.listeners.OrientationManager
abstract class MainFragment : MainActivityFragment() {
open val isMainView: Boolean = false;
@@ -45,6 +46,10 @@ abstract class MainFragment : MainActivityFragment() {
}
open fun onOrientationChanged(orientation: OrientationManager.Orientation) {
}
open fun onBackPressed(): Boolean {
return false;
}
@@ -70,7 +70,7 @@ class PlaylistFragment : MainFragment() {
private var _editPlaylistOverlay: SlideUpMenuOverlay? = null;
private var _url: String? = null;
private val _taskLoadPlaylist: TaskHandler<String, Playlist>;
private val _taskLoadPlaylist: TaskHandler<String, IPlatformPlaylistDetails>;
constructor(fragment: PlaylistFragment, inflater: LayoutInflater) : super(inflater) {
_fragment = fragment;
@@ -109,44 +109,32 @@ class PlaylistFragment : MainFragment() {
val reconstruction = StatePlaylists.instance.playlistStore.getReconstructionString(playlist);
UISlideOverlays.showOverlay(overlayContainer, context.getString(R.string.playlist) + " [${playlist.name}]", null, {},
SlideUpMenuItem(
context,
R.drawable.ic_list,
context.getString(R.string.share_as_text),
context.getString(R.string.share_as_a_list_of_video_urls),
tag = 1,
call = {
_fragment.startActivity(ShareCompat.IntentBuilder(context)
.setType("text/plain")
.setText(reconstruction)
.intent);
}),
SlideUpMenuItem(
context,
R.drawable.ic_move_up,
context.getString(R.string.share_as_import),
context.getString(R.string.share_as_a_import_file_for_grayjay),
tag = 2,
call = {
val shareUri = StatePlaylists.instance.createPlaylistShareJsonUri(context, playlist);
_fragment.startActivity(ShareCompat.IntentBuilder(context)
.setType("application/json")
.setStream(shareUri)
.intent);
})
SlideUpMenuItem(context, R.drawable.ic_list, context.getString(R.string.share_as_text), context.getString(R.string.share_as_a_list_of_video_urls), 1, {
_fragment.startActivity(ShareCompat.IntentBuilder(context)
.setType("text/plain")
.setText(reconstruction)
.intent);
}),
SlideUpMenuItem(context, R.drawable.ic_move_up, context.getString(R.string.share_as_import), context.getString(R.string.share_as_a_import_file_for_grayjay), 2, {
val shareUri = StatePlaylists.instance.createPlaylistShareJsonUri(context, playlist);
_fragment.startActivity(ShareCompat.IntentBuilder(context)
.setType("application/json")
.setStream(shareUri)
.intent);
})
);
};
_taskLoadPlaylist = TaskHandler<String, Playlist>(
_taskLoadPlaylist = TaskHandler<String, IPlatformPlaylistDetails>(
StateApp.instance.scopeGetter,
{
return@TaskHandler StatePlatform.instance.getPlaylist(it).toPlaylist();
return@TaskHandler StatePlatform.instance.getPlaylist(it);
})
.success {
setName(it.name);
//TODO: Implement support for pagination
setVideos(it.videos, false);
setVideoCount(it.videos.size);
setVideos(it.toPlaylist().videos, false);
setVideoCount(it.videoCount);
setLoading(false);
}
.exception<Throwable> {
@@ -156,14 +144,6 @@ class PlaylistFragment : MainFragment() {
};
}
private fun copyPlaylist(playlist: Playlist) {
StatePlaylists.instance.playlistStore.save(playlist)
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
arrayListOf()
)
UIDialogs.toast("Playlist saved")
}
fun onShown(parameter: Any?) {
_taskLoadPlaylist.cancel()
@@ -178,10 +158,14 @@ class PlaylistFragment : MainFragment() {
setButtonDownloadVisible(true)
setButtonEditVisible(true)
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
if (!StatePlaylists.instance.playlistStore.getItems().contains(parameter)) {
_fragment.topBar?.assume<NavigationTopBarFragment>()
?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
copyPlaylist(parameter)
StatePlaylists.instance.playlistStore.save(parameter)
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
arrayListOf()
)
UIDialogs.toast("Playlist saved")
}))
}
} else {
@@ -246,15 +230,6 @@ class PlaylistFragment : MainFragment() {
}
private fun download() {
val playlist = _playlist ?: return
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to download", {
copyPlaylist(playlist)
download()
})
return
}
_playlist?.let {
UISlideOverlays.showDownloadPlaylistOverlay(it, overlayContainer);
}
@@ -279,15 +254,6 @@ class PlaylistFragment : MainFragment() {
override fun canEdit(): Boolean { return _playlist != null; }
override fun onEditClick() {
val playlist = _playlist ?: return
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to edit the name", {
copyPlaylist(playlist)
onEditClick()
})
return
}
_editPlaylistNameInput?.activate();
_editPlaylistOverlay?.show();
}
@@ -12,7 +12,6 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
@@ -24,8 +23,6 @@ import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.views.adapters.*
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class PlaylistsFragment : MainFragment() {
@@ -122,9 +119,7 @@ class PlaylistsFragment : MainFragment() {
findViewById<TextView>(R.id.text_view_all).setOnClickListener { _fragment.navigate<WatchLaterFragment>(context.getString(R.string.watch_later)); };
StatePlaylists.instance.onWatchLaterChanged.subscribe(this) {
fragment.lifecycleScope.launch(Dispatchers.Main) {
updateWatchLater();
}
updateWatchLater();
};
}
@@ -9,7 +9,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewPropertyAnimator
import android.widget.Button
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
@@ -20,7 +19,6 @@ import androidx.core.view.children
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.Thumbnails
@@ -137,7 +135,10 @@ class PostDetailFragment : MainFragment {
private val _imageDislikeIcon: ImageView;
private val _textDislikes: TextView;
private val _textComments: TextView;
private val _textCommentType: TextView;
private val _addCommentView: AddCommentView;
private val _toggleCommentType: Toggle;
private val _rating: PillRatingLikesDislikes;
@@ -151,10 +152,6 @@ class PostDetailFragment : MainFragment {
private val _commentsList: CommentsList;
private var _commentType: Boolean? = null;
private val _buttonPolycentric: Button
private val _buttonPlatform: Button
private val _taskLoadPost = if(!isInEditMode) TaskHandler<String, IPlatformPostDetails>(
StateApp.instance.scopeGetter,
{
@@ -201,6 +198,9 @@ class PostDetailFragment : MainFragment {
_textDislikes = findViewById(R.id.text_dislikes);
_commentsList = findViewById(R.id.comments_list);
_textCommentType = findViewById(R.id.text_comment_type);
_toggleCommentType = findViewById(R.id.toggle_comment_type);
_textComments = findViewById(R.id.text_comments);
_addCommentView = findViewById(R.id.add_comment_view);
_rating = findViewById(R.id.rating);
@@ -213,9 +213,6 @@ class PostDetailFragment : MainFragment {
_repliesOverlay = findViewById(R.id.replies_overlay);
_buttonPolycentric = findViewById(R.id.button_polycentric)
_buttonPlatform = findViewById(R.id.button_platform)
_textContent.setPlatformPlayerLinkMovementMethod(context);
_buttonSubscribe.onSubscribed.subscribe {
@@ -227,10 +224,9 @@ class PostDetailFragment : MainFragment {
root.removeView(layoutTop);
_commentsList.setPrependedView(layoutTop);
/*TODO: Why is this here?
_commentsList.onCommentsLoaded.subscribe {
updateCommentType(false);
};*/
};
_commentsList.onRepliesClick.subscribe { c ->
val replyCount = c.replyCount ?: 0;
@@ -241,7 +237,7 @@ class PostDetailFragment : MainFragment {
if (c is PolycentricPlatformComment) {
var parentComment: PolycentricPlatformComment = c;
_repliesOverlay.load(_commentType!!, metadata, c.contextUrl, c.reference, c,
_repliesOverlay.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, c,
{ StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
{
val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1);
@@ -249,23 +245,22 @@ class PostDetailFragment : MainFragment {
parentComment = newComment;
});
} else {
_repliesOverlay.load(_commentType!!, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
_repliesOverlay.load(_toggleCommentType.value, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
}
setRepliesOverlayVisible(isVisible = true, animate = true);
};
if (StatePolycentric.instance.enabled) {
_buttonPolycentric.setOnClickListener {
updateCommentType(false)
}
} else {
_buttonPolycentric.visibility = View.GONE
}
_buttonPlatform.setOnClickListener {
updateCommentType(true)
}
_toggleCommentType.onValueChanged.subscribe {
updateCommentType(true);
};
_textCommentType.setOnClickListener {
_toggleCommentType.setValue(!_toggleCommentType.value, true);
updateCommentType(true);
};
_layoutMonetization.visibility = View.GONE;
_buttonSupport.setOnClickListener {
@@ -437,7 +432,7 @@ class PostDetailFragment : MainFragment {
_taskLoadPolycentricProfile.cancel();
_version++;
updateCommentType(null)
_toggleCommentType.setValue(false, false);
_url = null;
_post = null;
_postOverview = null;
@@ -481,8 +476,7 @@ class PostDetailFragment : MainFragment {
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
}
val commentType = !Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1
updateCommentType(commentType, true);
updateCommentType(true);
setLoading(false);
}
@@ -685,29 +679,20 @@ class PostDetailFragment : MainFragment {
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post!!.url, ref, listOfNotNull(extraBytesRef)); };
}
private fun updateCommentType(commentType: Boolean?, forceReload: Boolean = false) {
val changed = commentType != _commentType
_commentType = commentType
private fun updateCommentType(reloadComments: Boolean) {
if (_toggleCommentType.value) {
_textCommentType.text = "Platform";
_addCommentView.visibility = View.GONE;
if (commentType == null) {
_buttonPlatform.setTextColor(resources.getColor(R.color.gray_ac))
_buttonPolycentric.setTextColor(resources.getColor(R.color.gray_ac))
if (reloadComments) {
fetchComments();
}
} else {
_buttonPlatform.setTextColor(resources.getColor(if (commentType) R.color.white else R.color.gray_ac))
_buttonPolycentric.setTextColor(resources.getColor(if (!commentType) R.color.white else R.color.gray_ac))
_textCommentType.text = "Polycentric";
_addCommentView.visibility = View.VISIBLE;
if (commentType) {
_addCommentView.visibility = View.GONE;
if (forceReload || changed) {
fetchComments();
}
} else {
_addCommentView.visibility = View.VISIBLE;
if (forceReload || changed) {
fetchPolycentricComments()
}
if (reloadComments) {
fetchPolycentricComments()
}
}
}
@@ -397,43 +397,23 @@ class SourceDetailFragment : MainFragment() {
UIDialogs.Action("Cancel", {}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action("Login", {
LoginActivity.showLogin(StateApp.instance.context, config) {
try {
StatePlugins.instance.setPluginAuth(config.id, it);
reloadSource(config.id);
} catch (e: Throwable) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
context?.let { c -> UIDialogs.showGeneralErrorDialog(c, "Failed to set plugin authentication (loginSource, loginWarning)", e) }
}
Logger.e(TAG, "Failed to set plugin authentication (loginSource, loginWarning)", e)
}
StatePlugins.instance.setPluginAuth(config.id, it);
reloadSource(config.id);
};
}, UIDialogs.ActionStyle.PRIMARY))
}
else
LoginActivity.showLogin(StateApp.instance.context, config) {
try {
StatePlugins.instance.setPluginAuth(config.id, it);
reloadSource(config.id);
} catch (e: Throwable) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
context?.let { c -> UIDialogs.showGeneralErrorDialog(c, "Failed to set plugin authentication (loginSource)", e) }
}
Logger.e(TAG, "Failed to set plugin authentication (loginSource)", e)
}
StatePlugins.instance.setPluginAuth(config.id, it);
reloadSource(config.id);
};
}
private fun logoutSource(clear: Boolean = true) {
val config = _config ?: return;
try {
StatePlugins.instance.setPluginAuth(config.id, null);
reloadSource(config.id);
} catch (e: Throwable) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
context?.let { c -> UIDialogs.showGeneralErrorDialog(c, "Failed to clear plugin authentication", e) }
}
Logger.e(TAG, "Failed to clear plugin authentication", e)
}
StatePlugins.instance.setPluginAuth(config.id, null);
reloadSource(config.id);
//TODO: Maybe add a dialog option..
if(Settings.instance.plugins.clearCookiesOnLogout && clear) {
@@ -180,7 +180,7 @@ class SubscriptionGroupFragment : MainFragment() {
UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete Group", "Are you sure you want to this group?\n[${g.name}]?", null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Delete", {
StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id, true);
StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id);
_didDelete = true;
fragment.close(true);
}, UIDialogs.ActionStyle.DANGEROUS))
@@ -253,7 +253,7 @@ class SubscriptionGroupFragment : MainFragment() {
if(g.urls.isEmpty() && g.image == null) {
//Obtain image
for(sub in it) {
val sub = StateSubscriptions.instance.getSubscription(sub) ?: StateSubscriptions.instance.getSubscriptionOther(sub);
val sub = StateSubscriptions.instance.getSubscription(sub);
if(sub != null && sub.channel.thumbnail != null) {
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
g.image?.setImageView(_imageGroup);
@@ -308,10 +308,8 @@ class SubscriptionGroupFragment : MainFragment() {
if(group != null) {
val urls = group.urls.toList();
val subs = urls.map {
(StateSubscriptions.instance.getSubscription(it) ?: StateSubscriptions.instance.getSubscriptionOther(it))?.channel
}.filterNotNull();
_enabledCreators.addAll(subs);
val subs = StateSubscriptions.instance.getSubscriptions().map { it.channel }
_enabledCreators.addAll(subs.filter { urls.contains(it.url) });
}
updateMeta();
filterCreators();
@@ -14,7 +14,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.activities.AddSourceOptionsActivity
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
@@ -58,19 +57,10 @@ class SubscriptionGroupListFragment : MainFragment() {
};
it.onDelete.subscribe { group ->
context?.let { context ->
UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete Group", "Are you sure you want to this group?\n[${group.name}]?", null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Delete", {
StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id, true);
val loc = _subs.indexOf(group);
_subs.remove(group);
_list?.adapter?.notifyItemRangeRemoved(loc);
StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id, true);
}, UIDialogs.ActionStyle.DANGEROUS));
}
val loc = _subs.indexOf(group);
_subs.remove(group);
_list?.adapter?.notifyItemRangeRemoved(loc);
StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id);
};
it.onDragDrop.subscribe {
_touchHelper?.startDrag(it);
@@ -5,10 +5,12 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.futo.platformplayer.*
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
@@ -25,7 +27,6 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson
@@ -35,6 +36,7 @@ import com.futo.platformplayer.views.ToastView
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.announcements.AnnouncementView
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.subscriptions.SubscriptionBar
import kotlinx.coroutines.CancellationException
@@ -44,6 +46,7 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.nio.channels.Channel
import java.time.OffsetDateTime
import kotlin.system.measureTimeMillis
@@ -54,7 +57,7 @@ class SubscriptionsFeedFragment : MainFragment() {
private var _view: SubscriptionsFeedView? = 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>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
@@ -107,7 +110,7 @@ class SubscriptionsFeedFragment : MainFragment() {
var subGroup: SubscriptionGroup? = null;
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
StateSubscriptions.instance.global.onUpdateProgress.subscribe(this) { progress, total ->
};
@@ -124,9 +127,6 @@ class SubscriptionsFeedFragment : MainFragment() {
initializeToolbarContent();
setPreviewsEnabled(Settings.instance.subscriptions.previewFeedItems);
if (Settings.instance.tabs.find { it.id == 0 }?.enabled != true) {
showAnnouncementView()
}
}
fun onShown() {
@@ -147,6 +147,23 @@ class SubscriptionsFeedFragment : MainFragment() {
}
}
val announcementsView = _announcementsView;
val homeTab = Settings.instance.tabs.find { it.id == 0 };
val isHomeEnabled = homeTab?.enabled == true;
if (announcementsView != null && isHomeEnabled) {
headerView.removeView(announcementsView);
_announcementsView = null;
}
if (announcementsView == null && !isHomeEnabled) {
val c = context;
if (c != null) {
_announcementsView = AnnouncementView(c, null).apply {
headerView.addView(this)
};
}
}
if (!StateSubscriptions.instance.global.isGlobalUpdating) {
finishRefreshLayoutLoader();
}
@@ -174,6 +191,8 @@ class SubscriptionsFeedFragment : MainFragment() {
private var _subscriptionBar: SubscriptionBar? = null;
private var _announcementsView: AnnouncementView? = null;
@Serializable
class FeedFilterSettings: FragmentedStorageFileJson() {
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
@@ -195,7 +214,7 @@ class SubscriptionsFeedFragment : MainFragment() {
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group);
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true }
Logger.w(TAG, "Trying to refreshing subscriptions with requests:\n$reqCountStr");
Logger.w(TAG, "Trying to refreshing subscriptions with requests:\n" + reqCountStr);
if(rateLimitPlugins.any())
throw RateLimitException(rateLimitPlugins.map { it.key.id });
}
@@ -257,7 +276,7 @@ class SubscriptionsFeedFragment : MainFragment() {
private fun initializeToolbarContent() {
_subscriptionBar = SubscriptionBar(context).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
};
_subscriptionBar?.onClickChannel?.subscribe { c -> fragment.navigate<ChannelFragment>(c); };
_subscriptionBar?.onToggleGroup?.subscribe { g ->
@@ -345,7 +364,6 @@ class SubscriptionsFeedFragment : MainFragment() {
}
override fun reload() {
StatePlugins.instance.clearUpdating(); //Fallback in case it doesnt clear, UI should be blocked.
loadResults(true);
}
@@ -377,7 +395,7 @@ class SubscriptionsFeedFragment : MainFragment() {
_taskGetPager.run(withRefetch);
}
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
super.onRestoreCachedData(cachedData);
setEmptyPager(cachedData.results.isEmpty());
}
@@ -432,7 +450,7 @@ class SubscriptionsFeedFragment : MainFragment() {
if (toShow is PluginException)
UIDialogs.appToast(ToastView.Toast(
toShow.message +
(if(channel != null) "\nChannel: $channel" else ""), false, null,
(if(channel != null) "\nChannel: " + channel else ""), false, null,
"Plugin ${toShow.config.name} failed")
);
else
@@ -443,14 +461,14 @@ class SubscriptionsFeedFragment : MainFragment() {
val failedChannels = exs.filterIsInstance<ChannelException>().map { it.channelNameOrUrl }.distinct().toList();
val failedPlugins = exs.filter { it is PluginException || (it is ChannelException && it.cause is PluginException) }
.map { if(it is ChannelException) (it.cause as PluginException) else if(it is PluginException) it else null }
.filterNotNull()
.filter { it != null }
.distinctBy { it?.config?.name }
.map { it!! }
.toList();
for(distinctPluginFail in failedPlugins)
UIDialogs.appToast(context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
if(failedChannels.isNotEmpty())
UIDialogs.appToast(ToastView.Toast(failedChannels.take(3).map { "- $it" }.joinToString("\n") +
UIDialogs.appToast(ToastView.Toast(failedChannels.take(3).map { "- ${it}" }.joinToString("\n") +
(if(failedChannels.size >= 3) "\nAnd ${failedChannels.size - 3} more" else ""), false, null, "Failed Channels"));
}
} catch (e: Throwable) {
@@ -462,7 +480,7 @@ class SubscriptionsFeedFragment : MainFragment() {
}
companion object {
const val TAG = "SubscriptionsFeedFragment";
val TAG = "SubscriptionsFeedFragment";
fun newInstance() = SubscriptionsFeedFragment().apply {}
}
@@ -117,14 +117,8 @@ class SuggestionsFragment : MainFragment {
} else if (_searchType == SearchType.PLAYLIST) {
navigate<PlaylistSearchResultsFragment>(it);
} else {
if(it.isHttpUrl()) {
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
navigate<RemotePlaylistFragment>(it);
else if(StatePlatform.instance.hasEnabledChannelClient(it))
navigate<ChannelFragment>(it);
else
navigate<VideoDetailFragment>(it);
}
if(it.isHttpUrl())
navigate<VideoDetailFragment>(it);
else
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
}
@@ -6,7 +6,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.IPlatformClient
@@ -59,15 +58,7 @@ class TutorialFragment : MainFragment() {
}
@SuppressLint("ViewConstructor")
class TutorialView(fragment: TutorialFragment, inflater: LayoutInflater) :
ScrollView(inflater.context) {
init {
addView(TutorialContainer(fragment, inflater))
}
}
@SuppressLint("ViewConstructor")
class TutorialContainer : LinearLayout {
class TutorialView : LinearLayout {
val fragment: TutorialFragment
constructor(fragment: TutorialFragment, inflater: LayoutInflater) : super(inflater.context) {
@@ -159,7 +150,7 @@ class TutorialFragment : MainFragment() {
}
companion object {
const val TAG = "HomeFragment";
val TAG = "HomeFragment";
fun newInstance() = TutorialFragment().apply {}
val initialSetupVideos = listOf(
@@ -209,7 +200,7 @@ class TutorialFragment : MainFragment() {
TutorialVideo(
uuid = "94d36959-e3fc-4c24-a988-89147067a179",
name = "Casting",
description = "Learn about casting in Grayjay. How do I show video on my TV?\nhttps://fcast.org/",
description = "Learn about casting in Grayjay. How do I show video on my TV?",
thumbnailUrl = "https://releases.grayjay.app/tutorials/how-to-cast.jpg",
videoUrl = "https://releases.grayjay.app/tutorials/how-to-cast.mp4",
duration = 79
@@ -1,47 +1,36 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.OrientationEventListener
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.WindowCompat
import androidx.media3.common.util.UnstableApi
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.listeners.OrientationManager
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.min
//region Fragment
@UnstableApi
class VideoDetailFragment() : MainFragment() {
override val isMainView: Boolean = false;
class VideoDetailFragment : MainFragment {
override val isMainView : Boolean = false;
override val hasBottomBar: Boolean = true;
override val isOverlay: Boolean = true;
override val isOverlay : Boolean = true;
override val isHistory: Boolean = false;
private var _isActive: Boolean = false;
@@ -50,32 +39,21 @@ class VideoDetailFragment() : MainFragment() {
private var _view : SingleViewTouchableMotionLayout? = null;
var isFullscreen : Boolean = false;
/**
* whether the view is in the process of switching from full-screen maximized to minimized
* this is used to detect that the app is skipping the non full-screen maximized state
*/
var isMinimizingFromFullScreen : Boolean = false;
val onFullscreenChanged = Event1<Boolean>();
var isTransitioning : Boolean = false
private set;
var isInPictureInPicture : Boolean = false
private set;
private var _state: State = State.CLOSED
var state: State
get() = _state
set(value) {
_state = value
onStateChanged(value)
}
var state: State = State.CLOSED;
val currentUrl get() = _viewDetail?.currentUrl;
val onMinimize = Event0();
val onTransitioning = Event1<Boolean>();
val onMaximized = Event0();
var lastOrientation : OrientationManager.Orientation = OrientationManager.Orientation.PORTRAIT
private set;
private var _isInitialMaximize = true;
private val _maximizeProgress get() = _view?.progress ?: 0.0f;
@@ -83,8 +61,9 @@ class VideoDetailFragment() : MainFragment() {
private var _loadUrlOnCreate: UrlVideoWithTime? = null;
private var _leavingPiP = false;
private var _landscapeOrientationListener: LandscapeOrientationListener? = null
private var _portraitOrientationListener: PortraitOrientationListener? = null
//region Fragment
constructor() : super() {
}
fun nextVideo() {
_viewDetail?.nextVideo(true, true, true);
@@ -94,119 +73,6 @@ class VideoDetailFragment() : MainFragment() {
_viewDetail?.prevVideo(true);
}
private fun isSmallWindow(): Boolean {
return min(
resources.configuration.screenWidthDp,
resources.configuration.screenHeightDp
) < resources.getInteger(R.integer.column_width_dp) * 2
}
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) {
super.onConfigurationChanged(newConfig)
val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: false
val isSmallWindow = isSmallWindow()
if (
isSmallWindow
&& newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
&& !isFullscreen
&& state == State.MAXIMIZED
) {
_viewDetail?.setFullscreen(true)
} else if (
isSmallWindow
&& isFullscreen
&& !Settings.instance.playback.fullscreenPortrait
&& newConfig.orientation == Configuration.ORIENTATION_PORTRAIT
&& isLandscapeVideo
) {
_viewDetail?.setFullscreen(false)
}
}
private fun onStateChanged(state: State) {
if (
isSmallWindow()
&& state == State.MAXIMIZED
&& !isFullscreen
&& resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
) {
_viewDetail?.setFullscreen(true)
}
updateOrientation()
}
private fun onVideoChanged(videoWidth : Int, videoHeight: Int) {
if (
isSmallWindow()
&& state == State.MAXIMIZED
&& !isFullscreen
&& videoHeight > videoWidth
) {
_viewDetail?.setFullscreen(true)
}
}
fun updateOrientation() {
val a = activity ?: return
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait
val isReversePortraitAllowed = Settings.instance.playback.reversePortrait
val rotationLock = StatePlayer.instance.rotationLock
val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: false
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
if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT && !rotationLock && isLandscapeVideo) {
if (Settings.instance.playback.forceAllowFullScreenRotation) {
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 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) {
@SuppressLint("SourceLockedOrientationActivity")
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) {
_portraitOrientationListener?.disableListener()
_landscapeOrientationListener?.disableListener()
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
} else {
_portraitOrientationListener?.disableListener()
_landscapeOrientationListener?.disableListener()
a.requestedOrientation = if (isReversePortraitAllowed) {
ActivityInfo.SCREEN_ORIENTATION_FULL_USER
} else {
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
Logger.i(TAG, "onShownWithView parameter=$parameter")
@@ -232,6 +98,49 @@ class VideoDetailFragment() : MainFragment() {
}
}
override fun onOrientationChanged(orientation: OrientationManager.Orientation) {
super.onOrientationChanged(orientation);
if(!_isActive || state != State.MAXIMIZED)
return;
var newOrientation = orientation;
val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED) {
newOrientation = OrientationManager.Orientation.PORTRAIT;
} else if(StatePlayer.instance.rotationLock) {
return;
}
if(Settings.instance.other.bypassRotationPrevention && orientation == OrientationManager.Orientation.PORTRAIT)
changeOrientation(OrientationManager.Orientation.PORTRAIT);
if(lastOrientation == newOrientation)
return;
activity?.let {
if (isFullscreen) {
if (Settings.instance.playback.fullscreenPortrait) {
changeOrientation(newOrientation);
} else {
if(newOrientation == OrientationManager.Orientation.REVERSED_LANDSCAPE && it.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
changeOrientation(OrientationManager.Orientation.REVERSED_LANDSCAPE);
else if(newOrientation == OrientationManager.Orientation.LANDSCAPE && it.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE)
changeOrientation(OrientationManager.Orientation.LANDSCAPE);
else if(Settings.instance.playback.isAutoRotate() && (newOrientation == OrientationManager.Orientation.PORTRAIT || newOrientation == OrientationManager.Orientation.REVERSED_PORTRAIT)) {
_viewDetail?.setFullscreen(false);
}
}
}
else {
if(Settings.instance.playback.isAutoRotate() && (lastOrientation == OrientationManager.Orientation.PORTRAIT || lastOrientation == OrientationManager.Orientation.REVERSED_PORTRAIT)) {
lastOrientation = newOrientation;
_viewDetail?.setFullscreen(true);
}
}
}
lastOrientation = newOrientation;
}
override fun onBackPressed(): Boolean {
Logger.i(TAG, "onBackPressed")
@@ -245,13 +154,16 @@ class VideoDetailFragment() : MainFragment() {
closeVideoDetails();
return true;
}
override fun onHide() {
super.onHide();
}
fun preventPictureInPicture() {
Logger.i(TAG, "preventPictureInPicture() preventPictureInPicture = true");
_viewDetail?.preventPictureInPicture = true;
}
fun minimizeVideoDetail() {
fun minimizeVideoDetail(){
_viewDetail?.setFullscreen(false);
if(_view != null)
_view!!.transitionToStart();
@@ -285,9 +197,7 @@ class VideoDetailFragment() : MainFragment() {
_viewDetail = _view!!.findViewById<VideoDetailView>(R.id.fragview_videodetail).also {
it.applyFragment(this);
it.onFullscreenChanged.subscribe(::onFullscreenChanged);
it.onVideoChanged.subscribe(::onVideoChanged)
it.onMinimize.subscribe {
isMinimizingFromFullScreen = true
_view!!.transitionToStart();
};
it.onClose.subscribe {
@@ -324,7 +234,6 @@ class VideoDetailFragment() : MainFragment() {
if (state != State.MINIMIZED && progress < 0.1) {
state = State.MINIMIZED;
isMinimizingFromFullScreen = false
onMinimize.emit();
}
else if (state != State.MAXIMIZED && progress > 0.9) {
@@ -356,6 +265,7 @@ class VideoDetailFragment() : MainFragment() {
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { }
});
context
_view?.let {
if (it.progress >= 0.5 && it.progress < 1.0)
maximizeVideoDetail();
@@ -364,35 +274,8 @@ class VideoDetailFragment() : MainFragment() {
}
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
maximizeVideoDetail();
SettingsActivity.settingsActivityClosed.subscribe(this) {
updateOrientation()
}
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
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()
}
}
return _view!!;
}
@@ -450,6 +333,11 @@ class VideoDetailFragment() : MainFragment() {
}
}
val realOrientation = if(activity is MainActivity) (activity as MainActivity).orientation else lastOrientation;
Logger.i(TAG, "Real orientation on boot ${realOrientation}, lastOrientation: ${lastOrientation}");
if(realOrientation != lastOrientation)
onOrientationChanged(realOrientation);
StateCasting.instance.onResume();
}
override fun onPause() {
@@ -484,19 +372,13 @@ class VideoDetailFragment() : MainFragment() {
if(shouldStop) {
_viewDetail?.onStop();
StateCasting.instance.onStop();
Logger.v(TAG, "called onStop() shouldStop: $shouldStop");
}
}
override fun onDestroyMainView() {
super.onDestroyMainView();
Logger.v(TAG, "onDestroyMainView");
SettingsActivity.settingsActivityClosed.remove(this)
StatePlayer.instance.onRotationLockChanged.remove(this)
_landscapeOrientationListener?.disableListener()
_portraitOrientationListener?.disableListener()
_viewDetail?.let {
_viewDetail = null;
it.onDestroy();
@@ -504,6 +386,13 @@ class VideoDetailFragment() : MainFragment() {
_view = null;
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ ->
onOrientationChanged(lastOrientation);
};
}
override fun onDestroy() {
super.onDestroy()
@@ -519,66 +408,66 @@ class VideoDetailFragment() : MainFragment() {
onMaximized.clear();
}
private fun hideSystemUI() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
activity?.window?.insetsController?.let { controller ->
controller.hide(WindowInsets.Type.statusBars())
controller.hide(WindowInsets.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
} else {
@Suppress("DEPRECATION")
activity?.window?.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
@Suppress("DEPRECATION")
activity?.window?.decorView?.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
}
}
private fun showSystemUI() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, true)
activity?.window?.insetsController?.let { controller ->
controller.show(WindowInsets.Type.statusBars())
controller.show(WindowInsets.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_DEFAULT
}
} else {
@Suppress("DEPRECATION")
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
@Suppress("DEPRECATION")
activity?.window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
}
}
private fun onFullscreenChanged(fullscreen : Boolean) {
isFullscreen = fullscreen;
onFullscreenChanged.emit(isFullscreen);
if (isFullscreen) {
hideSystemUI()
} else {
showSystemUI()
activity?.let {
if (fullscreen) {
if (Settings.instance.playback.fullscreenPortrait) {
changeOrientation(lastOrientation);
} else {
var orient = lastOrientation;
if(orient == OrientationManager.Orientation.PORTRAIT || orient == OrientationManager.Orientation.REVERSED_PORTRAIT)
orient = OrientationManager.Orientation.LANDSCAPE;
changeOrientation(orient);
}
}
else
changeOrientation(OrientationManager.Orientation.PORTRAIT);
}
// 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();
isFullscreen = fullscreen;
_view?.allowMotion = !fullscreen;
}
private fun changeOrientation(orientation: OrientationManager.Orientation) {
Logger.i(TAG, "Orientation Change:" + orientation.name);
activity?.let {
when (orientation) {
OrientationManager.Orientation.LANDSCAPE -> {
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
_view?.allowMotion = false;
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
WindowInsetsControllerCompat(it.window, _viewDetail!!).let { controller ->
controller.hide(WindowInsetsCompat.Type.statusBars());
controller.hide(WindowInsetsCompat.Type.systemBars());
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
}
}
OrientationManager.Orientation.REVERSED_LANDSCAPE -> {
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
_view?.allowMotion = false;
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
WindowInsetsControllerCompat(it.window, _viewDetail!!).let { controller ->
controller.hide(WindowInsetsCompat.Type.statusBars());
controller.hide(WindowInsetsCompat.Type.systemBars());
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
}
}
else -> {
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
_view?.allowMotion = true;
WindowCompat.setDecorFitsSystemWindows(it.window, true)
WindowInsetsControllerCompat(it.window, _viewDetail!!).let { controller ->
controller.show(WindowInsetsCompat.Type.statusBars());
controller.show(WindowInsetsCompat.Type.systemBars())
}
}
}
}
}
companion object {
private const val TAG = "VideoDetailFragment";
private val TAG = "VideoDetailFragment";
fun newInstance() = VideoDetailFragment().apply {}
}
@@ -594,66 +483,4 @@ class VideoDetailFragment() : MainFragment() {
//region View
//TODO: Determine if encapsulated would be readable enough
//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()
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More