Working subscription groups and image pickers

This commit is contained in:
Kelvin 2023-12-15 19:38:33 +01:00
parent 02292fed04
commit 4930ea8183
24 changed files with 689 additions and 67 deletions

View file

@ -248,20 +248,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;
@ -277,7 +280,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;
@ -285,17 +288,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

@ -103,14 +103,14 @@ 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,
}, false) else null/*,,
SlideUpMenuGroup(container.context, "Actions",
"Various things you can do with this subscription",
-1, listOf()),
-1, listOf())
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
showCreateSubscriptionGroup(container, subscription.channel);
}, false)
}, false)*/
).filterNotNull());
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
@ -531,7 +531,7 @@ class UISlideOverlays {
return overlay;
}
fun showCreateSubscriptionGroup(container: ViewGroup, initialChannel: IPlatformChannel?, onCreate: ((String) -> Unit)? = null): SlideUpMenuOverlay {
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);

View file

@ -102,6 +102,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragImportPlaylists: ImportPlaylistsFragment;
lateinit var _fragBuy: BuyFragment;
lateinit var _fragSubGroup: SubscriptionGroupFragment;
lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
lateinit var _fragBrowser: BrowserFragment;
@ -238,6 +239,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragImportPlaylists = ImportPlaylistsFragment.newInstance();
_fragBuy = BuyFragment.newInstance();
_fragSubGroup = SubscriptionGroupFragment.newInstance();
_fragSubGroupList = SubscriptionGroupListFragment.newInstance();
_fragBrowser = BrowserFragment.newInstance();
@ -320,6 +322,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragImportSubscriptions.topBar = _fragTopBarImport;
_fragImportPlaylists.topBar = _fragTopBarImport;
_fragSubGroup.topBar = _fragTopBarNavigation;
_fragSubGroupList.topBar = _fragTopBarAdd;
_fragBrowser.topBar = _fragTopBarNavigation;
@ -987,6 +990,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
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

@ -10,6 +10,7 @@ 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
@ -48,7 +49,7 @@ class SubscriptionGroupFragment : MainFragment() {
super.onShownWithView(parameter, isBack);
if(parameter is SubscriptionGroup)
_view?.setGroup(parameter);
_view?.setGroup(StateSubscriptionGroups.instance.getSubscriptionGroup(parameter.id) ?: parameter);
else
_view?.setGroup(null);
}
@ -77,7 +78,8 @@ class SubscriptionGroupFragment : MainFragment() {
private val _textGroupMeta: TextView;
private val _buttonSettings: ImageView;
private val _buttonSettings: ImageButton;
private val _buttonDelete: ImageButton;
private val _enabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
private val _disabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
@ -107,6 +109,7 @@ class SubscriptionGroupFragment : MainFragment() {
_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);
@ -147,6 +150,16 @@ class SubscriptionGroupFragment : MainFragment() {
_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();
@ -255,8 +268,10 @@ class SubscriptionGroupFragment : MainFragment() {
_recyclerCreatorsEnabled.adapter.notifyItemInserted(_enabledCreatorsFiltered.size - 1);
_group?.let {
it.urls.remove(channel.url);
save();
if(!it.urls.contains(channel.url)) {
it.urls.add(channel.url);
save();
}
}
updateMeta();
}

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,11 +257,17 @@ 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?.onToggleGroup?.subscribe { g ->
if(g is SubscriptionGroup.Add)
UISlideOverlays.showCreateSubscriptionGroup(_overlayContainer);
else {
_subGroup = g;
loadCache(); //TODO: Proper subset update
}
};
_subscriptionBar?.onHoldGroup?.subscribe { g ->
fragment.navigate<SubscriptionGroupFragment>(g);
if(g !is SubscriptionGroup.Add)
fragment.navigate<SubscriptionGroupFragment>(g);
};
synchronized(_filterLock) {
@ -294,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

@ -3,13 +3,31 @@ package com.futo.platformplayer.models
import java.util.UUID
@kotlinx.serialization.Serializable
class SubscriptionGroup {
val id: String = UUID.randomUUID().toString();
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

@ -51,19 +51,25 @@ class StateSubscriptionGroups {
.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) {
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)
if(group != null) {
_subGroups.delete(group);
onGroupsChanged.emit();
}
}

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

@ -1,5 +1,6 @@
package com.futo.platformplayer.views.adapters
import android.annotation.SuppressLint
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
@ -50,6 +51,7 @@ open class BaseAnyAdapter<I, T : AnyAdapter.AnyViewHolder<I>, IT : ViewHolder> {
cb(item);
}
@SuppressLint("NotifyDataSetChanged")
fun notifyContentChanged() {
adapter.notifyDataSetChanged();
}

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
@ -41,12 +42,12 @@ class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
.build()
_viewGroup.setOnClickListener {
_view.setOnClickListener {
_group?.let {
onClick.emit(it);
}
}
_viewGroup.setOnLongClickListener {
_view.setOnLongClickListener {
_group?.let {
onClickLong.emit(it);
}
@ -59,9 +60,18 @@ class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
val img = value.image;
if(img != null)
img.setImageView(_image)
else
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 {

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

@ -1,5 +1,6 @@
package com.futo.platformplayer.views.overlays
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Color
@ -12,12 +13,16 @@ 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.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
@ -31,13 +36,17 @@ 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>;
@ -53,6 +62,7 @@ class ImageVariableOverlay: ConstraintLayout {
);
private var _selected: ImageVariable? = null;
private var _selectedFile: String? = null;
val onSelected = Event1<ImageVariable>();
val onClose = Event0();
@ -74,6 +84,8 @@ class ImageVariableOverlay: ConstraintLayout {
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 {
@ -97,23 +109,37 @@ class ImageVariableOverlay: ConstraintLayout {
this.orientation = LinearLayoutManager.VERTICAL;
};
_buttonGallery.setOnClickListener {
_buttonGallery.onClick.subscribe {
val context = StateApp.instance.contextOrNull;
if(context is IWithResultLauncher) {
val intent = Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
context.launchForResult(intent, 888) {
if(it.resultCode == 888) {
val url = it.data?.data ?: return@launchForResult;
//TODO: Write to local storage
_selected = ImageVariable(url.toString());
updateSelected();
}
};
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);
@ -133,13 +159,38 @@ class ImageVariableOverlay: ConstraintLayout {
_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();
}

View file

@ -3,9 +3,11 @@ 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 androidx.recyclerview.widget.RecyclerView.Recycler
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
@ -17,35 +19,97 @@ 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 onClickGroup = Event1<SubscriptionGroup>();
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);
};
};
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);
_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

@ -26,19 +26,37 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#AA000000">
<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"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:padding="10dp"
android:background="@color/transparent"
android:visibility="invisible"
android:scaleType="fitCenter" />
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"

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

@ -26,8 +26,24 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
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"
@ -41,6 +57,7 @@
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
@ -71,6 +88,7 @@
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

View file

@ -1,17 +1,19 @@
<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:layout_width="78dp"
android:layout_height="54dp"
android:orientation="vertical"
android:gravity="center_horizontal"
android:layout_margin="4dp"
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"

View file

@ -346,6 +346,9 @@
<string name="live_chat_webview">Live Chat Webview</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>