mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be2067067b | |||
| 67a7dd9698 | |||
| 6ffc067b24 | |||
| 56e6314c11 | |||
| e590bb4a19 | |||
| 35fe7f0e7a | |||
| 45d818ac81 | |||
| 7729681829 | |||
| b12d04b27d | |||
| e6608b9a5c | |||
| 2d503dfaf6 | |||
| 08934ef8de | |||
| 62d927739a | |||
| c8db8f58e8 | |||
| 0fc966a77d | |||
| 9f6c6c8cf3 | |||
| 43a6ff138c | |||
| 269a3460e7 | |||
| 18150e9e15 | |||
| 362c7f5b2c | |||
| 2adb8ad7f9 | |||
| 6b5d4e7507 | |||
| 49c82726f0 | |||
| c8ddcda384 | |||
| b75217f789 | |||
| 8ba8e535bd | |||
| e4c574db6b | |||
| fae73293d7 | |||
| 3bd0aac4f8 | |||
| 26b822e04b | |||
| 96b9b8843c |
@@ -13,6 +13,7 @@
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
||||
@@ -233,6 +233,9 @@ function pluginRemoteProp(objID, propName) {
|
||||
function pluginRemoteCall(objID, methodName, args) {
|
||||
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
|
||||
}
|
||||
function pluginRemoteTest(methodName, args) {
|
||||
return JSON.parse(syncPOST("/plugin/remoteTest?method=" + methodName, {}, JSON.stringify(args)));
|
||||
}
|
||||
|
||||
function pluginIsLoggedIn(cb, err) {
|
||||
fetch("/plugin/isLoggedIn", {
|
||||
|
||||
@@ -385,8 +385,8 @@
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div style="width: 50%" v-if="Plugin.currentPlugin">
|
||||
<!--Get Home-->
|
||||
<v-card class="requestCard" v-for="req in Testing.requests">
|
||||
<v-text-field v-model="searchTestMethods" label="Search for source methods.." style="margin-left: 35px; margin-right: 35px;"></v-text-field>
|
||||
<v-card class="requestCard" v-for="req in Testing.requests" v-show="req.title.indexOf(searchTestMethods) >= 0">
|
||||
<v-card-text>
|
||||
<div class="title">
|
||||
<span v-if="req.isOptional">(Optional)</span>
|
||||
@@ -416,6 +416,9 @@
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="testSourceRemotely(req)">
|
||||
Test Android
|
||||
</v-btn>
|
||||
<v-btn @click="testSource(req)">
|
||||
Test
|
||||
</v-btn>
|
||||
@@ -545,6 +548,7 @@
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
searchTestMethods: "",
|
||||
page: "Plugin",
|
||||
pastPluginUrls: [],
|
||||
settings: {},
|
||||
@@ -860,6 +864,53 @@
|
||||
"Error: " + ex;
|
||||
}
|
||||
},
|
||||
testSourceRemotely(req) {
|
||||
const name = req.title;
|
||||
const parameterVals = req.parameters.map(x=>{
|
||||
if(x.value && x.value.startsWith && x.value.startsWith("json:"))
|
||||
return JSON.parse(x.value.substring(5));
|
||||
return x.value
|
||||
});
|
||||
|
||||
if(name == "enable") {
|
||||
if(parameterVals.length > 0)
|
||||
parameterVals[0] = this.Plugin.currentPlugin;
|
||||
else
|
||||
parameterVals.push(this.Plugin.currentPlugin);
|
||||
if(parameterVals.length > 1)
|
||||
parameterVals[1] = __DEV_SETTINGS;
|
||||
else
|
||||
parameterVals.push(__DEV_SETTINGS);
|
||||
}
|
||||
|
||||
const func = source[name];
|
||||
if(!func)
|
||||
alert("Test func not found");
|
||||
|
||||
try {
|
||||
const remoteResult = pluginRemoteTest(name, parameterVals);
|
||||
console.log("Result for " + req.title, remoteResult);
|
||||
this.Testing.lastResult = "//Results [" + name + "]\n" +
|
||||
JSON.stringify(remoteResult, null, 3);
|
||||
this.Testing.lastResultError = "";
|
||||
}
|
||||
catch(ex) {
|
||||
if(ex.plugin_type == "CaptchaRequiredException") {
|
||||
let shouldCaptcha = confirm("Do you want to request captcha?");
|
||||
if(shouldCaptcha) {
|
||||
pluginCaptchaTestPlugin(ex.url, ex.body);
|
||||
}
|
||||
}
|
||||
console.error("Failed to run test for " + req.title, ex);
|
||||
this.Testing.lastResult = ""
|
||||
if(ex.message)
|
||||
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
|
||||
"Error: " + ex.message + "\n\n" + ex.stack;
|
||||
else
|
||||
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
|
||||
"Error: " + ex;
|
||||
}
|
||||
},
|
||||
showTestResults(results) {
|
||||
|
||||
},
|
||||
|
||||
@@ -13,6 +13,8 @@ import java.text.DecimalFormat
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
|
||||
//Long
|
||||
@@ -119,7 +121,8 @@ fun OffsetDateTime.getNowDiffMonths(): Long {
|
||||
return ChronoUnit.MONTHS.between(this, OffsetDateTime.now());
|
||||
}
|
||||
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 {
|
||||
@@ -150,6 +153,7 @@ fun OffsetDateTime.toHumanNowDiffString(abs: Boolean = false) : String {
|
||||
if(value >= secondsInYear) {
|
||||
value = getNowDiffYears();
|
||||
if(abs) value = abs(value);
|
||||
value = Math.max(1, value);
|
||||
unit = "year";
|
||||
}
|
||||
else if(value >= secondsInMonth) {
|
||||
|
||||
@@ -823,7 +823,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
var toggleFullscreen: Boolean = true;
|
||||
|
||||
@FormField(R.string.system_brightness, FieldForm.TOGGLE, R.string.system_brightness_descr, 4)
|
||||
var useSystemBrightness: Boolean = true;
|
||||
var useSystemBrightness: Boolean = false;
|
||||
|
||||
@FormField(R.string.system_volume, FieldForm.TOGGLE, R.string.system_volume_descr, 5)
|
||||
var useSystemVolume: Boolean = true;
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.futo.platformplayer.dialogs.MigrateDialog
|
||||
import com.futo.platformplayer.dialogs.ProgressDialog
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
@@ -304,12 +305,16 @@ class UIDialogs {
|
||||
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
||||
}
|
||||
|
||||
fun showUpdateAvailableDialog(context: Context, lastVersion: Int) {
|
||||
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
|
||||
val dialog = AutoUpdateDialog(context);
|
||||
registerDialogOpened(dialog);
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
dialog.setMaxVersion(lastVersion);
|
||||
|
||||
if (hideExceptionButtons) {
|
||||
dialog.hideExceptionButtons()
|
||||
}
|
||||
}
|
||||
|
||||
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
||||
@@ -339,8 +344,8 @@ class UIDialogs {
|
||||
}
|
||||
}
|
||||
|
||||
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, onConcluded: () -> Unit) {
|
||||
val dialog = ImportDialog(context, store, name, reconstructions, onConcluded);
|
||||
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, cache: ImportCache?, onConcluded: () -> Unit) {
|
||||
val dialog = ImportDialog(context, store, name, reconstructions, cache, onConcluded);
|
||||
registerDialogOpened(dialog);
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.activities.SettingsActivity
|
||||
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.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.views.AnyAdapterView
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
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.pills.RoundButton
|
||||
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
||||
@@ -87,7 +94,37 @@ class UISlideOverlays {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||
}, false),
|
||||
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||
SlideUpMenuGroup(container.context, "Subscription Groups",
|
||||
"You can select which groups this subscription is part of.",
|
||||
-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",
|
||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||
-1, listOf()),
|
||||
@@ -646,9 +683,17 @@ class UISlideOverlays {
|
||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||
(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), {
|
||||
showDownloadVideoOverlay(video, container, true);
|
||||
}, false),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
|
||||
showDownloadVideoOverlay(video, container, true);
|
||||
}, false),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
|
||||
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
|
||||
container.context.startActivity(Intent.createChooser(Intent().apply {
|
||||
action = Intent.ACTION_SEND;
|
||||
putExtra(Intent.EXTRA_TEXT, url);
|
||||
type = "text/plain";
|
||||
}, null));
|
||||
}, false),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
|
||||
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
@@ -15,6 +16,7 @@ import com.futo.platformplayer.logging.LogLevel
|
||||
import com.futo.platformplayer.logging.Logging
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateUpdate
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -28,6 +30,7 @@ class ExceptionActivity : AppCompatActivity() {
|
||||
private lateinit var _buttonSubmit: LinearLayout;
|
||||
private lateinit var _buttonRestart: LinearLayout;
|
||||
private lateinit var _buttonClose: LinearLayout;
|
||||
private lateinit var _buttonCheckForUpdates: LinearLayout;
|
||||
private var _file: File? = null;
|
||||
private var _submitted = false;
|
||||
|
||||
@@ -45,6 +48,7 @@ class ExceptionActivity : AppCompatActivity() {
|
||||
_buttonSubmit = findViewById(R.id.button_submit);
|
||||
_buttonRestart = findViewById(R.id.button_restart);
|
||||
_buttonClose = findViewById(R.id.button_close);
|
||||
_buttonCheckForUpdates = findViewById(R.id.button_check_for_updates);
|
||||
|
||||
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: getString(R.string.unknown_context);
|
||||
val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace);
|
||||
@@ -83,6 +87,17 @@ class ExceptionActivity : AppCompatActivity() {
|
||||
_buttonClose.setOnClickListener {
|
||||
finish();
|
||||
};
|
||||
|
||||
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||
_buttonCheckForUpdates.visibility = View.VISIBLE
|
||||
_buttonCheckForUpdates.setOnClickListener {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
StateUpdate.instance.checkForUpdates(this@ExceptionActivity, true, true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_buttonCheckForUpdates.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitFile() {
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||
@@ -40,6 +41,7 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||
import com.futo.platformplayer.listeners.OrientationManager
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||
import com.futo.platformplayer.states.*
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
@@ -141,7 +143,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
|
||||
try {
|
||||
handleUrlAll(content)
|
||||
runBlocking {
|
||||
handleUrlAll(content)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to handle URL.", e)
|
||||
UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
|
||||
@@ -540,7 +544,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
Pair("grayjay") { req ->
|
||||
StateApp.instance.contextOrNull?.let {
|
||||
if(it is MainActivity) {
|
||||
it.handleUrlAll(req.url.toString());
|
||||
runBlocking {
|
||||
it.handleUrlAll(req.url.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -552,7 +558,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
|
||||
try {
|
||||
if (targetData != null) {
|
||||
handleUrlAll(targetData)
|
||||
runBlocking {
|
||||
handleUrlAll(targetData)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
@@ -560,7 +568,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
fun handleUrlAll(url: String) {
|
||||
suspend fun handleUrlAll(url: String) {
|
||||
val uri = Uri.parse(url)
|
||||
when (uri.scheme) {
|
||||
"grayjay" -> {
|
||||
@@ -596,7 +604,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
UIDialogs.showSingleButtonDialog(
|
||||
this,
|
||||
R.drawable.ic_play,
|
||||
getString(R.string.unknown_content_format) + " [${url}]",
|
||||
getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
|
||||
"Ok",
|
||||
{ });
|
||||
}
|
||||
@@ -644,31 +652,38 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
fun handleUrl(url: String): Boolean {
|
||||
suspend fun handleUrl(url: String): Boolean {
|
||||
Logger.i(TAG, "handleUrl(url=$url)")
|
||||
|
||||
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
||||
navigate(_fragVideoDetail, url);
|
||||
_fragVideoDetail.maximizeVideoDetail(true);
|
||||
return true;
|
||||
} else if(StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||
navigate(_fragMainChannel, url);
|
||||
return withContext(Dispatchers.IO) {
|
||||
Logger.i(TAG, "handleUrl(url=$url) on IO");
|
||||
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
||||
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
navigate(_fragVideoDetail, url);
|
||||
|
||||
lifecycleScope.launch {
|
||||
delay(100);
|
||||
_fragVideoDetail.minimizeVideoDetail();
|
||||
};
|
||||
return true;
|
||||
_fragVideoDetail.maximizeVideoDetail(true);
|
||||
}
|
||||
return@withContext true;
|
||||
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||
Logger.i(TAG, "handleUrl(url=$url) found channel client");
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
navigate(_fragMainChannel, url);
|
||||
delay(100);
|
||||
_fragVideoDetail.minimizeVideoDetail();
|
||||
};
|
||||
return@withContext true;
|
||||
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
||||
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
navigate(_fragMainPlaylist, url);
|
||||
delay(100);
|
||||
_fragVideoDetail.minimizeVideoDetail();
|
||||
};
|
||||
return@withContext true;
|
||||
}
|
||||
return@withContext false;
|
||||
}
|
||||
else if(StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
||||
navigate(_fragMainPlaylist, url);
|
||||
lifecycleScope.launch {
|
||||
delay(100);
|
||||
_fragVideoDetail.minimizeVideoDetail();
|
||||
};
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
fun handleContent(file: String, mime: String? = null): Boolean {
|
||||
Logger.i(TAG, "handleContent(url=$file)");
|
||||
@@ -679,10 +694,22 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
if(!recon.trim().startsWith("["))
|
||||
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");
|
||||
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
||||
handleReconstruction(recon);
|
||||
handleReconstruction(recon, cache);
|
||||
return true;
|
||||
}
|
||||
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
|
||||
@@ -697,12 +724,25 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
fun handleFile(file: String): Boolean {
|
||||
Logger.i(TAG, "handleFile(url=$file)");
|
||||
if(file.lowercase().endsWith(".json")) {
|
||||
val recon = String(readSharedFile(file));
|
||||
var recon = String(readSharedFile(file));
|
||||
if(!recon.startsWith("["))
|
||||
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}");
|
||||
handleReconstruction(recon);
|
||||
handleReconstruction(recon, cache);
|
||||
return true;
|
||||
}
|
||||
else if(file.lowercase().endsWith(".zip")) {
|
||||
@@ -714,7 +754,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
fun handleReconstruction(recon: String) {
|
||||
fun handleReconstruction(recon: String, cache: ImportCache? = null) {
|
||||
val type = ManagedStore.getReconstructionIdentifier(recon);
|
||||
val store: ManagedStore<*> = when(type) {
|
||||
"Playlist" -> StatePlaylists.instance.playlistStore
|
||||
@@ -731,7 +771,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
|
||||
|
||||
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.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
@@ -70,6 +71,12 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
||||
processHandle = ProcessHandle.create();
|
||||
Store.instance.addProcessSecret(processHandle.processSecret);
|
||||
|
||||
try {
|
||||
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||
}
|
||||
|
||||
processHandle.addServer("https://srv1-stg.polycentric.io");
|
||||
processHandle.setUsername(username);
|
||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||
|
||||
+7
@@ -13,6 +13,7 @@ import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
@@ -126,6 +127,12 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
val processSecret = ProcessSecret(keyPair, Process.random());
|
||||
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();
|
||||
|
||||
for (e in exportBundle.events.eventsList) {
|
||||
|
||||
+4
@@ -37,6 +37,10 @@ class SerializedChannel(
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
fun isSameUrl(url: String): Boolean {
|
||||
return this.url == url || urlAlternatives.contains(url);
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromChannel(channel: IPlatformChannel): SerializedChannel {
|
||||
return SerializedChannel(
|
||||
|
||||
@@ -9,7 +9,10 @@ import com.futo.platformplayer.api.http.server.HttpGET
|
||||
import com.futo.platformplayer.api.http.server.HttpPOST
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.engine.dev.V8RemoteObject
|
||||
import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.gsonStandard
|
||||
@@ -20,18 +23,29 @@ import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateAssets
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.google.gson.ExclusionStrategy
|
||||
import com.google.gson.FieldAttributes
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParser
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Modifier
|
||||
import java.util.UUID
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.memberFunctions
|
||||
import kotlin.reflect.jvm.javaType
|
||||
import kotlin.reflect.jvm.jvmErasure
|
||||
|
||||
class DeveloperEndpoints(private val context: Context) {
|
||||
private val TAG = "DeveloperEndpoints";
|
||||
private val _client = ManagedHttpClient();
|
||||
private var _testPlugin: V8Plugin? = null;
|
||||
private var _testPluginFull: JSClient? = null;
|
||||
private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin");
|
||||
private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
|
||||
|
||||
@@ -190,6 +204,17 @@ class DeveloperEndpoints(private val context: Context) {
|
||||
val client = JSHttpClient(null, null, null, config);
|
||||
val clientAuth = JSHttpClient(null, null, null, config);
|
||||
_testPlugin = V8Plugin(StateApp.instance.context, config, null, client, clientAuth);
|
||||
try {
|
||||
val script = _client.get(config.absoluteScriptUrl);
|
||||
_testPluginFull = JSClient(StateApp.instance.context, SourcePluginDescriptor(
|
||||
config, null, null, null
|
||||
), null, script.body?.string() ?: "");
|
||||
_testPluginFull!!.initialize();
|
||||
}
|
||||
catch (ex: Throwable) {
|
||||
Logger.e(TAG, "Loading full client failed", ex);
|
||||
_testPluginFull = null;
|
||||
}
|
||||
|
||||
context.respondJson(200, testPluginOrThrow.getPackageVariables());
|
||||
}
|
||||
@@ -440,6 +465,68 @@ class DeveloperEndpoints(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private val _fieldAttributesField = FieldAttributes::class.java.getDeclaredField("field");
|
||||
init {
|
||||
_fieldAttributesField.isAccessible = true;
|
||||
}
|
||||
private val _remoteTestGson = GsonBuilder()
|
||||
.setExclusionStrategies(object : ExclusionStrategy {
|
||||
override fun shouldSkipClass(clazz: Class<*>?): Boolean {
|
||||
return clazz?.simpleName == "JSClient" ||
|
||||
clazz?.simpleName == "KSerializer[]" ||
|
||||
clazz?.simpleName == "V8ValueObject";
|
||||
}
|
||||
|
||||
override fun shouldSkipField(f: FieldAttributes?): Boolean {
|
||||
val isPublic = f?.hasModifier(Modifier.PUBLIC) ?: true;
|
||||
if(!isPublic) {
|
||||
val underlyingField = _fieldAttributesField.get(f) as Field;
|
||||
return !(underlyingField.declaringClass as Class).methods.any { it.name == "get" + underlyingField.name.replaceFirstChar { it.uppercaseChar() } && Modifier.isPublic(it.modifiers) };
|
||||
}
|
||||
else
|
||||
return !isPublic;
|
||||
}
|
||||
}).create();
|
||||
@HttpPOST("/plugin/remoteTest")
|
||||
fun pluginRemoteTest(context: HttpContext) {
|
||||
val method = context.query.getOrDefault("method", "");
|
||||
try {
|
||||
|
||||
val parameters = context.readContentString();
|
||||
val paras = JsonParser.parseString(parameters);
|
||||
if(!paras.isJsonArray)
|
||||
throw IllegalArgumentException("Expected json array as body");
|
||||
|
||||
val plugin = _testPluginFull ?: throw IllegalStateException("Plugin not loaded");
|
||||
|
||||
val function = plugin::class.memberFunctions.filter { it.findAnnotation<JSDocs>() != null }
|
||||
.find { it.name == method };
|
||||
if(function == null)
|
||||
throw java.lang.IllegalArgumentException("Plugin method [${function}] not found");
|
||||
val callResult = function.call(*(listOf(plugin) + paras.asJsonArray.take(function.parameters.size - 1).mapIndexed { index, jsonElement ->
|
||||
//For now, manual conversion.
|
||||
val parameter = function.parameters[index + 1];
|
||||
val value = _remoteTestGson.fromJson<Any>(jsonElement, parameter.type.javaType);
|
||||
return@mapIndexed value;
|
||||
}).toTypedArray());
|
||||
val json = if(callResult is IPager<*>)
|
||||
_remoteTestGson.toJson(callResult.getResults())
|
||||
else
|
||||
_remoteTestGson.toJson(callResult);
|
||||
//val json = wrapRemoteResult(callResult, false);
|
||||
|
||||
context.respondCode(200, json);
|
||||
}
|
||||
catch(ex: InvocationTargetException) {
|
||||
Logger.e(TAG, "Remote test for [${method}] is failed", ex.targetException);
|
||||
context.respondCode(500, ex.targetException.message ?: "", "text/plain")
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
Logger.e(TAG, "Remote test for [${method}] is failed", ex);
|
||||
context.respondCode(500, ex.message ?: "", "text/plain")
|
||||
}
|
||||
}
|
||||
|
||||
//Internal calls
|
||||
@HttpPOST("/get")
|
||||
fun get(context: HttpContext) {
|
||||
|
||||
@@ -96,6 +96,11 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
||||
Logger.i(TAG, "Cleared InstallReceiver.onReceiveResult handler.")
|
||||
}
|
||||
|
||||
fun hideExceptionButtons() {
|
||||
_buttonNever.visibility = View.GONE
|
||||
_buttonShowChangelog.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
_buttonShowChangelog.visibility = Button.GONE;
|
||||
_buttonNever.visibility = Button.GONE;
|
||||
|
||||
@@ -22,7 +22,9 @@ import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||
import com.futo.platformplayer.assume
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -66,13 +68,15 @@ class ImportDialog : AlertDialog {
|
||||
private val _name: 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;
|
||||
_store = importStore;
|
||||
_onConcluded = onConcluded;
|
||||
_name = name;
|
||||
_toImport = ArrayList(toReconstruct);
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -146,7 +150,7 @@ class ImportDialog : AlertDialog {
|
||||
val scope = StateApp.instance.scopeOrNull;
|
||||
scope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val migrationResult = _store.importReconstructions(_toImport) { finished, total ->
|
||||
val migrationResult = _store.importReconstructions(_toImport, _cache) { finished, total ->
|
||||
scope.launch(Dispatchers.Main) {
|
||||
_textProgress.text = "${finished}/${total}";
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine
|
||||
|
||||
import android.content.Context
|
||||
import com.caoccao.javet.exceptions.JavetCompilationException
|
||||
import com.caoccao.javet.exceptions.JavetException
|
||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||
import com.caoccao.javet.interop.V8Host
|
||||
import com.caoccao.javet.interop.V8Runtime
|
||||
@@ -10,6 +11,7 @@ import com.caoccao.javet.values.primitive.V8ValueBoolean
|
||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
@@ -173,8 +175,16 @@ class V8Plugin {
|
||||
isStopped = true;
|
||||
_runtime?.let {
|
||||
_runtime = null;
|
||||
if(!it.isClosed && !it.isDead)
|
||||
it.close();
|
||||
if(!it.isClosed && !it.isDead) {
|
||||
try {
|
||||
it.close();
|
||||
}
|
||||
catch(ex: JavetException) {
|
||||
//In case race conditions are going on, already closed runtimes are fine.
|
||||
if(ex.message?.contains("Runtime is already closed") != true)
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
Logger.i(TAG, "Stopped plugin [${config.name}]");
|
||||
};
|
||||
}
|
||||
|
||||
+6
-3
@@ -247,11 +247,14 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
||||
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
||||
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
||||
if (_buttonsVisible - 1 >= defs.size) {
|
||||
if (_buttonsVisible >= defs.size) {
|
||||
updateBottomMenuButtons(defs.toMutableList(), false);
|
||||
} else if (_buttonsVisible > 0) {
|
||||
updateBottomMenuButtons(defs.take(_buttonsVisible - 1).toMutableList(), true);
|
||||
updateMoreButtons(defs.drop(_buttonsVisible - 1).toMutableList());
|
||||
} else {
|
||||
updateBottomMenuButtons(defs.slice(IntRange(0, _buttonsVisible - 2)).toMutableList(), true);
|
||||
updateMoreButtons(defs.slice(IntRange(_buttonsVisible - 1, defs.size - 1)).toMutableList());
|
||||
updateBottomMenuButtons(mutableListOf(), false)
|
||||
updateMoreButtons(defs.toMutableList())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -32,6 +32,9 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||
import com.futo.platformplayer.views.announcements.AnnouncementView
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@@ -160,15 +163,17 @@ class HomeFragment : MainFragment() {
|
||||
val pluginsExist = StatePlatform.instance.getAvailableClients().isNotEmpty();
|
||||
if(StatePlatform.instance.getEnabledClients().isEmpty())
|
||||
//Initial setup
|
||||
return NoResultsView(context, "No enabled Sources", if(pluginsExist)
|
||||
"Enable or install some Sources"
|
||||
return NoResultsView(context, "No enabled sources", if(pluginsExist)
|
||||
"Enable or install some sources"
|
||||
else "This Grayjay version comes without any sources, install sources externally or using the button below.", R.drawable.ic_sources,
|
||||
listOf(BigButton(context, "Browse Online Sources", "View official sources online", R.drawable.ic_explore) {
|
||||
fragment.navigate<BrowserFragment>(BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
|
||||
Pair("grayjay") { req ->
|
||||
StateApp.instance.contextOrNull?.let {
|
||||
if(it is MainActivity) {
|
||||
it.handleUrlAll(req.url.toString());
|
||||
runBlocking {
|
||||
it.handleUrlAll(req.url.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+7
-1
@@ -25,6 +25,7 @@ import com.futo.platformplayer.models.SearchType
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateCache
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
@@ -197,6 +198,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
|
||||
var allowLive: Boolean = true;
|
||||
var allowPlanned: Boolean = false;
|
||||
var allowWatched: Boolean = true;
|
||||
override fun encode(): String {
|
||||
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.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.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 {
|
||||
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
|
||||
if(filterGroup != null && !filterGroup.urls.contains(it.author.url))
|
||||
return@filter false;
|
||||
|
||||
+13
-9
@@ -398,6 +398,10 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
};
|
||||
_monetization.onUrlTap.subscribe {
|
||||
fragment.navigate<BrowserFragment>(it);
|
||||
onMinimize.emit();
|
||||
}
|
||||
|
||||
_player.attachPlayer();
|
||||
|
||||
@@ -859,11 +863,11 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
|
||||
private val _historyIndexLock = Mutex(false);
|
||||
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){
|
||||
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index? = withContext(Dispatchers.IO){
|
||||
_historyIndexLock.withLock {
|
||||
val current = _historyIndex;
|
||||
if(current == null || current.url != video.url) {
|
||||
val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
|
||||
val index = StateHistory.instance.getHistoryByVideo(video, true);
|
||||
_historyIndex = index;
|
||||
return@withContext index;
|
||||
}
|
||||
@@ -1035,10 +1039,10 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
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")
|
||||
|
||||
if(this.video?.url == video.url)
|
||||
if(!bypassSameVideoCheck && this.video?.url == video.url)
|
||||
return;
|
||||
|
||||
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
|
||||
@@ -1390,7 +1394,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
if (video !is TutorialFragment.TutorialVideo) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val historyItem = getHistoryIndex(videoDetail);
|
||||
val historyItem = getHistoryIndex(videoDetail) ?: return@launch;
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
||||
@@ -1663,7 +1667,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
Logger.i(TAG, "prevVideo")
|
||||
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
|
||||
if(next != null) {
|
||||
setVideoOverview(next);
|
||||
setVideoOverview(next, true, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1673,7 +1677,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
if(next == null && forceLoop)
|
||||
next = StatePlayer.instance.restartQueue();
|
||||
if(next != null) {
|
||||
setVideoOverview(next);
|
||||
setVideoOverview(next, true, 0, true);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
@@ -2252,7 +2256,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
||||
if (v !is TutorialFragment.TutorialVideo) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val history = getHistoryIndex(v);
|
||||
val history = getHistoryIndex(v) ?: return@launch;
|
||||
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
||||
}
|
||||
}
|
||||
@@ -2562,7 +2566,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
else
|
||||
withContext(Dispatchers.Main) {
|
||||
setVideoDetails(videoDetail, true);
|
||||
setVideoDetails(videoDetail, false);
|
||||
_liveTryJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class HistoryVideo {
|
||||
}
|
||||
|
||||
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("|||");
|
||||
if(index < 0) throw IllegalArgumentException("Invalid history string: " + str);
|
||||
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
|
||||
);
|
||||
@@ -12,6 +12,7 @@ import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.timestampRegex
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class PlatformLinkMovementMethod : LinkMovementMethod {
|
||||
private val _context: Context;
|
||||
@@ -32,33 +33,36 @@ class PlatformLinkMovementMethod : LinkMovementMethod {
|
||||
val links = buffer.getSpans(off, off, URLSpan::class.java);
|
||||
|
||||
if (links.isNotEmpty()) {
|
||||
for (link in links) {
|
||||
Logger.i(TAG) { "Link clicked '${link.url}'." };
|
||||
runBlocking {
|
||||
for (link in links) {
|
||||
Logger.i(TAG) { "Link clicked '${link.url}'." };
|
||||
|
||||
if (_context is MainActivity) {
|
||||
if (_context.handleUrl(link.url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timestampRegex.matches(link.url)) {
|
||||
val tokens = link.url.split(':');
|
||||
|
||||
var time_s = -1L;
|
||||
if (tokens.size == 2) {
|
||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
||||
} else if (tokens.size == 3) {
|
||||
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
||||
}
|
||||
|
||||
if (time_s != -1L) {
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
||||
if (_context is MainActivity) {
|
||||
if (_context.handleUrl(link.url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timestampRegex.matches(link.url)) {
|
||||
val tokens = link.url.split(':');
|
||||
|
||||
var time_s = -1L;
|
||||
if (tokens.size == 2) {
|
||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
||||
} else if (tokens.size == 3) {
|
||||
time_s =
|
||||
tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
||||
}
|
||||
|
||||
if (time_s != -1L) {
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||
}
|
||||
|
||||
|
||||
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -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!!;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,18 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class AudioNoisyReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
Logger.i(TAG, "Audio Noisy received");
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
Logger.i(TAG, "Audio Noisy received");
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -37,6 +37,7 @@ class DownloadService : Service() {
|
||||
private val DOWNLOAD_NOTIF_ID = 3;
|
||||
private val DOWNLOAD_NOTIF_TAG = "download";
|
||||
private val DOWNLOAD_NOTIF_CHANNEL_ID = "downloadChannel";
|
||||
private val DOWNLOAD_NOTIF_CHANNEL_NAME = "Downloads";
|
||||
|
||||
//Context
|
||||
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
|
||||
@@ -95,7 +96,7 @@ class DownloadService : Service() {
|
||||
}
|
||||
fun setupNotificationRequirements() {
|
||||
_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.setSound(null, null);
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@ class ExportingService : Service() {
|
||||
private val EXPORT_NOTIF_ID = 4;
|
||||
private val EXPORT_NOTIF_TAG = "export";
|
||||
private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel";
|
||||
private val EXPORT_NOTIF_CHANNEL_NAME = "Export";
|
||||
|
||||
//Context
|
||||
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
|
||||
@@ -88,7 +89,7 @@ class ExportingService : Service() {
|
||||
}
|
||||
fun setupNotificationRequirements() {
|
||||
_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.setSound(null, null);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import android.net.NetworkRequest
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -473,7 +474,11 @@ class StateApp {
|
||||
Logger.i(TAG, "MainApp Started: Initialize [Noisy]");
|
||||
_receiverBecomingNoisy?.let {
|
||||
_receiverBecomingNoisy = null;
|
||||
context.unregisterReceiver(it);
|
||||
try {
|
||||
context.unregisterReceiver(it);
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to unregister receiver.", e)
|
||||
}
|
||||
}
|
||||
_receiverBecomingNoisy = AudioNoisyReceiver();
|
||||
context.registerReceiver(_receiverBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
||||
@@ -639,7 +644,11 @@ class StateApp {
|
||||
Logger.i(TAG, "App ended");
|
||||
_receiverBecomingNoisy?.let {
|
||||
_receiverBecomingNoisy = null;
|
||||
context.unregisterReceiver(it);
|
||||
try {
|
||||
context.unregisterReceiver(it);
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to unregister receiver.", e)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Unregistered network callback on connectivityManager.")
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
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.copyTo
|
||||
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.getNowDiffHours
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.readBytes
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
@@ -58,6 +60,19 @@ class StateBackup {
|
||||
StatePlaylists.instance.toMigrateCheck()
|
||||
).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 {
|
||||
val password = customPassword ?: Settings.instance.backup.autoBackupPassword ?: "";
|
||||
@@ -233,11 +248,10 @@ class StateBackup {
|
||||
.associateBy { it.config.id }
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -324,7 +338,7 @@ class StateBackup {
|
||||
continue;
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.showImportDialog(context, relevantStore, store.key, store.value) {
|
||||
UIDialogs.showImportDialog(context, relevantStore, store.key, store.value, export.cache) {
|
||||
synchronized(toAwait) {
|
||||
toAwait.remove(store.key);
|
||||
if(toAwait.isEmpty())
|
||||
@@ -453,8 +467,8 @@ class StateBackup {
|
||||
val stores: Map<String, List<String>>,
|
||||
val plugins: Map<String, String>,
|
||||
val pluginSettings: Map<String, Map<String, String?>>,
|
||||
var cache: ImportCache? = null
|
||||
) {
|
||||
var videoCache: List<SerializedPlatformVideo>? = null;
|
||||
|
||||
fun asZip(): ByteArray {
|
||||
return ByteArrayOutputStream().use { byteStream ->
|
||||
@@ -478,6 +492,17 @@ class StateBackup {
|
||||
|
||||
zipStream.putNextEntry(ZipEntry("plugin_settings"));
|
||||
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();
|
||||
}
|
||||
@@ -492,6 +517,8 @@ class StateBackup {
|
||||
val stores: MutableMap<String, List<String>> = mutableMapOf();
|
||||
var plugins: 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) {
|
||||
if(entry!!.isDirectory)
|
||||
@@ -503,6 +530,22 @@ class StateBackup {
|
||||
"settings" -> settings = String(zipStream.readBytes());
|
||||
"plugins" -> plugins = 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
|
||||
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}");
|
||||
}
|
||||
}
|
||||
return ExportStructure(exportInfo, settings, stores, plugins, pluginSettings);
|
||||
return ExportStructure(exportInfo, settings, stores, plugins, pluginSettings, ImportCache(
|
||||
videos = videoCache,
|
||||
channels = channelCache
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.futo.platformplayer.states
|
||||
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.HistoryVideo
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.db.ManagedDBStore
|
||||
import com.futo.platformplayer.stores.db.types.DBHistory
|
||||
@@ -19,8 +21,8 @@ class StateHistory {
|
||||
private val _historyStore = FragmentedStorage.storeJson<HistoryVideo>("history")
|
||||
.withRestore(object: ReconstructStore<HistoryVideo>() {
|
||||
override fun toReconstruction(obj: HistoryVideo): String = obj.toReconString();
|
||||
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): HistoryVideo
|
||||
= HistoryVideo.fromReconString(backup, null);
|
||||
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, cache: ImportCache?): HistoryVideo
|
||||
= HistoryVideo.fromReconString(backup) { url -> cache?.videos?.find { it.url == url } };
|
||||
})
|
||||
.load();
|
||||
|
||||
@@ -49,6 +51,9 @@ class StateHistory {
|
||||
fun getHistoryPosition(url: String): Long {
|
||||
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 {
|
||||
@@ -92,14 +97,20 @@ class StateHistory {
|
||||
}
|
||||
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
|
||||
val existing = historyIndex[video.url];
|
||||
if(existing != null)
|
||||
return _historyDBStore.get(existing.id!!);
|
||||
var result: DBHistory.Index? = null;
|
||||
if(existing != null) {
|
||||
result = _historyDBStore.getOrNull(existing.id!!);
|
||||
if(result == null)
|
||||
UIDialogs.toast("History item null?\nNo history tracking..");
|
||||
}
|
||||
else if(create) {
|
||||
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now());
|
||||
val id = _historyDBStore.insert(newHistItem);
|
||||
return _historyDBStore.get(id);
|
||||
result = _historyDBStore.getOrNull(id);
|
||||
if(result == null)
|
||||
UIDialogs.toast("History creation failed?\nNo history tracking..");
|
||||
}
|
||||
return null;
|
||||
return result;
|
||||
}
|
||||
|
||||
fun removeHistory(url: String) {
|
||||
|
||||
@@ -46,6 +46,7 @@ import com.futo.platformplayer.models.ImageVariable
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringArrayStorage
|
||||
import com.futo.platformplayer.stores.StringStorage
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -166,8 +167,13 @@ class StatePlatform {
|
||||
var enabled: Array<String>;
|
||||
synchronized(_clientsLock) {
|
||||
for(e in _enabledClients) {
|
||||
e.disable();
|
||||
onSourceDisabled.emit(e);
|
||||
try {
|
||||
e.disable();
|
||||
onSourceDisabled.emit(e);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
UIDialogs.appToast(ToastView.Toast("If this happens often, please inform the developers on Github", false, null, "Plugin [${e.name}] failed to disable"));
|
||||
}
|
||||
}
|
||||
|
||||
_enabledClients.clear();
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||
import com.futo.platformplayer.exceptions.ReconstructionException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.models.Playlist
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringArrayStorage
|
||||
@@ -32,8 +33,10 @@ class StatePlaylists {
|
||||
.withUnique { it.url }
|
||||
.withRestore(object: ReconstructStore<SerializedPlatformVideo>() {
|
||||
override fun toReconstruction(obj: SerializedPlatformVideo): String = obj.url;
|
||||
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): SerializedPlatformVideo
|
||||
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
|
||||
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): SerializedPlatformVideo
|
||||
= 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();
|
||||
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 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);
|
||||
}
|
||||
@@ -185,7 +192,7 @@ class StatePlaylists {
|
||||
items.addAll(obj.videos.map { it.url });
|
||||
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");
|
||||
if(items.size <= 0) {
|
||||
throw IllegalStateException("Cannot reconstructor playlist ${id}");
|
||||
@@ -194,10 +201,17 @@ class StatePlaylists {
|
||||
val name = items[0];
|
||||
val videos = items.drop(1).filter { it.isNotEmpty() }.map {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.resolveChannelUrl
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
@@ -67,28 +68,40 @@ class StatePolycentric {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val db = SqlLiteDbHelper(context);
|
||||
Store.initializeSqlLiteStore(db);
|
||||
for (i in 0 .. 1) {
|
||||
try {
|
||||
val db = SqlLiteDbHelper(context);
|
||||
Store.initializeSqlLiteStore(db);
|
||||
|
||||
val activeProcessHandleString = _activeProcessHandle.value;
|
||||
if (activeProcessHandleString.isNotEmpty()) {
|
||||
try {
|
||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
||||
} catch (e: Throwable) {
|
||||
db.upgradeOldSecrets(db.writableDatabase);
|
||||
val activeProcessHandleString = _activeProcessHandle.value;
|
||||
if (activeProcessHandleString.isNotEmpty()) {
|
||||
try {
|
||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
||||
} catch (e: Throwable) {
|
||||
db.upgradeOldSecrets(db.writableDatabase);
|
||||
|
||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||
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)
|
||||
}
|
||||
}
|
||||
} 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 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?) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.Event3
|
||||
import com.futo.platformplayer.functional.CentralizedFeed
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
@@ -38,8 +39,8 @@ class StateSubscriptions {
|
||||
.withRestore(object: ReconstructStore<Subscription>(){
|
||||
override fun toReconstruction(obj: Subscription): String =
|
||||
obj.channel.url;
|
||||
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): Subscription =
|
||||
Subscription(SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false)));
|
||||
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Subscription =
|
||||
Subscription(importCache?.channels?.find { it.isSameUrl(backup) } ?: SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false)));
|
||||
}).load();
|
||||
private val _subscriptionOthers = FragmentedStorage.storeJson<Subscription>("subscriptions_others")
|
||||
.withUnique { it.channel.url }
|
||||
|
||||
@@ -155,7 +155,7 @@ class StateUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean) = withContext(Dispatchers.IO) {
|
||||
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean, hideExceptionButtons: Boolean = false) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val client = ManagedHttpClient();
|
||||
val latestVersion = downloadVersionCode(client);
|
||||
@@ -167,7 +167,7 @@ class StateUpdate {
|
||||
if (latestVersion > currentVersion) {
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
UIDialogs.showUpdateAvailableDialog(context, latestVersion);
|
||||
UIDialogs.showUpdateAvailableDialog(context, latestVersion, hideExceptionButtons);
|
||||
} catch (e: Throwable) {
|
||||
UIDialogs.toast(context, "Failed to show update dialog");
|
||||
Logger.w(TAG, "Error occurred in update dialog.");
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
|
||||
import com.futo.platformplayer.stores.v2.StoreSerializer
|
||||
import kotlinx.serialization.KSerializer
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.lang.reflect.Field
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentMap
|
||||
@@ -209,7 +210,9 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
||||
|
||||
fun getObject(id: Long) = get(id).obj!!;
|
||||
fun get(id: Long): I {
|
||||
return deserializeIndex(dbDaoBase.get(_sqlGet(id)));
|
||||
val result = dbDaoBase.getNullable(_sqlGet(id))
|
||||
?: throw IllegalArgumentException("DB [${name}] has no entry with id ${id}");
|
||||
return deserializeIndex(result);
|
||||
}
|
||||
fun getOrNull(id: Long): I? {
|
||||
val result = dbDaoBase.getNullable(_sqlGet(id));
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.stores.v2
|
||||
|
||||
import com.futo.platformplayer.assume
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -105,7 +106,7 @@ class ManagedStore<T>{
|
||||
_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;
|
||||
val exs = ArrayList<Throwable>();
|
||||
|
||||
@@ -120,7 +121,7 @@ class ManagedStore<T>{
|
||||
for (i in 0 .. 1) {
|
||||
try {
|
||||
Logger.i(TAG, "Importing ${logName(recon)}");
|
||||
val reconId = createFromReconstruction(recon, builder);
|
||||
val reconId = createFromReconstruction(recon, builder, cache);
|
||||
successes++;
|
||||
Logger.i(TAG, "Imported ${logName(reconId)}");
|
||||
break;
|
||||
@@ -272,12 +273,12 @@ class ManagedStore<T>{
|
||||
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)
|
||||
throw IllegalStateException("Can't reconstruct as no reconstruction is implemented for this type");
|
||||
|
||||
val id = UUID.randomUUID().toString();
|
||||
val reconstruct = _reconstructStore!!.toObjectWithHeader(id, reconstruction, builder);
|
||||
val reconstruct = _reconstructStore!!.toObjectWithHeader(id, reconstruction, builder, cache);
|
||||
save(reconstruct);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.futo.platformplayer.stores.v2
|
||||
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
|
||||
abstract class ReconstructStore<T> {
|
||||
open val backupOnSave: Boolean = false;
|
||||
open val backupOnCreate: Boolean = true;
|
||||
@@ -11,18 +13,18 @@ abstract class ReconstructStore<T> {
|
||||
}
|
||||
|
||||
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 {
|
||||
val identifier = identifierName ?: fallbackName;
|
||||
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"))
|
||||
return toObject(id, backup.substring(backup.indexOf("\n") + 1), builder);
|
||||
return toObject(id, backup.substring(backup.indexOf("\n") + 1), builder, importCache);
|
||||
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.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.HorizontalSpaceItemDecoration
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
@@ -61,6 +63,7 @@ class MonetizationView : LinearLayout {
|
||||
|
||||
val onSupportTap = Event0();
|
||||
val onStoreTap = Event0();
|
||||
val onUrlTap = Event1<String>();
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.view_monetization, this);
|
||||
@@ -70,10 +73,12 @@ class MonetizationView : LinearLayout {
|
||||
_membershipPlatform = findViewById(R.id.membership_platform);
|
||||
_buttonMembership.setOnClickListener {
|
||||
_membershipUrl?.let {
|
||||
/*
|
||||
val uri = Uri.parse(it);
|
||||
val intent = Intent(Intent.ACTION_VIEW);
|
||||
intent.data = uri;
|
||||
context.startActivity(intent);
|
||||
context.startActivity(intent);*/
|
||||
onUrlTap.emit(it);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,9 +134,18 @@ class MonetizationView : LinearLayout {
|
||||
_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;
|
||||
} else {
|
||||
_root.visibility = View.GONE;
|
||||
_buttonSupport.isVisible = false;
|
||||
}
|
||||
|
||||
setMerchandise(null);
|
||||
|
||||
@@ -10,6 +10,8 @@ import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.size
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
@@ -33,6 +35,13 @@ class SupportView : LinearLayout {
|
||||
private var _textNoSupportOptionsSet: TextView
|
||||
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) {
|
||||
inflate(context, R.layout.view_support, this);
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ package com.futo.platformplayer.views.behavior
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.media.AudioManager
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.GestureDetector
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
@@ -24,6 +24,7 @@ import androidx.core.animation.doOnStart
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
@@ -67,6 +68,7 @@ class GestureControlView : LinearLayout {
|
||||
private var _animatorSound: ObjectAnimator? = null;
|
||||
private var _brightnessFactor = 1.0f;
|
||||
private var _originalBrightnessFactor = 1.0f;
|
||||
private var _originalBrightnessMode: Int = 0;
|
||||
private var _adjustingBrightness: Boolean = false;
|
||||
private val _layoutControlsBrightness: FrameLayout;
|
||||
private val _progressBrightness: CircularProgressBar;
|
||||
@@ -168,8 +170,6 @@ class GestureControlView : LinearLayout {
|
||||
if(p0 == null)
|
||||
return false;
|
||||
|
||||
Logger.i(TAG, "p0.pointerCount: " + p0.pointerCount)
|
||||
|
||||
if (!_isPanning && p1.pointerCount == 1) {
|
||||
val minDistance = Math.min(width, height)
|
||||
if (_isFullScreen && _adjustingBrightness) {
|
||||
@@ -739,16 +739,24 @@ class GestureControlView : LinearLayout {
|
||||
resetZoomPan()
|
||||
|
||||
if (isFullScreen) {
|
||||
val c = context
|
||||
if (c is Activity && Settings.instance.gestureControls.useSystemBrightness) {
|
||||
_brightnessFactor = c.window.attributes.screenBrightness
|
||||
if (_brightnessFactor == -1.0f) {
|
||||
_brightnessFactor = android.provider.Settings.System.getInt(
|
||||
context.contentResolver,
|
||||
android.provider.Settings.System.SCREEN_BRIGHTNESS
|
||||
) / 255.0f;
|
||||
if (Settings.instance.gestureControls.useSystemBrightness) {
|
||||
try {
|
||||
_originalBrightnessMode = android.provider.Settings.System.getInt(context.contentResolver, android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE)
|
||||
|
||||
val brightness = android.provider.Settings.System.getInt(context.contentResolver, android.provider.Settings.System.SCREEN_BRIGHTNESS)
|
||||
_brightnessFactor = brightness / 255.0f;
|
||||
Log.i(TAG, "Starting brightness brightness: $brightness, _brightnessFactor: $_brightnessFactor, _originalBrightnessMode: $_originalBrightnessMode")
|
||||
|
||||
_originalBrightnessFactor = _brightnessFactor
|
||||
} catch (e: Throwable) {
|
||||
Settings.instance.gestureControls.useSystemBrightness = false
|
||||
Settings.instance.save()
|
||||
UIDialogs.toast(context, "useSystemBrightness disabled due to an error")
|
||||
}
|
||||
_originalBrightnessFactor = _brightnessFactor
|
||||
}
|
||||
|
||||
if (!Settings.instance.gestureControls.useSystemBrightness) {
|
||||
_brightnessFactor = 1.0f;
|
||||
}
|
||||
|
||||
if (Settings.instance.gestureControls.useSystemVolume) {
|
||||
@@ -761,10 +769,19 @@ class GestureControlView : LinearLayout {
|
||||
onBrightnessAdjusted.emit(_brightnessFactor);
|
||||
onSoundAdjusted.emit(_soundFactor);
|
||||
} else {
|
||||
val c = context
|
||||
if (c is Activity && Settings.instance.gestureControls.useSystemBrightness) {
|
||||
if (Settings.instance.gestureControls.useSystemBrightness) {
|
||||
if (Settings.instance.gestureControls.restoreSystemBrightness) {
|
||||
onBrightnessAdjusted.emit(_originalBrightnessFactor);
|
||||
onBrightnessAdjusted.emit(_originalBrightnessFactor)
|
||||
|
||||
if (android.provider.Settings.System.canWrite(context)) {
|
||||
Log.i(TAG, "Restoring system brightness mode _originalBrightnessMode: $_originalBrightnessMode")
|
||||
|
||||
android.provider.Settings.System.putInt(
|
||||
context.contentResolver,
|
||||
android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE,
|
||||
_originalBrightnessMode
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onBrightnessAdjusted.emit(1.0f);
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.timestampRegex
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||
constructor(context: Context) : super(context) {}
|
||||
@@ -40,32 +41,34 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||
if (text is Spannable) {
|
||||
val links = text.getSpans(offset, offset, URLSpan::class.java)
|
||||
if (links.isNotEmpty()) {
|
||||
for (link in links) {
|
||||
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
|
||||
runBlocking {
|
||||
for (link in links) {
|
||||
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
|
||||
|
||||
val c = context;
|
||||
if (c is MainActivity) {
|
||||
if (c.handleUrl(link.url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timestampRegex.matches(link.url)) {
|
||||
val tokens = link.url.split(':');
|
||||
|
||||
var time_s = -1L;
|
||||
if (tokens.size == 2) {
|
||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
||||
} else if (tokens.size == 3) {
|
||||
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
||||
}
|
||||
|
||||
if (time_s != -1L) {
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
||||
val c = context;
|
||||
if (c is MainActivity) {
|
||||
if (c.handleUrl(link.url)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||
if (timestampRegex.matches(link.url)) {
|
||||
val tokens = link.url.split(':');
|
||||
|
||||
var time_s = -1L;
|
||||
if (tokens.size == 2) {
|
||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
||||
} else if (tokens.size == 3) {
|
||||
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
||||
}
|
||||
|
||||
if (time_s != -1L) {
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,9 +51,11 @@ class RepliesOverlay : LinearLayout {
|
||||
private var _onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null;
|
||||
private val _loaderOverlay: LoaderOverlay
|
||||
private val _client = ManagedHttpClient()
|
||||
private val _layoutItems: LinearLayout
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.overlay_replies, this)
|
||||
_layoutItems = findViewById(R.id.layout_items)
|
||||
_topbar = findViewById(R.id.topbar);
|
||||
_commentsList = findViewById(R.id.comments_list);
|
||||
_addCommentView = findViewById(R.id.add_comment_view);
|
||||
@@ -65,6 +67,9 @@ class RepliesOverlay : LinearLayout {
|
||||
_loaderOverlay = findViewById(R.id.loader_overlay)
|
||||
setLoading(false);
|
||||
|
||||
_layoutItems.removeView(_layoutParentComment)
|
||||
_commentsList.setPrependedView(_layoutParentComment)
|
||||
|
||||
_addCommentView.onCommentAdded.subscribe {
|
||||
_commentsList.addComment(it);
|
||||
_onCommentAdded?.invoke(it);
|
||||
|
||||
@@ -14,6 +14,10 @@ class SupportOverlay : LinearLayout {
|
||||
private val _topbar: OverlayTopbar;
|
||||
private val _support: SupportView;
|
||||
|
||||
val hasSupportItems: Boolean get() {
|
||||
return _support.hasSupportItems;
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.overlay_support, this)
|
||||
_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) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
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 val _prependedView: FrameLayout;
|
||||
private var _readonly: Boolean = false;
|
||||
private val _layoutScrollToTop: FrameLayout;
|
||||
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onCommentsLoaded = Event1<Int>();
|
||||
@@ -90,6 +94,13 @@ class CommentsList : ConstraintLayout {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_comments_list, this, true);
|
||||
|
||||
_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 {
|
||||
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||
setMargins(0, 30, 0, 0)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package com.futo.platformplayer.views.video
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
@@ -122,6 +122,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
private var _currentChapterLoopActive = false;
|
||||
private var _currentChapterLoopId: Int = 0;
|
||||
private var _currentChapter: IChapter? = null;
|
||||
private var _promptedForPermissions: Boolean = false;
|
||||
|
||||
|
||||
//Events
|
||||
@@ -249,11 +250,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
};
|
||||
gestureControl.onToggleFullscreen.subscribe { setFullScreen(!isFullScreen) };
|
||||
gestureControl.onBrightnessAdjusted.subscribe {
|
||||
if (context is Activity && Settings.instance.gestureControls.useSystemBrightness) {
|
||||
val window = context.window
|
||||
val layout: WindowManager.LayoutParams = window.attributes
|
||||
layout.screenBrightness = it
|
||||
window.attributes = layout
|
||||
if (Settings.instance.gestureControls.useSystemBrightness) {
|
||||
setSystemBrightness(it)
|
||||
} else {
|
||||
if (it == 1.0f) {
|
||||
_overlay_brightness.visibility = View.GONE;
|
||||
@@ -433,6 +431,30 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSystemBrightness(brightness: Float) {
|
||||
Log.i(TAG, "setSystemBrightness $brightness")
|
||||
if (android.provider.Settings.System.canWrite(context)) {
|
||||
Log.i(TAG, "setSystemBrightness canWrite $brightness")
|
||||
android.provider.Settings.System.putInt(context.contentResolver, android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE, android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
|
||||
android.provider.Settings.System.putInt(context.contentResolver, android.provider.Settings.System.SCREEN_BRIGHTNESS, (brightness * 255.0f).toInt().coerceAtLeast(1).coerceAtMost(255));
|
||||
} else if (!_promptedForPermissions) {
|
||||
Log.i(TAG, "setSystemBrightness prompt $brightness")
|
||||
_promptedForPermissions = true
|
||||
UIDialogs.showConfirmationDialog(context, "System brightness controls require explicit permission", action = {
|
||||
openAndroidPermissionsMenu()
|
||||
})
|
||||
} else {
|
||||
Log.i(TAG, "setSystemBrightness no permission?")
|
||||
//No permissions but already prompted, ignore
|
||||
}
|
||||
}
|
||||
|
||||
private fun openAndroidPermissionsMenu() {
|
||||
val intent = Intent(android.provider.Settings.ACTION_MANAGE_WRITE_SETTINGS)
|
||||
intent.setData(Uri.parse("package:" + context.packageName))
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun updateNextPrevious() {
|
||||
val vidPrev = StatePlayer.instance.getPrevQueueItem(true);
|
||||
val vidNext = StatePlayer.instance.getNextQueueItem(true);
|
||||
@@ -560,6 +582,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
_videoControls_fullscreen.show();
|
||||
videoControls.hideImmediately();
|
||||
videoControls.visibility = View.GONE;
|
||||
}
|
||||
else {
|
||||
val lp = background.layoutParams as ConstraintLayout.LayoutParams;
|
||||
@@ -572,6 +595,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
videoControls.show();
|
||||
_videoControls_fullscreen.hideImmediately();
|
||||
_videoControls_fullscreen.visibility = View.GONE;
|
||||
}
|
||||
|
||||
fitOrFill(fullScreen);
|
||||
|
||||
@@ -37,9 +37,22 @@
|
||||
android:fontFamily="@font/inter_extra_light" />
|
||||
</FrameLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="match_parent" />
|
||||
<LinearLayout
|
||||
android:id="@+id/button_check_for_updates"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/background_button_primary_round_4dp"
|
||||
android:layout_gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/check_for_updates"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
android:id="@+id/no_sources"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
android:visibility="gone"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -1,111 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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
|
||||
android:id="@+id/topbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
app:title="Replies"
|
||||
app:metadata="3 replies"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
<LinearLayout android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/layout_items">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/layout_parent_comment"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/topbar"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
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.overlays.OverlayTopbar
|
||||
android:id="@+id/topbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
app:title="Replies"
|
||||
app:metadata="3 replies" />
|
||||
|
||||
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
android:id="@+id/image_thumbnail"
|
||||
android:layout_width="25dp"
|
||||
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"
|
||||
<com.futo.platformplayer.views.comments.AddCommentView
|
||||
android:id="@+id/add_comment_view"
|
||||
android:layout_width="match_parent"
|
||||
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" />
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_metadata"
|
||||
android:layout_width="0dp"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/layout_parent_comment"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/gray_ac"
|
||||
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" />
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:padding="12dp"
|
||||
android:background="@drawable/background_16_round_4dp">
|
||||
|
||||
<com.futo.platformplayer.views.behavior.NonScrollingTextView
|
||||
android:id="@+id/text_body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:background="@color/transparent"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
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" />
|
||||
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
android:id="@+id/image_thumbnail"
|
||||
android:layout_width="25dp"
|
||||
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" />
|
||||
|
||||
</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
|
||||
android:id="@+id/add_comment_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_parent_comment"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
<TextView
|
||||
android:id="@+id/text_metadata"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/gray_ac"
|
||||
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
|
||||
android:id="@+id/comments_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_comment_view"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginTop="12dp" />
|
||||
<com.futo.platformplayer.views.behavior.NonScrollingTextView
|
||||
android:id="@+id/text_body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginStart="10dp"
|
||||
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
|
||||
android:id="@+id/loader_overlay"
|
||||
@@ -113,5 +110,4 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="true" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
||||
@@ -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>
|
||||
@@ -8,4 +8,24 @@
|
||||
android:layout_width="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>
|
||||
@@ -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="posts">Posts</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="overlay">Overlay</string>
|
||||
<string name="reload">Reload</string>
|
||||
@@ -749,6 +750,8 @@
|
||||
<string name="add_creator">Add Creators</string>
|
||||
<string name="select">Select</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="scroll_to_top">Scroll to top</string>
|
||||
<string-array name="home_screen_array">
|
||||
<item>Recommendations</item>
|
||||
<item>Subscriptions</item>
|
||||
|
||||
Submodule app/src/stable/assets/sources/rumble updated: 263ed8c7df...bedbc4a989
Submodule app/src/stable/assets/sources/youtube updated: c86c73db0c...bef199baa9
Submodule app/src/unstable/assets/sources/rumble updated: 263ed8c7df...bedbc4a989
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