Merge branch 'wip-subgroups' into 'master'

Subscription Groups

See merge request videostreaming/grayjay!12
This commit is contained in:
Kelvin 2023-12-15 21:28:03 +00:00
commit 7d366110b1
38 changed files with 2023 additions and 20 deletions

View file

@ -0,0 +1,20 @@
package com.futo.platformplayer
class PresetImages {
companion object {
val images = mapOf<String, Int>(
Pair("xp_book", R.drawable.xp_book),
Pair("xp_forest", R.drawable.xp_forest),
Pair("xp_code", R.drawable.xp_code),
Pair("xp_controller", R.drawable.xp_controller),
Pair("xp_laptop", R.drawable.xp_laptop)
);
fun getPresetResIdByName(name: String): Int {
return images[name] ?: -1;
}
fun getPresetNameByResId(id: Int): String? {
return images.entries.find { it.value == id }?.key;
}
}
}

View file

@ -261,20 +261,23 @@ class Settings : FragmentedStorageFileJson() {
return FeedStyle.THUMBNAIL;
}
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
var showSubscriptionGroups: Boolean = true;
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true;
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
var progressBar: Boolean = true;
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 7)
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
@Serializable(with = FlexibleBooleanSerializer::class)
var fetchOnAppBoot: Boolean = true;
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 8)
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
var fetchOnTabOpen: Boolean = true;
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 9)
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 10)
@DropdownFieldOptionsId(R.array.background_interval)
var subscriptionsBackgroundUpdateInterval: Int = 0;
@ -290,7 +293,7 @@ class Settings : FragmentedStorageFileJson() {
};
@FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 10)
@FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 11)
@DropdownFieldOptionsId(R.array.thread_count)
var subscriptionConcurrency: Int = 3;
@ -298,17 +301,17 @@ class Settings : FragmentedStorageFileJson() {
return threadIndexToCount(subscriptionConcurrency);
}
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 11)
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
var showWatchMetrics: Boolean = false;
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 12)
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
var allowPlaytimeTracking: Boolean = true;
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 13)
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
var alwaysReloadFromCache: Boolean = false;
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 14)
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 15)
fun clearChannelCache() {
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
StateCache.instance.clear();

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
@ -46,7 +51,7 @@ class UISlideOverlays {
companion object {
private const val TAG = "UISlideOverlays";
fun showOverlay(container: ViewGroup, title: String, okButton: String?, onOk: ()->Unit, vararg views: View) {
fun showOverlay(container: ViewGroup, title: String, okButton: String?, onOk: ()->Unit, vararg views: View): SlideUpMenuOverlay {
var menu = SlideUpMenuOverlay(container.context, container, title, okButton, true, *views);
menu.onOK.subscribe {
@ -54,6 +59,7 @@ class UISlideOverlays {
onOk.invoke();
};
menu.show();
return menu;
}
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
@ -78,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()),
@ -96,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);
@ -134,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)
@ -512,6 +531,48 @@ class UISlideOverlays {
return overlay;
}
fun showCreateSubscriptionGroup(container: ViewGroup, initialChannel: IPlatformChannel? = null, 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,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
lateinit var _fragImportPlaylists: ImportPlaylistsFragment;
lateinit var _fragBuy: BuyFragment;
lateinit var _fragSubGroup: SubscriptionGroupFragment;
lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
lateinit var _fragBrowser: BrowserFragment;
@ -235,6 +238,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
_fragImportPlaylists = ImportPlaylistsFragment.newInstance();
_fragBuy = BuyFragment.newInstance();
_fragSubGroup = SubscriptionGroupFragment.newInstance();
_fragSubGroupList = SubscriptionGroupListFragment.newInstance();
_fragBrowser = BrowserFragment.newInstance();
@ -316,6 +321,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragDownloads.topBar = _fragTopBarGeneral;
_fragImportSubscriptions.topBar = _fragTopBarImport;
_fragImportPlaylists.topBar = _fragTopBarImport;
_fragSubGroup.topBar = _fragTopBarNavigation;
_fragSubGroupList.topBar = _fragTopBarAdd;
_fragBrowser.topBar = _fragTopBarNavigation;
@ -982,6 +989,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
ImportPlaylistsFragment::class -> _fragImportPlaylists as T;
BrowserFragment::class -> _fragBrowser as T;
BuyFragment::class -> _fragBuy as T;
SubscriptionGroupFragment::class -> _fragSubGroup as T;
SubscriptionGroupListFragment::class -> _fragSubGroupList as T;
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
}
}

View file

@ -55,10 +55,10 @@ class ManageTabsActivity : AppCompatActivity() {
Settings.instance.save()
}
val items = Settings.instance.tabs.mapNotNull {
val items = ArrayList(Settings.instance.tabs.mapNotNull {
val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.find { d -> it.id == d.id } ?: return@mapNotNull null
TabViewHolderData(buttonDefinition, it.enabled)
};
});
_listTabs = _recyclerTabs.asAny(items) {
it.onDragDrop.subscribe { vh ->

View file

@ -348,6 +348,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
ButtonDefinition(5, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate<HistoryFragment>() }),
ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>() }),
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>() }),
ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>() }),
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings, R.string.settings, canToggle = false, { false }, {
val c = it.context ?: return@ButtonDefinition;
Logger.i(TAG, "settings preventPictureInPicture()");

View file

@ -0,0 +1,302 @@
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.ImageButton
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;
override val isTab: Boolean = false;
override val hasBottomBar: Boolean get() = true;
private var _view: SubscriptionGroupView? = null;
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
if(parameter is SubscriptionGroup)
_view?.setGroup(StateSubscriptionGroups.instance.getSubscriptionGroup(parameter.id) ?: parameter);
else
_view?.setGroup(null);
}
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = SubscriptionGroupView(requireContext(), this);
_view = view;
return view;
}
companion object {
private const val TAG = "SourcesFragment";
fun newInstance() = SubscriptionGroupFragment().apply {}
}
private class SubscriptionGroupView: ConstraintLayout {
private val _fragment: SubscriptionGroupFragment;
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;
private val _buttonSettings: ImageButton;
private val _buttonDelete: ImageButton;
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;
private val _recyclerCreatorsEnabled: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>;
private val _recyclerCreatorsDisabled: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>;
private val _overlay: FrameLayout;
private var _group: SubscriptionGroup? = null;
constructor(context: Context, fragment: SubscriptionGroupFragment): super(context) {
inflate(context, R.layout.fragment_subscriptions_group, this);
_fragment = fragment;
_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);
_buttonDelete = findViewById(R.id.button_delete);
_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(_enabledCreatorsFiltered) {
it.itemView.setPadding(0, dp6, 0, dp6);
it.onClick.subscribe { channel ->
disableCreator(channel);
};
}
_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) };
};
_buttonEditImage.setOnClickListener {
_group?.let { editImage(it) }
};
_buttonSettings.setOnClickListener {
}
_buttonDelete.setOnClickListener {
_group?.let {
StateSubscriptionGroups.instance.deleteSubscriptionGroup(it.id);
};
fragment.close(true);
}
_buttonSettings.visibility = View.GONE;
_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", {
editView.deactivate();
val text = editView.text;
if(!text.isNullOrEmpty()) {
group.name = text;
_textGroupTitle.text = text;
save();
}
}, editView).onCancel.subscribe {
editView.deactivate();
}
editView.activate();
}
fun editImage(group: SubscriptionGroup) {
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 {
group.image = it;
it.setImageView(_imageGroup);
it.setImageView(_imageGroupBackground);
save();
};
overlay.onClose.subscribe {
_overlay.visibility = View.GONE;
overlay.removeAllViews();
}
}
fun setGroup(group: SubscriptionGroup?) {
_group = group;
_textGroupTitle.text = group?.name;
val image = group?.image;
if(image != null) {
image.setImageView(_imageGroupBackground);
image.setImageView(_imageGroup);
}
else {
_imageGroupBackground.setImageResource(0);
_imageGroup.setImageResource(0);
}
updateMeta();
reloadCreators(group);
}
@SuppressLint("NotifyDataSetChanged")
private fun reloadCreators(group: SubscriptionGroup?) {
_enabledCreators.clear();
_disabledCreators.clear();
if(group != null) {
val urls = group.urls.toList();
val subs = StateSubscriptions.instance.getSubscriptions().map { it.channel }
_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 = _disabledCreatorsFiltered.indexOf(channel);
if (index >= 0) {
_disabledCreators.remove(channel)
_disabledCreatorsFiltered.remove(channel);
_recyclerCreatorsDisabled.adapter.notifyItemRangeRemoved(index);
_enabledCreators.add(channel);
_enabledCreatorsFiltered.add(channel);
_recyclerCreatorsEnabled.adapter.notifyItemInserted(_enabledCreatorsFiltered.size - 1);
_group?.let {
if(!it.urls.contains(channel.url)) {
it.urls.add(channel.url);
save();
}
}
updateMeta();
}
}
private fun disableCreator(channel: IPlatformChannel) {
val index = _enabledCreatorsFiltered.indexOf(channel);
if (index >= 0) {
_enabledCreators.remove(channel)
_enabledCreatorsFiltered.removeAt(index);
_recyclerCreatorsEnabled.adapter.notifyItemRangeRemoved(index);
_disabledCreators.add(channel);
_disabledCreatorsFiltered.add(channel);
_recyclerCreatorsDisabled.adapter.notifyItemInserted(_disabledCreatorsFiltered.size - 1);
_group?.let {
it.urls.remove(channel.url);
save();
}
updateMeta();
}
}
private fun updateMeta() {
_textGroupMeta.text = "${_enabledCreators.size} creators";
}
}
}

View file

@ -0,0 +1,141 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.activities.AddSourceOptionsActivity
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.adapters.ItemMoveCallback
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupListViewHolder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Collections
class SubscriptionGroupListFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
private var _touchHelper: ItemTouchHelper? = null;
private var _subs: ArrayList<SubscriptionGroup> = arrayListOf();
private var _list: AnyAdapterView<SubscriptionGroup, SubscriptionGroupListViewHolder>? = null;
private var _overlay: FrameLayout? = null;
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_subscriptions_group_list, container, false);
_overlay = view.findViewById(R.id.overlay);
val recycler = view.findViewById<RecyclerView>(R.id.list);
val callback = ItemMoveCallback();
_touchHelper = ItemTouchHelper(callback);
_touchHelper?.attachToRecyclerView(recycler);
_subs.clear();
_subs.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups().sortedBy { it.priority });
_list = recycler.asAny(_subs, RecyclerView.VERTICAL){
it.onClick.subscribe {
navigate<SubscriptionGroupFragment>(it);
};
it.onSettings.subscribe {
};
it.onDelete.subscribe { group ->
val loc = _subs.indexOf(group);
_subs.remove(group);
_list?.adapter?.notifyItemRangeRemoved(loc);
StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id);
};
it.onDragDrop.subscribe {
_touchHelper?.startDrag(it);
};
};
callback.onRowMoved.subscribe(::groupMoved);
return view;
}
private fun groupMoved(fromPosition: Int, toPosition: Int) {
Logger.i("SubscriptionGroupListFragment", "Moved ${fromPosition} to ${toPosition}");
synchronized(_subs) {
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(_subs, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(_subs, i, i - 1)
}
}
}
_list?.adapter?.notifyItemMoved(fromPosition, toPosition);
synchronized(_subs) {
for(i in 0 until _subs.size) {
val sub = _subs[i];
if(sub.priority != i) {
sub.priority = i;
StateSubscriptionGroups.instance.updateSubscriptionGroup(sub, true);
}
}
}
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
updateGroups();
StateSubscriptionGroups.instance.onGroupsChanged.subscribe(this) {
updateGroups();
}
if(topBar is AddTopBarFragment)
(topBar as AddTopBarFragment).onAdd.subscribe {
_overlay?.let {
UISlideOverlays.showCreateSubscriptionGroup(it)
}
};
}
private fun updateGroups() {
lifecycleScope.launch(Dispatchers.Main) {
_subs.clear();
_subs.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups().sortedBy { it.priority });
_list?.adapter?.notifyContentChanged();
}
}
override fun onHide() {
super.onHide();
StateSubscriptionGroups.instance.onGroupsChanged.remove(this);
if(topBar is AddTopBarFragment)
(topBar as AddTopBarFragment).onAdd.remove(this);
}
override fun onBackPressed(): Boolean {
return false;
}
companion object {
fun newInstance() = SubscriptionGroupListFragment().apply {}
}
}

View file

@ -21,6 +21,7 @@ import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.exceptions.RateLimitException
import com.futo.platformplayer.logging.Logger
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.StatePlatform
@ -99,6 +100,8 @@ class SubscriptionsFeedFragment : MainFragment() {
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
override val shouldShowTimeBar: Boolean get() = Settings.instance.subscriptions.progressBar
private var _subGroup: SubscriptionGroup? = null;
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total ->
@ -254,6 +257,18 @@ class SubscriptionsFeedFragment : MainFragment() {
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
};
_subscriptionBar?.onClickChannel?.subscribe { c -> fragment.navigate<ChannelFragment>(c); };
_subscriptionBar?.onToggleGroup?.subscribe { g ->
if(g is SubscriptionGroup.Add)
UISlideOverlays.showCreateSubscriptionGroup(_overlayContainer);
else {
_subGroup = g;
loadCache(); //TODO: Proper subset update
}
};
_subscriptionBar?.onHoldGroup?.subscribe { g ->
if(g !is SubscriptionGroup.Add)
fragment.navigate<SubscriptionGroupFragment>(g);
};
synchronized(_filterLock) {
_subscriptionBar?.setToggles(
@ -288,9 +303,15 @@ class SubscriptionsFeedFragment : MainFragment() {
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
val nowSoon = OffsetDateTime.now().plusMinutes(5);
val filterGroup = _subGroup;
return results.filter {
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
//TODO: Check against a sub cache
if(filterGroup != null && !filterGroup.urls.contains(it.author.url))
return@filter false;
if(it.datetime?.isAfter(nowSoon) == true) {
if(!_filterSettings.allowPlanned)
return@filter false;

View file

@ -1,14 +1,26 @@
package com.futo.platformplayer.models
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.futo.platformplayer.PresetImages
import com.futo.platformplayer.R
import kotlinx.serialization.Contextual
import kotlinx.serialization.Transient
import java.io.File
data class ImageVariable(val url: String? = null, val resId: Int? = null, val bitmap: Bitmap? = null) {
@kotlinx.serialization.Serializable
data class ImageVariable(
val url: String? = null,
val resId: Int? = null,
@Transient
@Contextual
private val bitmap: Bitmap? = null,
val presetName: String? = null) {
@SuppressLint("DiscouragedApi")
fun setImageView(imageView: ImageView, fallbackResId: Int = -1) {
if(bitmap != null) {
Glide.with(imageView)
@ -23,6 +35,9 @@ data class ImageVariable(val url: String? = null, val resId: Int? = null, val bi
.load(url)
.placeholder(R.drawable.placeholder_channel_thumbnail)
.into(imageView);
} else if(!presetName.isNullOrEmpty()) {
val resId = PresetImages.getPresetResIdByName(presetName);
imageView.setImageResource(resId);
} else if (fallbackResId != -1) {
Glide.with(imageView)
.load(fallbackResId)
@ -44,6 +59,9 @@ data class ImageVariable(val url: String? = null, val resId: Int? = null, val bi
fun fromBitmap(bitmap: Bitmap): ImageVariable {
return ImageVariable(null, null, bitmap);
}
fun fromPresetName(str: String): ImageVariable {
return ImageVariable(null, null, null, str);
}
fun fromFile(file: File): ImageVariable {
return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath));
}

View file

@ -0,0 +1,33 @@
package com.futo.platformplayer.models
import java.util.UUID
@kotlinx.serialization.Serializable
open class SubscriptionGroup {
var id: String = UUID.randomUUID().toString();
var name: String;
var image: ImageVariable? = null;
var urls: MutableList<String> = mutableListOf();
var priority: Int = 99;
constructor(name: String) {
this.name = name;
}
constructor(parent: SubscriptionGroup) {
this.id = parent.id;
this.name = parent.name;
this.image = parent.image;
this.urls = parent.urls;
this.priority = parent.priority;
}
class Selectable(parent: SubscriptionGroup, isSelected: Boolean = false): SubscriptionGroup(parent) {
var selected: Boolean = isSelected;
}
class Add: SubscriptionGroup("+") {
init {
urls.add("+");
}
}
}

View file

@ -133,6 +133,7 @@ class StateApp {
//Files
private var _tempDirectory: File? = null;
private var _persistentDirectory: File? = null;
//AutoRotate
@ -165,6 +166,16 @@ class StateApp {
return File(_tempDirectory, name);
}
fun getPersistFile(extension: String? = null): File {
val name = UUID.randomUUID().toString() +
if(extension != null)
".${extension}"
else
"";
return File(_persistentDirectory, name);
}
fun getCurrentSystemAutoRotate(): Boolean {
_context?.let {
systemAutoRotate = android.provider.Settings.System.getInt(
@ -290,6 +301,10 @@ class StateApp {
_tempDirectory?.deleteRecursively();
}
_tempDirectory?.mkdirs();
_persistentDirectory = File(context.filesDir, "persist");
if(_persistentDirectory?.exists() == false) {
_persistentDirectory?.mkdirs();
}
}
}

View file

@ -0,0 +1,94 @@
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();
val onGroupsChanged = Event0();
fun getSubscriptionGroup(id: String): SubscriptionGroup? {
return _subGroups.findItem { it.id == id };
}
fun getSubscriptionGroups(): List<SubscriptionGroup> {
return _subGroups.getItems();
}
fun updateSubscriptionGroup(subGroup: SubscriptionGroup, preventNotify: Boolean = false) {
_subGroups.save(subGroup);
if(!preventNotify)
onGroupsChanged.emit();
}
fun deleteSubscriptionGroup(id: String){
val group = getSubscriptionGroup(id);
if(group != null) {
_subGroups.delete(group);
onGroupsChanged.emit();
}
}
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

@ -46,9 +46,10 @@ class AnyAdapterView<I, T>(view: RecyclerView, adapter: BaseAnyAdapter<I, T, T>,
where T : AnyAdapter.AnyViewHolder<I>{
companion object {
/*
inline fun <I, reified T : AnyAdapter.AnyViewHolder<I>> RecyclerView.asAny(list: List<I>, orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null): AnyAdapterView<I, T> {
return asAny(ArrayList(list), orientation, reversed, onCreate);
}
}*/
inline fun <I, reified T : AnyAdapter.AnyViewHolder<I>> RecyclerView.asAny(list: ArrayList<I>, orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null): AnyAdapterView<I, T> {
return AnyAdapterView(this, AnyAdapter.create(list, onCreate), orientation, reversed);
}

View file

@ -0,0 +1,33 @@
package com.futo.platformplayer.views
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.widget.addTextChangedListener
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
class SearchView : FrameLayout {
val textSearch: TextView;
val buttonClear: ImageButton;
var onSearchChanged = Event1<String>();
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.view_search_bar, this);
textSearch = findViewById(R.id.edit_search)
buttonClear = findViewById(R.id.button_clear_search)
buttonClear.setOnClickListener { textSearch.text = "" };
textSearch.addTextChangedListener {
onSearchChanged.emit(it.toString());
};
}
}

View file

@ -1,7 +1,11 @@
package com.futo.platformplayer.views.adapters
import android.annotation.SuppressLint
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
@ -47,6 +51,7 @@ open class BaseAnyAdapter<I, T : AnyAdapter.AnyViewHolder<I>, IT : ViewHolder> {
cb(item);
}
@SuppressLint("NotifyDataSetChanged")
fun notifyContentChanged() {
adapter.notifyDataSetChanged();
}
@ -116,7 +121,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

@ -0,0 +1,164 @@
package com.futo.platformplayer.views.adapters.viewholders
import android.graphics.Color
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.api.media.models.channels.SerializedChannel
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.Subscription
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
class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<IPlatformChannel>(
LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_bar_icon, _viewGroup, false)) {
private val _creatorThumbnail: CreatorThumbnail;
private val _name: TextView;
private var _channel: IPlatformChannel? = null;
val onClick = Event1<IPlatformChannel>();
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: IPlatformChannel) {
_taskLoadProfile.cancel();
_channel = value;
_creatorThumbnail.setThumbnail(value.thumbnail, false);
_name.text = value.name;
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.url, true);
if (cachedProfile != null) {
onProfileLoaded(cachedProfile, false);
if (cachedProfile.expired) {
_taskLoadProfile.run(value.id);
}
} else {
_taskLoadProfile.run(value.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?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
if (profile != null) {
_name.text = profile.systemState.username;
}
}
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,80 @@
package com.futo.platformplayer.views.adapters.viewholders
import android.graphics.Color
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()
_view.setOnClickListener {
_group?.let {
onClick.emit(it);
}
}
_view.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);
if(value is SubscriptionGroup.Add)
_image.setBackgroundColor(Color.DKGRAY);
}
_textSubGroup.text = value.name;
if(value is SubscriptionGroup.Selectable && value.selected)
_view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimary, null));
else
_view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null));
}
companion object {
private const val TAG = "SubscriptionGroupBarViewHolder";
}
}

View file

@ -0,0 +1,109 @@
package com.futo.platformplayer.views.adapters.viewholders
import android.graphics.Color
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
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.Subscription
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.adapters.ItemMoveCallback
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 SubscriptionGroupListViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<SubscriptionGroup>(
LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_subscription_group, _viewGroup, false)) {
private var _group: SubscriptionGroup? = null;
private val _thumb: ImageView;
private val _image: ShapeableImageView;
private val _textSubGroup: TextView;
private val _textSubGroupMeta: TextView;
private val _buttonSettings: ImageButton;
private val _buttonDelete: ImageButton;
val onClick = Event1<SubscriptionGroup>();
val onSettings = Event1<SubscriptionGroup>();
val onDelete = Event1<SubscriptionGroup>();
val onDragDrop = Event1<RecyclerView.ViewHolder>();
init {
_thumb = _view.findViewById(R.id.thumb);
_image = _view.findViewById(R.id.image);
_textSubGroup = _view.findViewById(R.id.text_sub_group);
_textSubGroupMeta = _view.findViewById(R.id.text_sub_group_meta);
_buttonSettings = _view.findViewById(R.id.button_settings);
_buttonDelete = _view.findViewById(R.id.button_trash);
val dp6 = 6.dp(_view.resources);
_image.shapeAppearanceModel = ShapeAppearanceModel.builder()
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
.build()
_view.setOnClickListener {
_group?.let {
onClick.emit(it);
}
}
_thumb.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
onDragDrop.emit(this);
}
false
};
_buttonSettings.setOnClickListener {
_group?.let {
onSettings.emit(it);
};
}
_buttonDelete.setOnClickListener {
_group?.let {
onDelete.emit(it);
};
}
}
override fun bind(value: SubscriptionGroup) {
_group = value;
val img = value.image;
if(img != null)
img.setImageView(_image)
else {
_image.setImageResource(0);
if(value is SubscriptionGroup.Add)
_image.setBackgroundColor(Color.DKGRAY);
}
_textSubGroup.text = value.name;
_textSubGroupMeta.text = "${value.urls.size} subscriptions";
if(value is SubscriptionGroup.Selectable && value.selected)
_view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimary, null));
else
_view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null));
}
companion object {
private const val TAG = "SubscriptionGroupBarViewHolder";
}
}

View file

@ -0,0 +1,234 @@
package com.futo.platformplayer.views.overlays
import android.app.Activity
import android.content.Context
import android.content.Intent
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.core.net.toFile
import androidx.core.net.toUri
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.futo.platformplayer.PresetImages
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity
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.github.dhaval2404.imagepicker.ImagePicker
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
import java.io.File
class ImageVariableOverlay: ConstraintLayout {
private val _buttonGallery: BigButton;
private val _imageGallerySelected: ImageView;
private val _imageGallerySelectedContainer: LinearLayout;
private val _buttonSelect: Button;
private val _topbar: OverlayTopbar;
private val _recyclerPresets: AnyAdapterView<PresetImage, PresetViewHolder>;
private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>;
private val _creators: ArrayList<SelectableCreatorBarViewHolder.Selectable> = arrayListOf();
private val _presets: ArrayList<PresetImage> =
ArrayList(PresetImages.images.map { PresetImage(it.value, it.key, false) });
private var _selected: ImageVariable? = null;
private var _selectedFile: String? = null;
val onSelected = Event1<ImageVariable>();
val onClose = Event0();
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);
_imageGallerySelected = findViewById(R.id.gallery_selected);
_imageGallerySelectedContainer = findViewById(R.id.gallery_selected_container);
_buttonSelect = findViewById(R.id.button_select);
_recyclerPresets = findViewById<RecyclerView>(R.id.recycler_presets).asAny(_presets, RecyclerView.HORIZONTAL) {
it.onClick.subscribe {
_selected = ImageVariable.fromPresetName(it.name);
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.channel.thumbnail == null) {
UIDialogs.toast(context, "No thumbnail found");
return@subscribe;
}
_selected = ImageVariable(it.channel.thumbnail);
updateSelected();
};
};
_recyclerCreators.view.layoutManager = GridLayoutManager(context, 5).apply {
this.orientation = LinearLayoutManager.VERTICAL;
};
_buttonGallery.onClick.subscribe {
val context = StateApp.instance.contextOrNull;
if(context is IWithResultLauncher && context is MainActivity) {
ImagePicker.with(context)
.compress(512)
.maxResultSize(750, 500)
.createIntent {
context.launchForResult(it, 888) {
if(it.resultCode == Activity.RESULT_OK) {
cleanupLastFile();
val fileUri = it.data?.data;
if(fileUri != null) {
val file = fileUri.toFile();
val ext = file.extension;
val persistFile = StateApp.instance.getPersistFile(ext);
file.copyTo(persistFile);
_selectedFile = persistFile.toUri().toString();
_selected = ImageVariable(_selectedFile);
updateSelected();
}
}
};
};
}
};
_imageGallerySelectedContainer.setOnClickListener {
if(_selectedFile != null) {
_selected = ImageVariable(_selectedFile);
updateSelected();
}
}
_buttonSelect.setOnClickListener {
_selected?.let {
select(it);
}
};
_topbar.onClose.subscribe {
onClose.emit();
}
updateSelected();
}
fun updateSelected() {
val id = _selected?.resId;
val name = _selected?.presetName;
val url = _selected?.url;
_presets.forEach { p -> p.active = p.name == name };
_recyclerPresets.notifyContentChanged();
_creators.forEach { p -> p.active = p.channel.thumbnail == url };
_recyclerCreators.notifyContentChanged();
if(_selectedFile != null) {
_imageGallerySelectedContainer.visibility = View.VISIBLE;
Glide.with(_imageGallerySelected)
.load(_selectedFile)
.into(_imageGallerySelected);
}
else
_imageGallerySelectedContainer.visibility = View.GONE;
if(_selected?.url == _selectedFile)
_imageGallerySelectedContainer.setBackgroundColor(resources.getColor(R.color.colorPrimary, null));
else
_imageGallerySelectedContainer.setBackgroundColor(resources.getColor(R.color.transparent, null));
if(_selected != null)
_buttonSelect.alpha = 1f;
else
_buttonSelect.alpha = 0.5f;
}
fun cleanupLastFile() {
_selectedFile?.let {
val file = File(it);
if(file.exists())
file.delete();
_selectedFile = null;
}
}
fun select(variable: ImageVariable) {
if(_selected?.url != _selectedFile)
cleanupLastFile();
onSelected.emit(variable);
onClose.emit();
}
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: PresetImage = PresetImage(0, "", false);
val onClick = Event1<PresetImage>();
init {
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: 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 name: String, 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

@ -3,37 +3,113 @@ package com.futo.platformplayer.views.subscriptions
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class SubscriptionBar : LinearLayout {
private var _adapterView: AnyAdapterView<Subscription, SubscriptionBarViewHolder>? = null;
private var _subGroups: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>
private val _tagsContainer: LinearLayout;
private val _groups: ArrayList<SubscriptionGroup>;
private var _group: SubscriptionGroup? = null;
val onClickChannel = Event1<SerializedChannel>();
val onToggleGroup = Event1<SubscriptionGroup?>();
val onHoldGroup = Event1<SubscriptionGroup>();
override fun onAttachedToWindow() {
super.onAttachedToWindow();
StateSubscriptionGroups.instance.onGroupsChanged.subscribe(this) {
findViewTreeLifecycleOwner()?.lifecycleScope?.launch(Dispatchers.Main) {
reloadGroups();
}
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow();
StateSubscriptionGroups.instance.onGroupsChanged.remove(this);
}
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.view_subscription_bar, this);
val subscriptions = StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds };
val subscriptions = ArrayList(StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds });
_adapterView = findViewById<RecyclerView>(R.id.recycler_creators).asAny(subscriptions, orientation = RecyclerView.HORIZONTAL) {
it.onClick.subscribe { c ->
onClickChannel.emit(c.channel);
};
};
_groups = ArrayList(getGroups());
_subGroups = findViewById<RecyclerView>(R.id.recycler_subgroups).asAny(_groups, orientation = RecyclerView.HORIZONTAL) {
it.onClick.subscribe(::groupClicked);
it.onClickLong.subscribe { g ->
onHoldGroup.emit(g);
}
}
_tagsContainer = findViewById(R.id.container_tags);
}
private fun groupClicked(g: SubscriptionGroup) {
if(g is SubscriptionGroup.Add) {
onToggleGroup.emit(g);
return;
}
val isSame = _group == g;
_group?.let {
if (it is SubscriptionGroup.Selectable) {
it.selected = false;
val index = _groups.indexOf(it);
if (index >= 0)
_subGroups.notifyContentChanged(index);
}
}
if(isSame) {
_group = null;
onToggleGroup.emit(null);
}
else {
_group = g;
if(g is SubscriptionGroup.Selectable)
g.selected = true;
_subGroups.notifyContentChanged(_groups.indexOf(g));
onToggleGroup.emit(g);
}
}
private fun reloadGroups() {
val results = getGroups();
_groups.clear();
_groups.addAll(results);
_subGroups.notifyContentChanged();
}
private fun getGroups(): List<SubscriptionGroup> {
return if(Settings.instance.subscriptions.showSubscriptionGroups)
(StateSubscriptionGroups.instance.getSubscriptionGroups()
.sortedBy { it.priority }
.map { SubscriptionGroup.Selectable(it, it.id == _group?.id) } +
listOf(SubscriptionGroup.Add()));
else listOf();
}
fun setToggles(vararg buttons: Toggle) {
_tagsContainer.removeAllViews();

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorPrimary" />
<corners android:radius="1dp" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M164.62,720Q137.96,720 118.98,701.02Q100,682.04 100,655.39L100,304.61Q100,277.96 118.98,258.98Q137.96,240 164.62,240L515.39,240Q542.04,240 561.02,258.98Q580,277.96 580,304.61L580,655.39Q580,682.04 561.02,701.02Q542.04,720 515.39,720L164.62,720ZM692.69,440Q678.39,440 669.19,430.81Q660,421.61 660,407.31L660,272.69Q660,258.38 669.19,249.19Q678.39,240 692.69,240L827.31,240Q841.62,240 850.81,249.19Q860,258.38 860,272.69L860,407.31Q860,421.61 850.81,430.81Q841.62,440 827.31,440L692.69,440ZM700,400L820,400L820,280L700,280L700,400ZM164.62,680L515.39,680Q526.15,680 533.08,673.08Q540,666.15 540,655.39L540,304.61Q540,293.85 533.08,286.92Q526.15,280 515.39,280L164.62,280Q153.85,280 146.92,286.92Q140,293.85 140,304.61L140,655.39Q140,666.15 146.92,673.08Q153.85,680 164.62,680ZM187.69,596.15L492.31,596.15L395,466.15L320,566.15L265,493.15L187.69,596.15ZM692.69,720Q678.39,720 669.19,710.81Q660,701.62 660,687.31L660,552.69Q660,538.39 669.19,529.19Q678.39,520 692.69,520L827.31,520Q841.62,520 850.81,529.19Q860,538.39 860,552.69L860,687.31Q860,701.62 850.81,710.81Q841.62,720 827.31,720L692.69,720ZM700,680L820,680L820,560L700,560L700,680ZM140,680Q140,680 140,673.08Q140,666.15 140,655.39L140,304.61Q140,293.85 140,286.92Q140,280 140,280L140,280Q140,280 140,286.92Q140,293.85 140,304.61L140,655.39Q140,666.15 140,673.08Q140,680 140,680L140,680ZM700,400L700,280L700,280L700,400L700,400ZM700,680L700,560L700,560L700,680L700,680Z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

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

@ -0,0 +1,235 @@
<?xml version="1.0" encoding="utf-8"?>
<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="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:animateLayoutChanges="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="150dp"
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="#AA000000">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:orientation="horizontal">
<ImageButton
android:id="@+id/button_delete"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="0dp"
android:src="@drawable/ic_trash"
app:tint="#CC0000"
android:padding="10dp"
android:background="@color/transparent"
android:visibility="visible"
android:scaleType="fitCenter" />
<ImageButton
android:id="@+id/button_settings"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:src="@drawable/ic_settings"
android:padding="10dp"
android:background="@color/transparent"
android:visibility="visible"
android:scaleType="fitCenter" />
</LinearLayout>
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image_group"
android:layout_width="110dp"
android:layout_height="70dp"
android:adjustViewBounds="true"
app:circularflow_defaultRadius="10dp"
android:layout_marginLeft="30dp"
android:src="@drawable/xp_book"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent" />
<LinearLayout
android:id="@+id/button_edit_image"
android:layout_width="30dp"
android:layout_height="30dp"
app:layout_constraintBottom_toTopOf="@id/image_group"
app:layout_constraintLeft_toRightOf="@id/image_group"
android:layout_marginLeft="-15dp"
android:layout_marginBottom="-15dp"
android:background="@drawable/background_pill">
<ImageButton
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"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintLeft_toRightOf="@id/image_group"
app:layout_constraintTop_toTopOf="@id/image_group"
app:layout_constraintBottom_toBottomOf="@id/image_group"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginStart="25dp"
android:layout_marginTop="10dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/text_group_title_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text_group_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_bold"
android:textSize="15dp"
android:text="News" />
<ImageButton
android:layout_width="20dp"
android:layout_height="20dp"
android:padding="2dp"
android:layout_marginStart="5dp"
android:layout_marginBottom="-5dp"
android:scaleType="fitCenter"
android:background="@color/transparent"
android:src="@drawable/ic_edit"/>
</LinearLayout>
<TextView
android:id="@+id/text_group_meta"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_light"
android:textSize="12dp"
android:text="42 creators" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
<com.futo.platformplayer.views.SearchView
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="0dp"
android:paddingEnd="0dp">
<LinearLayout
android:id="@+id/container_enabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp">
<TextView
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" />
<TextView
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" />
<androidx.recyclerview.widget.RecyclerView
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>
<LinearLayout
android:id="@+id/container_disabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="10dp">
<TextView
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" />
<TextView
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" />
<androidx.recyclerview.widget.RecyclerView
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>
</LinearLayout>
</ScrollView>
</LinearLayout>
<FrameLayout
android:id="@+id/overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
</androidx.recyclerview.widget.RecyclerView>
<FrameLayout
android:id="@+id/overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,99 @@
<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="match_parent"
android:layout_height="70dp"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="2dp"
android:layout_margin="2dp"
android:clickable="true"
android:id="@+id/root">
<ImageView
android:id="@+id/thumb"
android:layout_width="50dp"
android:layout_height="match_parent"
android:padding="12dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:src="@drawable/ic_dragdrop_white"
android:background="@color/transparent"
/>
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:layout_width="75dp"
android:layout_height="50dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/thumb"
android:layout_marginLeft="10dp"
android:scaleType="centerCrop"
android:src="@drawable/xp_book" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintLeft_toRightOf="@id/image"
app:layout_constraintRight_toLeftOf="@id/buttons"
android:layout_marginLeft="10dp"
android:gravity="center"
android:orientation="vertical">
<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:textColor="@color/white"
android:textAlignment="textStart"
android:text="News" />
<TextView
android:id="@+id/text_sub_group_meta"
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="10dp"
android:textColor="@color/gray_ac"
android:textAlignment="textStart"
android:text="News" />
</LinearLayout>
<LinearLayout
android:id="@+id/buttons"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:orientation="horizontal">
<ImageButton
android:id="@+id/button_trash"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_marginRight="10dp"
android:padding="10dp"
android:scaleType="fitCenter"
android:background="@color/transparent"
app:tint="@color/pastel_red"
android:src="@drawable/ic_trash"
/>
<ImageButton
android:id="@+id/button_settings"
android:layout_width="50dp"
android:layout_height="match_parent"
android:padding="10dp"
android:scaleType="fitCenter"
android:background="@color/transparent"
android:src="@drawable/ic_settings"
android:visibility="gone"
/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,134 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/black"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.futo.platformplayer.views.overlays.OverlayTopbar
android:id="@+id/topbar"
android:layout_width="match_parent"
android:layout_height="40dp"
app:title="Select an Image"
app:metadata=""
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/topbar"
app:layout_constraintBottom_toTopOf="@id/container_select"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal">
<LinearLayout
android:id="@+id/gallery_selected_container"
android:layout_width="150dp"
android:layout_height="100dp"
android:gravity="center_horizontal"
android:padding="2dp"
android:layout_marginTop="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp">
<ImageView
android:id="@+id/gallery_selected"
android:layout_width="150dp"
android:layout_height="100dp"
android:scaleType="centerCrop" />
</LinearLayout>
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_gallery"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
app:buttonIcon="@drawable/ic_gallery"
app:buttonText="Open Photo Gallery"
app:buttonSubText="Pick an image from the gallery" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="10dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_regular"
android:textSize="20dp"
android:text="Preset" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textColor="@color/gray_ac"
android:textSize="12dp"
android:text="Pick a preset image" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_presets"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="10dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_regular"
android:textSize="20dp"
android:text="Creator" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textColor="@color/gray_ac"
android:textSize="12dp"
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">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
</LinearLayout>
</ScrollView>
<LinearLayout
android:id="@+id/container_select"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<Button
android:id="@+id/button_select"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:text="Select" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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="match_parent"
android:layout_height="40dp"
android:layout_margin="10dp"
android:orientation="vertical"
android:id="@+id/root">
<EditText
android:id="@+id/edit_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:imeOptions="actionDone"
android:singleLine="true"
android:hint="Search"
android:paddingEnd="46dp" />
<ImageButton
android:id="@+id/button_clear_search"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingStart="18dp"
android:paddingEnd="18dp"
android:layout_gravity="right|center_vertical"
android:visibility="invisible"
android:src="@drawable/ic_clear_16dp" />
</FrameLayout>

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,36 @@
<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="78dp"
android:layout_height="54dp"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="2dp"
android:layout_margin="2dp"
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:scaleType="centerCrop"
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

@ -69,6 +69,8 @@
<string name="discover">Discover</string>
<string name="find_new_video_sources_to_add">Find new video sources to add</string>
<string name="these_sources_have_been_disabled">These sources have been disabled</string>
<string name="these_creators_in_group">These are the creators that are visible for this group.</string>
<string name="these_creators_not_in_group">These creators are not in this group.</string>
<string name="disabled">Disabled</string>
<string name="watch_later">Watch Later</string>
<string name="create">Create</string>
@ -346,6 +348,9 @@
<string name="allow_full_screen_portrait">Allow fullscreen portrait</string>
<string name="background_switch_audio">Switch to Audio in Background</string>
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
<string name="subscription_group_menu">Groups</string>
<string name="show_subscription_group">Show Subscription Groups</string>
<string name="show_subscription_group_description">If subscription groups should be shown above your subscriptions to filter</string>
<string name="preview_feed_items">Preview Feed Items</string>
<string name="preview_feed_items_description">When the preview feedstyle is used, if items should auto-preview when scrolling over them</string>
<string name="log_level">Log Level</string>
@ -564,6 +569,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>