This commit is contained in:
Kelvin 2023-12-13 23:28:34 +01:00
parent 27eb5aa6e1
commit 02292fed04
21 changed files with 587 additions and 60 deletions

View file

@ -3,8 +3,10 @@ package com.futo.platformplayer
import android.content.ContentResolver
import android.view.View
import android.view.ViewGroup
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
@ -17,10 +19,13 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads
@ -79,6 +84,7 @@ class UISlideOverlays {
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
}, false),
SlideUpMenuGroup(container.context, "Fetch Settings",
"Depending on the platform you might not need to enable a type for it to be available.",
-1, listOf()),
@ -97,7 +103,15 @@ class UISlideOverlays {
}, false) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
}, false) else null).filterNotNull());
}, false) else null,
SlideUpMenuGroup(container.context, "Actions",
"Various things you can do with this subscription",
-1, listOf()),
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
showCreateSubscriptionGroup(container, subscription.channel);
}, false)
).filterNotNull());
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
@ -135,6 +149,10 @@ class UISlideOverlays {
}
}
fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
}
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>(LoaderView(container.context))
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
@ -513,6 +531,48 @@ class UISlideOverlays {
return overlay;
}
fun showCreateSubscriptionGroup(container: ViewGroup, initialChannel: IPlatformChannel?, onCreate: ((String) -> Unit)? = null): SlideUpMenuOverlay {
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
val addSubGroupOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_subgroup), container.context.getString(R.string.ok), false, nameInput);
addSubGroupOverlay.onOK.subscribe {
val text = nameInput.text;
if (text.isBlank()) {
return@subscribe;
}
addSubGroupOverlay.hide();
nameInput.deactivate();
nameInput.clear();
if(onCreate == null)
{
//TODO: Do this better, temp
StateApp.instance.contextOrNull?.let {
if(it is MainActivity) {
val subGroup = SubscriptionGroup(text);
if(initialChannel != null) {
subGroup.urls.add(initialChannel.url);
if(initialChannel.thumbnail != null)
subGroup.image = ImageVariable(initialChannel.thumbnail);
}
it.navigate(it.getFragment<SubscriptionGroupFragment>(), subGroup);
}
}
}
else
onCreate(text)
};
addSubGroupOverlay.onCancel.subscribe {
nameInput.deactivate();
nameInput.clear();
};
addSubGroupOverlay.show();
nameInput.activate();
return addSubGroupOverlay
}
fun showCreatePlaylistOverlay(container: ViewGroup, onCreate: (String) -> Unit): SlideUpMenuOverlay {
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);

View file

@ -36,6 +36,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.SubscriptionGroup
import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.*
import com.futo.platformplayer.stores.FragmentedStorage
@ -100,6 +101,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
lateinit var _fragImportPlaylists: ImportPlaylistsFragment;
lateinit var _fragBuy: BuyFragment;
lateinit var _fragSubGroup: SubscriptionGroupFragment;
lateinit var _fragBrowser: BrowserFragment;
@ -235,6 +237,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
_fragImportPlaylists = ImportPlaylistsFragment.newInstance();
_fragBuy = BuyFragment.newInstance();
_fragSubGroup = SubscriptionGroupFragment.newInstance();
_fragBrowser = BrowserFragment.newInstance();
@ -316,6 +319,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragDownloads.topBar = _fragTopBarGeneral;
_fragImportSubscriptions.topBar = _fragTopBarImport;
_fragImportPlaylists.topBar = _fragTopBarImport;
_fragSubGroup.topBar = _fragTopBarNavigation;
_fragBrowser.topBar = _fragTopBarNavigation;
@ -982,6 +986,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
ImportPlaylistsFragment::class -> _fragImportPlaylists as T;
BrowserFragment::class -> _fragBrowser as T;
BuyFragment::class -> _fragBuy as T;
SubscriptionGroupFragment::class -> _fragSubGroup as T;
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
}
}

View file

@ -2,29 +2,40 @@ package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.getSystemService
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.dp
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
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.SearchView
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
import com.futo.platformplayer.views.overlays.ImageVariableOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.ShapeAppearanceModel
class SubscriptionGroupFragment : MainFragment() {
override val isMainView : Boolean = true;
@ -60,7 +71,9 @@ class SubscriptionGroupFragment : MainFragment() {
private val _textGroupTitleContainer: LinearLayout;
private val _textGroupTitle: TextView;
private val _imageGroup: ShapeableImageView;
private val _imageGroupBackground: ImageView;
private val _buttonEditImage: LinearLayout;
private val _searchBar: SearchView;
private val _textGroupMeta: TextView;
@ -68,6 +81,8 @@ class SubscriptionGroupFragment : MainFragment() {
private val _enabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
private val _disabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
private val _enabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
private val _disabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
private val _containerEnabled: LinearLayout;
private val _containerDisabled: LinearLayout;
@ -79,39 +94,53 @@ class SubscriptionGroupFragment : MainFragment() {
private var _group: SubscriptionGroup? = null;
private val _editNameOverlayField: SlideUpMenuTextInput;
constructor(context: Context, fragment: SubscriptionGroupFragment): super(context) {
inflate(context, R.layout.fragment_subscriptions_group, this);
_fragment = fragment;
_editNameOverlayField = SlideUpMenuTextInput(context, "Group name");
_overlay = findViewById(R.id.overlay);
_searchBar = findViewById(R.id.search_bar);
_textGroupTitleContainer = findViewById(R.id.text_group_title_container);
_textGroupTitle = findViewById(R.id.text_group_title);
_imageGroup = findViewById(R.id.image_group);
_imageGroupBackground = findViewById(R.id.group_image_background);
_buttonEditImage = findViewById(R.id.button_edit_image);
_textGroupMeta = findViewById(R.id.text_group_meta);
_buttonSettings = findViewById(R.id.button_settings);
_imageGroup.setBackgroundColor(Color.GRAY);
val dp6 = 6.dp(resources);
_imageGroup.shapeAppearanceModel = ShapeAppearanceModel.builder()
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
.build()
_containerEnabled = findViewById(R.id.container_enabled);
_containerDisabled = findViewById(R.id.container_disabled);
_recyclerCreatorsEnabled = findViewById<RecyclerView>(R.id.recycler_creators_enabled).asAny(_enabledCreators) {
_recyclerCreatorsEnabled = findViewById<RecyclerView>(R.id.recycler_creators_enabled).asAny(_enabledCreatorsFiltered) {
it.itemView.setPadding(0, dp6, 0, dp6);
it.onClick.subscribe { channel ->
disableCreator(channel);
};
}
_recyclerCreatorsDisabled = findViewById<RecyclerView>(R.id.recycler_creators_disabled).asAny(_disabledCreators) {
_recyclerCreatorsDisabled = findViewById<RecyclerView>(R.id.recycler_creators_disabled).asAny(_disabledCreatorsFiltered) {
it.itemView.setPadding(0, dp6, 0, dp6);
it.onClick.subscribe { channel ->
enableCreator(channel);
};
}
_recyclerCreatorsEnabled.view.layoutManager = GridLayoutManager(context, 5).apply {
this.orientation = LinearLayoutManager.VERTICAL;
};
_recyclerCreatorsDisabled.view.layoutManager = GridLayoutManager(context, 5).apply {
this.orientation = LinearLayoutManager.VERTICAL;
};
_textGroupTitleContainer.setOnClickListener {
_group?.let { editName(it) };
};
_textGroupMeta.setOnClickListener {
_group?.let { editName(it) };
};
_imageGroup.setOnClickListener {
_group?.let { editImage(it) };
};
@ -119,29 +148,52 @@ class SubscriptionGroupFragment : MainFragment() {
_group?.let { editImage(it) }
};
_searchBar.onSearchChanged.subscribe {
filterCreators();
}
setGroup(null);
}
fun save() {
_group?.let {
StateSubscriptionGroups.instance.updateSubscriptionGroup(it);
};
}
fun editName(group: SubscriptionGroup) {
val editView = SlideUpMenuTextInput(context, "Group name");
editView.text = group.name;
UISlideOverlays.showOverlay(_overlay, "Edit name", "Save", {
val text = _editNameOverlayField.text;
editView.deactivate();
val text = editView.text;
if(!text.isNullOrEmpty()) {
group.name = text;
_textGroupTitle.text = text;
//TODO: Save
save();
}
}, _editNameOverlayField);
}, editView).onCancel.subscribe {
editView.deactivate();
}
editView.activate();
}
fun editImage(group: SubscriptionGroup) {
val overlay = ImageVariableOverlay(context);
val view = UISlideOverlays.showOverlay(_overlay, "Temp", null, {},
overlay);
val overlay = ImageVariableOverlay(context, _enabledCreators.map { it.url });
_overlay.removeAllViews();
_overlay.addView(overlay);
_overlay.alpha = 0f
_overlay.visibility = View.VISIBLE;
_overlay.animate().alpha(1f).setDuration(300).start();
overlay.onSelected.subscribe {
view.hide(true);
group.image = it;
it.setImageView(_imageGroup);
//TODO: Save
it.setImageView(_imageGroupBackground);
save();
};
overlay.onClose.subscribe {
_overlay.visibility = View.GONE;
overlay.removeAllViews();
}
}
@ -150,10 +202,14 @@ class SubscriptionGroupFragment : MainFragment() {
_textGroupTitle.text = group?.name;
val image = group?.image;
if(image != null)
if(image != null) {
image.setImageView(_imageGroupBackground);
image.setImageView(_imageGroup);
else
}
else {
_imageGroupBackground.setImageResource(0);
_imageGroup.setImageResource(0);
}
updateMeta();
reloadCreators(group);
}
@ -169,43 +225,63 @@ class SubscriptionGroupFragment : MainFragment() {
_enabledCreators.addAll(subs.filter { urls.contains(it.url) });
_disabledCreators.addAll(subs.filter { !urls.contains(it.url) });
}
filterCreators();
}
private fun filterCreators() {
val query = _searchBar.textSearch.text.toString().lowercase();
val filteredEnabled = _enabledCreators.filter { it.name.lowercase().contains(query) };
val filteredDisabled = _disabledCreators.filter { it.name.lowercase().contains(query) };
//Optimize
_enabledCreatorsFiltered.clear();
_enabledCreatorsFiltered.addAll(filteredEnabled);
_disabledCreatorsFiltered.clear();
_disabledCreatorsFiltered.addAll(filteredDisabled);
_recyclerCreatorsEnabled.notifyContentChanged();
_recyclerCreatorsDisabled.notifyContentChanged();
}
private fun enableCreator(channel: IPlatformChannel) {
val index = _disabledCreators.indexOf(channel);
val index = _disabledCreatorsFiltered.indexOf(channel);
if (index >= 0) {
_disabledCreators.removeAt(index)
_disabledCreators.remove(channel)
_disabledCreatorsFiltered.remove(channel);
_recyclerCreatorsDisabled.adapter.notifyItemRangeRemoved(index);
_enabledCreators.add(channel);
_recyclerCreatorsEnabled.adapter.notifyItemInserted(_enabledCreators.size - 1);
_enabledCreatorsFiltered.add(channel);
_recyclerCreatorsEnabled.adapter.notifyItemInserted(_enabledCreatorsFiltered.size - 1);
_group?.let {
it.urls.remove(channel.url);
//TODO: Save
save();
}
updateMeta();
}
}
private fun disableCreator(channel: IPlatformChannel) {
val index = _disabledCreators.indexOf(channel);
val index = _enabledCreatorsFiltered.indexOf(channel);
if (index >= 0) {
_disabledCreators.removeAt(index)
_recyclerCreatorsDisabled.adapter.notifyItemRangeRemoved(index);
_enabledCreators.remove(channel)
_enabledCreatorsFiltered.removeAt(index);
_recyclerCreatorsEnabled.adapter.notifyItemRangeRemoved(index);
_enabledCreators.add(channel);
_recyclerCreatorsEnabled.adapter.notifyItemInserted(_enabledCreators.size - 1);
_disabledCreators.add(channel);
_disabledCreatorsFiltered.add(channel);
_recyclerCreatorsDisabled.adapter.notifyItemInserted(_disabledCreatorsFiltered.size - 1);
_group?.let {
it.urls.remove(channel.url);
//TODO: Save
save();
}
updateMeta();
}
}
private fun updateMeta() {
_textGroupMeta.text = "${_group?.urls?.size} creators";
_textGroupMeta.text = "${_enabledCreators.size} creators";
}
}
}

View file

@ -254,6 +254,12 @@ class SubscriptionsFeedFragment : MainFragment() {
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
};
_subscriptionBar?.onClickChannel?.subscribe { c -> fragment.navigate<ChannelFragment>(c); };
_subscriptionBar?.onClickGroup?.subscribe { g ->
};
_subscriptionBar?.onHoldGroup?.subscribe { g ->
fragment.navigate<SubscriptionGroupFragment>(g);
};
synchronized(_filterLock) {
_subscriptionBar?.setToggles(

View file

@ -0,0 +1,88 @@
package com.futo.platformplayer.states
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.*
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.findNonRuntimeException
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.getNowDiffDays
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.SubscriptionStorage
import com.futo.platformplayer.stores.v2.ReconstructStore
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
import kotlinx.coroutines.*
import java.time.OffsetDateTime
import java.util.concurrent.ExecutionException
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask
import kotlin.collections.ArrayList
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.streams.asSequence
import kotlin.streams.toList
import kotlin.system.measureTimeMillis
/***
* Used to maintain subscription groups
*/
class StateSubscriptionGroups {
private val _subGroups = FragmentedStorage.storeJson<SubscriptionGroup>("subscription_groups")
.withUnique { it.id }
.load();
fun getSubscriptionGroup(id: String): SubscriptionGroup? {
return _subGroups.findItem { it.id == id };
}
fun getSubscriptionGroups(): List<SubscriptionGroup> {
return _subGroups.getItems();
}
fun updateSubscriptionGroup(subGroup: SubscriptionGroup) {
_subGroups.save(subGroup);
}
fun deleteSubscriptionGroup(id: String){
val group = getSubscriptionGroup(id);
if(group != null)
_subGroups.delete(group);
}
companion object {
const val TAG = "StateSubscriptionGroups";
const val VERSION = 1;
private var _instance : StateSubscriptionGroups? = null;
val instance : StateSubscriptionGroups
get(){
if(_instance == null)
_instance = StateSubscriptionGroups();
return _instance!!;
};
fun finish() {
_instance?.let {
_instance = null;
}
}
}
}

View file

@ -2,6 +2,9 @@ package com.futo.platformplayer.views.adapters
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Filter
import android.widget.Filterable
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import java.lang.reflect.Constructor
@ -116,7 +119,6 @@ class AnyAdapter<I, T : AnyAdapter.AnyViewHolder<I>> : BaseAnyAdapter<I, T, T> {
private class Adapter<I, T : AnyViewHolder<I>> : RecyclerView.Adapter<T> {
private val _parent: AnyAdapter<I, T>;
constructor(parentAdapter: AnyAdapter<I, T>) {
_parent = parentAdapter;
}

View file

@ -1,5 +1,6 @@
package com.futo.platformplayer.views.adapters.viewholders
import android.graphics.Color
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
@ -86,4 +87,78 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi
companion object {
private const val TAG = "CreatorBarViewHolder";
}
}
class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<SelectableCreatorBarViewHolder.Selectable>(
LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_bar_icon, _viewGroup, false)) {
private val _creatorThumbnail: CreatorThumbnail;
private val _name: TextView;
private var _channel: Selectable? = null;
val onClick = Event1<Selectable>();
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
StateApp.instance.scopeGetter,
{ PolycentricCache.instance.getProfileAsync(it) })
.success { onProfileLoaded(it, true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load profile.", it);
};
init {
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
_name = _view.findViewById(R.id.text_channel_name);
_view.findViewById<LinearLayout>(R.id.root).setOnClickListener {
val s = _channel ?: return@setOnClickListener;
onClick.emit(s);
}
}
override fun bind(value: Selectable) {
_taskLoadProfile.cancel();
_channel = value;
if(value.active)
_view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimaryDark, null))
else
_view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null))
_creatorThumbnail.setThumbnail(value.channel.thumbnail, false);
_name.text = value.channel.name;
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.channel.url, true);
if (cachedProfile != null) {
onProfileLoaded(cachedProfile, false);
if (cachedProfile.expired) {
_taskLoadProfile.run(value.channel.id);
}
} else {
_taskLoadProfile.run(value.channel.id);
}
}
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
val dp_55 = 55.dp(itemView.context.resources)
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
if (profile != null) {
_name.text = profile.systemState.username;
}
}
companion object {
private const val TAG = "CreatorBarViewHolder";
}
data class Selectable(var channel: IPlatformChannel, var active: Boolean)
}

View file

@ -0,0 +1,70 @@
package com.futo.platformplayer.views.adapters.viewholders
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.ShapeAppearanceModel
class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<SubscriptionGroup>(
LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_group_bar, _viewGroup, false)) {
private var _group: SubscriptionGroup? = null;
private val _image: ShapeableImageView;
private val _textSubGroup: TextView;
val onClick = Event1<SubscriptionGroup>();
val onClickLong = Event1<SubscriptionGroup>();
init {
_image = _view.findViewById(R.id.image);
_textSubGroup = _view.findViewById(R.id.text_sub_group);
val dp6 = 6.dp(_view.resources);
_image.shapeAppearanceModel = ShapeAppearanceModel.builder()
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
.build()
_viewGroup.setOnClickListener {
_group?.let {
onClick.emit(it);
}
}
_viewGroup.setOnLongClickListener {
_group?.let {
onClickLong.emit(it);
}
true;
}
}
override fun bind(value: SubscriptionGroup) {
_group = value;
val img = value.image;
if(img != null)
img.setImageView(_image)
else
_image.setImageResource(0);
_textSubGroup.text = value.name;
}
companion object {
private const val TAG = "SubscriptionGroupBarViewHolder";
}
}

View file

@ -6,62 +6,96 @@ import android.graphics.Color
import android.graphics.drawable.shapes.Shape
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.activity.result.contract.ActivityResultContracts
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.dp
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
import com.futo.platformplayer.views.adapters.viewholders.SelectableCreatorBarViewHolder
import com.futo.platformplayer.views.buttons.BigButton
import com.google.android.flexbox.FlexboxLayout
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.ShapeAppearanceModel
class ImageVariableOverlay: ConstraintLayout {
private val _buttonGallery: BigButton;
private val _buttonSelect: Button;
private val _recyclerPresets: AnyAdapterView<Int, PresetViewHolder>;
private val _recyclerCreators: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>;
private val _topbar: OverlayTopbar;
private val _recyclerPresets: AnyAdapterView<PresetImage, PresetViewHolder>;
private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>;
private val _creators: ArrayList<IPlatformChannel> = arrayListOf();
private val _presets: ArrayList<Int> = arrayListOf();
private val _creators: ArrayList<SelectableCreatorBarViewHolder.Selectable> = arrayListOf();
private val _presets: ArrayList<PresetImage> = arrayListOf(
PresetImage(R.drawable.xp_book, false),
PresetImage(R.drawable.xp_forest, false),
PresetImage(R.drawable.xp_laptop, false),
PresetImage(R.drawable.xp_controller, false),
PresetImage(R.drawable.xp_code, false),
);
private var _selected: ImageVariable? = null;
val onSelected = Event1<ImageVariable>();
val onClose = Event0();
constructor(context: Context): super(context) {
inflate(context, R.layout.overlay_image_variable, this);
}
constructor(context: Context, attrs: AttributeSet?): super(context, attrs) {
inflate(context, R.layout.overlay_image_variable, this);
constructor(context: Context, creatorFilters: List<String>? = null): super(context) {
val subs = StateSubscriptions.instance.getSubscriptions();
if(creatorFilters != null) {
_creators.addAll(subs
.filter { creatorFilters.contains(it.channel.url) }
.map { SelectableCreatorBarViewHolder.Selectable(it.channel, false) });
}
else
_creators.addAll(subs
.map { SelectableCreatorBarViewHolder.Selectable(it.channel, false) });
_recyclerCreators.notifyContentChanged();
}
constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { }
init {
inflate(context, R.layout.overlay_image_variable, this);
_topbar = findViewById(R.id.topbar);
_buttonGallery = findViewById(R.id.button_gallery);
_buttonSelect = findViewById(R.id.button_select);
_recyclerPresets = findViewById<RecyclerView>(R.id.recycler_presets).asAny(_presets, RecyclerView.HORIZONTAL) {
it.onClick.subscribe {
_selected = ImageVariable(null, it);
_selected = ImageVariable(null, it.id);
updateSelected();
};
};
val dp6 = 6.dp(resources);
_recyclerCreators = findViewById<RecyclerView>(R.id.recycler_creators).asAny(_creators, RecyclerView.HORIZONTAL) { creatorView ->
creatorView.itemView.setPadding(0, dp6, 0, dp6);
creatorView.onClick.subscribe {
if(it.thumbnail == null) {
if(it.channel.thumbnail == null) {
UIDialogs.toast(context, "No thumbnail found");
return@subscribe;
}
_selected = ImageVariable(it.thumbnail);
_selected = ImageVariable(it.channel.thumbnail);
updateSelected();
};
};
_recyclerCreators.view.layoutManager = GridLayoutManager(context, 5).apply {
this.orientation = LinearLayoutManager.VERTICAL;
};
_buttonGallery.setOnClickListener {
val context = StateApp.instance.contextOrNull;
@ -85,9 +119,20 @@ class ImageVariableOverlay: ConstraintLayout {
select(it);
}
};
_topbar.onClose.subscribe {
onClose.emit();
}
updateSelected();
}
fun updateSelected() {
val id = _selected?.resId;
val url = _selected?.url;
_presets.forEach { p -> p.active = p.id == id };
_recyclerPresets.notifyContentChanged();
_creators.forEach { p -> p.active = p.channel.thumbnail == url };
_recyclerCreators.notifyContentChanged();
if(_selected != null)
_buttonSelect.alpha = 1f;
else
@ -96,23 +141,46 @@ class ImageVariableOverlay: ConstraintLayout {
fun select(variable: ImageVariable) {
onSelected.emit(variable);
onClose.emit();
}
class PresetViewHolder(context: Context) : AnyAdapter.AnyViewHolder<Int>(ShapeableImageView(context)) {
private val view = _view as ShapeableImageView;
class PresetViewHolder(viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<PresetImage>(LinearLayout(viewGroup.context)) {
private val view = _view as LinearLayout;
private val imageView = ShapeableImageView(viewGroup.context);
private var value: Int = 0;
private var value: PresetImage = PresetImage(0, false);
val onClick = Event1<Int>();
val onClick = Event1<PresetImage>();
init {
view.setOnClickListener {
view.addView(imageView);
val dp2 = 2.dp(viewGroup.context.resources);
val dp6 = 6.dp(viewGroup.context.resources);
view.setPadding(dp2, dp2, dp2, dp2);
imageView.setOnClickListener {
onClick.emit(value);
}
imageView.layoutParams = LinearLayout.LayoutParams(110.dp(viewGroup.context.resources), 70.dp(viewGroup.context.resources)).apply {
//this.rightMargin = dp6
}
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
imageView.shapeAppearanceModel = ShapeAppearanceModel.builder()
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
.build()
}
override fun bind(value: Int) {
view.setImageResource(value);
override fun bind(value: PresetImage) {
imageView.setImageResource(value.id);
this.value = value;
setActive(value.active);
}
fun setActive(active: Boolean) {
if(active)
_view.setBackgroundColor(view.context.resources.getColor(R.color.colorPrimary, null));
else
_view.setBackgroundColor(view.context.resources.getColor(R.color.transparent, null));
}
}
data class PresetImage(var id: Int, var active: Boolean);
}

View file

@ -18,7 +18,8 @@ class SlideUpMenuTextInput : LinearLayout {
private lateinit var _editText: EditText;
private lateinit var _inputMethodManager: InputMethodManager;
val text: String get() = _editText.text.toString();
var text: String get() = _editText.text.toString()
set(v: String) = _editText.setText(v);
constructor(context: Context, attrs: AttributeSet? = null): super(context, attrs) {
init();

View file

@ -4,21 +4,28 @@ import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Recycler
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
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.others.ToggleTagView
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionBarViewHolder
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
class SubscriptionBar : LinearLayout {
private var _adapterView: AnyAdapterView<Subscription, SubscriptionBarViewHolder>? = null;
private var _subGroups: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>
private val _tagsContainer: LinearLayout;
val onClickChannel = Event1<SerializedChannel>();
val onClickGroup = Event1<SubscriptionGroup>();
val onHoldGroup = Event1<SubscriptionGroup>();
@ -31,6 +38,11 @@ class SubscriptionBar : LinearLayout {
onClickChannel.emit(c.channel);
};
};
val subgroups = StateSubscriptionGroups.instance.getSubscriptionGroups();
_subGroups = findViewById<RecyclerView>(R.id.recycler_subgroups).asAny(subgroups, orientation = RecyclerView.HORIZONTAL) {
it.onClick.subscribe(onClickGroup::emit);
it.onClickLong.subscribe(onHoldGroup::emit);
}
_tagsContainer = findViewById(R.id.container_tags);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

View file

@ -12,23 +12,32 @@
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
<FrameLayout
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#222222">
android:background="#AAAAAA">
<ImageView
android:id="@+id/group_image_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/xp_book"
android:scaleType="centerCrop" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#55000000">
android:background="#AA000000">
<ImageButton
android:id="@+id/button_settings"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:src="@drawable/ic_settings"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:padding="5dp"
android:padding="10dp"
android:background="@color/transparent"
android:visibility="invisible"
android:scaleType="fitCenter" />
<com.google.android.material.imageview.ShapeableImageView
@ -57,6 +66,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="5dp"
android:clickable="false"
android:scaleType="fitCenter"
android:background="@color/transparent"
android:src="@drawable/ic_edit"/>
@ -102,7 +112,7 @@
android:text="42 creators" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</FrameLayout>
<com.futo.platformplayer.views.SearchView
android:id="@+id/search_bar"
@ -116,8 +126,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp">
android:paddingStart="0dp"
android:paddingEnd="0dp">
<LinearLayout
android:id="@+id/container_enabled"
android:layout_width="match_parent"
@ -130,6 +140,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_light"
android:text="@string/enabled" />
@ -138,6 +150,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:textColor="@color/gray_ac"
android:fontFamily="@font/inter_extra_light"
android:text="@string/these_creators_in_group" />
@ -146,6 +160,9 @@
android:id="@+id/recycler_creators_enabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:paddingTop="10dp"
android:paddingBottom="10dp" />
</LinearLayout>
@ -161,6 +178,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_light"
android:text="@string/disabled" />
@ -169,6 +188,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:textColor="@color/gray_ac"
android:fontFamily="@font/inter_extra_light"
android:text="@string/these_creators_not_in_group" />
@ -177,6 +198,8 @@
android:id="@+id/recycler_creators_disabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp"
android:paddingTop="10dp"
android:paddingBottom="10dp" />
</LinearLayout>

View file

@ -88,6 +88,7 @@
android:text="Pick a creator as group image" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_creators"
android:layout_width="match_parent"
android:layout_height="wrap_content">

View file

@ -23,4 +23,9 @@
android:layout_height="wrap_content"
android:orientation="horizontal" />
</ScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_subgroups"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" />
</LinearLayout>

View file

@ -0,0 +1,34 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="75dp"
android:layout_height="50dp"
android:orientation="vertical"
android:gravity="center_horizontal"
android:layout_margin="4dp"
android:clickable="true"
android:id="@+id/root">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/xp_book" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#99000000"
android:gravity="center">
<TextView
android:id="@+id/text_sub_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:maxLines="2"
android:ellipsize="end"
android:textSize="12dp"
android:textAlignment="center"
android:text="News" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -2,7 +2,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="27dp"
android:layout_height="32dp"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:background="@drawable/background_pill"

View file

@ -564,6 +564,7 @@
<string name="playlist_copied_as_local_playlist">Playlist copied as local playlist</string>
<string name="are_you_sure_you_want_to_delete_the_downloaded_videos">Are you sure you want to delete the downloaded videos?</string>
<string name="create_new_playlist">Create new playlist</string>
<string name="create_new_subgroup">Create new subscription group</string>
<string name="expected_media_content_found">Expected media content, found</string>
<string name="failed_to_load_post">Failed to load post.</string>
<string name="replies">replies</string>