diff --git a/.gitmodules b/.gitmodules
index 2e67308c..cfc1f2fa 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -64,3 +64,9 @@
[submodule "app/src/stable/assets/sources/bilibili"]
path = app/src/stable/assets/sources/bilibili
url = ../plugins/bilibili.git
+[submodule "app/src/stable/assets/sources/spotify"]
+ path = app/src/stable/assets/sources/spotify
+ url = ../plugins/spotify.git
+[submodule "app/src/unstable/assets/sources/spotify"]
+ path = app/src/unstable/assets/sources/spotify
+ url = ../plugins/spotify.git
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index a3d0f019..00000000
--- a/LICENSE
+++ /dev/null
@@ -1,32 +0,0 @@
-# FUTO TEMPORARY LICENSE
-This license grants you the rights, and only the rights, set out below in respect of the source code provided. If you take advantage of these rights, you accept this license. If you do not accept the license, do not access the code.
-
-Words used in the Terms of Service have the same meaning in this license. Where there is any inconsistency between this license and those Terms of Service, these terms prevail.
-
-## Section 1: Definitions
-- "code" means the source code made available from time, in our sole discretion, for access under this license. Reference to code in this license means the code and any part of it and any derivative of it.
-- “compilation” means to compile the code from ‘source code’ to ‘machine code’.
-- "defect" means a defect, bug, backdoor, security issue or other deficiency in the code.
-- “non-commercial distribution” means distribution of the code or any compilation of the code, or of any other application or program containing the code or any compilation of the code, where such distribution is not intended for or directed towards commercial advantage or monetary compensation.
-- "review" means to access, analyse, test and otherwise review the code as a reference, for the sole purpose of analysing it for defects.
-- "you" means the licensee of rights set out in this license.
-
-## Section 2: Grant of Rights
-1. Subject to the terms of this license, we grant you a non-transferable, non-exclusive, worldwide, royalty-free license to access and use the code solely for the purposes of review, compilation and non-commercial distribution.
-2. You may provide the code to anyone else and publish excerpts of it for the purposes of review, compilation and non-commercial distribution, provided that when you do so you make any recipient of the code aware of the terms of this license, they must agree to be bound by the terms of this license and you must attribute the code to the provider.
-3. Other than in respect of those parts of the code that were developed by other parties and as specified strictly in accordance with the open source and other licenses under which those parts of the code have been made available, as set out on our website or in those items of code, you are not entitled to use or do anything with the code for any commercial or other purpose, other than review, compilation and non-commercial distribution in accordance with the terms of this license.
-4. Subject to the terms of this license, you must at all times comply with and shall be bound by our Terms of Use, Privacy and Data Policy.
-
-## Section 3: Limitations
-1. This license does not grant you any rights to use the provider's name, logo, or trademarks and you must not in any way indicate you are authorised to speak on behalf of the provider.
-2. If you issue proceedings in any jurisdiction against the provider because you consider the provider has infringed copyright or any patent right in respect of the code (including any joinder or counterclaim), your license to the code is automatically terminated.
-3. THE CODE IS MADE AVAILABLE "AS-IS" AND WITHOUT ANY EXPRESS OR IMPLIED GUARANTEES AS TO FITNESS, MERCHANTABILITY, NON-INFRINGEMENT OR OTHERWISE. IT IS NOT BEING PROVIDED IN TRADE BUT ON A VOLUNTARY BASIS ON OUR PART AND IS NOT MADE AVAILABLE FOR ANY USE OUTSIDE THE TERMS OF THIS LICENSE. ANYONE ACCESSING THE CODE MUST ENSURE THEY HAVE THE REQUISITE EXPERTISE TO SECURE THEIR OWN SYSTEM AND DEVICES AND TO ACCESS AND USE THE CODE IN ACCORDANCE WITH THE TERMS OF THIS LICENSE. YOU BEAR THE RISK OF ACCESSING AND USING THE CODE. IN PARTICULAR, THE PROVIDER BEARS NO LIABILITY FOR ANY INTERFERENCE WITH OR ADVERSE EFFECT ON YOUR SYSTEM OR DEVICES AS A RESULT OF YOUR ACCESSING AND USING THE CODE IN ACCORDANCE WITH THE TERMS OF THIS LICENSE OR OTHERWISE.
-
-## Section 4: Termination, suspension and variation
-1. We may suspend, terminate or vary the terms of this license and any access to the code at any time, without notice, for any reason or no reason, in respect of any licensee, group of licensees or all licensees including as may be applicable any sub-licensees.
-
-## Section 5: General
-1. This license and its interpretation and operation are governed solely by the local law. You agree to submit to the exclusive jurisdiction of the local arbitral tribunals as further described in our Terms of Service and you agree not to raise any jurisdictional issue if we need to enforce an arbitral award or judgment in our jurisdiction or another country.
-2. Questions and comments regarding this license are welcomed and should be addressed at https://chat.futo.org/login/.
-
-Last updated 7 June 2023.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 00000000..38414394
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,43 @@
+# Grayjay Core License 1.0
+
+## Acceptance
+By using the software, you agree to all of the terms and conditions below.
+
+## Copyright License
+FUTO Holdings, Inc. (the “Licensor”) grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations below.
+
+## Limitations
+You may use or modify the software only for non-commercial purposes such as personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, all without any anticipated commercial application.
+
+You may distribute the software or provide it to others only if you do so free of charge for non-commercial purposes.
+
+Notwithstanding the above, you may not remove or obscure any functionality in the software related to payment to the Licensor in any copy you distribute to others.
+
+You may not alter, remove, or obscure any licensing, copyright, or other notices of the Licensor in the software. Any use of the Licensor’s 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 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.
+
+## Fair Use
+You may have "fair use" rights for the software under the law. These terms do not limit them.
+
+## No Other Rights
+These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the Licensor from granting licenses to anyone else. These terms do not imply any other licenses.
+
+## Termination
+If you use the software in violation of these terms, such use is not licensed, and your license will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your license will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your license to terminate automatically and permanently.
+
+## No Liability
+As far as the law allows, the software comes as is, without any warranty or condition, and the Licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.
+
+## Definitions
+- The “Licensor” is the entity offering these terms, FUTO Holdings, Inc.
+- The “software” is the software the licensor makes available under these terms, including any portion of it.
+- “You” refers to the individual or entity agreeing to these terms.
+- “Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. Control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
+- “Your license” is the license granted to you for the software under these terms.
+- “Use” means anything you do with the software requiring your license.
+- “Trademark” means trademarks, service marks, and similar rights.
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 49076988..be7cd437 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -41,9 +41,6 @@
-
diff --git a/app/src/main/assets/scripts/source.js b/app/src/main/assets/scripts/source.js
index 7e619b3e..0324e0f5 100644
--- a/app/src/main/assets/scripts/source.js
+++ b/app/src/main/assets/scripts/source.js
@@ -436,7 +436,7 @@ class PlatformPlaylist extends PlatformContent {
constructor(obj) {
super(obj, 4);
this.plugin_type = "PlatformPlaylist";
- this.videoCount = obj.videoCount ?? 0;
+ this.videoCount = obj.videoCount ?? -1;
this.thumbnail = obj.thumbnail;
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/SettingsDev.kt b/app/src/main/java/com/futo/platformplayer/SettingsDev.kt
index 91bba5e4..aa5e4c94 100644
--- a/app/src/main/java/com/futo/platformplayer/SettingsDev.kt
+++ b/app/src/main/java/com/futo/platformplayer/SettingsDev.kt
@@ -33,6 +33,7 @@ import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.fields.ButtonField
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
+import com.futo.platformplayer.views.fields.FormFieldWarning
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -493,6 +494,17 @@ class SettingsDev : FragmentedStorageFileJson() {
}
+
+ @FormField(R.string.networking, FieldForm.GROUP, -1, 18)
+ var networking = Networking();
+ @Serializable
+ class Networking {
+ @FormField(R.string.allow_all_certificates, FieldForm.TOGGLE, -1, 0)
+ @FormFieldWarning(R.string.allow_all_certificates_warning)
+ var allowAllCertificates: Boolean = false;
+ }
+
+
@Contextual
@Transient
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
@@ -503,6 +515,8 @@ class SettingsDev : FragmentedStorageFileJson() {
var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount;
}
+
+
//region BOILERPLATE
override fun encode(): String {
return Json.encodeToString(this);
diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
index f9565916..90902f5e 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
@@ -104,6 +104,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragMainTutorial: TutorialFragment;
lateinit var _fragMainPlaylists: PlaylistsFragment;
lateinit var _fragMainPlaylist: PlaylistFragment;
+ lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
lateinit var _fragWatchlist: WatchLaterFragment;
lateinit var _fragHistory: HistoryFragment;
lateinit var _fragSourceDetail: SourceDetailFragment;
@@ -246,6 +247,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainSources = SourcesFragment.newInstance();
_fragMainPlaylists = PlaylistsFragment.newInstance();
_fragMainPlaylist = PlaylistFragment.newInstance();
+ _fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
_fragPostDetail = PostDetailFragment.newInstance();
_fragWatchlist = WatchLaterFragment.newInstance();
_fragHistory = HistoryFragment.newInstance();
@@ -331,6 +333,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainSources.topBar = _fragTopBarAdd;
_fragMainPlaylists.topBar = _fragTopBarGeneral;
_fragMainPlaylist.topBar = _fragTopBarNavigation;
+ _fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
_fragPostDetail.topBar = _fragTopBarNavigation;
_fragWatchlist.topBar = _fragTopBarNavigation;
_fragHistory.topBar = _fragTopBarNavigation;
@@ -1044,6 +1047,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
SourcesFragment::class -> _fragMainSources as T;
PlaylistsFragment::class -> _fragMainPlaylists as T;
PlaylistFragment::class -> _fragMainPlaylist as T;
+ RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
PostDetailFragment::class -> _fragPostDetail as T;
WatchLaterFragment::class -> _fragWatchlist as T;
HistoryFragment::class -> _fragHistory as T;
diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt
index 3326c068..a0a0fac1 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt
@@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
+import android.net.Uri
import android.os.Bundle
import android.util.TypedValue
import android.view.View
@@ -19,7 +20,12 @@ import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton
-import com.futo.polycentric.core.*
+import com.futo.polycentric.core.ContentType
+import com.futo.polycentric.core.SignedEvent
+import com.futo.polycentric.core.StorageTypeCRDTItem
+import com.futo.polycentric.core.StorageTypeCRDTSetItem
+import com.futo.polycentric.core.Store
+import com.futo.polycentric.core.toBase64Url
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
@@ -64,11 +70,8 @@ class PolycentricBackupActivity : AppCompatActivity() {
}
_buttonShare.onClick.subscribe {
- val shareIntent = Intent(Intent.ACTION_SEND).apply {
- type = "text/plain";
- putExtra(Intent.EXTRA_TEXT, _exportBundle);
- }
- startActivity(Intent.createChooser(shareIntent, getString(R.string.share_text)));
+ val shareIntent = Intent(Intent.ACTION_VIEW, Uri.parse(_exportBundle))
+ startActivity(Intent.createChooser(shareIntent, "Share ID"));
};
_buttonCopy.onClick.subscribe {
diff --git a/app/src/main/java/com/futo/platformplayer/api/http/ManagedHttpClient.kt b/app/src/main/java/com/futo/platformplayer/api/http/ManagedHttpClient.kt
index 9c79b665..5121f41e 100644
--- a/app/src/main/java/com/futo/platformplayer/api/http/ManagedHttpClient.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/http/ManagedHttpClient.kt
@@ -1,5 +1,7 @@
package com.futo.platformplayer.api.http
+import androidx.collection.arrayMapOf
+import com.futo.platformplayer.SettingsDev
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.ensureNotMainThread
import com.futo.platformplayer.logging.Logger
@@ -13,6 +15,11 @@ import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.WebSocket
import okhttp3.WebSocketListener
+import java.security.SecureRandom
+import java.security.cert.X509Certificate
+import javax.net.ssl.SSLContext
+import javax.net.ssl.TrustManager
+import javax.net.ssl.X509TrustManager
import kotlin.system.measureTimeMillis
open class ManagedHttpClient {
@@ -25,8 +32,29 @@ open class ManagedHttpClient {
var user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
+ private val trustAllCerts = arrayOf(
+ object: X509TrustManager {
+ override fun checkClientTrusted(chain: Array?, authType: String?) { }
+ override fun checkServerTrusted(chain: Array?, authType: String?) { }
+ override fun getAcceptedIssuers(): Array {
+ return arrayOf();
+ }
+ }
+ );
+ private fun trustAllCertificates(builder: OkHttpClient.Builder) {
+ val sslContext = SSLContext.getInstance("SSL");
+ sslContext.init(null, trustAllCerts, SecureRandom());
+ builder.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager);
+ builder.hostnameVerifier { a, b ->
+ return@hostnameVerifier true;
+ }
+ Logger.w(TAG, "Creating INSECURE client (TrustAll)");
+ }
+
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
_builderTemplate = builder;
+ if(SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
+ trustAllCertificates(builder);
client = builder.addNetworkInterceptor { chain ->
val request = beforeRequest(chain.request());
val response = afterRequest(chain.proceed(request));
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/CachedPlatformClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/CachedPlatformClient.kt
deleted file mode 100644
index c604a7e6..00000000
--- a/app/src/main/java/com/futo/platformplayer/api/media/CachedPlatformClient.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-package com.futo.platformplayer.api.media
-
-import androidx.collection.LruCache
-import com.futo.platformplayer.api.media.models.ResultCapabilities
-import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
-import com.futo.platformplayer.api.media.models.chapters.IChapter
-import com.futo.platformplayer.api.media.models.comments.IPlatformComment
-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.live.ILiveChatWindowDescriptor
-import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
-import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
-import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
-import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
-import com.futo.platformplayer.api.media.structures.IPager
-import com.futo.platformplayer.models.ImageVariable
-
-/**
- * A temporary class that caches video results
- * In future this should be part of a bigger system
- */
-class CachedPlatformClient : IPlatformClient {
- private val _client : IPlatformClient;
- override val id: String get() = _client.id;
- override val name: String get() = _client.name;
- override val icon: ImageVariable? get() = _client.icon;
-
- private val _cache: LruCache;
-
- override val capabilities: PlatformClientCapabilities
- get() = _client.capabilities;
-
- constructor(client : IPlatformClient, cacheSize : Int = 10 * 1024 * 1024) {
- this._client = client;
- this._cache = LruCache(cacheSize);
- }
- override fun initialize() { _client.initialize() }
- override fun disable() { _client.disable() }
-
- override fun isContentDetailsUrl(url: String): Boolean = _client.isContentDetailsUrl(url);
- override fun getContentDetails(url: String): IPlatformContentDetails {
- var result = _cache.get(url);
- if(result == null) {
- result = _client.getContentDetails(url);
- _cache.put(url, result);
- }
- return result;
- }
-
- override fun getContentChapters(url: String): List = _client.getContentChapters(url);
- override fun getPlaybackTracker(url: String): IPlaybackTracker? = _client.getPlaybackTracker(url);
-
- override fun isChannelUrl(url: String): Boolean = _client.isChannelUrl(url);
- override fun getChannel(channelUrl: String): IPlatformChannel = _client.getChannel(channelUrl);
-
- override fun getChannelCapabilities(): ResultCapabilities = _client.getChannelCapabilities();
- override fun getChannelContents(
- channelUrl: String,
- type: String?,
- order: String?,
- filters: Map>?
- ): IPager = _client.getChannelContents(channelUrl);
-
- override fun getChannelPlaylists(channelUrl: String): IPager = _client.getChannelPlaylists(channelUrl);
-
- override fun getPeekChannelTypes(): List = _client.getPeekChannelTypes();
- override fun peekChannelContents(channelUrl: String, type: String?): List = _client.peekChannelContents(channelUrl, type);
-
- override fun getChannelUrlByClaim(claimType: Int, claimValues: Map): String? = _client.getChannelUrlByClaim(claimType, claimValues)
-
- override fun searchSuggestions(query: String): Array = _client.searchSuggestions(query);
- override fun getSearchCapabilities(): ResultCapabilities = _client.getSearchCapabilities();
- override fun search(
- query: String,
- type: String?,
- order: String?,
- filters: Map>?
- ): IPager = _client.search(query, type, order, filters);
-
- override fun getSearchChannelContentsCapabilities(): ResultCapabilities = _client.getSearchChannelContentsCapabilities();
- override fun searchChannelContents(
- channelUrl: String,
- query: String,
- type: String?,
- order: String?,
- filters: Map>?
- ): IPager = _client.searchChannelContents(channelUrl, query, type, order, filters);
-
- override fun searchChannels(query: String) = _client.searchChannels(query);
-
- override fun getComments(url: String): IPager = _client.getComments(url);
- override fun getSubComments(comment: IPlatformComment): IPager = _client.getSubComments(comment);
-
- override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = _client.getLiveChatWindow(url);
- override fun getLiveEvents(url: String): IPager? = _client.getLiveEvents(url);
-
- override fun getHome(): IPager = _client.getHome();
-
- override fun getUserSubscriptions(): Array { return arrayOf(); };
-
- override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map>?): IPager = _client.searchPlaylists(query, type, order, filters);
- override fun isPlaylistUrl(url: String): Boolean = _client.isPlaylistUrl(url);
- override fun getPlaylist(url: String): IPlatformPlaylistDetails = _client.getPlaylist(url);
- override fun getUserPlaylists(): Array { return arrayOf(); };
-
- override fun isClaimTypeSupported(claimType: Int): Boolean {
- return _client.isClaimTypeSupported(claimType);
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/IPlatformClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/IPlatformClient.kt
index 9aaf08c4..590ecc32 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/IPlatformClient.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/IPlatformClient.kt
@@ -121,6 +121,11 @@ interface IPlatformClient {
*/
fun getPlaybackTracker(url: String): IPlaybackTracker?;
+ /**
+ * Get content recommendations
+ */
+ fun getContentRecommendations(url: String): IPager?;
+
//Comments
/**
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientCapabilities.kt b/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientCapabilities.kt
index 1b76ae28..cb62b66c 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientCapabilities.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientCapabilities.kt
@@ -19,7 +19,8 @@ data class PlatformClientCapabilities(
val hasGetLiveChatWindow: Boolean = false,
val hasGetContentChapters: Boolean = false,
val hasPeekChannelContents: Boolean = false,
- val hasGetChannelPlaylists: Boolean = false
+ val hasGetChannelPlaylists: Boolean = false,
+ val hasGetContentRecommendations: Boolean = false
) {
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/contents/IPlatformContentDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/contents/IPlatformContentDetails.kt
index 781e4665..0f642764 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/contents/IPlatformContentDetails.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/contents/IPlatformContentDetails.kt
@@ -10,4 +10,6 @@ interface IPlatformContentDetails : IPlatformContent {
fun getComments(client: IPlatformClient): IPager?;
fun getPlaybackTracker(): IPlaybackTracker?;
+
+ fun getContentRecommendations(client: IPlatformClient): IPager?;
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/playlists/IPlatformPlaylistDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/playlists/IPlatformPlaylistDetails.kt
index b7783668..28947655 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/playlists/IPlatformPlaylistDetails.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/playlists/IPlatformPlaylistDetails.kt
@@ -8,5 +8,5 @@ interface IPlatformPlaylistDetails: IPlatformPlaylist {
//TODO: Determine if this should be IPlatformContent (probably not?)
val contents: IPager;
- fun toPlaylist(): Playlist;
+ fun toPlaylist(onProgress: ((progress: Int) -> Unit)? = null): Playlist;
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/video/SerializedPlatformVideoDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/video/SerializedPlatformVideoDetails.kt
index 39851441..1a0435d2 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/video/SerializedPlatformVideoDetails.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/video/SerializedPlatformVideoDetails.kt
@@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
+import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.streams.sources.*
@@ -56,6 +57,7 @@ open class SerializedPlatformVideoDetails(
override fun getComments(client: IPlatformClient): IPager? = null;
override fun getPlaybackTracker(): IPlaybackTracker? = null;
+ override fun getContentRecommendations(client: IPlatformClient): IPager? = null;
companion object {
fun fromVideo(video : IPlatformVideoDetails, subtitleSources: List) : SerializedPlatformVideoDetails {
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt
index aeec0ea3..0c3288fb 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt
@@ -560,7 +560,7 @@ open class JSClient : IPlatformClient {
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
}
- @JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
+ @JSDocs(18, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
@JSDocsParameter("url", "Url of live stream")
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
if(!capabilities.hasGetLiveChatWindow)
@@ -569,7 +569,7 @@ open class JSClient : IPlatformClient {
return@isBusyWith JSLiveChatWindowDescriptor(config,
plugin.executeTyped("source.getLiveChatWindow(${Json.encodeToString(url)})"));
}
- @JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream")
+ @JSDocs(19, "source.getLiveEvents(url)", "Gets live events for a livestream")
@JSDocsParameter("url", "Url of live stream")
override fun getLiveEvents(url: String): IPager? = isBusyWith("getLiveEvents") {
if(!capabilities.hasGetLiveEvents)
@@ -578,6 +578,20 @@ open class JSClient : IPlatformClient {
return@isBusyWith JSLiveEventPager(config, this,
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
}
+
+
+ @JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
+ @JSDocsParameter("url", "Url of content")
+ override fun getContentRecommendations(url: String): IPager? = isBusyWith("getContentRecommendations") {
+ if(!capabilities.hasGetContentRecommendations)
+ return@isBusyWith null;
+ ensureEnabled();
+ return@isBusyWith JSContentPager(config, this,
+ plugin.executeTyped("source.getContentRecommendations(${Json.encodeToString(url)})"));
+ }
+
+
+
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
@JSDocsParameter("query", "Query that search results should match")
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylist.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylist.kt
index b47ae9ea..787b53e4 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylist.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylist.kt
@@ -14,6 +14,6 @@ open class JSPlaylist : JSContent, IPlatformPlaylist {
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
val contextName = "Playlist";
thumbnail = obj.getOrDefault(config, "thumbnail", contextName, null);
- videoCount = obj.getOrDefault(config, "videoCount", contextName, 0)!!;
+ videoCount = obj.getOrDefault(config, "videoCount", contextName, -1)!!;
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt
index cbd4e013..f2c3935c 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt
@@ -7,7 +7,7 @@ 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.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
-import com.futo.platformplayer.engine.V8Plugin
+import com.futo.platformplayer.api.media.structures.ReusablePager
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.models.Playlist
@@ -15,22 +15,26 @@ class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
override val contents: IPager;
constructor(plugin: JSClient, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
- contents = JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails"));
+ contents = ReusablePager(JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails")));
}
- override fun toPlaylist(): Playlist {
- val videos = contents.getResults().toMutableList();
+ override fun toPlaylist(onProgress: ((progress: Int) -> Unit)?): Playlist {
+ val playlist = if (contents is ReusablePager) contents.getWindow() else contents;
+ val videos = playlist.getResults().toMutableList();
+ onProgress?.invoke(videos.size);
//Download all pages
var allowedEmptyCount = 2;
- while(contents.hasMorePages()) {
- contents.nextPage();
- if(!videos.addAll(contents.getResults())) {
+ while(playlist.hasMorePages()) {
+ playlist.nextPage();
+ if(!videos.addAll(playlist.getResults())) {
allowedEmptyCount--;
if(allowedEmptyCount <= 0)
break;
}
else allowedEmptyCount = 2;
+
+ onProgress?.invoke(videos.size);
}
return Playlist(id.toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt
index bc455fcc..6c80d7dc 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt
@@ -3,6 +3,7 @@ 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.models.comments.IPlatformComment
+import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.post.IPlatformPostDetails
@@ -18,6 +19,7 @@ import com.futo.platformplayer.states.StateDeveloper
class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
private val _hasGetComments: Boolean;
+ private val _hasGetContentRecommendations: Boolean;
override val rating: IRating;
@@ -34,6 +36,7 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
content = obj.getOrDefault(config, "content", contextName, "") ?: "";
_hasGetComments = _content.has("getComments");
+ _hasGetContentRecommendations = _content.has("getContentRecommendations");
}
override fun getComments(client: IPlatformClient): IPager? {
@@ -51,9 +54,27 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
}
override fun getPlaybackTracker(): IPlaybackTracker? = null;
+ override fun getContentRecommendations(client: IPlatformClient): IPager? {
+ 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("getContentRecommendations", arrayOf());
+ return JSContentPager(_pluginConfig, client, contentPager);
+ }
private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke("getComments", arrayOf());
return JSCommentPager(_pluginConfig, client, commentPager);
}
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt
index 83137f23..da495498 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt
@@ -6,6 +6,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
+import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
@@ -27,6 +28,7 @@ import com.futo.platformplayer.states.StateDeveloper
class JSVideoDetails : JSVideo, IPlatformVideoDetails {
private val _hasGetComments: Boolean;
+ private val _hasGetContentRecommendations: Boolean;
private val _hasGetPlaybackTracker: Boolean;
//Details
@@ -66,6 +68,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
_hasGetComments = _content.has("getComments");
_hasGetPlaybackTracker = _content.has("getPlaybackTracker");
+ _hasGetContentRecommendations = _content.has("getContentRecommendations");
}
override fun getPlaybackTracker(): IPlaybackTracker? {
@@ -89,6 +92,24 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
};
}
+ override fun getContentRecommendations(client: IPlatformClient): IPager? {
+ if(!_hasGetContentRecommendations || _content.isClosed)
+ return null;
+
+ if(client is DevJSClient)
+ return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.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("getContentRecommendations", arrayOf());
+ return JSContentPager(_pluginConfig, client, contentPager);
+ }
+
override fun getComments(client: IPlatformClient): IPager? {
if(client !is JSClient || !_hasGetComments || _content.isClosed)
return null;
diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt
index 81f0fb09..7c1c4e09 100644
--- a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt
+++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt
@@ -1,47 +1,37 @@
package com.futo.platformplayer.downloads
import android.content.Context
-import android.net.Uri
-import android.os.Environment
import androidx.documentfile.provider.DocumentFile
-import com.arthenica.ffmpegkit.*
-import com.futo.platformplayer.api.media.models.streams.sources.*
-import com.futo.platformplayer.constructs.Event1
+import com.arthenica.ffmpegkit.FFmpegKit
+import com.arthenica.ffmpegkit.LogCallback
+import com.arthenica.ffmpegkit.ReturnCode
+import com.arthenica.ffmpegkit.StatisticsCallback
+import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
+import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSource
+import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.toHumanBitrate
-import kotlinx.coroutines.*
-import java.io.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.io.FileInputStream
+import java.io.IOException
+import java.io.OutputStream
import java.util.UUID
-import java.util.concurrent.CancellationException
import java.util.concurrent.Executors
import kotlin.coroutines.resumeWithException
@kotlinx.serialization.Serializable
class VideoExport {
- var state: State = State.QUEUED;
-
var videoLocal: VideoLocal;
var videoSource: LocalVideoSource?;
var audioSource: LocalAudioSource?;
var subtitleSource: LocalSubtitleSource?;
- var progress: Double = 0.0;
- var isCancelled = false;
-
- var error: String? = null;
-
- @kotlinx.serialization.Transient
- val onStateChanged = Event1();
- @kotlinx.serialization.Transient
- val onProgressChanged = Event1();
-
- fun changeState(newState: State) {
- state = newState;
- onStateChanged.emit(newState);
- }
-
constructor(videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
this.videoLocal = videoLocal;
this.videoSource = videoSource;
@@ -50,8 +40,6 @@ class VideoExport {
}
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
- if(isCancelled) throw CancellationException("Export got cancelled");
-
val v = videoSource;
val a = audioSource;
val s = subtitleSource;
@@ -107,7 +95,6 @@ class VideoExport {
throw Exception("Cannot export when no audio or video source is set.");
}
- onProgressChanged.emit(100.0);
return@coroutineScope outputFile;
}
diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoLocal.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoLocal.kt
index 3b985796..7308b1c6 100644
--- a/app/src/main/java/com/futo/platformplayer/downloads/VideoLocal.kt
+++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoLocal.kt
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
+import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
@@ -81,6 +82,8 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
override fun getComments(client: IPlatformClient): IPager? = null;
override fun getPlaybackTracker(): IPlaybackTracker? = null;
+ override fun getContentRecommendations(client: IPlatformClient): IPager? = null;
+
fun toPlatformVideo() : IPlatformVideoDetails {
throw NotImplementedError();
diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt
index d07720e8..44464b0d 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt
@@ -1,6 +1,8 @@
package com.futo.platformplayer.engine.packages
import com.caoccao.javet.annotations.V8Function
+import com.caoccao.javet.annotations.V8Property
+import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.UIDialogs
@@ -35,6 +37,19 @@ class PackageBridge : V8Package {
_clientAuth = plugin.httpClientAuth;
}
+
+ @V8Property
+ fun buildVersion(): Int {
+ //If debug build, assume max version
+ if(BuildConfig.VERSION_CODE == 1)
+ return Int.MAX_VALUE;
+ return BuildConfig.VERSION_CODE;
+ }
+ @V8Property
+ fun buildFlavor(): String {
+ return BuildConfig.FLAVOR;
+ }
+
@V8Function
fun toast(str: String) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt
index f7d9d9bf..19f1454b 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt
@@ -114,7 +114,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
}
- fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
+ override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
_lastPolycentricProfile = polycentricProfile;
if (polycentricProfile == null) {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt
index 0647afed..ea8ee4fa 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt
@@ -309,7 +309,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
_adapterResults?.setLoading(loading);
}
- fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
+ override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
val p = _lastPolycentricProfile;
if (p != null && polycentricProfile != null && p.system == polycentricProfile.system) {
Logger.i(TAG, "setPolycentricProfile skipped because previous was same");
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelListFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelListFragment.kt
index 00ec5982..807fbd90 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelListFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelListFragment.kt
@@ -124,7 +124,7 @@ class ChannelListFragment : Fragment, IChannelTabFragment {
}
}
- fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
+ override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
_taskLoadChannel.cancel();
_lastPolycentricProfile = polycentricProfile;
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt
index e2e39ee7..53268d16 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt
@@ -46,7 +46,7 @@ class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
_lastChannel = channel;
}
- fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
+ override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
_lastPolycentricProfile = polycentricProfile
if (polycentricProfile != null) {
_supportView?.setPolycentricProfile(polycentricProfile)
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelPlaylistsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelPlaylistsFragment.kt
new file mode 100644
index 00000000..dbb58b68
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelPlaylistsFragment.kt
@@ -0,0 +1,297 @@
+package com.futo.platformplayer.fragment.channel.tab
+
+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.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.futo.platformplayer.R
+import com.futo.platformplayer.Settings
+import com.futo.platformplayer.UIDialogs
+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.playlists.IPlatformPlaylist
+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
+import com.futo.platformplayer.api.media.structures.IRefreshPager
+import com.futo.platformplayer.api.media.structures.IReplacerPager
+import com.futo.platformplayer.api.media.structures.MultiPager
+import com.futo.platformplayer.constructs.Event1
+import com.futo.platformplayer.constructs.Event2
+import com.futo.platformplayer.constructs.TaskHandler
+import com.futo.platformplayer.engine.exceptions.PluginException
+import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
+import com.futo.platformplayer.exceptions.ChannelException
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.states.StatePlatform
+import com.futo.platformplayer.views.FeedStyle
+import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
+import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
+import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
+ private var _recyclerResults: RecyclerView? = null
+ private var _llmPlaylist: LinearLayoutManager? = null
+ private var _loading = false
+ private var _pagerParent: IPager? = null
+ private var _pager: IPager? = null
+ private var _channel: IPlatformChannel? = null
+ private var _results: ArrayList = arrayListOf()
+ private var _adapterResults: InsertedViewAdapterWithLoader? = null
+
+ val onContentClicked = Event2()
+ val onContentUrlClicked = Event2()
+ val onUrlClicked = Event1()
+ val onChannelClicked = Event1()
+ val onAddToClicked = Event1()
+ val onAddToQueueClicked = Event1()
+ val onAddToWatchLaterClicked = Event1()
+ val onLongPress = Event1()
+
+ private fun getPlaylistPager(channel: IPlatformChannel): IPager {
+ Logger.i(TAG, "getPlaylistPager")
+
+ return StatePlatform.instance.getChannelPlaylists(channel.url)
+ }
+
+ private val _taskLoadPlaylists =
+ TaskHandler>({ lifecycleScope }, {
+ val livePager = getPlaylistPager(it)
+ return@TaskHandler livePager
+ }).success { livePager ->
+ setLoading(false)
+
+ setPager(livePager)
+ }.exception { }.exception {
+ Logger.w(TAG, "Failed to load initial playlists.", it)
+ UIDialogs.showGeneralRetryErrorDialog(requireContext(),
+ it.message ?: "",
+ it,
+ { loadNextPage() })
+ }
+
+ private var _nextPageHandler: TaskHandler, List> =
+ TaskHandler, List>({ lifecycleScope }, {
+ if (it is IAsyncPager<*>) it.nextPageAsync()
+ else it.nextPage()
+
+ processPagerExceptions(it)
+ return@TaskHandler it.getResults()
+ }).success {
+ setLoading(false)
+ val posBefore = _results.size
+ _results.addAll(it)
+ _adapterResults?.let { adapterResult ->
+ adapterResult.notifyItemRangeInserted(
+ adapterResult.childToParentPosition(
+ posBefore
+ ), it.size
+ )
+ }
+ }.exception {
+ Logger.w(TAG, "Failed to load next page.", it)
+ UIDialogs.showGeneralRetryErrorDialog(requireContext(),
+ it.message ?: "",
+ it,
+ { loadNextPage() })
+ }
+
+ private val _scrollListener = object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ super.onScrolled(recyclerView, dx, dy)
+
+ val recyclerResults = _recyclerResults ?: return
+ val llmPlaylist = _llmPlaylist ?: return
+
+ val visibleItemCount = recyclerResults.childCount
+ val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
+ val visibleThreshold = 15
+ if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= _results.size) {
+ loadNextPage()
+ }
+ }
+ }
+
+ override fun setChannel(channel: IPlatformChannel) {
+ val c = _channel
+ if (c != null && c.url == channel.url) {
+ Logger.i(TAG, "setChannel skipped because previous was same")
+ return
+ }
+
+ Logger.i(TAG, "setChannel setChannel=${channel}")
+
+ _taskLoadPlaylists.cancel()
+
+ _channel = channel
+ _results.clear()
+ _adapterResults?.notifyDataSetChanged()
+
+ loadInitial()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
+ ): View? {
+ val view = inflater.inflate(R.layout.fragment_channel_videos, container, false)
+
+ _recyclerResults = view.findViewById(R.id.recycler_videos)
+
+ _adapterResults = PreviewContentListAdapter(
+ view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar
+ ).apply {
+ this.onContentUrlClicked.subscribe(this@ChannelPlaylistsFragment.onContentUrlClicked::emit)
+ this.onUrlClicked.subscribe(this@ChannelPlaylistsFragment.onUrlClicked::emit)
+ this.onContentClicked.subscribe(this@ChannelPlaylistsFragment.onContentClicked::emit)
+ this.onChannelClicked.subscribe(this@ChannelPlaylistsFragment.onChannelClicked::emit)
+ this.onAddToClicked.subscribe(this@ChannelPlaylistsFragment.onAddToClicked::emit)
+ this.onAddToQueueClicked.subscribe(this@ChannelPlaylistsFragment.onAddToQueueClicked::emit)
+ this.onAddToWatchLaterClicked.subscribe(this@ChannelPlaylistsFragment.onAddToWatchLaterClicked::emit)
+ this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
+ }
+
+ _llmPlaylist = LinearLayoutManager(view.context)
+ _recyclerResults?.adapter = _adapterResults
+ _recyclerResults?.layoutManager = _llmPlaylist
+ _recyclerResults?.addOnScrollListener(_scrollListener)
+
+ return view
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _recyclerResults?.removeOnScrollListener(_scrollListener)
+ _recyclerResults = null
+ _pager = null
+
+ _taskLoadPlaylists.cancel()
+ _nextPageHandler.cancel()
+ }
+
+ private fun setPager(
+ pager: IPager
+ ) {
+ if (_pagerParent != null && _pagerParent is IRefreshPager<*>) {
+ (_pagerParent as IRefreshPager<*>).onPagerError.remove(this)
+ (_pagerParent as IRefreshPager<*>).onPagerChanged.remove(this)
+ _pagerParent = null
+ }
+ if (_pager is IReplacerPager<*>) (_pager as IReplacerPager<*>).onReplaced.remove(this)
+
+ val pagerToSet: IPager?
+ if (pager is IRefreshPager<*>) {
+ _pagerParent = pager
+ pagerToSet = pager.getCurrentPager() as IPager
+ pager.onPagerChanged.subscribe(this) {
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ try {
+ loadPagerInternal(it as IPager)
+ } catch (e: Throwable) {
+ Logger.e(TAG, "loadPagerInternal failed.", e)
+ }
+ }
+ }
+ pager.onPagerError.subscribe(this) {
+ Logger.e(TAG, "Search pager failed: ${it.message}", it)
+ if (it is PluginException) UIDialogs.toast("Plugin [${it.config.name}] failed due to:\n${it.message}")
+ else UIDialogs.toast("Plugin failed due to:\n${it.message}")
+ }
+ } else pagerToSet = pager
+
+ loadPagerInternal(pagerToSet)
+ }
+
+ private fun loadPagerInternal(
+ pager: IPager
+ ) {
+ if (_pager is IReplacerPager<*>) (_pager as IReplacerPager<*>).onReplaced.remove(this)
+ if (pager is IReplacerPager<*>) {
+ pager.onReplaced.subscribe(this) { oldItem, newItem ->
+ if (_pager != pager) return@subscribe
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ val toReplaceIndex = _results.indexOfFirst { it == oldItem }
+ if (toReplaceIndex >= 0) {
+ _results[toReplaceIndex] = newItem as IPlatformPlaylist
+ _adapterResults?.let {
+ it.notifyItemChanged(it.childToParentPosition(toReplaceIndex))
+ }
+ }
+ }
+ }
+ }
+
+ _pager = pager
+
+ processPagerExceptions(pager)
+
+ _results.clear()
+ val toAdd = pager.getResults()
+ _results.addAll(toAdd)
+ _adapterResults?.notifyDataSetChanged()
+ _recyclerResults?.scrollToPosition(0)
+ }
+
+ private fun loadInitial() {
+ val channel: IPlatformChannel = _channel ?: return
+ setLoading(true)
+ _taskLoadPlaylists.run(channel)
+ }
+
+ private fun loadNextPage() {
+ val pager: IPager = _pager ?: return
+ if (_pager?.hasMorePages() == true) {
+ setLoading(true)
+ _nextPageHandler.run(pager)
+ }
+ }
+
+ private fun setLoading(loading: Boolean) {
+ _loading = loading
+ _adapterResults?.setLoading(loading)
+ }
+
+ private fun processPagerExceptions(pager: IPager<*>) {
+ if (pager is MultiPager<*> && pager.allowFailure) {
+ val ex = pager.getResultExceptions()
+ for (kv in ex) {
+ val jsPager: JSPager<*>? = when (kv.key) {
+ is MultiPager<*> -> (kv.key as MultiPager<*>).findPager { it is JSPager<*> } as JSPager<*>?
+ is JSPager<*> -> kv.key as JSPager<*>
+ else -> null
+ }
+
+ context?.let {
+ lifecycleScope.launch(Dispatchers.Main) {
+ try {
+ val channel =
+ if (kv.value is ChannelException) (kv.value as ChannelException).channelNameOrUrl else null
+ if (jsPager != null) UIDialogs.toast(
+ it,
+ "Plugin ${jsPager.getPluginConfig().name} failed:\n" + (if (!channel.isNullOrEmpty()) "(${channel}) " else "") + "${kv.value.message}",
+ false
+ )
+ else UIDialogs.toast(it, kv.value.message ?: "", false)
+ } catch (e: Throwable) {
+ Logger.e(TAG, "Failed to show toast.", e)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ const val TAG = "PlaylistsFragment"
+ fun newInstance() = ChannelPlaylistsFragment().apply { }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/IChannelTabFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/IChannelTabFragment.kt
index e1da77c5..2b615d25 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/IChannelTabFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/IChannelTabFragment.kt
@@ -1,7 +1,11 @@
package com.futo.platformplayer.fragment.channel.tab
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
+import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
interface IChannelTabFragment {
- fun setChannel(channel: IPlatformChannel);
-}
\ No newline at end of file
+ fun setChannel(channel: IPlatformChannel)
+ fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
+
+ }
+}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt
index 3359119f..ad4aa84d 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt
@@ -15,8 +15,9 @@ import androidx.appcompat.widget.AppCompatImageView
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2
import com.bumptech.glide.Glide
-import com.futo.platformplayer.*
import com.futo.platformplayer.R
+import com.futo.platformplayer.UIDialogs
+import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
@@ -27,26 +28,32 @@ import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
+import com.futo.platformplayer.assume
import com.futo.platformplayer.constructs.TaskHandler
-import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment
-import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment
-import com.futo.platformplayer.fragment.channel.tab.ChannelListFragment
-import com.futo.platformplayer.fragment.channel.tab.ChannelMonetizationFragment
+import com.futo.platformplayer.dp
+import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
+import com.futo.platformplayer.selectBestImage
+import com.futo.platformplayer.selectHighestResolutionImage
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StateSubscriptions
+import com.futo.platformplayer.toHumanNumber
+import com.futo.platformplayer.views.adapters.ChannelTab
import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.subscriptions.SubscribeButton
-import com.futo.polycentric.core.*
+import com.futo.polycentric.core.OwnedClaim
+import com.futo.polycentric.core.PublicKey
+import com.futo.polycentric.core.SystemState
+import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.Dispatchers
@@ -55,459 +62,530 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
@Serializable
-data class PolycentricProfile(val system: PublicKey, val systemState: SystemState, val ownedClaims: List);
+data class PolycentricProfile(
+ val system: PublicKey, val systemState: SystemState, val ownedClaims: List
+)
class ChannelFragment : MainFragment() {
- override val isMainView : Boolean = true;
- override val hasBottomBar: Boolean = true;
- private var _view: ChannelView? = null;
+ override val isMainView: Boolean = true
+ override val hasBottomBar: Boolean = true
+ private var _view: ChannelView? = null
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
- super.onShownWithView(parameter, isBack);
- _view?.onShown(parameter, isBack);
+ super.onShownWithView(parameter, isBack)
+ _view?.onShown(parameter, isBack)
}
- override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
- val view = ChannelView(this, inflater);
- _view = view;
- return view;
+ override fun onCreateMainView(
+ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
+ ): View {
+ val view = ChannelView(this, inflater)
+ _view = view
+ return view
}
override fun onBackPressed(): Boolean {
- return _view?.onBackPressed() ?: false;
+ return _view?.onBackPressed() ?: false
}
override fun onDestroyMainView() {
- super.onDestroyMainView();
+ super.onDestroyMainView()
- _view?.cleanup();
- _view = null;
+ _view?.cleanup()
+ _view = null
}
- fun selectTab(selectedTabIndex: Int) {
- _view?.selectTab(selectedTabIndex);
+ fun selectTab(tab: ChannelTab) {
+ _view?.selectTab(tab)
}
@SuppressLint("ViewConstructor")
- class ChannelView : LinearLayout {
- private val _fragment: ChannelFragment;
+ class ChannelView
+ (fragment: ChannelFragment, inflater: LayoutInflater) : LinearLayout(inflater.context) {
+ private val _fragment: ChannelFragment = fragment
- private var _textChannel: TextView;
- private var _textChannelSub: TextView;
- private var _creatorThumbnail: CreatorThumbnail;
- private var _imageBanner: AppCompatImageView;
+ private var _textChannel: TextView
+ private var _textChannelSub: TextView
+ private var _creatorThumbnail: CreatorThumbnail
+ private var _imageBanner: AppCompatImageView
- private var _tabs: TabLayout;
- private var _viewPager: ViewPager2;
- private var _tabLayoutMediator: TabLayoutMediator;
- private var _buttonSubscribe: SubscribeButton;
- private var _buttonSubscriptionSettings: ImageButton;
+ private var _tabs: TabLayout
+ private var _viewPager: ViewPager2
- private var _overlayContainer: FrameLayout;
- private var _overlay_loading: LinearLayout;
- private var _overlay_loading_spinner: ImageView;
+ // private var _adapter: ChannelViewPagerAdapter;
+ private var _tabLayoutMediator: TabLayoutMediator
+ private var _buttonSubscribe: SubscribeButton
+ private var _buttonSubscriptionSettings: ImageButton
- private var _slideUpOverlay: SlideUpMenuOverlay? = null;
+ private var _overlayContainer: FrameLayout
+ private var _overlayLoading: LinearLayout
+ private var _overlayLoadingSpinner: ImageView
- private var _isLoading: Boolean = false;
- private var _selectedTabIndex: Int = -1;
+ private var _slideUpOverlay: SlideUpMenuOverlay? = null
+
+ private var _isLoading: Boolean = false
+ private var _selectedTabIndex: Int = -1
var channel: IPlatformChannel? = null
- private set;
- private var _url: String? = null;
+ private set
+ private var _url: String? = null
- private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
- override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
- super.onPageScrolled(position, positionOffset, positionOffsetPixels);
- //recalculate(position, positionOffset);
- }
- }
+ private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
- private val _taskLoadPolycentricProfile: TaskHandler;
- private val _taskGetChannel: TaskHandler;
+ private val _taskLoadPolycentricProfile: TaskHandler
+ private val _taskGetChannel: TaskHandler
- constructor(fragment: ChannelFragment, inflater: LayoutInflater) : super(inflater.context) {
- _fragment = fragment;
- inflater.inflate(R.layout.fragment_channel, this);
-
- _taskLoadPolycentricProfile = TaskHandler({fragment.lifecycleScope}, { id ->
- return@TaskHandler PolycentricCache.instance.getProfileAsync(id);
- })
- .success { it -> setPolycentricProfile(it, animate = true) }
- .exception {
- Logger.w(TAG, "Failed to load polycentric profile.", it);
- };
-
- _taskGetChannel = TaskHandler({fragment.lifecycleScope}, { url -> StatePlatform.instance.getChannelLive(url) })
- .success { showChannel(it); }
+ init {
+ inflater.inflate(R.layout.fragment_channel, this)
+ _taskLoadPolycentricProfile =
+ TaskHandler({ fragment.lifecycleScope },
+ { id ->
+ return@TaskHandler PolycentricCache.instance.getProfileAsync(id)
+ }).success { setPolycentricProfile(it, animate = true) }.exception {
+ Logger.w(TAG, "Failed to load polycentric profile.", it)
+ }
+ _taskGetChannel = TaskHandler({ fragment.lifecycleScope },
+ { url -> StatePlatform.instance.getChannelLive(url) }).success { showChannel(it); }
.exception {
- UIDialogs.showDialog(context,
+ UIDialogs.showDialog(
+ context,
R.drawable.ic_sources,
- context.getString(R.string.no_source_enabled_to_support_this_channel) + "\n(${_url})", null, null,
+ context.getString(R.string.no_source_enabled_to_support_this_channel) + "\n(${_url})",
+ null,
+ null,
0,
UIDialogs.Action("Back", {
- fragment.close(true);
+ fragment.close(true)
}, UIDialogs.ActionStyle.PRIMARY)
- );
+ )
+ }.exception {
+ Logger.e(TAG, "Failed to load channel.", it)
+ UIDialogs.showGeneralRetryErrorDialog(
+ context, it.message ?: "", it, { loadChannel() }, null, fragment
+ )
}
- .exception {
- Logger.e(TAG, "Failed to load channel.", it);
- UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadChannel() }, null, fragment);
- }
-
- val tabs: TabLayout = findViewById(R.id.tabs);
- val viewPager: ViewPager2 = findViewById(R.id.view_pager);
- _textChannel = findViewById(R.id.text_channel_name);
- _textChannelSub = findViewById(R.id.text_metadata);
- _creatorThumbnail = findViewById(R.id.creator_thumbnail);
- _imageBanner = findViewById(R.id.image_channel_banner);
- _buttonSubscribe = findViewById(R.id.button_subscribe);
- _buttonSubscriptionSettings = findViewById(R.id.button_sub_settings);
- _overlay_loading = findViewById(R.id.channel_loading_overlay);
- _overlay_loading_spinner = findViewById(R.id.channel_loader);
- _overlayContainer = findViewById(R.id.overlay_container);
-
+ val tabs: TabLayout = findViewById(R.id.tabs)
+ val viewPager: ViewPager2 = findViewById(R.id.view_pager)
+ _textChannel = findViewById(R.id.text_channel_name)
+ _textChannelSub = findViewById(R.id.text_metadata)
+ _creatorThumbnail = findViewById(R.id.creator_thumbnail)
+ _imageBanner = findViewById(R.id.image_channel_banner)
+ _buttonSubscribe = findViewById(R.id.button_subscribe)
+ _buttonSubscriptionSettings = findViewById(R.id.button_sub_settings)
+ _overlayLoading = findViewById(R.id.channel_loading_overlay)
+ _overlayLoadingSpinner = findViewById(R.id.channel_loader)
+ _overlayContainer = findViewById(R.id.overlay_container)
_buttonSubscribe.onSubscribed.subscribe {
- UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
- _buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
+ UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer)
+ _buttonSubscriptionSettings.visibility =
+ if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
}
_buttonSubscribe.onUnSubscribed.subscribe {
- _buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
+ _buttonSubscriptionSettings.visibility =
+ if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
}
-
_buttonSubscriptionSettings.setOnClickListener {
- val url = channel?.url ?: _url ?: return@setOnClickListener;
- val sub = StateSubscriptions.instance.getSubscription(url) ?: return@setOnClickListener;
- UISlideOverlays.showSubscriptionOptionsOverlay(sub, _overlayContainer);
- };
+ val url = channel?.url ?: _url ?: return@setOnClickListener
+ val sub =
+ StateSubscriptions.instance.getSubscription(url) ?: return@setOnClickListener
+ UISlideOverlays.showSubscriptionOptionsOverlay(sub, _overlayContainer)
+ }
//TODO: Determine if this is really the only solution (isSaveEnabled=false)
- viewPager.isSaveEnabled = false;
- viewPager.registerOnPageChangeCallback(_onPageChangeCallback);
- val adapter = ChannelViewPagerAdapter(fragment.childFragmentManager, fragment.lifecycle);
+ viewPager.isSaveEnabled = false
+ viewPager.registerOnPageChangeCallback(_onPageChangeCallback)
+ val adapter = ChannelViewPagerAdapter(fragment.childFragmentManager, fragment.lifecycle)
adapter.onChannelClicked.subscribe { c -> fragment.navigate(c) }
adapter.onContentClicked.subscribe { v, _ ->
- if(v is IPlatformVideo) {
- StatePlayer.instance.clearQueue();
- fragment.navigate(v).maximizeVideoDetail();
- } else if (v is IPlatformPlaylist) {
- fragment.navigate(v);
- } else if (v is IPlatformPost) {
- fragment.navigate(v);
+ when (v) {
+ is IPlatformVideo -> {
+ StatePlayer.instance.clearQueue()
+ fragment.navigate(v).maximizeVideoDetail()
+ }
+
+ is IPlatformPlaylist -> {
+ fragment.navigate(v)
+ }
+
+ is IPlatformPost -> {
+ fragment.navigate(v)
+ }
}
}
- adapter.onAddToClicked.subscribe {content ->
+ adapter.onAddToClicked.subscribe { content ->
_overlayContainer.let {
- if(content is IPlatformVideo)
- _slideUpOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it);
+ if (content is IPlatformVideo) _slideUpOverlay =
+ UISlideOverlays.showVideoOptionsOverlay(content, it)
}
}
adapter.onAddToQueueClicked.subscribe { content ->
- if(content is IPlatformVideo) {
- StatePlayer.instance.addToQueue(content);
+ if (content is IPlatformVideo) {
+ StatePlayer.instance.addToQueue(content)
}
}
adapter.onAddToWatchLaterClicked.subscribe { content ->
- if(content is IPlatformVideo) {
- StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content));
- UIDialogs.toast("Added to watch later\n[${content.name}]");
+ if (content is IPlatformVideo) {
+ StatePlaylists.instance.addToWatchLater(
+ SerializedPlatformVideo.fromVideo(
+ content
+ )
+ )
+ UIDialogs.toast("Added to watch later\n[${content.name}]")
}
}
adapter.onUrlClicked.subscribe { url ->
- fragment.navigate(url);
+ fragment.navigate(url)
}
adapter.onContentUrlClicked.subscribe { url, contentType ->
- when(contentType) {
+ when (contentType) {
ContentType.MEDIA -> {
- StatePlayer.instance.clearQueue();
- fragment.navigate(url).maximizeVideoDetail();
- };
- ContentType.URL -> fragment.navigate(url);
- else -> {};
+ StatePlayer.instance.clearQueue()
+ fragment.navigate(url).maximizeVideoDetail()
+ }
+
+ ContentType.URL -> fragment.navigate(url)
+ else -> {}
}
}
adapter.onLongPress.subscribe { content ->
_overlayContainer.let {
- if(content is IPlatformVideo)
- _slideUpOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it);
+ if (content is IPlatformVideo) _slideUpOverlay =
+ UISlideOverlays.showVideoOptionsOverlay(content, it)
}
}
- viewPager.adapter = adapter;
-
- val tabLayoutMediator = TabLayoutMediator(tabs, viewPager) { tab, position ->
- tab.text = when (position) {
- 0 -> "VIDEOS"
- 1 -> "CHANNELS"
- //2 -> "STORE"
- 2 -> "SUPPORT"
- 3 -> "ABOUT"
- else -> "Unknown $position"
- };
- };
- tabLayoutMediator.attach();
-
- _tabLayoutMediator = tabLayoutMediator;
- _tabs = tabs;
- _viewPager = viewPager;
+ viewPager.adapter = adapter
+ val tabLayoutMediator = TabLayoutMediator(
+ tabs, viewPager, (viewPager.adapter as ChannelViewPagerAdapter)::getTabNames
+ )
+ tabLayoutMediator.attach()
+ _tabLayoutMediator = tabLayoutMediator
+ _tabs = tabs
+ _viewPager = viewPager
if (_selectedTabIndex != -1) {
- selectTab(_selectedTabIndex);
+ selectTab(_selectedTabIndex)
}
+ setLoading(true)
+ }
- setLoading(true);
+ fun selectTab(tab: ChannelTab) {
+ (_viewPager.adapter as ChannelViewPagerAdapter).getTabPosition(tab)
}
fun cleanup() {
- _taskLoadPolycentricProfile.cancel();
- _taskGetChannel.cancel();
- _tabLayoutMediator.detach();
- _viewPager.unregisterOnPageChangeCallback(_onPageChangeCallback);
- hideSlideUpOverlay();
- (_overlay_loading_spinner.drawable as Animatable?)?.stop();
+ _taskLoadPolycentricProfile.cancel()
+ _taskGetChannel.cancel()
+ _tabLayoutMediator.detach()
+ _viewPager.unregisterOnPageChangeCallback(_onPageChangeCallback)
+ hideSlideUpOverlay()
+ (_overlayLoadingSpinner.drawable as Animatable?)?.stop()
}
fun onShown(parameter: Any?, isBack: Boolean) {
- hideSlideUpOverlay();
- _taskLoadPolycentricProfile.cancel();
- _selectedTabIndex = -1;
+ hideSlideUpOverlay()
+ _taskLoadPolycentricProfile.cancel()
+ _selectedTabIndex = -1
if (!isBack || _url == null) {
- _imageBanner.setImageDrawable(null);
+ _imageBanner.setImageDrawable(null)
- if (parameter is String) {
- _buttonSubscribe.setSubscribeChannel(parameter);
- _buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
- setPolycentricProfileOr(parameter) {
- _textChannel.text = "";
- _textChannelSub.text = "";
- _creatorThumbnail.setThumbnail(null, true);
- Glide.with(_imageBanner)
- .clear(_imageBanner);
- };
+ when (parameter) {
+ is String -> {
+ _buttonSubscribe.setSubscribeChannel(parameter)
+ _buttonSubscriptionSettings.visibility =
+ if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
+ setPolycentricProfileOr(parameter) {
+ _textChannel.text = ""
+ _textChannelSub.text = ""
+ _creatorThumbnail.setThumbnail(null, true)
+ Glide.with(_imageBanner).clear(_imageBanner)
+ }
- _url = parameter;
- loadChannel();
- } else if (parameter is SerializedChannel) {
- showChannel(parameter);
- _url = parameter.url;
- loadChannel();
- } else if (parameter is IPlatformChannel)
- showChannel(parameter);
- else if (parameter is PlatformAuthorLink) {
- setPolycentricProfileOr(parameter.url) {
- _textChannel.text = parameter.name;
- _textChannelSub.text = "";
- _creatorThumbnail.setThumbnail(parameter.thumbnail, true);
- Glide.with(_imageBanner)
- .clear(_imageBanner);
+ _url = parameter
+ loadChannel()
+ }
- loadPolycentricProfile(parameter.id, parameter.url)
- };
+ is SerializedChannel -> {
+ showChannel(parameter)
+ _url = parameter.url
+ loadChannel()
+ }
- _url = parameter.url;
- loadChannel();
- } else if (parameter is Subscription) {
- setPolycentricProfileOr(parameter.channel.url) {
- _textChannel.text = parameter.channel.name;
- _textChannelSub.text = "";
- _creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true);
- Glide.with(_imageBanner)
- .clear(_imageBanner);
+ is IPlatformChannel -> showChannel(parameter)
+ is PlatformAuthorLink -> {
+ setPolycentricProfileOr(parameter.url) {
+ _textChannel.text = parameter.name
+ _textChannelSub.text = ""
+ _creatorThumbnail.setThumbnail(parameter.thumbnail, true)
+ Glide.with(_imageBanner).clear(_imageBanner)
- loadPolycentricProfile(parameter.channel.id, parameter.channel.url)
- };
+ loadPolycentricProfile(parameter.id, parameter.url)
+ }
- _url = parameter.channel.url;
- loadChannel();
+ _url = parameter.url
+ loadChannel()
+ }
+
+ is Subscription -> {
+ setPolycentricProfileOr(parameter.channel.url) {
+ _textChannel.text = parameter.channel.name
+ _textChannelSub.text = ""
+ _creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true)
+ Glide.with(_imageBanner).clear(_imageBanner)
+
+ loadPolycentricProfile(parameter.channel.id, parameter.channel.url)
+ }
+
+ _url = parameter.channel.url
+ loadChannel()
+ }
}
} else {
- loadChannel();
+ loadChannel()
}
}
- fun selectTab(selectedTabIndex: Int) {
- _selectedTabIndex = selectedTabIndex;
- _tabs.selectTab(_tabs.getTabAt(selectedTabIndex));
+ private fun selectTab(selectedTabIndex: Int) {
+ _selectedTabIndex = selectedTabIndex
+ _tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
}
private fun loadPolycentricProfile(id: PlatformID, url: String) {
- val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true);
+ val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true)
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = true)
if (cachedPolycentricProfile.expired) {
- _taskLoadPolycentricProfile.run(id);
+ _taskLoadPolycentricProfile.run(id)
}
} else {
- _taskLoadPolycentricProfile.run(id);
+ _taskLoadPolycentricProfile.run(id)
}
}
private fun setLoading(isLoading: Boolean) {
if (_isLoading == isLoading) {
- return;
+ return
}
- _isLoading = isLoading;
- if(isLoading){
- _overlay_loading.visibility = View.VISIBLE;
- (_overlay_loading_spinner.drawable as Animatable?)?.start();
- }
- else {
- (_overlay_loading_spinner.drawable as Animatable?)?.stop();
- _overlay_loading.visibility = View.GONE;
+ _isLoading = isLoading
+ if (isLoading) {
+ _overlayLoading.visibility = View.VISIBLE
+ (_overlayLoadingSpinner.drawable as Animatable?)?.start()
+ } else {
+ (_overlayLoadingSpinner.drawable as Animatable?)?.stop()
+ _overlayLoading.visibility = View.GONE
}
}
fun onBackPressed(): Boolean {
if (_slideUpOverlay != null) {
- hideSlideUpOverlay();
- return true;
+ hideSlideUpOverlay()
+ return true
}
- return false;
+ return false
}
private fun hideSlideUpOverlay() {
- _slideUpOverlay?.hide(false);
- _slideUpOverlay = null;
+ _slideUpOverlay?.hide(false)
+ _slideUpOverlay = null
}
private fun loadChannel() {
- val url = _url;
+ val url = _url
if (url != null) {
- setLoading(true);
- _taskGetChannel.run(url);
+ setLoading(true)
+ _taskGetChannel.run(url)
}
}
private fun showChannel(channel: IPlatformChannel) {
- setLoading(false);
+ setLoading(false)
- _fragment.topBar?.onShown(channel);
+ _fragment.topBar?.onShown(channel)
val buttons = arrayListOf(Pair(R.drawable.ic_playlist_add) {
- UIDialogs.showConfirmationDialog(context, context.getString(R.string.do_you_want_to_convert_channel_channelname_to_a_playlist).replace("{channelName}", channel.name), {
- UIDialogs.showDialogProgress(context) {
- _fragment.lifecycleScope.launch(Dispatchers.IO) {
- try {
- StatePlaylists.instance.createPlaylistFromChannel(channel) { page ->
- _fragment.lifecycleScope.launch(Dispatchers.Main) {
- it.setText("${channel.name}\n" + context.getString(R.string.page) + " $page");
+ UIDialogs.showConfirmationDialog(context,
+ context.getString(R.string.do_you_want_to_convert_channel_channelname_to_a_playlist)
+ .replace("{channelName}", channel.name),
+ {
+ UIDialogs.showDialogProgress(context) {
+ _fragment.lifecycleScope.launch(Dispatchers.IO) {
+ try {
+ StatePlaylists.instance.createPlaylistFromChannel(channel) { page ->
+ _fragment.lifecycleScope.launch(Dispatchers.Main) {
+ it.setText("${channel.name}\n" + context.getString(R.string.page) + " $page")
+ }
}
- };
- }
- catch(ex: Exception) {
- Logger.e(TAG, "Error", ex);
- UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_convert_channel), ex);
- }
+ } catch (ex: Exception) {
+ Logger.e(TAG, "Error", ex)
+ UIDialogs.showGeneralErrorDialog(
+ context,
+ context.getString(R.string.failed_to_convert_channel),
+ ex
+ )
+ }
- withContext(Dispatchers.Main) {
- it.hide();
+ withContext(Dispatchers.Main) {
+ it.hide()
+ }
}
- };
- };
- });
- });
+ }
+ })
+ })
_fragment.lifecycleScope.launch(Dispatchers.IO) {
- val plugin = StatePlatform.instance.getChannelClientOrNull(channel.url);
+ val plugin = StatePlatform.instance.getChannelClientOrNull(channel.url)
withContext(Dispatchers.Main) {
if (plugin != null && plugin.capabilities.hasSearchChannelContents) {
buttons.add(Pair(R.drawable.ic_search) {
- _fragment.navigate(SuggestionsFragmentData("", SearchType.VIDEO, channel.url));
- });
+ _fragment.navigate(
+ SuggestionsFragmentData(
+ "", SearchType.VIDEO, channel.url
+ )
+ )
+ })
- _fragment.topBar?.assume()?.setMenuItems(buttons);
+ _fragment.topBar?.assume()?.setMenuItems(buttons)
}
}
}
- _buttonSubscribe.setSubscribeChannel(channel);
- _buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
- _textChannel.text = channel.name;
- _textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(R.string.subscribers).lowercase() else "";
+ _buttonSubscribe.setSubscribeChannel(channel)
+ _buttonSubscriptionSettings.visibility =
+ if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
+ _textChannel.text = channel.name
+ _textChannelSub.text =
+ if (channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(
+ R.string.subscribers
+ ).lowercase() else ""
- //TODO: Find a better way to access the adapter fragments..
+ val supportsPlaylists =
+ StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists
+ val playlistPosition = 1
+ if (supportsPlaylists && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
+ ChannelTab.PLAYLISTS.ordinal.toLong()
+ )
+ ) {
+ // keep the current tab selected
+ if (_viewPager.currentItem >= playlistPosition) {
+ _viewPager.setCurrentItem(_viewPager.currentItem + 1, false)
+ }
- (_viewPager.adapter as ChannelViewPagerAdapter?)?.let {
- it.getFragment().setChannel(channel);
- it.getFragment().setChannel(channel);
- it.getFragment().setChannel(channel);
- it.getFragment().setChannel(channel);
- //TODO: Call on other tabs as needed
+ (_viewPager.adapter as ChannelViewPagerAdapter).insert(
+ playlistPosition,
+ ChannelTab.PLAYLISTS
+ )
+ }
+ if (!supportsPlaylists && (_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
+ ChannelTab.PLAYLISTS.ordinal.toLong()
+ )
+ ) {
+ // keep the current tab selected
+ if (_viewPager.currentItem >= playlistPosition) {
+ _viewPager.setCurrentItem(_viewPager.currentItem - 1, false)
+ }
+
+ (_viewPager.adapter as ChannelViewPagerAdapter).remove(playlistPosition)
}
- this.channel = channel;
+ // sets the channel for each tab
+ for (fragment in _fragment.childFragmentManager.fragments) {
+ (fragment as IChannelTabFragment).setChannel(channel)
+ }
+
+ (_viewPager.adapter as ChannelViewPagerAdapter).channel = channel
+
+
+ _viewPager.adapter!!.notifyDataSetChanged()
+
+ this.channel = channel
setPolycentricProfileOr(channel.url) {
- _textChannel.text = channel.name;
- _creatorThumbnail.setThumbnail(channel.thumbnail, true);
- Glide.with(_imageBanner)
- .load(channel.banner)
- .crossfade()
- .into(_imageBanner);
+ _textChannel.text = channel.name
+ _creatorThumbnail.setThumbnail(channel.thumbnail, true)
+ Glide.with(_imageBanner).load(channel.banner).crossfade().into(_imageBanner)
- _taskLoadPolycentricProfile.run(channel.id);
- };
+ _taskLoadPolycentricProfile.run(channel.id)
+ }
}
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
- setPolycentricProfile(null, animate = false);
+ setPolycentricProfile(null, animate = false)
- val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(url) };
+ val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(url) }
if (cachedProfile != null) {
- setPolycentricProfile(cachedProfile, animate = false);
+ setPolycentricProfile(cachedProfile, animate = false)
} else {
- or();
+ or()
}
}
- private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
- val dp_35 = 35.dp(resources)
- val profile = cachedPolycentricProfile?.profile;
- val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
- ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
-
- if (avatar != null) {
- _creatorThumbnail.setThumbnail(avatar, animate);
- } else {
- _creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
- _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
+ private fun setPolycentricProfile(
+ cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean
+ ) {
+ val dp35 = 35.dp(resources)
+ val profile = cachedPolycentricProfile?.profile
+ val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let {
+ it.toURLInfoSystemLinkUrl(
+ profile.system.toProto(), it.process, profile.systemState.servers.toList()
+ )
}
- val banner = profile?.systemState?.banner?.selectHighestResolutionImage()
- ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
+ if (avatar != null) {
+ _creatorThumbnail.setThumbnail(avatar, animate)
+ } else {
+ _creatorThumbnail.setThumbnail(channel?.thumbnail, animate)
+ _creatorThumbnail.setHarborAvailable(
+ profile != null, animate, profile?.system?.toProto()
+ )
+ }
+
+ val banner = profile?.systemState?.banner?.selectHighestResolutionImage()?.let {
+ it.toURLInfoSystemLinkUrl(
+ profile.system.toProto(), it.process, profile.systemState.servers.toList()
+ )
+ }
if (banner != null) {
- Glide.with(_imageBanner)
- .load(banner)
- .crossfade()
- .into(_imageBanner);
+ Glide.with(_imageBanner).load(banner).crossfade().into(_imageBanner)
} else {
- Glide.with(_imageBanner)
- .load(channel?.banner)
- .crossfade()
- .into(_imageBanner);
+ Glide.with(_imageBanner).load(channel?.banner).crossfade().into(_imageBanner)
}
if (profile != null) {
- _fragment.topBar?.onShown(profile);
- _textChannel.text = profile.systemState.username;
+ _fragment.topBar?.onShown(profile)
+ _textChannel.text = profile.systemState.username
}
- (_viewPager.adapter as ChannelViewPagerAdapter?)?.let {
- it.getFragment().setPolycentricProfile(profile);
- it.getFragment().setPolycentricProfile(profile);
- it.getFragment().setPolycentricProfile(profile);
- it.getFragment().setPolycentricProfile(profile);
- //TODO: Call on other tabs as needed
+ // sets the profile for each tab
+ for (fragment in _fragment.childFragmentManager.fragments) {
+ (fragment as IChannelTabFragment).setPolycentricProfile(profile)
}
+
+ val insertPosition = 1
+
+ //TODO only add channels and support if its setup on the polycentric profile
+ if (profile != null && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
+ ChannelTab.SUPPORT.ordinal.toLong()
+ )
+ ) {
+ (_viewPager.adapter as ChannelViewPagerAdapter).insert(insertPosition, ChannelTab.SUPPORT)
+ }
+ if (profile != null && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
+ ChannelTab.CHANNELS.ordinal.toLong()
+ )
+ ) {
+ (_viewPager.adapter as ChannelViewPagerAdapter).insert(insertPosition, ChannelTab.CHANNELS)
+ }
+ (_viewPager.adapter as ChannelViewPagerAdapter).profile = profile
+ _viewPager.adapter!!.notifyDataSetChanged()
}
}
companion object {
- val TAG = "ChannelFragment";
+ const val TAG = "ChannelFragment"
fun newInstance() = ChannelFragment().apply { }
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt
index cde70f32..fcabcb5b 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt
@@ -6,28 +6,32 @@ import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
-import com.futo.platformplayer.*
+import com.futo.platformplayer.R
+import com.futo.platformplayer.Settings
+import com.futo.platformplayer.UIDialogs
+import com.futo.platformplayer.UISlideOverlays
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.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
-import com.futo.platformplayer.api.media.structures.*
+import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.video.PlayerManager
import com.futo.platformplayer.views.FeedStyle
-import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
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.adapters.feedtypes.PreviewContentListAdapter
import com.futo.platformplayer.views.adapters.feedtypes.PreviewNestedVideoViewHolder
import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoViewHolder
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
abstract class ContentFeedView : FeedView, ContentPreviewViewHolder> where TFragment : MainFragment {
@@ -183,7 +187,7 @@ abstract class ContentFeedView : FeedView(content).maximizeVideoDetail();
}
} else if (content is IPlatformPlaylist) {
- fragment.navigate(content);
+ fragment.navigate(content);
} else if (content is IPlatformPost) {
fragment.navigate(content);
}
@@ -194,7 +198,7 @@ abstract class ContentFeedView : FeedView(url).maximizeVideoDetail();
};
- ContentType.PLAYLIST -> fragment.navigate(url);
+ ContentType.PLAYLIST -> fragment.navigate(url);
ContentType.URL -> fragment.navigate(url);
else -> {};
}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt
index f7a34777..076c0542 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt
@@ -156,7 +156,7 @@ class ContentSearchResultsFragment : MainFragment() {
onSearch.subscribe(this) {
if(it.isHttpUrl()) {
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
- navigate(it);
+ navigate(it);
else if(StatePlatform.instance.hasEnabledChannelClient(it))
navigate(it);
else
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt
index ae584d40..93b5e7e9 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt
@@ -8,7 +8,7 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
-import com.futo.platformplayer.*
+import com.futo.platformplayer.R
import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.logging.Logger
@@ -16,12 +16,13 @@ import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
+import com.futo.platformplayer.toHumanBytesSize
import com.futo.platformplayer.views.AnyInsertedAdapterView
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
-import com.futo.platformplayer.views.others.ProgressBar
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
import com.futo.platformplayer.views.items.ActiveDownloadItem
import com.futo.platformplayer.views.items.PlaylistDownloadItem
+import com.futo.platformplayer.views.others.ProgressBar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -64,16 +65,6 @@ class DownloadsFragment : MainFragment() {
}
}
};
- StateDownloads.instance.onExportsChanged.subscribe(this) {
- lifecycleScope.launch(Dispatchers.Main) {
- try {
- Logger.i(TAG, "Reloading UI for exports");
- _view?.reloadUI()
- } catch (e: Throwable) {
- Logger.e(TAG, "Failed to reload UI for exports", e)
- }
- }
- };
}
override fun onPause() {
@@ -81,7 +72,6 @@ class DownloadsFragment : MainFragment() {
StateDownloads.instance.onDownloadsChanged.remove(this);
StateDownloads.instance.onDownloadedChanged.remove(this);
- StateDownloads.instance.onExportsChanged.remove(this);
}
private class DownloadsView : LinearLayout {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ImportPlaylistsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ImportPlaylistsFragment.kt
index e90a535d..fbfe75c8 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ImportPlaylistsFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ImportPlaylistsFragment.kt
@@ -12,16 +12,21 @@ import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.*
+import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlaylists
+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.adapters.viewholders.ImportPlaylistsViewHolder
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
class ImportPlaylistsFragment : MainFragment() {
override val isMainView : Boolean = true;
@@ -67,7 +72,7 @@ class ImportPlaylistsFragment : MainFragment() {
private val _items: ArrayList = arrayListOf();
private var _currentLoadIndex = 0;
- private var _taskLoadPlaylist: TaskHandler;
+ private var _taskLoadPlaylist: TaskHandler;
constructor(fragment: ImportPlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
_fragment = fragment;
@@ -102,7 +107,7 @@ class ImportPlaylistsFragment : MainFragment() {
setLoading(false);
- _taskLoadPlaylist = TaskHandler({fragment.lifecycleScope}, { link -> StatePlatform.instance.getPlaylist(link).toPlaylist(); })
+ _taskLoadPlaylist = TaskHandler({fragment.lifecycleScope}, { link -> StatePlatform.instance.getPlaylist(link); })
.success {
if (it != null) {
_items.add(SelectablePlaylist(it));
@@ -113,7 +118,7 @@ class ImportPlaylistsFragment : MainFragment() {
}.exceptionWithParameter { ex, para ->
//setLoading(false);
Logger.w(ChannelFragment.TAG, "Failed to load results.", ex);
- UIDialogs.toast(context, context.getString(R.string.failed_to_fetch) + "\n${para}", false)
+ UIDialogs.appToast(context.getString(R.string.failed_to_fetch) + "\n${para}\n" + ex.message, false)
//UIDialogs.showDataRetryDialog(layoutInflater, { load(); });
loadNext();
};
@@ -147,12 +152,32 @@ class ImportPlaylistsFragment : MainFragment() {
it.title = context.getString(R.string.import_playlists);
it.onImport.subscribe(this) {
val playlistsToImport = _items.filter { i -> i.selected }.toList();
- for (playlistToImport in playlistsToImport) {
- StatePlaylists.instance.createOrUpdatePlaylist(playlistToImport.playlist);
- }
- UIDialogs.toast("${playlistsToImport.size} " + context.getString(R.string.playlists_imported));
- _fragment.closeSegment();
+ UIDialogs.showDialogProgress(context) {
+ it.setText("Importing playlists..");
+ it.setProgress(0f);
+ _fragment.lifecycleScope.launch(Dispatchers.IO) {
+ for ((i, playlistToImport) in playlistsToImport.withIndex()) {
+ withContext(Dispatchers.Main) {
+ it.setText("Importing playlists..\n[${playlistToImport.playlist.name}]");
+ }
+ try {
+ StatePlaylists.instance.createOrUpdatePlaylist(playlistToImport.playlist.toPlaylist());
+ }
+ catch(ex: Throwable) {
+ UIDialogs.appToast("Failed to import [${playlistToImport.playlist.name}]\n" + ex.message);
+ }
+ withContext(Dispatchers.Main) {
+ it.setProgress(i.toDouble() / playlistsToImport.size);
+ }
+ }
+ withContext(Dispatchers.Main) {
+ UIDialogs.toast("${playlistsToImport.size} " + context.getString(R.string.playlists_imported));
+ _fragment.closeSegment();
+ it.dismiss();
+ }
+ }
+ }
};
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt
index d938e970..af128741 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt
@@ -1,14 +1,11 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint
-import android.graphics.drawable.Animatable
import android.os.Bundle
-import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.app.ShareCompat
-import androidx.core.view.setPadding
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
@@ -30,7 +27,6 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
class PlaylistFragment : MainFragment() {
override val isMainView : Boolean = true;
@@ -70,7 +66,6 @@ class PlaylistFragment : MainFragment() {
private val _fragment: PlaylistFragment;
private var _playlist: Playlist? = null;
- private var _remotePlaylist: IPlatformPlaylistDetails? = null;
private var _editPlaylistNameInput: SlideUpMenuTextInput? = null;
private var _editPlaylistOverlay: SlideUpMenuOverlay? = null;
private var _url: String? = null;
@@ -136,12 +131,11 @@ class PlaylistFragment : MainFragment() {
return@TaskHandler StatePlatform.instance.getPlaylist(it);
})
.success {
- setLoading(false);
- _remotePlaylist = it;
setName(it.name);
- setVideos(it.contents.getResults(), false);
- setVideoCount(it.videoCount);
//TODO: Implement support for pagination
+ setVideos(it.toPlaylist().videos, false);
+ setVideoCount(it.videoCount);
+ setLoading(false);
}
.exception {
Logger.w(TAG, "Failed to load playlist.", it);
@@ -151,58 +145,62 @@ class PlaylistFragment : MainFragment() {
}
fun onShown(parameter: Any?) {
- _taskLoadPlaylist.cancel();
+ _taskLoadPlaylist.cancel()
if (parameter is Playlist?) {
- _playlist = parameter;
- _remotePlaylist = null;
- _url = null;
+ _playlist = parameter
+ _url = null
- if(parameter != null) {
- setName(parameter.name);
- setVideos(parameter.videos, true);
- setVideoCount(parameter.videos.size);
- setButtonDownloadVisible(true);
- setButtonEditVisible(true);
+ if (parameter != null) {
+ setName(parameter.name)
+ setVideos(parameter.videos, true)
+ setVideoCount(parameter.videos.size)
+ setButtonDownloadVisible(true)
+ setButtonEditVisible(true)
+
+ if (!StatePlaylists.instance.playlistStore.getItems().contains(parameter)) {
+ _fragment.topBar?.assume()
+ ?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
+ StatePlaylists.instance.playlistStore.save(parameter)
+ _fragment.topBar?.assume()?.setMenuItems(
+ arrayListOf()
+ )
+ UIDialogs.toast("Playlist saved")
+ }))
+ }
} else {
- setName(null);
- setVideos(null, false);
- setVideoCount(-1);
- setButtonDownloadVisible(false);
- setButtonEditVisible(false);
+ setName(null)
+ setVideos(null, false)
+ setVideoCount(-1)
+ setButtonDownloadVisible(false)
+ setButtonEditVisible(false)
}
-
- //TODO: Do I have to remove the showConvertPlaylistButton(); button here?
} else if (parameter is IPlatformPlaylist) {
- _playlist = null;
- _remotePlaylist = null;
- _url = parameter.url;
+ _playlist = null
+ _url = parameter.url
- setVideoCount(parameter.videoCount);
- setName(parameter.name);
- setVideos(null, false);
- setButtonDownloadVisible(false);
- setButtonEditVisible(false);
+ setVideoCount(parameter.videoCount)
+ setName(parameter.name)
+ setVideos(null, false)
+ setButtonDownloadVisible(false)
+ setButtonEditVisible(false)
- fetchPlaylist();
- showConvertPlaylistButton();
+ fetchPlaylist()
} else if (parameter is String) {
- _playlist = null;
- _remotePlaylist = null;
- _url = parameter;
+ _playlist = null
+ _url = parameter
- setName(null);
- setVideos(null, false);
- setVideoCount(-1);
- setButtonDownloadVisible(false);
- setButtonEditVisible(false);
+ setName(null)
+ setVideos(null, false)
+ setVideoCount(-1)
+ setButtonDownloadVisible(false)
+ setButtonEditVisible(false)
- fetchPlaylist();
- showConvertPlaylistButton();
+ fetchPlaylist()
}
_playlist?.let {
- updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download);
+ updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download)
}
}
@@ -242,34 +240,6 @@ class PlaylistFragment : MainFragment() {
StateDownloads.instance.onDownloadedChanged.remove(this);
}
- private fun showConvertPlaylistButton() {
- _fragment.topBar?.assume()?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
- val remotePlaylist = _remotePlaylist;
- if (remotePlaylist == null) {
- UIDialogs.toast(context.getString(R.string.please_wait_for_playlist_to_finish_loading));
- return@Pair;
- }
-
- setLoading(true);
- StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
- try {
- StatePlaylists.instance.playlistStore.save(remotePlaylist.toPlaylist());
-
- withContext(Dispatchers.Main) {
- setLoading(false);
- UIDialogs.toast(context.getString(R.string.playlist_copied_as_local_playlist));
- }
- } catch (e: Throwable) {
- withContext(Dispatchers.Main) {
- setLoading(false);
- }
-
- throw e;
- }
- }
- }));
- }
-
private fun fetchPlaylist() {
Logger.i(TAG, "fetchPlaylist")
@@ -290,21 +260,15 @@ class PlaylistFragment : MainFragment() {
override fun onPlayAllClick() {
val playlist = _playlist;
- val remotePlaylist = _remotePlaylist;
if (playlist != null) {
StatePlayer.instance.setPlaylist(playlist, focus = true);
- } else if (remotePlaylist != null) {
- StatePlayer.instance.setPlaylist(remotePlaylist, focus = true, shuffle = false);
}
}
override fun onShuffleClick() {
val playlist = _playlist;
- val remotePlaylist = _remotePlaylist;
if (playlist != null) {
StatePlayer.instance.setPlaylist(playlist, focus = true, shuffle = true);
- } else if (remotePlaylist != null) {
- StatePlayer.instance.setPlaylist(remotePlaylist, focus = true, shuffle = true);
}
}
@@ -320,19 +284,12 @@ class PlaylistFragment : MainFragment() {
}
override fun onVideoClicked(video: IPlatformVideo) {
val playlist = _playlist;
- val remotePlaylist = _remotePlaylist;
if (playlist != null) {
val index = playlist.videos.indexOf(video);
if (index == -1)
return;
StatePlayer.instance.setPlaylist(playlist, index, true);
- } else if (remotePlaylist != null) {
- val index = remotePlaylist.contents.getResults().indexOf(video);
- if (index == -1)
- return;
-
- StatePlayer.instance.setPlaylist(remotePlaylist, index, true);
}
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt
index 22ad5e93..df09b741 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt
@@ -41,6 +41,7 @@ import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanNumber
+import com.futo.platformplayer.views.adapters.ChannelTab
import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView
import com.futo.platformplayer.views.comments.AddCommentView
import com.futo.platformplayer.views.others.CreatorThumbnail
@@ -264,7 +265,7 @@ class PostDetailFragment : MainFragment {
_buttonSupport.setOnClickListener {
val author = _post?.author ?: _postOverview?.author;
- author?.let { _fragment.navigate(it).selectTab(2); };
+ author?.let { _fragment.navigate(it).selectTab(ChannelTab.SUPPORT); };
};
_buttonStore.setOnClickListener {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/RemotePlaylistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/RemotePlaylistFragment.kt
new file mode 100644
index 00000000..349b4fb0
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/RemotePlaylistFragment.kt
@@ -0,0 +1,408 @@
+package com.futo.platformplayer.fragment.mainactivity.main
+
+import android.annotation.SuppressLint
+import android.graphics.drawable.Animatable
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.app.ShareCompat
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.futo.platformplayer.*
+import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
+import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
+import com.futo.platformplayer.api.media.models.video.IPlatformVideo
+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
+import com.futo.platformplayer.api.media.structures.MultiPager
+import com.futo.platformplayer.api.media.structures.ReusablePager
+import com.futo.platformplayer.constructs.TaskHandler
+import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
+import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.states.StateApp
+import com.futo.platformplayer.states.StatePlatform
+import com.futo.platformplayer.states.StatePlayer
+import com.futo.platformplayer.states.StatePlaylists
+import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
+import com.futo.platformplayer.views.adapters.VideoListEditorViewHolder
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+enum class Action {
+ PLAY_ALL, SHUFFLE, PLAY, NONE
+}
+
+class RemotePlaylistFragment : MainFragment() {
+ override val isMainView : Boolean = true;
+ override val isTab: Boolean = true;
+ override val hasBottomBar: Boolean get() = true;
+
+ private var _view: RemotePlaylistView? = null;
+
+ override fun onShownWithView(parameter: Any?, isBack: Boolean) {
+ super.onShownWithView(parameter, isBack);
+ _view?.onShown(parameter);
+ }
+
+ override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ val view = RemotePlaylistView(this, inflater);
+ _view = view;
+ return view;
+ }
+
+ override fun onDestroyMainView() {
+ super.onDestroyMainView();
+ _view = null;
+ }
+
+ @SuppressLint("ViewConstructor")
+ class RemotePlaylistView : LinearLayout {
+ private val _fragment: RemotePlaylistFragment;
+
+ private var _remotePlaylist: IPlatformPlaylistDetails? = null;
+ private var _remotePlaylistPagerWindow: IPager? = null;
+ private var _url: String? = null;
+ private val _videos: ArrayList = arrayListOf();
+
+ private val _taskLoadPlaylist: TaskHandler;
+ private var _nextPageHandler: TaskHandler, List>;
+
+ private var _imagePlaylistThumbnail: ImageView;
+ private var _textName: TextView;
+ private var _textMetadata: TextView;
+ private var _loaderOverlay: FrameLayout;
+ private var _imageLoader: ImageView;
+ private var _overlayContainer: FrameLayout;
+ private var _buttonShare: ImageButton;
+ private var _recyclerPlaylist: RecyclerView;
+ private var _llmPlaylist: LinearLayoutManager;
+ private val _adapterVideos: InsertedViewAdapterWithLoader;
+ private val _scrollListener: RecyclerView.OnScrollListener
+
+
+
+ constructor(fragment: RemotePlaylistFragment, inflater: LayoutInflater) : super(inflater.context) {
+ inflater.inflate(R.layout.fragment_remote_playlist, this);
+
+ _fragment = fragment;
+
+ _textName = findViewById(R.id.text_name);
+ _textMetadata = findViewById(R.id.text_metadata);
+ _imagePlaylistThumbnail = findViewById(R.id.image_playlist_thumbnail);
+ _loaderOverlay = findViewById(R.id.layout_loading_overlay);
+ _imageLoader = findViewById(R.id.image_loader);
+ _recyclerPlaylist = findViewById(R.id.recycler_playlist);
+ _llmPlaylist = LinearLayoutManager(context);
+ _adapterVideos = InsertedViewAdapterWithLoader(context,
+ arrayListOf(),
+ arrayListOf(),
+ childCountGetter = { _videos.size },
+ childViewHolderBinder = { viewHolder, position ->
+ viewHolder.bind(
+ _videos[position],
+ false
+ )
+ },
+ childViewHolderFactory = { viewGroup, _ ->
+ val view = LayoutInflater.from(viewGroup.context)
+ .inflate(R.layout.list_playlist, viewGroup, false)
+ val holder = VideoListEditorViewHolder(view, null)
+ holder.onClick.subscribe {
+ convertPlaylist(false, Action.PLAY, holder.video)
+ }
+ return@InsertedViewAdapterWithLoader holder
+ })
+
+ _recyclerPlaylist.adapter = _adapterVideos;
+ _recyclerPlaylist.layoutManager = _llmPlaylist;
+
+ _overlayContainer = findViewById(R.id.overlay_container);
+ val buttonPlayAll = findViewById(R.id.button_play_all);
+ val buttonShuffle = findViewById(R.id.button_shuffle);
+
+ _buttonShare = findViewById(R.id.button_share);
+ _buttonShare.setOnClickListener {
+ val remotePlaylist = _remotePlaylist ?: return@setOnClickListener;
+
+ _fragment.startActivity(ShareCompat.IntentBuilder(context)
+ .setType("text/plain")
+ .setText(remotePlaylist.shareUrl)
+ .intent);
+ };
+
+ buttonPlayAll.setOnClickListener {
+ convertPlaylist(false, Action.PLAY_ALL);
+ };
+ buttonShuffle.setOnClickListener {
+ convertPlaylist(false, Action.SHUFFLE);
+ };
+
+ _taskLoadPlaylist = TaskHandler(
+ StateApp.instance.scopeGetter,
+ {
+ return@TaskHandler StatePlatform.instance.getPlaylist(it);
+ })
+ .success {
+ _remotePlaylist = it;
+ val c = it.contents;
+ _remotePlaylistPagerWindow = if (c is ReusablePager) c.getWindow() else c;
+ setName(it.name);
+ setVideos(_remotePlaylistPagerWindow!!.getResults());
+ setVideoCount(it.videoCount);
+ setLoading(false);
+ }
+ .exception {
+ Logger.w(TAG, "Failed to load playlist.", it);
+ val c = context ?: return@exception;
+ UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist, null, fragment);
+ };
+
+ _nextPageHandler = TaskHandler, List>({fragment.lifecycleScope}, {
+ if (it is IAsyncPager<*>)
+ it.nextPageAsync();
+ else
+ it.nextPage();
+
+ processPagerExceptions(it);
+ return@TaskHandler it.getResults();
+ }).success {
+ _adapterVideos.setLoading(false);
+ addVideos(it);
+ //TODO: ensureEnoughContentVisible()
+ }.exception {
+ Logger.w(TAG, "Failed to load next page.", it);
+ UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
+ loadNextPage();
+ }, null, fragment);
+ };
+
+ _scrollListener = object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ super.onScrolled(recyclerView, dx, dy)
+
+ val visibleItemCount = _recyclerPlaylist.childCount
+ val firstVisibleItem = _llmPlaylist.findFirstVisibleItemPosition()
+ val visibleThreshold = 15
+ if (!_adapterVideos.isLoading && firstVisibleItem + visibleItemCount + visibleThreshold >= _videos.size) {
+ loadNextPage()
+ }
+ }
+ }
+
+ _recyclerPlaylist.addOnScrollListener(_scrollListener)
+ }
+
+ private fun loadNextPage() {
+ val pager: IPager = _remotePlaylistPagerWindow ?: return;
+ val hasMorePages = pager.hasMorePages();
+ Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages, page size=${pager.getResults().size}");
+
+ if (pager.hasMorePages()) {
+ _adapterVideos.setLoading(true);
+ _nextPageHandler.run(pager);
+ }
+ }
+
+ private fun processPagerExceptions(pager: IPager<*>) {
+ if(pager is MultiPager<*> && pager.allowFailure) {
+ val ex = pager.getResultExceptions();
+ for(kv in ex) {
+ val jsVideoPager: JSPager<*>? = if(kv.key is MultiPager<*>)
+ (kv.key as MultiPager<*>).findPager { it is JSPager<*> } as JSPager<*>?;
+ else if(kv.key is JSPager<*>)
+ kv.key as JSPager<*>;
+ else null;
+
+ context?.let {
+ _fragment.lifecycleScope.launch(Dispatchers.Main) {
+ try {
+ if(jsVideoPager != null)
+ UIDialogs.toast(it, context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", jsVideoPager.getPluginConfig().name).replace("{message}", kv.value.message ?: ""), false);
+ else
+ UIDialogs.toast(it, kv.value.message ?: "", false);
+ } catch (e: Throwable) {
+ Logger.e(TAG, "Failed to show toast.", e)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun onShown(parameter: Any?) {
+ _taskLoadPlaylist.cancel();
+ _nextPageHandler.cancel();
+
+ if (parameter is IPlatformPlaylist) {
+ _remotePlaylist = null;
+ _url = parameter.url;
+
+ setVideoCount(parameter.videoCount);
+ setName(parameter.name);
+ setVideos(null);
+
+ fetchPlaylist();
+ showConvertPlaylistButton();
+ } else if (parameter is String) {
+ _remotePlaylist = null;
+ _url = parameter;
+
+ setName(null);
+ setVideos(null);
+ setVideoCount(-1);
+
+ fetchPlaylist();
+ showConvertPlaylistButton();
+ }
+ }
+
+ private fun convertPlaylist(
+ savePlaylist: Boolean, action: Action, video: IPlatformVideo? = null
+ ) {
+ val remotePlaylist = _remotePlaylist
+ if (remotePlaylist == null) {
+ UIDialogs.toast(context.getString(R.string.please_wait_for_playlist_to_finish_loading))
+ return
+ }
+
+ val convert = {
+ setLoading(true)
+
+ UIDialogs.showDialogProgress(context) {
+ it.setText("Converting playlist..")
+ it.setProgress(0f)
+
+ _fragment.lifecycleScope.launch(Dispatchers.IO) {
+ try {
+ val playlist = remotePlaylist.toPlaylist { progress ->
+ _fragment.lifecycleScope.launch(Dispatchers.Main) {
+ it.setProgress(progress.toDouble() / remotePlaylist.videoCount)
+ }
+ }
+
+ if (savePlaylist) {
+ StatePlaylists.instance.playlistStore.save(playlist)
+ }
+
+ _fragment.lifecycleScope.launch(Dispatchers.Main) {
+ UIDialogs.toast("Playlist converted")
+ it.dismiss()
+ _fragment.navigate(playlist)
+ when (action) {
+ Action.SHUFFLE -> StatePlayer.instance.setPlaylist(
+ playlist, focus = true, shuffle = true
+ )
+
+ Action.PLAY_ALL -> StatePlayer.instance.setPlaylist(
+ playlist, focus = true
+ )
+
+ Action.PLAY -> {
+ StatePlayer.instance.setPlaylist(
+ playlist, _videos.indexOf(video), true
+ )
+ }
+
+ Action.NONE -> {}
+ }
+ }
+ } catch (ex: Throwable) {
+ UIDialogs.appToast("Failed to convert playlist.\n" + ex.message)
+ }
+ }
+ }
+ }
+
+ if (remotePlaylist.videoCount > 100) {
+ val c = context ?: return
+ UIDialogs.showConfirmationDialog(
+ c, "Conversion to local playlist is required for this action", convert
+ )
+ } else {
+ convert()
+ }
+ }
+
+ private fun showConvertPlaylistButton() {
+ _fragment.topBar?.assume()?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
+ convertPlaylist(true, Action.NONE);
+ }));
+ }
+
+ private fun fetchPlaylist() {
+ Logger.i(TAG, "fetchPlaylist")
+
+ val url = _url;
+ if (!url.isNullOrBlank()) {
+ setLoading(true);
+ _taskLoadPlaylist.run(url);
+ }
+ }
+
+ private fun setName(name: String?) {
+ _textName.text = name ?: "";
+ }
+
+ private fun setVideoCount(videoCount: Int = -1) {
+ _textMetadata.text = if (videoCount == -1) "" else "${videoCount} " + context.getString(R.string.videos);
+ }
+
+ private fun setVideos(videos: List?) {
+ if (!videos.isNullOrEmpty()) {
+ val video = videos.first();
+ _imagePlaylistThumbnail.let {
+ Glide.with(it)
+ .load(video.thumbnails.getHQThumbnail())
+ .placeholder(R.drawable.placeholder_video_thumbnail)
+ .crossfade()
+ .into(it);
+ };
+ } else {
+ _textMetadata.text = "0 " + context.getString(R.string.videos);
+ Glide.with(_imagePlaylistThumbnail)
+ .load(R.drawable.placeholder_video_thumbnail)
+ .into(_imagePlaylistThumbnail)
+ }
+
+ synchronized(_videos) {
+ _videos.clear();
+ _videos.addAll(videos ?: listOf());
+ _adapterVideos.notifyDataSetChanged();
+ }
+ }
+
+ private fun addVideos(videos: List) {
+ synchronized(_videos) {
+ val index = _videos.size;
+ _videos.addAll(videos);
+ _adapterVideos.notifyItemRangeInserted(_adapterVideos.childToParentPosition(index), videos.size);
+ }
+ }
+
+ private fun setLoading(isLoading: Boolean) {
+ if (isLoading){
+ (_imageLoader.drawable as Animatable?)?.start()
+ _loaderOverlay.visibility = View.VISIBLE;
+ }
+ else {
+ _loaderOverlay.visibility = View.GONE;
+ (_imageLoader.drawable as Animatable?)?.stop()
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "RemotePlaylistFragment";
+ fun newInstance() = RemotePlaylistFragment().apply {}
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt
index 20cdcaa2..5139c0f8 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.models.Thumbnail
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
+import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
@@ -144,10 +145,8 @@ class TutorialFragment : MainFragment() {
override fun getComments(client: IPlatformClient): IPager {
return EmptyPager()
}
-
- override fun getPlaybackTracker(): IPlaybackTracker? {
- return null
- }
+ override fun getPlaybackTracker(): IPlaybackTracker? = null;
+ override fun getContentRecommendations(client: IPlatformClient): IPager? = null;
}
companion object {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
index 07ba3e7f..6d81df24 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
@@ -1063,6 +1063,11 @@ class VideoDetailView : ConstraintLayout {
if(!bypassSameVideoCheck && this.video?.url == video.url)
return;
+ //Loop workaround
+ if(bypassSameVideoCheck && this.video?.url == video.url && StatePlayer.instance.loopVideo) {
+ _player.seekTo(0);
+ return;
+ }
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
if(cachedVideo != null) {
diff --git a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt
index 9ca4aa8e..df4cf4eb 100644
--- a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt
+++ b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt
@@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
+import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
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
@@ -44,7 +45,7 @@ class VideoHelper {
}
fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource || source is IHLSManifestSource;
- fun isDownloadable(source: IAudioSource) = source is IAudioUrlSource || source is IHLSManifestAudioSource;
+ fun isDownloadable(source: IAudioSource) = (source is IAudioUrlSource || source is IHLSManifestAudioSource) && source !is IAudioUrlWidevineSource
fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
fun selectBestVideoSource(sources: Iterable, desiredPixelCount : Int, prefContainers : Array) : IVideoSource? {
diff --git a/app/src/main/java/com/futo/platformplayer/serializers/PlatformContentSerializer.kt b/app/src/main/java/com/futo/platformplayer/serializers/PlatformContentSerializer.kt
index 70808ba2..02a39160 100644
--- a/app/src/main/java/com/futo/platformplayer/serializers/PlatformContentSerializer.kt
+++ b/app/src/main/java/com/futo/platformplayer/serializers/PlatformContentSerializer.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.serializers
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
+import com.futo.platformplayer.api.media.models.video.SerializedPlatformLockedContent
import com.futo.platformplayer.api.media.models.video.SerializedPlatformNestedContent
import com.futo.platformplayer.api.media.models.video.SerializedPlatformPost
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
@@ -30,6 +31,7 @@ class PlatformContentSerializer : JsonContentPolymorphicSerializer SerializedPlatformNestedContent.serializer();
"ARTICLE" -> throw NotImplementedError("Articles not yet implemented");
"POST" -> SerializedPlatformPost.serializer();
+ "LOCKED" -> SerializedPlatformLockedContent.serializer();
else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.contentOrNull}")
};
} else {
@@ -38,6 +40,7 @@ class PlatformContentSerializer : JsonContentPolymorphicSerializer SerializedPlatformNestedContent.serializer();
ContentType.ARTICLE.value -> throw NotImplementedError("Articles not yet implemented");
ContentType.POST.value -> SerializedPlatformPost.serializer();
+ ContentType.LOCKED.value -> SerializedPlatformLockedContent.serializer();
else -> throw NotImplementedError("Unknown Content Type Value: ${obj.jsonPrimitive.int}")
};
}
diff --git a/app/src/main/java/com/futo/platformplayer/services/ExportingService.kt b/app/src/main/java/com/futo/platformplayer/services/ExportingService.kt
deleted file mode 100644
index a447f70c..00000000
--- a/app/src/main/java/com/futo/platformplayer/services/ExportingService.kt
+++ /dev/null
@@ -1,236 +0,0 @@
-package com.futo.platformplayer.services
-
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.app.Service
-import android.content.Context
-import android.content.Intent
-import android.content.pm.ServiceInfo
-import android.os.Build
-import android.os.IBinder
-import androidx.core.app.NotificationCompat
-import com.futo.platformplayer.R
-import com.futo.platformplayer.activities.MainActivity
-import com.futo.platformplayer.api.http.ManagedHttpClient
-import com.futo.platformplayer.downloads.VideoExport
-import com.futo.platformplayer.logging.Logger
-import com.futo.platformplayer.share
-import com.futo.platformplayer.states.Announcement
-import com.futo.platformplayer.states.AnnouncementType
-import com.futo.platformplayer.states.StateAnnouncement
-import com.futo.platformplayer.states.StateDownloads
-import com.futo.platformplayer.stores.FragmentedStorage
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.time.OffsetDateTime
-import java.util.UUID
-
-
-class ExportingService : Service() {
- private val TAG = "ExportingService";
-
- private val EXPORT_NOTIF_ID = 4;
- private val EXPORT_NOTIF_TAG = "export";
- private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel";
- private val EXPORT_NOTIF_CHANNEL_NAME = "Export";
-
- //Context
- private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
- private var _notificationManager: NotificationManager? = null;
- private var _notificationChannel: NotificationChannel? = null;
-
- private val _client = ManagedHttpClient();
-
- private var _started = false;
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- Logger.i(TAG, "onStartCommand");
-
- synchronized(this) {
- if(_started)
- return START_STICKY;
-
- if(!FragmentedStorage.isInitialized) {
- closeExportSession();
- return START_NOT_STICKY;
- }
-
- _started = true;
- }
- setupNotificationRequirements();
-
- _callOnStarted?.invoke(this);
- _instance = this;
-
- _scope.launch {
- try {
- doExporting();
- }
- catch(ex: Throwable) {
- try {
- StateAnnouncement.instance.registerAnnouncementSession(
- Announcement(
- "rootExportException",
- "An root export service exception happened",
- ex.message ?: "",
- AnnouncementType.SESSION,
- OffsetDateTime.now()
- )
- );
- } catch(_: Throwable){}
- }
- };
-
- return START_STICKY;
- }
- fun setupNotificationRequirements() {
- _notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
- _notificationChannel = NotificationChannel(EXPORT_NOTIF_CHANNEL_ID, EXPORT_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
- this.enableVibration(false);
- this.setSound(null, null);
- };
- _notificationManager!!.createNotificationChannel(_notificationChannel!!);
- }
-
- override fun onCreate() {
- Logger.i(TAG, "onCreate");
- super.onCreate()
- }
-
- override fun onBind(p0: Intent?): IBinder? {
- return null;
- }
-
- private suspend fun doExporting() {
- Logger.i(TAG, "doExporting - Starting Exports");
- val ignore = mutableListOf();
- var currentExport: VideoExport? = StateDownloads.instance.getExporting().firstOrNull();
- while (currentExport != null)
- {
- try{
- notifyExport(currentExport);
- doExport(applicationContext, currentExport);
- }
- catch(ex: Throwable) {
- Logger.e(TAG, "Failed export [${currentExport.videoLocal.name}]: ${ex.message}", ex);
- currentExport.error = ex.message;
- currentExport.changeState(VideoExport.State.ERROR);
- ignore.add(currentExport);
-
- //Give it a sec
- Thread.sleep(500);
- }
-
- currentExport = StateDownloads.instance.getExporting().filter { !ignore.contains(it) }.firstOrNull();
- }
- Logger.i(TAG, "doExporting - Ending Exports");
- stopService(this);
- }
-
- private suspend fun doExport(context: Context, export: VideoExport) {
- Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
-
- export.changeState(VideoExport.State.EXPORTING);
-
- var lastNotifyTime: Long = 0L;
- val file = export.export(context) { progress ->
- export.progress = progress;
-
- val currentTime = System.currentTimeMillis();
- if (currentTime - lastNotifyTime > 500) {
- notifyExport(export);
- lastNotifyTime = currentTime;
- }
- }
- export.changeState(VideoExport.State.COMPLETED);
- Logger.i(TAG, "Export [${export.videoLocal.name}] finished");
- StateDownloads.instance.removeExport(export);
- notifyExport(export);
-
- withContext(Dispatchers.Main) {
- StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.uri}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") {
- file.share(this@ExportingService);
- };
- }
- }
-
- private fun notifyExport(export: VideoExport) {
- val channel = _notificationChannel ?: return;
-
- val bringUpIntent = Intent(this, MainActivity::class.java);
- bringUpIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
- bringUpIntent.action = "TAB";
- bringUpIntent.putExtra("TAB", "Exports");
-
- var builder = NotificationCompat.Builder(this, EXPORT_NOTIF_TAG)
- .setSmallIcon(R.drawable.ic_export)
- .setOngoing(true)
- .setSilent(true)
- .setContentIntent(PendingIntent.getActivity(this, 5, bringUpIntent, PendingIntent.FLAG_IMMUTABLE))
- .setContentTitle("${export.state}: ${export.videoLocal.name}")
- .setContentText(export.getExportInfo())
- .setProgress(100, (export.progress * 100).toInt(), export.progress == 0.0)
- .setChannelId(channel.id)
-
- val notif = builder.build();
- notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR;
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- startForeground(EXPORT_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
- } else {
- startForeground(EXPORT_NOTIF_ID, notif);
- }
- }
-
- fun closeExportSession() {
- Logger.i(TAG, "closeExportSession");
- stopForeground(STOP_FOREGROUND_REMOVE);
- _notificationManager?.cancel(EXPORT_NOTIF_ID);
- stopService();
- _started = false;
- super.stopSelf();
- }
- override fun onDestroy() {
- Logger.i(TAG, "onDestroy");
- _instance = null;
- _scope.cancel("onDestroy");
- super.onDestroy();
- }
-
- companion object {
- private var _instance: ExportingService? = null;
- private var _callOnStarted: ((ExportingService)->Unit)? = null;
-
- @Synchronized
- fun getOrCreateService(context: Context, handle: ((ExportingService)->Unit)? = null) {
- if(!FragmentedStorage.isInitialized)
- return;
- if(_instance == null) {
- _callOnStarted = handle;
- val intent = Intent(context, ExportingService::class.java);
- context.startForegroundService(intent);
- }
- else _instance?.let {
- if(handle != null)
- handle(it);
- }
- }
- @Synchronized
- fun getService() : ExportingService? {
- return _instance;
- }
-
- @Synchronized
- fun stopService(service: ExportingService? = null) {
- (service ?: _instance)?.let {
- if(_instance == it)
- _instance = null;
- it.closeExportSession();
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
index 7fd26d63..ae0f24a3 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
@@ -445,9 +445,6 @@ class StateApp {
DownloadService.getOrCreateService(context);
}
- Logger.i(TAG, "MainApp Started: Check [Exports]");
- StateDownloads.instance.checkForExportTodos();
-
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]");
val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled();
val shouldDownload = Settings.instance.autoUpdate.shouldDownload();
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt b/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt
index edeb1859..c07f74b9 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt
@@ -1,13 +1,13 @@
package com.futo.platformplayer.states
import android.content.ContentResolver
+import android.content.Context
import android.os.StatFs
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.PlatformID
-import com.futo.platformplayer.api.media.exceptions.AlreadyQueuedException
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
@@ -27,10 +27,14 @@ import com.futo.platformplayer.models.DiskUsage
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.models.PlaylistDownloaded
import com.futo.platformplayer.services.DownloadService
-import com.futo.platformplayer.services.ExportingService
+import com.futo.platformplayer.share
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.io.File
+import java.util.UUID
/***
* Used to maintain downloads
@@ -50,12 +54,8 @@ class StateDownloads {
private val _downloadPlaylists = FragmentedStorage.storeJson("playlistDownloads")
.load();
- private val _exporting = FragmentedStorage.storeJson("exporting")
- .load();
-
private lateinit var _downloadedSet: HashSet;
- val onExportsChanged = Event0();
val onDownloadsChanged = Event0();
val onDownloadedChanged = Event0();
@@ -457,17 +457,6 @@ class StateDownloads {
}
}
- try {
- val currentDownloads = _downloaded.getItems().map { it.url }.toHashSet();
- val exporting = _exporting.findItems { !currentDownloads.contains(it.videoLocal.url) };
- for (export in exporting)
- _exporting.delete(export);
- }
- catch(ex: Throwable) {
- Logger.e(TAG, "Failed to delete dangling export:", ex);
- UIDialogs.toast("Failed to delete dangling export:\n" + ex);
- }
-
return Pair(totalDeletedCount, totalDeleted);
}
@@ -475,66 +464,41 @@ class StateDownloads {
return _downloadsDirectory;
}
+ fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
+ var lastNotifyTime = -1L;
+ UIDialogs.showDialogProgress(context) {
+ it.setText("Exporting content..");
+ it.setProgress(0f);
+ StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
+ val export = VideoExport(videoLocal, videoSource, audioSource, subtitleSource);
+ try {
+ Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
- //Export
- fun getExporting(): List {
- return _exporting.getItems();
- }
- fun checkForExportTodos() {
- if(_exporting.hasItems()) {
- StateApp.withContext {
- ExportingService.getOrCreateService(it);
+ val file = export.export(context) { progress ->
+ val now = System.currentTimeMillis();
+ if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
+ it.setProgress(progress);
+ lastNotifyTime = now;
+ }
+ }
+
+ withContext(Dispatchers.Main) {
+ it.setProgress(100.0f)
+ it.dismiss()
+
+ StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.uri}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") {
+ file.share(context);
+ };
+ }
+ } catch(ex: Throwable) {
+ Logger.e(TAG, "Failed export [${export.videoLocal.name}]: ${ex.message}", ex);
+
+ }
}
}
}
- fun validateExport(export: VideoExport) {
- if(_exporting.hasItem { it.videoLocal.url == export.videoLocal.url })
- throw AlreadyQueuedException("Video [${export.videoLocal.name}] is already queued for export");
- }
- fun export(videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, notify: Boolean = true) {
- val shortName = if(videoLocal.name.length > 23)
- videoLocal.name.substring(0, 20) + "...";
- else
- videoLocal.name;
-
- val videoExport = VideoExport(videoLocal, videoSource, audioSource, subtitleSource);
-
- try {
- validateExport(videoExport);
- _exporting.save(videoExport);
-
- if(notify) {
- UIDialogs.toast("Exporting [${shortName}]");
- StateApp.withContext { ExportingService.getOrCreateService(it) };
- onExportsChanged.emit();
- }
- }
- catch (ex: AlreadyQueuedException) {
- Logger.e(TAG, "File is already queued for export.", ex);
- StateApp.withContext { ExportingService.getOrCreateService(it) };
- }
- catch(ex: Throwable) {
- StateApp.withContext {
- UIDialogs.showDialog(
- it,
- R.drawable.ic_error,
- "Failed to start export due to:\n${ex.message}", null, null,
- 0,
- UIDialogs.Action("Ok", {}, UIDialogs.ActionStyle.PRIMARY)
- );
- }
- }
- }
-
-
- fun removeExport(export: VideoExport) {
- _exporting.delete(export);
- export.isCancelled = true;
- onExportsChanged.emit();
- }
-
companion object {
const val TAG = "StateDownloads";
diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt
index dbfa8886..3e14ff2f 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt
@@ -647,6 +647,15 @@ class StatePlatform {
return client.getPlaybackTracker(url);
}
+ fun getContentRecommendations(url: String): IPager? {
+ val baseClient = getContentClientOrNull(url) ?: return null;
+ if (baseClient !is JSClient) {
+ return baseClient.getContentRecommendations(url);
+ }
+ val client = _mainClientPool.getClientPooled(baseClient);
+ return client.getContentRecommendations(url);
+ }
+
fun hasEnabledChannelClient(url : String) : Boolean = getEnabledClients().any { it.isChannelUrl(url) };
fun getChannelClient(url : String, exclude: List? = null) : IPlatformClient = getChannelClientOrNull(url, exclude)
?: throw NoPlatformClientException("No client enabled that supports this channel url (${url})");
diff --git a/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt b/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt
index 7dc464a0..62da748b 100644
--- a/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt
@@ -1,7 +1,6 @@
package com.futo.platformplayer.views
import android.content.Context
-import android.content.Intent
import android.net.Uri
import android.util.AttributeSet
import android.view.View
@@ -49,6 +48,7 @@ class MonetizationView : LinearLayout {
private val _taskLoadMerchandise = TaskHandler>(StateApp.instance.scopeGetter, { url ->
val client = ManagedHttpClient();
+ Logger.i(TAG, "Loading https://storecache.grayjay.app/StoreData?url=$url")
val result = client.get("https://storecache.grayjay.app/StoreData?url=$url")
if (!result.isOk) {
throw Exception("Failed to retrieve store data.");
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt
index de0f6c97..8bb2b946 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt
@@ -5,69 +5,121 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
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.constructs.Event1
import com.futo.platformplayer.constructs.Event2
-import com.futo.platformplayer.fragment.channel.tab.*
+import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment
+import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment
+import com.futo.platformplayer.fragment.channel.tab.ChannelListFragment
+import com.futo.platformplayer.fragment.channel.tab.ChannelMonetizationFragment
+import com.futo.platformplayer.fragment.channel.tab.ChannelPlaylistsFragment
+import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment
+import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
+import com.google.android.material.tabs.TabLayout
-class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) {
- private val _cache: Array = arrayOfNulls(4);
- val onContentUrlClicked = Event2();
- val onUrlClicked = Event1();
- val onContentClicked = Event2();
- val onChannelClicked = Event1();
- val onAddToClicked = Event1();
- val onAddToQueueClicked = Event1();
- val onAddToWatchLaterClicked = Event1();
- val onLongPress = Event1();
+enum class ChannelTab {
+ VIDEOS, CHANNELS, PLAYLISTS, SUPPORT, ABOUT
+}
+
+class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
+ FragmentStateAdapter(fragmentManager, lifecycle) {
+ private val _supportedFragments = mutableMapOf(
+ ChannelTab.VIDEOS.ordinal to ChannelTab.VIDEOS, ChannelTab.ABOUT.ordinal to ChannelTab.ABOUT
+ )
+ private val _tabs = arrayListOf(ChannelTab.VIDEOS, ChannelTab.ABOUT)
+
+ var profile: PolycentricProfile? = null
+ var channel: IPlatformChannel? = null
+
+ val onContentUrlClicked = Event2()
+ val onUrlClicked = Event1()
+ val onContentClicked = Event2()
+ val onChannelClicked = Event1()
+ val onAddToClicked = Event1()
+ val onAddToQueueClicked = Event1()
+ val onAddToWatchLaterClicked = Event1()
+ val onLongPress = Event1()
+
+ override fun getItemId(position: Int): Long {
+ return _tabs[position].ordinal.toLong()
+ }
+
+ override fun containsItem(itemId: Long): Boolean {
+ return _supportedFragments.containsKey(itemId.toInt())
+ }
override fun getItemCount(): Int {
- return _cache.size;
+ return _supportedFragments.size
}
- inline fun getFragment(): T {
- //TODO: I have a feeling this can somehow be synced with createFragment so only 1 mapping exists (without a Map<>)
- if(T::class == ChannelContentsFragment::class)
- return createFragment(0) as T;
- else if(T::class == ChannelListFragment::class)
- return createFragment(1) as T;
- //else if(T::class == ChannelStoreFragment::class)
- // return createFragment(2) as T;
- else if(T::class == ChannelMonetizationFragment::class)
- return createFragment(2) as T;
- else if(T::class == ChannelAboutFragment::class)
- return createFragment(3) as T;
- else
- throw NotImplementedError("Implement other types");
+ fun getTabPosition(tab: ChannelTab): Int {
+ return _tabs.indexOf(tab)
+ }
+
+ fun getTabNames(tab: TabLayout.Tab, position: Int) {
+ tab.text = _tabs[position].name
+ }
+
+ fun insert(position: Int, tab: ChannelTab) {
+ _supportedFragments[tab.ordinal] = tab
+ _tabs.add(position, tab)
+ notifyItemInserted(position)
+ }
+
+ fun remove(position: Int) {
+ _supportedFragments.remove(_tabs[position].ordinal)
+ _tabs.removeAt(position)
+ notifyItemRemoved(position)
}
override fun createFragment(position: Int): Fragment {
- val cachedFragment = _cache[position];
- if (cachedFragment != null) {
- return cachedFragment;
+ val fragment: Fragment
+ when (_tabs[position]) {
+ ChannelTab.VIDEOS -> {
+ fragment = ChannelContentsFragment.newInstance().apply {
+ onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit)
+ onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit)
+ onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit)
+ onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit)
+ onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit)
+ onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit)
+ onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit)
+ onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit)
+ }
+ }
+
+ ChannelTab.CHANNELS -> {
+ fragment = ChannelListFragment.newInstance()
+ .apply { onClickChannel.subscribe(onChannelClicked::emit) }
+ }
+
+ ChannelTab.PLAYLISTS -> {
+ fragment = ChannelPlaylistsFragment.newInstance().apply {
+ onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit)
+ onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit)
+ onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit)
+ onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit)
+ onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit)
+ onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit)
+ onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit)
+ onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit)
+ }
+ }
+
+ ChannelTab.SUPPORT -> {
+ fragment = ChannelMonetizationFragment.newInstance()
+ }
+
+ ChannelTab.ABOUT -> {
+ fragment = ChannelAboutFragment.newInstance()
+ }
}
+ channel?.let { (fragment as IChannelTabFragment).setChannel(it) }
+ profile?.let { (fragment as IChannelTabFragment).setPolycentricProfile(it) }
- val fragment = when (position) {
- 0 -> ChannelContentsFragment.newInstance().apply {
- onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit);
- onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit);
- onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit);
- onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit);
- onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit);
- onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit);
- onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit);
- onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit);
- };
- 1 -> ChannelListFragment.newInstance().apply { onClickChannel.subscribe(onChannelClicked::emit) };
- //2 -> ChannelStoreFragment.newInstance();
- 2 -> ChannelMonetizationFragment.newInstance();
- 3 -> ChannelAboutFragment.newInstance();
- else -> throw IllegalStateException("Invalid tab position $position")
- };
-
- _cache[position]= fragment;
- return fragment;
+ return fragment
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/InsertedViewAdapterWithLoader.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/InsertedViewAdapterWithLoader.kt
index de012903..19989056 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/InsertedViewAdapterWithLoader.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/InsertedViewAdapterWithLoader.kt
@@ -15,6 +15,7 @@ import com.futo.platformplayer.R
open class InsertedViewAdapterWithLoader : InsertedViewAdapter where TViewHolder : ViewHolder {
private var _loaderView: ImageView? = null;
private var _loading = false;
+ val isLoading get() = _loading;
constructor(
context: Context,
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt
index a35d8147..b606bf26 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt
@@ -33,6 +33,7 @@ open class PlaylistView : LinearLayout {
protected val _platformIndicator: PlatformIndicator;
protected val _textPlaylistName: TextView
protected val _textVideoCount: TextView
+ protected val _textVideoCountLabel: TextView;
protected val _textPlaylistItems: TextView
protected val _textChannelName: TextView
protected var _neopassAnimator: ObjectAnimator? = null;
@@ -62,6 +63,7 @@ open class PlaylistView : LinearLayout {
_platformIndicator = findViewById(R.id.thumbnail_platform);
_textPlaylistName = findViewById(R.id.text_playlist_name);
_textVideoCount = findViewById(R.id.text_video_count);
+ _textVideoCountLabel = findViewById(R.id.text_video_count_label);
_textChannelName = findViewById(R.id.text_channel_name);
_textPlaylistItems = findViewById(R.id.text_playlist_items);
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
@@ -137,7 +139,15 @@ open class PlaylistView : LinearLayout {
.crossfade()
.into(_imageThumbnail);
- _textVideoCount.text = content.videoCount.toString();
+ if(content.videoCount >= 0) {
+ _textVideoCount.text = content.videoCount.toString();
+ _textVideoCount.visibility = View.VISIBLE;
+ _textVideoCountLabel.visibility = VISIBLE;
+ }
+ else {
+ _textVideoCount.visibility = View.GONE;
+ _textVideoCountLabel.visibility = GONE;
+ }
}
else {
currentPlaylist = null;
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt
index 8f94f6d2..3cf3194b 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt
@@ -43,7 +43,7 @@ class VideoListEditorViewHolder : ViewHolder {
val onRemove = Event1();
@SuppressLint("ClickableViewAccessibility")
- constructor(view: View, touchHelper: ItemTouchHelper) : super(view) {
+ constructor(view: View, touchHelper: ItemTouchHelper? = null) : super(view) {
_root = view.findViewById(R.id.root);
_imageThumbnail = view.findViewById(R.id.image_video_thumbnail);
_imageThumbnail?.clipToOutline = true;
@@ -59,7 +59,7 @@ class VideoListEditorViewHolder : ViewHolder {
_layoutDownloaded = view.findViewById(R.id.layout_downloaded);
_imageDragDrop.setOnTouchListener { _, event ->
- if (event.action == MotionEvent.ACTION_DOWN) {
+ if (touchHelper != null && event.action == MotionEvent.ACTION_DOWN) {
touchHelper.startDrag(this);
}
false
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/ImportPlaylistsViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/ImportPlaylistsViewHolder.kt
index 9cbf400c..66ab0193 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/ImportPlaylistsViewHolder.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/ImportPlaylistsViewHolder.kt
@@ -1,12 +1,14 @@
package com.futo.platformplayer.views.adapters.viewholders
import android.view.LayoutInflater
+import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
+import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.views.adapters.AnyAdapter
@@ -45,10 +47,15 @@ class ImportPlaylistsViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
override fun bind(value: SelectablePlaylist) {
_textName.text = value.playlist.name;
- _textMetadata.text = "${value.playlist.videos.size} " + _view.context.getString(R.string.videos);
+ if(value.playlist.videoCount >= 0) {
+ _textMetadata.text = "${value.playlist.videoCount} " + _view.context.getString(R.string.videos);
+ _textMetadata.visibility = View.VISIBLE;
+ }
+ else
+ _textMetadata.visibility = View.GONE;
_checkbox.value = value.selected;
- val thumbnail = value.playlist.videos.firstOrNull()?.thumbnails?.getHQThumbnail();
+ val thumbnail = value.playlist.thumbnail;
if (thumbnail != null)
Glide.with(_imageThumbnail)
.load(thumbnail)
@@ -62,6 +69,6 @@ class ImportPlaylistsViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
}
class SelectablePlaylist(
- val playlist: Playlist,
+ val playlist: IPlatformPlaylistDetails,
var selected: Boolean = false
) { }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/VideoDownloadViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/VideoDownloadViewHolder.kt
index ae4e6ec9..5be5a4b0 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/VideoDownloadViewHolder.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/VideoDownloadViewHolder.kt
@@ -16,6 +16,8 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.adapters.AnyAdapter
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder(
@@ -57,10 +59,14 @@ class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<
return@changeExternalDownloadDirectory;
}
- StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
+ StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
+ StateDownloads.instance.export(_viewGroup.context, v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
+ }
};
} else {
- StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
+ StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
+ StateDownloads.instance.export(_viewGroup.context, v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
+ }
}
}
}
diff --git a/app/src/main/res/layout/dialog_progress.xml b/app/src/main/res/layout/dialog_progress.xml
index 75b37e42..7bcc6228 100644
--- a/app/src/main/res/layout/dialog_progress.xml
+++ b/app/src/main/res/layout/dialog_progress.xml
@@ -45,6 +45,7 @@
android:textColor="@color/white"
android:textSize="14dp"
android:fontFamily="@font/inter_regular"
+ android:textAlignment="center"
android:layout_marginTop="30dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp" />
diff --git a/app/src/main/res/layout/fragment_remote_playlist.xml b/app/src/main/res/layout/fragment_remote_playlist.xml
new file mode 100644
index 00000000..bee1f714
--- /dev/null
+++ b/app/src/main/res/layout/fragment_remote_playlist.xml
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+W
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_playlist_feed.xml b/app/src/main/res/layout/list_playlist_feed.xml
index a9eb1ddc..f2f30845 100644
--- a/app/src/main/res/layout/list_playlist_feed.xml
+++ b/app/src/main/res/layout/list_playlist_feed.xml
@@ -68,6 +68,7 @@
android:textColor="@color/gray_7f"/>
+ app:layout_constraintRight_toLeftOf="@id/text_video_count_label"
+ app:layout_constraintBottom_toBottomOf="@id/text_video_count_label" />
+ app:layout_constraintBottom_toTopOf="@id/text_video_count_label"/>
Get answers to common questions
Give feedback on the application
Info
+ Networking
Gesture controls
Volume slider
Enable slide gesture to change volume
@@ -461,6 +462,8 @@
Deletes all ongoing downloads
Deletes all unresolved source files
Developer Mode
+ Allow All Certificates
+ This risks exposing all your Grayjay network traffic.
Development Server
Experimental
Cache
diff --git a/app/src/stable/AndroidManifest.xml b/app/src/stable/AndroidManifest.xml
index 0f4a00b8..a5fdd260 100644
--- a/app/src/stable/AndroidManifest.xml
+++ b/app/src/stable/AndroidManifest.xml
@@ -30,6 +30,9 @@
+
+
+
@@ -51,6 +54,9 @@
+
+
+
diff --git a/app/src/stable/assets/sources/spotify b/app/src/stable/assets/sources/spotify
new file mode 160000
index 00000000..4e826dcb
--- /dev/null
+++ b/app/src/stable/assets/sources/spotify
@@ -0,0 +1 @@
+Subproject commit 4e826dcb6a237313e32ec81b0e973a4f69c429c3
diff --git a/app/src/stable/res/raw/plugin_config.json b/app/src/stable/res/raw/plugin_config.json
index f679a30a..a1da4004 100644
--- a/app/src/stable/res/raw/plugin_config.json
+++ b/app/src/stable/res/raw/plugin_config.json
@@ -9,7 +9,8 @@
"4a78c2ff-c20f-43ac-8f75-34515df1d320": "sources/kick/KickConfig.json",
"aac9e9f0-24b5-11ee-be56-0242ac120002": "sources/patreon/PatreonConfig.json",
"9d703ff5-c556-4962-a990-4f000829cb87": "sources/nebula/NebulaConfig.json",
- "cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json"
+ "cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json",
+ "4e365633-6d3f-4267-8941-fdc36631d813": "sources/spotify/build/SpotifyConfig.json"
},
"SOURCES_EMBEDDED_DEFAULT": [
"35ae969a-a7db-11ed-afa1-0242ac120002"
diff --git a/app/src/unstable/AndroidManifest.xml b/app/src/unstable/AndroidManifest.xml
index 8a600a38..a5fdd260 100644
--- a/app/src/unstable/AndroidManifest.xml
+++ b/app/src/unstable/AndroidManifest.xml
@@ -4,7 +4,7 @@
-
+
@@ -30,6 +30,9 @@
+
+
+
@@ -51,6 +54,9 @@
+
+
+
diff --git a/app/src/unstable/assets/sources/spotify b/app/src/unstable/assets/sources/spotify
new file mode 160000
index 00000000..4e826dcb
--- /dev/null
+++ b/app/src/unstable/assets/sources/spotify
@@ -0,0 +1 @@
+Subproject commit 4e826dcb6a237313e32ec81b0e973a4f69c429c3
diff --git a/app/src/unstable/res/raw/plugin_config.json b/app/src/unstable/res/raw/plugin_config.json
index db526f8f..551c5470 100644
--- a/app/src/unstable/res/raw/plugin_config.json
+++ b/app/src/unstable/res/raw/plugin_config.json
@@ -9,7 +9,8 @@
"4a78c2ff-c20f-43ac-8f75-34515df1d320": "sources/kick/KickConfig.json",
"aac9e9f0-24b5-11ee-be56-0242ac120002": "sources/patreon/PatreonConfig.json",
"9d703ff5-c556-4962-a990-4f000829cb87": "sources/nebula/NebulaConfig.json",
- "cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json"
+ "cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json",
+ "4e365633-6d3f-4267-8941-fdc36631d813": "sources/spotify/build/SpotifyConfig.json"
},
"SOURCES_EMBEDDED_DEFAULT": [
"35ae969a-a7db-11ed-afa1-0242ac120002"
diff --git a/docs/packages/packageHttp.md b/docs/packages/packageHttp.md
index 98491d14..d6a3bc3c 100644
--- a/docs/packages/packageHttp.md
+++ b/docs/packages/packageHttp.md
@@ -2,12 +2,12 @@
Package http is the main way for a plugin to make web requests, and is likely a package you will always need.
It offers several ways to make web requests as well as websocket connections.
-Before you can use http you need to register it in your plugin config. See [Packages](_blank).
+Before you can use http you need to register it in your plugin config. See [Packages](/app/src/main/java/com/futo/platformplayer/engine/packages).
## Basic Info
Underneath the http package by default exist two web clients. An authenticated client and a unauthenticated client.
The authenticated client has will apply headers and cookies if the user is logged in with your plugin.
-See [Plugin Authentication](_blank).
+See [Plugin Authentication](/docs/Authentication.md).
These two clients are always available even when the user is not logged in, meaning it behaves similar to the unauthenticated client and can safely use it either way.
>:warning: **Requests are synchronous**
diff --git a/sign-all-sources.sh b/sign-all-sources.sh
index ff4c0de6..3a6d783f 100755
--- a/sign-all-sources.sh
+++ b/sign-all-sources.sh
@@ -1,21 +1,27 @@
#!/bin/bash
-# Array of directories to look in
dirs=("app/src/unstable/assets/sources" "app/src/stable/assets/sources")
-# Loop through each directory
+sign_scripts() {
+ local plugin_dir=$1
+
+ if [[ -d "$plugin_dir" ]]; then
+ script_file=$(find "$plugin_dir" -maxdepth 2 -name '*Script.js')
+ config_file=$(find "$plugin_dir" -maxdepth 2 -name '*Config.json')
+ sign_script="$plugin_dir/sign.sh"
+
+ if [[ -f "$sign_script" && -n "$script_file" && -n "$config_file" ]]; then
+ sh "$sign_script" "$script_file" "$config_file"
+ fi
+ fi
+}
+
for dir in "${dirs[@]}"; do
- if [[ -d "$dir" ]]; then # Check if directory exists
- for plugin in "$dir"/*; do # Loop through each plugin folder
+ if [[ -d "$dir" ]]; then
+ for plugin in "$dir"/*; do
if [[ -d "$plugin" ]]; then
- script_file=$(find "$plugin" -maxdepth 1 -name '*Script.js')
- config_file=$(find "$plugin" -maxdepth 1 -name '*Config.json')
- sign_script="$plugin/sign.sh"
-
- if [[ -f "$sign_script" && -n "$script_file" && -n "$config_file" ]]; then
- sh "$sign_script" "$script_file" "$config_file"
- fi
+ sign_scripts "$plugin"
fi
done
fi
-done
+done
\ No newline at end of file