Compare commits

..

18 Commits

Author SHA1 Message Date
Kelvin be2067067b Year rounding 2024-03-06 21:59:55 +01:00
Kelvin 67a7dd9698 Refs 2024-03-06 21:44:48 +01:00
Kelvin 6ffc067b24 Support for cache in reconstructions, non-required cache added to exports, playlists shares now add a cache aswell for quicker importing 2024-03-06 21:39:30 +01:00
Kelvin 56e6314c11 Ref 2024-03-05 17:15:36 +01:00
Kelvin e590bb4a19 Fix 0 year issue 2024-03-05 00:05:46 +01:00
Kelvin 35fe7f0e7a Add type to unknown content exception 2024-03-01 15:31:30 +01:00
Koen 45d818ac81 Reverted dependencies. 2024-02-16 15:51:59 +01:00
Kelvin 7729681829 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-02-16 14:58:28 +01:00
Kelvin b12d04b27d Attempted fix for double controls 2024-02-16 14:58:17 +01:00
Koen e6608b9a5c Updated PolycentricAndroid. 2024-02-16 14:07:27 +01:00
Koen 2d503dfaf6 Added scroll to top. Full scrollable parent comment and Polycentric process secret backup and automatic database recovery. 2024-02-16 13:56:14 +01:00
Kelvin 08934ef8de Modify subscription groups in sub settings 2024-02-14 23:25:58 +01:00
Kelvin 62d927739a Sharing from overview options, notification channel names 2024-02-14 20:15:12 +01:00
Kelvin c8db8f58e8 Refs 2024-02-14 19:19:24 +01:00
Kelvin 0fc966a77d Subscription watched filter 2024-02-14 19:18:35 +01:00
Kelvin 9f6c6c8cf3 Fix support, fix membership urls 2024-01-23 23:51:21 +01:00
Kelvin 43a6ff138c Fix queue looping 2024-01-22 20:54:40 +01:00
Kelvin 269a3460e7 Fix live stream retrying 2024-01-22 15:52:51 +01:00
41 changed files with 561 additions and 209 deletions
+1 -3
View File
@@ -243,9 +243,7 @@ declare class DashSource implements IVideoSource {
declare interface IRequest { declare interface IRequest {
url: string, url: string,
headers: Map<string, string>?, headers: Map<string, string>
method: string?,
body: string?
} }
declare interface IRequestModifierDef { declare interface IRequestModifierDef {
allowByteSkip: boolean allowByteSkip: boolean
@@ -13,6 +13,8 @@ import java.text.DecimalFormat
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.roundToLong
//Long //Long
@@ -119,7 +121,8 @@ fun OffsetDateTime.getNowDiffMonths(): Long {
return ChronoUnit.MONTHS.between(this, OffsetDateTime.now()); return ChronoUnit.MONTHS.between(this, OffsetDateTime.now());
} }
fun OffsetDateTime.getNowDiffYears(): Long { fun OffsetDateTime.getNowDiffYears(): Long {
return ChronoUnit.YEARS.between(this, OffsetDateTime.now()); val diff = ChronoUnit.MONTHS.between(this, OffsetDateTime.now()) / 12.0;
return diff.roundToLong();
} }
fun OffsetDateTime.getDiffDays(otherDate: OffsetDateTime): Long { fun OffsetDateTime.getDiffDays(otherDate: OffsetDateTime): Long {
@@ -150,6 +153,7 @@ fun OffsetDateTime.toHumanNowDiffString(abs: Boolean = false) : String {
if(value >= secondsInYear) { if(value >= secondsInYear) {
value = getNowDiffYears(); value = getNowDiffYears();
if(abs) value = abs(value); if(abs) value = abs(value);
value = Math.max(1, value);
unit = "year"; unit = "year";
} }
else if(value >= secondsInMonth) { else if(value >= secondsInMonth) {
@@ -34,6 +34,7 @@ import com.futo.platformplayer.dialogs.MigrateDialog
import com.futo.platformplayer.dialogs.ProgressDialog import com.futo.platformplayer.dialogs.ProgressDialog
import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
@@ -343,8 +344,8 @@ class UIDialogs {
} }
} }
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, onConcluded: () -> Unit) { fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, cache: ImportCache?, onConcluded: () -> Unit) {
val dialog = ImportDialog(context, store, name, reconstructions, onConcluded); val dialog = ImportDialog(context, store, name, reconstructions, cache, onConcluded);
registerDialogOpened(dialog); registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
@@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
@@ -37,11 +38,17 @@ import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.LoaderView import com.futo.platformplayer.views.LoaderView
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuRecycler
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import com.futo.platformplayer.views.pills.RoundButton import com.futo.platformplayer.views.pills.RoundButton
import com.futo.platformplayer.views.pills.RoundButtonGroup import com.futo.platformplayer.views.pills.RoundButtonGroup
@@ -87,7 +94,37 @@ class UISlideOverlays {
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", { SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications; subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
}, false), }, false),
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
SlideUpMenuGroup(container.context, "Subscription Groups",
"You can select which groups this subscription is part of.",
-1, listOf()) else null,
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
SlideUpMenuRecycler(container.context, "as") {
val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
.sortedBy { !it.selected });
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? = null;
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
it.onClick.subscribe {
if(it is SubscriptionGroup.Selectable) {
val actualGroup = StateSubscriptionGroups.instance.getSubscriptionGroup(it.id)
?: return@subscribe;
groups.clear();
if(it.selected)
actualGroup.urls.remove(subscription.channel.url);
else
actualGroup.urls.add(subscription.channel.url);
StateSubscriptionGroups.instance.updateSubscriptionGroup(actualGroup);
groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
.sortedBy { !it.selected });
adapter?.notifyContentChanged();
}
}
};
return@SlideUpMenuRecycler adapter;
} else null,
SlideUpMenuGroup(container.context, "Fetch Settings", SlideUpMenuGroup(container.context, "Fetch Settings",
"Depending on the platform you might not need to enable a type for it to be available.", "Depending on the platform you might not need to enable a type for it to be available.",
-1, listOf()), -1, listOf()),
@@ -646,9 +683,17 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater(); val watchLater = StatePlaylists.instance.getWatchLater();
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions", items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
(listOf( (listOf(
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download), { SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
showDownloadVideoOverlay(video, container, true); showDownloadVideoOverlay(video, container, true);
}, false), }, false),
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
container.context.startActivity(Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND;
putExtra(Intent.EXTRA_TEXT, url);
type = "text/plain";
}, null));
}, false),
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", { SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
StateMeta.instance.addHiddenCreator(video.author.url); StateMeta.instance.addHiddenCreator(video.author.url);
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home"); UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
@@ -41,6 +41,7 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
import com.futo.platformplayer.listeners.OrientationManager import com.futo.platformplayer.listeners.OrientationManager
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.UrlVideoWithTime import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.* import com.futo.platformplayer.states.*
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
@@ -603,7 +604,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
UIDialogs.showSingleButtonDialog( UIDialogs.showSingleButtonDialog(
this, this,
R.drawable.ic_play, R.drawable.ic_play,
getString(R.string.unknown_content_format) + " [${url}]", getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
"Ok", "Ok",
{ }); { });
} }
@@ -693,10 +694,22 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if(!recon.trim().startsWith("[")) if(!recon.trim().startsWith("["))
return handleUnknownJson(recon); return handleUnknownJson(recon);
val reconLines = Json.decodeFromString<List<String>>(recon); var reconLines = Json.decodeFromString<List<String>>(recon);
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
var cache: ImportCache? = null;
try {
if(cacheStr != null)
cache = Json.decodeFromString(cacheStr);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to deserialize cache");
}
recon = reconLines.joinToString("\n"); recon = reconLines.joinToString("\n");
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}"); Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
handleReconstruction(recon); handleReconstruction(recon, cache);
return true; return true;
} }
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") { else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
@@ -711,12 +724,25 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
fun handleFile(file: String): Boolean { fun handleFile(file: String): Boolean {
Logger.i(TAG, "handleFile(url=$file)"); Logger.i(TAG, "handleFile(url=$file)");
if(file.lowercase().endsWith(".json")) { if(file.lowercase().endsWith(".json")) {
val recon = String(readSharedFile(file)); var recon = String(readSharedFile(file));
if(!recon.startsWith("[")) if(!recon.startsWith("["))
return handleUnknownJson(recon); return handleUnknownJson(recon);
var reconLines = Json.decodeFromString<List<String>>(recon);
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
var cache: ImportCache? = null;
try {
if(cacheStr != null)
cache = Json.decodeFromString(cacheStr);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to deserialize cache");
}
recon = reconLines.joinToString("\n");
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}"); Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
handleReconstruction(recon); handleReconstruction(recon, cache);
return true; return true;
} }
else if(file.lowercase().endsWith(".zip")) { else if(file.lowercase().endsWith(".zip")) {
@@ -728,7 +754,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
return false; return false;
} }
fun handleReconstruction(recon: String) { fun handleReconstruction(recon: String, cache: ImportCache? = null) {
val type = ManagedStore.getReconstructionIdentifier(recon); val type = ManagedStore.getReconstructionIdentifier(recon);
val store: ManagedStore<*> = when(type) { val store: ManagedStore<*> = when(type) {
"Playlist" -> StatePlaylists.instance.playlistStore "Playlist" -> StatePlaylists.instance.playlistStore
@@ -745,7 +771,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if(!type.isNullOrEmpty()) { if(!type.isNullOrEmpty()) {
UIDialogs.showImportDialog(this, store, name, listOf(recon)) { UIDialogs.showImportDialog(this, store, name, listOf(recon), cache) {
} }
} }
@@ -12,6 +12,7 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
@@ -70,6 +71,12 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
processHandle = ProcessHandle.create(); processHandle = ProcessHandle.create();
Store.instance.addProcessSecret(processHandle.processSecret); Store.instance.addProcessSecret(processHandle.processSecret);
try {
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
processHandle.addServer("https://srv1-stg.polycentric.io"); processHandle.addServer("https://srv1-stg.polycentric.io");
processHandle.setUsername(username); processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle); StatePolycentric.instance.setProcessHandle(processHandle);
@@ -13,6 +13,7 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
@@ -126,6 +127,12 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
val processSecret = ProcessSecret(keyPair, Process.random()); val processSecret = ProcessSecret(keyPair, Process.random());
Store.instance.addProcessSecret(processSecret); Store.instance.addProcessSecret(processSecret);
try {
PolycentricStorage.instance.addProcessSecret(processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
val processHandle = processSecret.toProcessHandle(); val processHandle = processSecret.toProcessHandle();
for (e in exportBundle.events.eventsList) { for (e in exportBundle.events.eventsList) {
@@ -37,6 +37,10 @@ class SerializedChannel(
TODO("Not yet implemented") TODO("Not yet implemented")
} }
fun isSameUrl(url: String): Boolean {
return this.url == url || urlAlternatives.contains(url);
}
companion object { companion object {
fun fromChannel(channel: IPlatformChannel): SerializedChannel { fun fromChannel(channel: IPlatformChannel): SerializedChannel {
return SerializedChannel( return SerializedChannel(
@@ -2,7 +2,5 @@ package com.futo.platformplayer.api.media.models.modifier
interface IRequest { interface IRequest {
val url: String?; val url: String?;
val headers: Map<String, String>?; val headers: Map<String, String>;
val method: String?;
val body: String?;
} }
@@ -13,20 +13,14 @@ class JSRequest : IRequest {
private val _v8Url: String?; private val _v8Url: String?;
private val _v8Headers: Map<String, String>?; private val _v8Headers: Map<String, String>?;
private val _v8Options: Options?; private val _v8Options: Options?;
private val _v8Method: String?;
private val _v8Body: String?;
override var url: String? = null; override var url: String? = null;
override lateinit var headers: Map<String, String>; override lateinit var headers: Map<String, String>;
override var method: String? = null;
override var body: String? = null;
constructor(plugin: JSClient, url: String?, headers: Map<String, String>?, method: String?, body: String? = null, options: Options?, originalUrl: String?, originalHeaders: Map<String, String>?) { constructor(plugin: JSClient, url: String?, headers: Map<String, String>?, options: Options?, originalUrl: String?, originalHeaders: Map<String, String>?) {
_v8Url = url; _v8Url = url;
_v8Headers = headers; _v8Headers = headers;
_v8Options = options; _v8Options = options;
_v8Method = method;
_v8Body = body;
initialize(plugin, originalUrl, originalHeaders); initialize(plugin, originalUrl, originalHeaders);
} }
constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?, applyOtherHeadersByDefault: Boolean = false) { constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?, applyOtherHeadersByDefault: Boolean = false) {
@@ -37,17 +31,12 @@ class JSRequest : IRequest {
_v8Options = obj.getOrDefault<V8ValueObject>(config, "options", "JSRequestModifier.options", null)?.let { _v8Options = obj.getOrDefault<V8ValueObject>(config, "options", "JSRequestModifier.options", null)?.let {
Options(config, it, applyOtherHeadersByDefault); Options(config, it, applyOtherHeadersByDefault);
} ?: Options(null, null, applyOtherHeadersByDefault); } ?: Options(null, null, applyOtherHeadersByDefault);
_v8Method = obj.getOrDefault<String>(config, "method", contextName, null);
_v8Body = obj.getOrDefault<String>(config, "body", contextName, null);
initialize(plugin, originalUrl, originalHeaders); initialize(plugin, originalUrl, originalHeaders);
} }
private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) { private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) {
val config = plugin.config; val config = plugin.config;
url = _v8Url ?: originalUrl; url = _v8Url ?: originalUrl;
method = _v8Method;
body = _v8Body;
if(_v8Options?.applyOtherHeaders ?: false) { if(_v8Options?.applyOtherHeaders ?: false) {
val headersToSet = _v8Headers?.toMutableMap() ?: mutableMapOf(); val headersToSet = _v8Headers?.toMutableMap() ?: mutableMapOf();
@@ -81,7 +70,7 @@ class JSRequest : IRequest {
} }
fun modify(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?): JSRequest { fun modify(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?): JSRequest {
return JSRequest(plugin, _v8Url, _v8Headers, _v8Method, _v8Body, _v8Options, originalUrl, originalHeaders); return JSRequest(plugin, _v8Url, _v8Headers, _v8Options, originalUrl, originalHeaders);
} }
@@ -45,8 +45,5 @@ class JSRequestModifier: IRequestModifier {
} }
data class Request(override val url: String, data class Request(override val url: String, override val headers: Map<String, String>) : IRequest;
override val headers: Map<String, String>,
override val method: String? = null,
override val body: String? = null) : IRequest;
} }
@@ -22,7 +22,9 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
import com.futo.platformplayer.assume import com.futo.platformplayer.assume
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -66,13 +68,15 @@ class ImportDialog : AlertDialog {
private val _name: String; private val _name: String;
private val _toImport: List<String>; private val _toImport: List<String>;
private val _cache: ImportCache?;
constructor(context: Context, importStore: ManagedStore<*>, name: String, toReconstruct: List<String>, onConcluded: ()->Unit): super(context) { constructor(context: Context, importStore: ManagedStore<*>, name: String, toReconstruct: List<String>, cache: ImportCache?, onConcluded: ()->Unit): super(context) {
_context = context; _context = context;
_store = importStore; _store = importStore;
_onConcluded = onConcluded; _onConcluded = onConcluded;
_name = name; _name = name;
_toImport = ArrayList(toReconstruct); _toImport = ArrayList(toReconstruct);
_cache = cache;
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -146,7 +150,7 @@ class ImportDialog : AlertDialog {
val scope = StateApp.instance.scopeOrNull; val scope = StateApp.instance.scopeOrNull;
scope?.launch(Dispatchers.IO) { scope?.launch(Dispatchers.IO) {
try { try {
val migrationResult = _store.importReconstructions(_toImport) { finished, total -> val migrationResult = _store.importReconstructions(_toImport, _cache) { finished, total ->
scope.launch(Dispatchers.Main) { scope.launch(Dispatchers.Main) {
_textProgress.text = "${finished}/${total}"; _textProgress.text = "${finished}/${total}";
} }
@@ -25,6 +25,7 @@ import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.models.SubscriptionGroup import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateCache import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
@@ -197,6 +198,7 @@ class SubscriptionsFeedFragment : MainFragment() {
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST); val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
var allowLive: Boolean = true; var allowLive: Boolean = true;
var allowPlanned: Boolean = false; var allowPlanned: Boolean = false;
var allowWatched: Boolean = true;
override fun encode(): String { override fun encode(): String {
return Json.encodeToString(this); return Json.encodeToString(this);
} }
@@ -304,7 +306,8 @@ class SubscriptionsFeedFragment : MainFragment() {
SubscriptionBar.Toggle(context.getString(R.string.videos), _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); }, SubscriptionBar.Toggle(context.getString(R.string.videos), _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); },
SubscriptionBar.Toggle(context.getString(R.string.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); }, SubscriptionBar.Toggle(context.getString(R.string.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); },
SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); }, SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); },
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); } SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); },
SubscriptionBar.Toggle(context.getString(R.string.watched), _filterSettings.allowWatched) { _filterSettings.allowWatched = it; _filterSettings.save(); loadResults(false); }
); );
} }
@@ -336,6 +339,9 @@ class SubscriptionsFeedFragment : MainFragment() {
return results.filter { return results.filter {
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType); val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
if(it is IPlatformVideo && it.duration > 0 && !_filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration))
return@filter false;
//TODO: Check against a sub cache //TODO: Check against a sub cache
if(filterGroup != null && !filterGroup.urls.contains(it.author.url)) if(filterGroup != null && !filterGroup.urls.contains(it.author.url))
return@filter false; return@filter false;
@@ -398,6 +398,10 @@ class VideoDetailView : ConstraintLayout {
} }
} }
}; };
_monetization.onUrlTap.subscribe {
fragment.navigate<BrowserFragment>(it);
onMinimize.emit();
}
_player.attachPlayer(); _player.attachPlayer();
@@ -1035,10 +1039,10 @@ class VideoDetailView : ConstraintLayout {
switchContentView(_container_content_main); switchContentView(_container_content_main);
} }
fun setVideoOverview(video: IPlatformVideo, fetch: Boolean = true, resumeSeconds: Long = 0) { fun setVideoOverview(video: IPlatformVideo, fetch: Boolean = true, resumeSeconds: Long = 0, bypassSameVideoCheck: Boolean = false) {
Logger.i(TAG, "setVideoOverview") Logger.i(TAG, "setVideoOverview")
if(this.video?.url == video.url) if(!bypassSameVideoCheck && this.video?.url == video.url)
return; return;
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id); val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
@@ -1663,7 +1667,7 @@ class VideoDetailView : ConstraintLayout {
Logger.i(TAG, "prevVideo") Logger.i(TAG, "prevVideo")
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9); val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
if(next != null) { if(next != null) {
setVideoOverview(next); setVideoOverview(next, true, 0, true);
} }
} }
@@ -1673,7 +1677,7 @@ class VideoDetailView : ConstraintLayout {
if(next == null && forceLoop) if(next == null && forceLoop)
next = StatePlayer.instance.restartQueue(); next = StatePlayer.instance.restartQueue();
if(next != null) { if(next != null) {
setVideoOverview(next); setVideoOverview(next, true, 0, true);
return true; return true;
} }
else else
@@ -2562,7 +2566,7 @@ class VideoDetailView : ConstraintLayout {
} }
else else
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
setVideoDetails(videoDetail, true); setVideoDetails(videoDetail, false);
_liveTryJob = null; _liveTryJob = null;
} }
} }
@@ -30,7 +30,7 @@ class HistoryVideo {
} }
companion object { companion object {
fun fromReconString(str: String, resolve: ((url: String)->SerializedPlatformVideo)? = null): HistoryVideo { fun fromReconString(str: String, resolve: ((url: String)->SerializedPlatformVideo?)? = null): HistoryVideo {
var index = str.indexOf("|||"); var index = str.indexOf("|||");
if(index < 0) throw IllegalArgumentException("Invalid history string: " + str); if(index < 0) throw IllegalArgumentException("Invalid history string: " + str);
val url = str.substring(0, index); val url = str.substring(0, index);
@@ -0,0 +1,11 @@
package com.futo.platformplayer.models
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import kotlinx.serialization.Serializable
@Serializable
class ImportCache(
var videos: List<SerializedPlatformVideo>? = null,
var channels: List<SerializedChannel>? = null
);
@@ -0,0 +1,42 @@
package com.futo.platformplayer.polycentric
import com.futo.platformplayer.encryption.GEncryptionProviderV1
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.polycentric.core.ProcessSecret
import com.futo.polycentric.core.base64ToByteArray
import com.futo.polycentric.core.toBase64
import userpackage.Protocol
class PolycentricStorage {
private val _processSecrets = FragmentedStorage.get<StringArrayStorage>("processSecrets");
fun addProcessSecret(processSecret: ProcessSecret) {
_processSecrets.addDistinct(GEncryptionProviderV1.instance.encrypt(processSecret.toProto().toByteArray()).toBase64())
_processSecrets.saveBlocking()
}
fun getProcessSecrets(): List<ProcessSecret> {
val processSecrets = arrayListOf<ProcessSecret>()
for (p in _processSecrets.getAllValues()) {
try {
processSecrets.add(ProcessSecret.fromProto(Protocol.StorageTypeProcessSecret.parseFrom(GEncryptionProviderV1.instance.decrypt(p.base64ToByteArray()))))
} catch (e: Throwable) {
Logger.i(TAG, "Failed to decrypt process secret", e);
}
}
return processSecrets
}
companion object {
val TAG = "PolycentricStorage";
private var _instance : PolycentricStorage? = null;
val instance : PolycentricStorage
get(){
if(_instance == null)
_instance = PolycentricStorage();
return _instance!!;
};
}
}
@@ -37,6 +37,7 @@ class DownloadService : Service() {
private val DOWNLOAD_NOTIF_ID = 3; private val DOWNLOAD_NOTIF_ID = 3;
private val DOWNLOAD_NOTIF_TAG = "download"; private val DOWNLOAD_NOTIF_TAG = "download";
private val DOWNLOAD_NOTIF_CHANNEL_ID = "downloadChannel"; private val DOWNLOAD_NOTIF_CHANNEL_ID = "downloadChannel";
private val DOWNLOAD_NOTIF_CHANNEL_NAME = "Downloads";
//Context //Context
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default); private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
@@ -95,7 +96,7 @@ class DownloadService : Service() {
} }
fun setupNotificationRequirements() { fun setupNotificationRequirements() {
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager; _notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
_notificationChannel = NotificationChannel(DOWNLOAD_NOTIF_CHANNEL_ID, "Temp", NotificationManager.IMPORTANCE_DEFAULT).apply { _notificationChannel = NotificationChannel(DOWNLOAD_NOTIF_CHANNEL_ID, DOWNLOAD_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
this.enableVibration(false); this.enableVibration(false);
this.setSound(null, null); this.setSound(null, null);
}; };
@@ -36,6 +36,7 @@ class ExportingService : Service() {
private val EXPORT_NOTIF_ID = 4; private val EXPORT_NOTIF_ID = 4;
private val EXPORT_NOTIF_TAG = "export"; private val EXPORT_NOTIF_TAG = "export";
private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel"; private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel";
private val EXPORT_NOTIF_CHANNEL_NAME = "Export";
//Context //Context
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default); private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
@@ -88,7 +89,7 @@ class ExportingService : Service() {
} }
fun setupNotificationRequirements() { fun setupNotificationRequirements() {
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager; _notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
_notificationChannel = NotificationChannel(EXPORT_NOTIF_CHANNEL_ID, "Temp", NotificationManager.IMPORTANCE_DEFAULT).apply { _notificationChannel = NotificationChannel(EXPORT_NOTIF_CHANNEL_ID, EXPORT_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
this.enableVibration(false); this.enableVibration(false);
this.setSound(null, null); this.setSound(null, null);
}; };
@@ -10,6 +10,7 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.IWithResultLauncher import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.copyTo import com.futo.platformplayer.copyTo
import com.futo.platformplayer.encryption.GPasswordEncryptionProvider import com.futo.platformplayer.encryption.GPasswordEncryptionProvider
@@ -17,6 +18,7 @@ import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
import com.futo.platformplayer.getNowDiffHours import com.futo.platformplayer.getNowDiffHours
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.readBytes import com.futo.platformplayer.readBytes
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
@@ -58,6 +60,19 @@ class StateBackup {
StatePlaylists.instance.toMigrateCheck() StatePlaylists.instance.toMigrateCheck()
).flatten(); ).flatten();
fun getCache(): ImportCache {
val allPlaylists = StatePlaylists.instance.getPlaylists();
val videos = allPlaylists.flatMap { it.videos }.distinctBy { it.url };
val allSubscriptions = StateSubscriptions.instance.getSubscriptions();
val channels = allSubscriptions.map { it.channel };
return ImportCache(
videos = videos,
channels = channels
);
}
private fun getAutomaticBackupPassword(customPassword: String? = null): String { private fun getAutomaticBackupPassword(customPassword: String? = null): String {
val password = customPassword ?: Settings.instance.backup.autoBackupPassword ?: ""; val password = customPassword ?: Settings.instance.backup.autoBackupPassword ?: "";
@@ -233,11 +248,10 @@ class StateBackup {
.associateBy { it.config.id } .associateBy { it.config.id }
.mapValues { it.value.config.sourceUrl!! }; .mapValues { it.value.config.sourceUrl!! };
val cache = getCache();
val export = ExportStructure(exportInfo, settings, storesToSave, pluginUrls, pluginSettings, cache);
val export = ExportStructure(exportInfo, settings, storesToSave, pluginUrls, pluginSettings);
//export.videoCache = StatePlaylists.instance.getHistory()
// .distinctBy { it.video.url }
// .map { it.video };
return export; return export;
} }
@@ -324,7 +338,7 @@ class StateBackup {
continue; continue;
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
UIDialogs.showImportDialog(context, relevantStore, store.key, store.value) { UIDialogs.showImportDialog(context, relevantStore, store.key, store.value, export.cache) {
synchronized(toAwait) { synchronized(toAwait) {
toAwait.remove(store.key); toAwait.remove(store.key);
if(toAwait.isEmpty()) if(toAwait.isEmpty())
@@ -453,8 +467,8 @@ class StateBackup {
val stores: Map<String, List<String>>, val stores: Map<String, List<String>>,
val plugins: Map<String, String>, val plugins: Map<String, String>,
val pluginSettings: Map<String, Map<String, String?>>, val pluginSettings: Map<String, Map<String, String?>>,
var cache: ImportCache? = null
) { ) {
var videoCache: List<SerializedPlatformVideo>? = null;
fun asZip(): ByteArray { fun asZip(): ByteArray {
return ByteArrayOutputStream().use { byteStream -> return ByteArrayOutputStream().use { byteStream ->
@@ -478,6 +492,17 @@ class StateBackup {
zipStream.putNextEntry(ZipEntry("plugin_settings")); zipStream.putNextEntry(ZipEntry("plugin_settings"));
zipStream.write(Json.encodeToString(pluginSettings).toByteArray()); zipStream.write(Json.encodeToString(pluginSettings).toByteArray());
if(cache != null) {
if(cache?.videos != null) {
zipStream.putNextEntry(ZipEntry("cache_videos"));
zipStream.write(Json.encodeToString(cache!!.videos).toByteArray());
}
if(cache?.channels != null) {
zipStream.putNextEntry(ZipEntry("cache_channels"));
zipStream.write(Json.encodeToString(cache!!.channels).toByteArray());
}
}
}; };
return byteStream.toByteArray(); return byteStream.toByteArray();
} }
@@ -492,6 +517,8 @@ class StateBackup {
val stores: MutableMap<String, List<String>> = mutableMapOf(); val stores: MutableMap<String, List<String>> = mutableMapOf();
var plugins: Map<String, String> = mapOf(); var plugins: Map<String, String> = mapOf();
var pluginSettings: Map<String, Map<String, String?>> = mapOf(); var pluginSettings: Map<String, Map<String, String?>> = mapOf();
var videoCache: List<SerializedPlatformVideo>? = null
var channelCache: List<SerializedChannel>? = null
while (zipStream.nextEntry.also { entry = it } != null) { while (zipStream.nextEntry.also { entry = it } != null) {
if(entry!!.isDirectory) if(entry!!.isDirectory)
@@ -503,6 +530,22 @@ class StateBackup {
"settings" -> settings = String(zipStream.readBytes()); "settings" -> settings = String(zipStream.readBytes());
"plugins" -> plugins = Json.decodeFromString(String(zipStream.readBytes())); "plugins" -> plugins = Json.decodeFromString(String(zipStream.readBytes()));
"plugin_settings" -> pluginSettings = Json.decodeFromString(String(zipStream.readBytes())); "plugin_settings" -> pluginSettings = Json.decodeFromString(String(zipStream.readBytes()));
"cache_videos" -> {
try {
videoCache = Json.decodeFromString(String(zipStream.readBytes()));
}
catch(ex: Exception) {
Logger.e(TAG, "Couldn't deserialize video cache", ex);
}
};
"cache_channels" -> {
try {
channelCache = Json.decodeFromString(String(zipStream.readBytes()));
}
catch(ex: Exception) {
Logger.e(TAG, "Couldn't deserialize channel cache", ex);
}
};
} }
else else
stores[entry!!.name.substring("stores/".length)] = Json.decodeFromString(String(zipStream.readBytes())); stores[entry!!.name.substring("stores/".length)] = Json.decodeFromString(String(zipStream.readBytes()));
@@ -511,7 +554,10 @@ class StateBackup {
throw IllegalStateException("Failed to parse zip [${entry?.name}] due to ${ex.message}"); throw IllegalStateException("Failed to parse zip [${entry?.name}] due to ${ex.message}");
} }
} }
return ExportStructure(exportInfo, settings, stores, plugins, pluginSettings); return ExportStructure(exportInfo, settings, stores, plugins, pluginSettings, ImportCache(
videos = videoCache,
channels = channelCache
));
} }
} }
} }
@@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.db.ManagedDBStore import com.futo.platformplayer.stores.db.ManagedDBStore
import com.futo.platformplayer.stores.db.types.DBHistory import com.futo.platformplayer.stores.db.types.DBHistory
@@ -20,8 +21,8 @@ class StateHistory {
private val _historyStore = FragmentedStorage.storeJson<HistoryVideo>("history") private val _historyStore = FragmentedStorage.storeJson<HistoryVideo>("history")
.withRestore(object: ReconstructStore<HistoryVideo>() { .withRestore(object: ReconstructStore<HistoryVideo>() {
override fun toReconstruction(obj: HistoryVideo): String = obj.toReconString(); override fun toReconstruction(obj: HistoryVideo): String = obj.toReconString();
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): HistoryVideo override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, cache: ImportCache?): HistoryVideo
= HistoryVideo.fromReconString(backup, null); = HistoryVideo.fromReconString(backup) { url -> cache?.videos?.find { it.url == url } };
}) })
.load(); .load();
@@ -50,6 +51,9 @@ class StateHistory {
fun getHistoryPosition(url: String): Long { fun getHistoryPosition(url: String): Long {
return historyIndex[url]?.position ?: 0; return historyIndex[url]?.position ?: 0;
} }
fun isHistoryWatched(url: String, duration: Long): Boolean {
return getHistoryPosition(url) > duration * 0.7;
}
fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L): Long { fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L): Long {
@@ -14,6 +14,7 @@ import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.exceptions.ReconstructionException import com.futo.platformplayer.exceptions.ReconstructionException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage import com.futo.platformplayer.stores.StringArrayStorage
@@ -32,8 +33,10 @@ class StatePlaylists {
.withUnique { it.url } .withUnique { it.url }
.withRestore(object: ReconstructStore<SerializedPlatformVideo>() { .withRestore(object: ReconstructStore<SerializedPlatformVideo>() {
override fun toReconstruction(obj: SerializedPlatformVideo): String = obj.url; override fun toReconstruction(obj: SerializedPlatformVideo): String = obj.url;
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): SerializedPlatformVideo override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): SerializedPlatformVideo
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails); = SerializedPlatformVideo.fromVideo(
importCache?.videos?.find { it.url == backup }?.let { Logger.i(TAG, "Reconstruction [${backup}] from cache"); return@let it; } ?:
StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
}) })
.load(); .load();
private val _watchlistOrderStore = FragmentedStorage.get<StringArrayStorage>("watchListOrder"); //Temporary workaround to add order.. private val _watchlistOrderStore = FragmentedStorage.get<StringArrayStorage>("watchListOrder"); //Temporary workaround to add order..
@@ -154,7 +157,11 @@ class StatePlaylists {
val reconstruction = playlistStore.getReconstructionString(playlist, true); val reconstruction = playlistStore.getReconstructionString(playlist, true);
val newFile = File(playlistShareDir, playlist.name + ".json"); val newFile = File(playlistShareDir, playlist.name + ".json");
newFile.writeText(Json.encodeToString(reconstruction.split("\n")), Charsets.UTF_8); newFile.writeText(Json.encodeToString(reconstruction.split("\n") + listOf(
"__CACHE:" + Json.encodeToString(ImportCache(
videos = playlist.videos.toList()
))
)), Charsets.UTF_8);
return FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), newFile); return FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), newFile);
} }
@@ -185,7 +192,7 @@ class StatePlaylists {
items.addAll(obj.videos.map { it.url }); items.addAll(obj.videos.map { it.url });
return items.map { it.replace("\n","") }.joinToString("\n"); return items.map { it.replace("\n","") }.joinToString("\n");
} }
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): Playlist { override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Playlist {
val items = backup.split("\n"); val items = backup.split("\n");
if(items.size <= 0) { if(items.size <= 0) {
throw IllegalStateException("Cannot reconstructor playlist ${id}"); throw IllegalStateException("Cannot reconstructor playlist ${id}");
@@ -194,10 +201,17 @@ class StatePlaylists {
val name = items[0]; val name = items[0];
val videos = items.drop(1).filter { it.isNotEmpty() }.map { val videos = items.drop(1).filter { it.isNotEmpty() }.map {
try { try {
val video = StatePlatform.instance.getContentDetails(it).await(); val videoUrl = it;
val video = importCache?.videos?.find { it.url == videoUrl } ?:
StatePlatform.instance.getContentDetails(it).await();
if (video is IPlatformVideoDetails) { if (video is IPlatformVideoDetails) {
return@map SerializedPlatformVideo.fromVideo(video); return@map SerializedPlatformVideo.fromVideo(video);
} else { }
else if(video is SerializedPlatformVideo) {
Logger.i(TAG, "Reconstruction [${it}] from cache");
return@map video;
}
else {
return@map null return@map null
} }
} }
@@ -23,6 +23,7 @@ import com.futo.platformplayer.dp
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
@@ -67,28 +68,40 @@ class StatePolycentric {
return return
} }
try { for (i in 0 .. 1) {
val db = SqlLiteDbHelper(context); try {
Store.initializeSqlLiteStore(db); val db = SqlLiteDbHelper(context);
Store.initializeSqlLiteStore(db);
val activeProcessHandleString = _activeProcessHandle.value; val activeProcessHandleString = _activeProcessHandle.value;
if (activeProcessHandleString.isNotEmpty()) { if (activeProcessHandleString.isNotEmpty()) {
try { try {
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray())); val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle()); setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
} catch (e: Throwable) { } catch (e: Throwable) {
db.upgradeOldSecrets(db.writableDatabase); db.upgradeOldSecrets(db.writableDatabase);
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray())); val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle()); setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
Log.i(TAG, "Failed to initialize Polycentric.", e)
}
}
getProcessHandles()
break;
} catch (e: Throwable) {
if (i == 0) {
Logger.i(TAG, "Clearing Polycentric database due to corruption");
val db = SqlLiteDbHelper(context);
db.recreate()
} else {
_transientEnabled = false
UIDialogs.showGeneralErrorDialog(context, "Failed to initialize Polycentric.", e);
Log.i(TAG, "Failed to initialize Polycentric.", e) Log.i(TAG, "Failed to initialize Polycentric.", e)
} }
} }
} catch (e: Throwable) {
_transientEnabled = false
UIDialogs.showGeneralErrorDialog(context, "Failed to initialize Polycentric.", e);
Log.i(TAG, "Failed to initialize Polycentric.", e)
} }
} }
@@ -103,7 +116,32 @@ class StatePolycentric {
return listOf() return listOf()
} }
return Store.instance.getProcessSecrets().map { it.toProcessHandle(); }; val storeProcessSecrets = Store.instance.getProcessSecrets().toMutableList()
val processSecrets = PolycentricStorage.instance.getProcessSecrets()
for (processSecret in processSecrets)
{
if (!storeProcessSecrets.contains(processSecret)) {
try {
Store.instance.addProcessSecret(processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to backfill process secret.")
}
}
}
for (processSecret in storeProcessSecrets)
{
if (!processSecrets.contains(processSecret)) {
try {
PolycentricStorage.instance.addProcessSecret(processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to backfill process secret.")
}
}
}
return (storeProcessSecrets + processSecrets).distinct().map { it.toProcessHandle() }
} }
fun setProcessHandle(processHandle: ProcessHandle?) { fun setProcessHandle(processHandle: ProcessHandle?) {
@@ -12,6 +12,7 @@ import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.Event3 import com.futo.platformplayer.constructs.Event3
import com.futo.platformplayer.functional.CentralizedFeed import com.futo.platformplayer.functional.CentralizedFeed
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricCache
@@ -38,8 +39,8 @@ class StateSubscriptions {
.withRestore(object: ReconstructStore<Subscription>(){ .withRestore(object: ReconstructStore<Subscription>(){
override fun toReconstruction(obj: Subscription): String = override fun toReconstruction(obj: Subscription): String =
obj.channel.url; obj.channel.url;
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): Subscription = override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Subscription =
Subscription(SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false))); Subscription(importCache?.channels?.find { it.isSameUrl(backup) } ?: SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false)));
}).load(); }).load();
private val _subscriptionOthers = FragmentedStorage.storeJson<Subscription>("subscriptions_others") private val _subscriptionOthers = FragmentedStorage.storeJson<Subscription>("subscriptions_others")
.withUnique { it.channel.url } .withUnique { it.channel.url }
@@ -2,6 +2,7 @@ package com.futo.platformplayer.stores.v2
import com.futo.platformplayer.assume import com.futo.platformplayer.assume
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -105,7 +106,7 @@ class ManagedStore<T>{
_toReconstruct.clear(); _toReconstruct.clear();
} }
} }
suspend fun importReconstructions(items: List<String>, onProgress: ((Int, Int)->Unit)? = null): ReconstructionResult { suspend fun importReconstructions(items: List<String>, cache: ImportCache? = null, onProgress: ((Int, Int)->Unit)? = null): ReconstructionResult {
var successes = 0; var successes = 0;
val exs = ArrayList<Throwable>(); val exs = ArrayList<Throwable>();
@@ -120,7 +121,7 @@ class ManagedStore<T>{
for (i in 0 .. 1) { for (i in 0 .. 1) {
try { try {
Logger.i(TAG, "Importing ${logName(recon)}"); Logger.i(TAG, "Importing ${logName(recon)}");
val reconId = createFromReconstruction(recon, builder); val reconId = createFromReconstruction(recon, builder, cache);
successes++; successes++;
Logger.i(TAG, "Imported ${logName(reconId)}"); Logger.i(TAG, "Imported ${logName(reconId)}");
break; break;
@@ -272,12 +273,12 @@ class ManagedStore<T>{
save(obj, withReconstruction, onlyExisting); save(obj, withReconstruction, onlyExisting);
} }
suspend fun createFromReconstruction(reconstruction: String, builder: ReconstructStore.Builder): String { suspend fun createFromReconstruction(reconstruction: String, builder: ReconstructStore.Builder, cache: ImportCache? = null): String {
if(_reconstructStore == null) if(_reconstructStore == null)
throw IllegalStateException("Can't reconstruct as no reconstruction is implemented for this type"); throw IllegalStateException("Can't reconstruct as no reconstruction is implemented for this type");
val id = UUID.randomUUID().toString(); val id = UUID.randomUUID().toString();
val reconstruct = _reconstructStore!!.toObjectWithHeader(id, reconstruction, builder); val reconstruct = _reconstructStore!!.toObjectWithHeader(id, reconstruction, builder, cache);
save(reconstruct); save(reconstruct);
return id; return id;
} }
@@ -1,5 +1,7 @@
package com.futo.platformplayer.stores.v2 package com.futo.platformplayer.stores.v2
import com.futo.platformplayer.models.ImportCache
abstract class ReconstructStore<T> { abstract class ReconstructStore<T> {
open val backupOnSave: Boolean = false; open val backupOnSave: Boolean = false;
open val backupOnCreate: Boolean = true; open val backupOnCreate: Boolean = true;
@@ -11,18 +13,18 @@ abstract class ReconstructStore<T> {
} }
abstract fun toReconstruction(obj: T): String; abstract fun toReconstruction(obj: T): String;
abstract suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): T; abstract suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache? = null): T;
fun toReconstructionWithHeader(obj: T, fallbackName: String): String { fun toReconstructionWithHeader(obj: T, fallbackName: String): String {
val identifier = identifierName ?: fallbackName; val identifier = identifierName ?: fallbackName;
return "@/${identifier}\n${toReconstruction(obj)}"; return "@/${identifier}\n${toReconstruction(obj)}";
} }
suspend fun toObjectWithHeader(id: String, backup: String, builder: Builder): T { suspend fun toObjectWithHeader(id: String, backup: String, builder: Builder, importCache: ImportCache? = null): T {
if(backup.startsWith("@/") && backup.contains("\n")) if(backup.startsWith("@/") && backup.contains("\n"))
return toObject(id, backup.substring(backup.indexOf("\n") + 1), builder); return toObject(id, backup.substring(backup.indexOf("\n") + 1), builder, importCache);
else else
return toObject(id, backup, builder); return toObject(id, backup, builder, importCache);
} }
@@ -8,11 +8,13 @@ import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.HorizontalSpaceItemDecoration import com.futo.platformplayer.HorizontalSpaceItemDecoration
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.polycentric.PolycentricCache
@@ -61,6 +63,7 @@ class MonetizationView : LinearLayout {
val onSupportTap = Event0(); val onSupportTap = Event0();
val onStoreTap = Event0(); val onStoreTap = Event0();
val onUrlTap = Event1<String>();
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.view_monetization, this); inflate(context, R.layout.view_monetization, this);
@@ -70,10 +73,12 @@ class MonetizationView : LinearLayout {
_membershipPlatform = findViewById(R.id.membership_platform); _membershipPlatform = findViewById(R.id.membership_platform);
_buttonMembership.setOnClickListener { _buttonMembership.setOnClickListener {
_membershipUrl?.let { _membershipUrl?.let {
/*
val uri = Uri.parse(it); val uri = Uri.parse(it);
val intent = Intent(Intent.ACTION_VIEW); val intent = Intent(Intent.ACTION_VIEW);
intent.data = uri; intent.data = uri;
context.startActivity(intent); context.startActivity(intent);*/
onUrlTap.emit(it);
} }
} }
@@ -129,9 +134,18 @@ class MonetizationView : LinearLayout {
_buttonStore.visibility = View.GONE; _buttonStore.visibility = View.GONE;
} }
if(profile.systemState.donationDestinations.isNotEmpty() ||
profile.systemState.membershipUrls.isNotEmpty() ||
profile.systemState.store.isNotEmpty() ||
profile.systemState.promotion.isNotEmpty())
_buttonSupport.isVisible = true;
else
_buttonSupport.isVisible = false;
_root.visibility = View.VISIBLE; _root.visibility = View.VISIBLE;
} else { } else {
_root.visibility = View.GONE; _root.visibility = View.GONE;
_buttonSupport.isVisible = false;
} }
setMerchandise(null); setMerchandise(null);
@@ -10,6 +10,8 @@ import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible
import androidx.core.view.size
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
@@ -33,6 +35,13 @@ class SupportView : LinearLayout {
private var _textNoSupportOptionsSet: TextView private var _textNoSupportOptionsSet: TextView
private var _polycentricProfile: PolycentricProfile? = null private var _polycentricProfile: PolycentricProfile? = null
val hasSupportItems: Boolean get() {
return (_layoutPromotions.isVisible && _buttonPromotion.isVisible) ||
(_layoutMemberships.isVisible && _layoutMembershipEntries.isVisible && _layoutMembershipEntries.size > 0) ||
(_layoutDonation.isVisible && _layoutDonationEntries.isVisible && _layoutDonationEntries.size > 0) ||
_buttonStore.isVisible;
};
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.view_support, this); inflate(context, R.layout.view_support, this);
@@ -51,9 +51,11 @@ class RepliesOverlay : LinearLayout {
private var _onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null; private var _onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null;
private val _loaderOverlay: LoaderOverlay private val _loaderOverlay: LoaderOverlay
private val _client = ManagedHttpClient() private val _client = ManagedHttpClient()
private val _layoutItems: LinearLayout
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.overlay_replies, this) inflate(context, R.layout.overlay_replies, this)
_layoutItems = findViewById(R.id.layout_items)
_topbar = findViewById(R.id.topbar); _topbar = findViewById(R.id.topbar);
_commentsList = findViewById(R.id.comments_list); _commentsList = findViewById(R.id.comments_list);
_addCommentView = findViewById(R.id.add_comment_view); _addCommentView = findViewById(R.id.add_comment_view);
@@ -65,6 +67,9 @@ class RepliesOverlay : LinearLayout {
_loaderOverlay = findViewById(R.id.loader_overlay) _loaderOverlay = findViewById(R.id.loader_overlay)
setLoading(false); setLoading(false);
_layoutItems.removeView(_layoutParentComment)
_commentsList.setPrependedView(_layoutParentComment)
_addCommentView.onCommentAdded.subscribe { _addCommentView.onCommentAdded.subscribe {
_commentsList.addComment(it); _commentsList.addComment(it);
_onCommentAdded?.invoke(it); _onCommentAdded?.invoke(it);
@@ -14,6 +14,10 @@ class SupportOverlay : LinearLayout {
private val _topbar: OverlayTopbar; private val _topbar: OverlayTopbar;
private val _support: SupportView; private val _support: SupportView;
val hasSupportItems: Boolean get() {
return _support.hasSupportItems;
}
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.overlay_support, this) inflate(context, R.layout.overlay_support, this)
_topbar = findViewById(R.id.topbar); _topbar = findViewById(R.id.topbar);
@@ -0,0 +1,38 @@
package com.futo.platformplayer.views.overlays.slideup
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.adapters.AnyAdapter
class SlideUpMenuRecycler<T : Any, VType : AnyAdapter.AnyViewHolder<T>> : LinearLayout {
private lateinit var recyclerView: RecyclerView;
private val adapter: AnyAdapterView<T, VType>?;
var groupTag: Any? = null;
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
init();
adapter = null;
}
constructor(context: Context, tag: Any, creation: (RecyclerView)->AnyAdapterView<T, VType>) : super(context){
init();
groupTag = tag;
adapter = creation(recyclerView);
}
private fun init(){
LayoutInflater.from(context).inflate(R.layout.overlay_slide_up_menu_recycler, this, true);
recyclerView = findViewById(R.id.slide_up_menu_recycler);
}
}
@@ -71,6 +71,9 @@ class CommentsList : ConstraintLayout {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy); super.onScrolled(recyclerView, dx, dy);
onScrolled(); onScrolled();
val totalScrollDistance = recyclerView.computeVerticalScrollOffset()
_layoutScrollToTop.visibility = if (totalScrollDistance > recyclerView.height) View.VISIBLE else View.GONE
} }
}; };
@@ -82,6 +85,7 @@ class CommentsList : ConstraintLayout {
private var _loading = false; private var _loading = false;
private val _prependedView: FrameLayout; private val _prependedView: FrameLayout;
private var _readonly: Boolean = false; private var _readonly: Boolean = false;
private val _layoutScrollToTop: FrameLayout;
var onRepliesClick = Event1<IPlatformComment>(); var onRepliesClick = Event1<IPlatformComment>();
var onCommentsLoaded = Event1<Int>(); var onCommentsLoaded = Event1<Int>();
@@ -90,6 +94,13 @@ class CommentsList : ConstraintLayout {
LayoutInflater.from(context).inflate(R.layout.view_comments_list, this, true); LayoutInflater.from(context).inflate(R.layout.view_comments_list, this, true);
_recyclerComments = findViewById(R.id.recycler_comments); _recyclerComments = findViewById(R.id.recycler_comments);
_layoutScrollToTop = findViewById(R.id.layout_scroll_to_top);
_layoutScrollToTop.setOnClickListener {
_recyclerComments.smoothScrollToPosition(0)
}
_layoutScrollToTop.visibility = View.GONE
_textMessage = TextView(context).apply { _textMessage = TextView(context).apply {
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply { layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply {
setMargins(0, 30, 0, 0) setMargins(0, 30, 0, 0)
@@ -582,6 +582,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_videoControls_fullscreen.show(); _videoControls_fullscreen.show();
videoControls.hideImmediately(); videoControls.hideImmediately();
videoControls.visibility = View.GONE;
} }
else { else {
val lp = background.layoutParams as ConstraintLayout.LayoutParams; val lp = background.layoutParams as ConstraintLayout.LayoutParams;
@@ -594,6 +595,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
videoControls.show(); videoControls.show();
_videoControls_fullscreen.hideImmediately(); _videoControls_fullscreen.hideImmediately();
_videoControls_fullscreen.visibility = View.GONE;
} }
fitOrFill(fullScreen); fitOrFill(fullScreen);
@@ -39,7 +39,6 @@ import java.net.HttpURLConnection;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.NoRouteToHostException; import java.net.NoRouteToHostException;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -575,25 +574,12 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
requestHeaders.put(HttpHeaders.ACCEPT_ENCODING, allowGzip ? "gzip" : "identity"); requestHeaders.put(HttpHeaders.ACCEPT_ENCODING, allowGzip ? "gzip" : "identity");
String requestMethod = DataSpec.getStringForHttpMethod(httpMethod);
String requestUrl = url.toString(); String requestUrl = url.toString();
if (requestModifier != null) { if (requestModifier != null) {
IRequest result = requestModifier.modifyRequest(requestUrl, requestHeaders); IRequest result = requestModifier.modifyRequest(requestUrl, requestHeaders);
String modifiedUrl = result.getUrl(); String modifiedUrl = result.getUrl();
if (modifiedUrl != null) requestUrl = (modifiedUrl != null) ? modifiedUrl : requestUrl;
requestUrl = modifiedUrl; requestHeaders = result.getHeaders();
Map<String, String> modifiedHeaders = result.getHeaders();
if (modifiedHeaders != null)
requestHeaders = modifiedHeaders;
String modifiedMethod = result.getMethod();
if (modifiedMethod != null)
requestMethod = modifiedMethod;
String modifiedBody = result.getBody();
if (modifiedBody != null)
httpBody = modifiedBody.getBytes(StandardCharsets.UTF_8);
} }
HttpURLConnection connection = openConnection(new URL(requestUrl)); HttpURLConnection connection = openConnection(new URL(requestUrl));
@@ -606,7 +592,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
connection.setInstanceFollowRedirects(followRedirects); connection.setInstanceFollowRedirects(followRedirects);
connection.setDoOutput(httpBody != null); connection.setDoOutput(httpBody != null);
connection.setRequestMethod(requestMethod); connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod));
if (httpBody != null) { if (httpBody != null) {
connection.setFixedLengthStreamingMode(httpBody.length); connection.setFixedLengthStreamingMode(httpBody.length);
+94 -98
View File
@@ -1,111 +1,108 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout
android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:background="@color/black" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto"> android:layout_width="match_parent"
android:layout_height="match_parent">
<com.futo.platformplayer.views.overlays.OverlayTopbar <LinearLayout android:layout_width="match_parent"
android:id="@+id/topbar" android:layout_height="match_parent"
android:layout_width="match_parent" android:background="@color/black"
android:layout_height="40dp" android:orientation="vertical"
app:title="Replies" android:id="@+id/layout_items">
app:metadata="3 replies"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout <com.futo.platformplayer.views.overlays.OverlayTopbar
android:id="@+id/layout_parent_comment" android:id="@+id/topbar"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="40dp"
app:layout_constraintTop_toBottomOf="@id/topbar" app:title="Replies"
app:layout_constraintLeft_toLeftOf="parent" app:metadata="3 replies" />
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginTop="6dp"
android:padding="12dp"
android:background="@drawable/background_16_round_4dp">
<com.futo.platformplayer.views.others.CreatorThumbnail <com.futo.platformplayer.views.comments.AddCommentView
android:id="@+id/image_thumbnail" android:id="@+id/add_comment_view"
android:layout_width="25dp" android:layout_width="match_parent"
android:layout_height="25dp"
android:contentDescription="@string/channel_image"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/placeholder_channel_thumbnail" />
<TextView
android:id="@+id/text_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:layout_marginTop="12dp"
android:ellipsize="end" android:layout_marginStart="12dp"
android:gravity="center_vertical" android:layout_marginEnd="12dp" />
android:maxLines="1"
android:fontFamily="@font/inter_regular"
android:textColor="@color/white"
android:textSize="14sp"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintTop_toTopOf="@id/image_thumbnail"
tools:text="ShortCircuit" />
<TextView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/text_metadata" android:id="@+id/layout_parent_comment"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:layout_width="match_parent"
android:gravity="center_vertical" android:layout_marginStart="12dp"
android:maxLines="1" android:layout_marginEnd="12dp"
android:fontFamily="@font/inter_regular" android:layout_marginBottom="12dp"
android:textColor="@color/gray_ac" android:padding="12dp"
android:textSize="14sp" android:background="@drawable/background_16_round_4dp">
app:layout_constraintBottom_toBottomOf="@id/text_author"
app:layout_constraintLeft_toRightOf="@id/text_author"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/text_author"
tools:text=" • 3 years ago" />
<com.futo.platformplayer.views.behavior.NonScrollingTextView <com.futo.platformplayer.views.others.CreatorThumbnail
android:id="@+id/text_body" android:id="@+id/image_thumbnail"
android:layout_width="0dp" android:layout_width="25dp"
android:layout_height="wrap_content" android:layout_height="25dp"
android:layout_marginTop="5dp" android:contentDescription="@string/channel_image"
android:layout_marginStart="10dp" app:layout_constraintLeft_toLeftOf="parent"
android:background="@color/transparent" app:layout_constraintTop_toTopOf="parent"
android:fontFamily="@font/inter_regular" tools:src="@drawable/placeholder_channel_thumbnail" />
android:isScrollContainer="false"
android:textColor="#CCCCCC"
android:textSize="13sp"
android:maxLines="3"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/text_metadata"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintRight_toRightOf="parent"
tools:text="@string/lorem_ipsum" />
</androidx.constraintlayout.widget.ConstraintLayout> <TextView
android:id="@+id/text_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:fontFamily="@font/inter_regular"
android:textColor="@color/white"
android:textSize="14sp"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintTop_toTopOf="@id/image_thumbnail"
tools:text="ShortCircuit" />
<com.futo.platformplayer.views.comments.AddCommentView <TextView
android:id="@+id/add_comment_view" android:id="@+id/text_metadata"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:ellipsize="end"
android:layout_marginStart="12dp" android:gravity="center_vertical"
android:layout_marginEnd="12dp" android:maxLines="1"
app:layout_constraintTop_toBottomOf="@id/layout_parent_comment" android:fontFamily="@font/inter_regular"
app:layout_constraintLeft_toLeftOf="parent" android:textColor="@color/gray_ac"
app:layout_constraintRight_toRightOf="parent" /> android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@id/text_author"
app:layout_constraintLeft_toRightOf="@id/text_author"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/text_author"
tools:text=" • 3 years ago" />
<com.futo.platformplayer.views.segments.CommentsList <com.futo.platformplayer.views.behavior.NonScrollingTextView
android:id="@+id/comments_list" android:id="@+id/text_body"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/add_comment_view" android:layout_marginTop="5dp"
app:layout_constraintBottom_toBottomOf="parent" android:layout_marginStart="10dp"
android:layout_marginTop="12dp" /> android:background="@color/transparent"
android:fontFamily="@font/inter_regular"
android:isScrollContainer="false"
android:textColor="#CCCCCC"
android:textSize="13sp"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/text_metadata"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintRight_toRightOf="parent"
tools:text="@string/lorem_ipsum" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.futo.platformplayer.views.segments.CommentsList
android:id="@+id/comments_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="12dp" />
</LinearLayout>
<com.futo.platformplayer.views.overlays.LoaderOverlay <com.futo.platformplayer.views.overlays.LoaderOverlay
android:id="@+id/loader_overlay" android:id="@+id/loader_overlay"
@@ -113,5 +110,4 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clickable="true" /> android:clickable="true" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/slide_up_menu_recycler"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
+21 -1
View File
@@ -7,5 +7,25 @@
android:id="@+id/recycler_comments" android:id="@+id/recycler_comments"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="12dp"
android:paddingBottom="7dp"
android:paddingEnd="14dp"
android:paddingTop="7dp"
android:paddingStart="14dp"
android:background="@drawable/background_pill"
android:id="@+id/layout_scroll_to_top">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/scroll_to_top"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular"
android:textSize="14dp"/>
</FrameLayout>
</FrameLayout> </FrameLayout>
+2
View File
@@ -625,6 +625,7 @@
<string name="you_have_too_many_subscriptions_for_the_following_plugins">\n\nYou have too many subscriptions for the following plugins:\n</string> <string name="you_have_too_many_subscriptions_for_the_following_plugins">\n\nYou have too many subscriptions for the following plugins:\n</string>
<string name="posts">Posts</string> <string name="posts">Posts</string>
<string name="planned">Planned</string> <string name="planned">Planned</string>
<string name="watched">Watched</string>
<string name="no_results_found_swipe_down_to_refresh">No results found\nSwipe down to refresh</string> <string name="no_results_found_swipe_down_to_refresh">No results found\nSwipe down to refresh</string>
<string name="overlay">Overlay</string> <string name="overlay">Overlay</string>
<string name="reload">Reload</string> <string name="reload">Reload</string>
@@ -750,6 +751,7 @@
<string name="select">Select</string> <string name="select">Select</string>
<string name="zoom">Zoom</string> <string name="zoom">Zoom</string>
<string name="check_to_see_if_an_update_is_available">Check to see if an update is available.</string> <string name="check_to_see_if_an_update_is_available">Check to see if an update is available.</string>
<string name="scroll_to_top">Scroll to top</string>
<string-array name="home_screen_array"> <string-array name="home_screen_array">
<item>Recommendations</item> <item>Recommendations</item>
<item>Subscriptions</item> <item>Subscriptions</item>