mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 13:02:39 +02:00
Compare commits
22 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 |
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -343,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");
|
||||
|
||||
@@ -41,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
|
||||
@@ -603,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",
|
||||
{ });
|
||||
}
|
||||
@@ -693,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") {
|
||||
@@ -711,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")) {
|
||||
@@ -728,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
|
||||
@@ -745,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(
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
|
||||
+1
-1
@@ -246,7 +246,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
fun updateAllButtonVisibility() {
|
||||
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
||||
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
||||
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt() - 1;
|
||||
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
||||
if (_buttonsVisible >= defs.size) {
|
||||
updateBottomMenuButtons(defs.toMutableList(), false);
|
||||
} else if (_buttonsVisible > 0) {
|
||||
|
||||
+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
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.futo.platformplayer.polycentric
|
||||
|
||||
import com.futo.platformplayer.encryption.GEncryptionProviderV1
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringArrayStorage
|
||||
import com.futo.polycentric.core.ProcessSecret
|
||||
import com.futo.polycentric.core.base64ToByteArray
|
||||
import com.futo.polycentric.core.toBase64
|
||||
import userpackage.Protocol
|
||||
|
||||
class PolycentricStorage {
|
||||
private val _processSecrets = FragmentedStorage.get<StringArrayStorage>("processSecrets");
|
||||
|
||||
fun addProcessSecret(processSecret: ProcessSecret) {
|
||||
_processSecrets.addDistinct(GEncryptionProviderV1.instance.encrypt(processSecret.toProto().toByteArray()).toBase64())
|
||||
_processSecrets.saveBlocking()
|
||||
}
|
||||
|
||||
fun getProcessSecrets(): List<ProcessSecret> {
|
||||
val processSecrets = arrayListOf<ProcessSecret>()
|
||||
for (p in _processSecrets.getAllValues()) {
|
||||
try {
|
||||
processSecrets.add(ProcessSecret.fromProto(Protocol.StorageTypeProcessSecret.parseFrom(GEncryptionProviderV1.instance.decrypt(p.base64ToByteArray()))))
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to decrypt process secret", e);
|
||||
}
|
||||
}
|
||||
return processSecrets
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = "PolycentricStorage";
|
||||
private var _instance : PolycentricStorage? = null;
|
||||
val instance : PolycentricStorage
|
||||
get(){
|
||||
if(_instance == null)
|
||||
_instance = PolycentricStorage();
|
||||
return _instance!!;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ class DownloadService : Service() {
|
||||
private val DOWNLOAD_NOTIF_ID = 3;
|
||||
private val DOWNLOAD_NOTIF_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);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
@@ -20,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();
|
||||
|
||||
@@ -50,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 {
|
||||
@@ -106,7 +110,7 @@ class StateHistory {
|
||||
if(result == null)
|
||||
UIDialogs.toast("History creation failed?\nNo history tracking..");
|
||||
}
|
||||
return null;
|
||||
return result;
|
||||
}
|
||||
|
||||
fun removeHistory(url: String) {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -748,7 +748,6 @@ class GestureControlView : LinearLayout {
|
||||
Log.i(TAG, "Starting brightness brightness: $brightness, _brightnessFactor: $_brightnessFactor, _originalBrightnessMode: $_originalBrightnessMode")
|
||||
|
||||
_originalBrightnessFactor = _brightnessFactor
|
||||
android.provider.Settings.System.putInt(context.contentResolver, android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE, android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
|
||||
} catch (e: Throwable) {
|
||||
Settings.instance.gestureControls.useSystemBrightness = false
|
||||
Settings.instance.save()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -582,6 +582,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
_videoControls_fullscreen.show();
|
||||
videoControls.hideImmediately();
|
||||
videoControls.visibility = View.GONE;
|
||||
}
|
||||
else {
|
||||
val lp = background.layoutParams as ConstraintLayout.LayoutParams;
|
||||
@@ -594,6 +595,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
videoControls.show();
|
||||
_videoControls_fullscreen.hideImmediately();
|
||||
_videoControls_fullscreen.visibility = View.GONE;
|
||||
}
|
||||
|
||||
fitOrFill(fullScreen);
|
||||
|
||||
@@ -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>
|
||||
@@ -7,5 +7,25 @@
|
||||
android:id="@+id/recycler_comments"
|
||||
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>
|
||||
@@ -750,6 +751,7 @@
|
||||
<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/youtube updated: c86c73db0c...bef199baa9
Submodule app/src/unstable/assets/sources/youtube updated: c86c73db0c...bef199baa9
+1
-1
Submodule dep/polycentricandroid updated: 7695198eea...00927bb700
Reference in New Issue
Block a user