mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-29 11:03:01 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be2067067b | |||
| 67a7dd9698 | |||
| 6ffc067b24 | |||
| 56e6314c11 | |||
| e590bb4a19 | |||
| 35fe7f0e7a | |||
| 45d818ac81 | |||
| 7729681829 | |||
| b12d04b27d | |||
| e6608b9a5c | |||
| 2d503dfaf6 | |||
| 08934ef8de | |||
| 62d927739a | |||
| c8db8f58e8 | |||
| 0fc966a77d | |||
| 9f6c6c8cf3 | |||
| 43a6ff138c | |||
| 269a3460e7 |
+1
-3
@@ -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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -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);
|
||||||
|
|||||||
+7
@@ -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) {
|
||||||
|
|||||||
+4
@@ -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?;
|
|
||||||
}
|
}
|
||||||
+2
-13
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-4
@@ -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}";
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -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;
|
||||||
|
|||||||
+9
-5
@@ -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);
|
||||||
|
|||||||
+38
@@ -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);
|
||||||
|
|||||||
+3
-17
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
Submodule app/src/stable/assets/sources/youtube updated: c86c73db0c...bef199baa9
Submodule app/src/unstable/assets/sources/youtube updated: c86c73db0c...bef199baa9
+1
-1
Submodule dep/polycentricandroid updated: 7695198eea...00927bb700
Reference in New Issue
Block a user