diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 28057a51..4815b71c 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -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(); diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index 3e37b94a..e08996f7 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -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); diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index 26cab3a6..6dfe8472 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -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"); } } diff --git a/app/src/main/java/com/futo/platformplayer/activities/ManageTabsActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/ManageTabsActivity.kt index d191f642..ed42b722 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/ManageTabsActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/ManageTabsActivity.kt @@ -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 -> diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt index 48f68590..979a679a 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt @@ -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() }), ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate() }), ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate() }), + ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate() }), 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()"); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt index 497d327c..f77df2cd 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt @@ -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 = arrayListOf(); private val _disabledCreators: ArrayList = 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(); } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupListFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupListFragment.kt new file mode 100644 index 00000000..52ed90dd --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupListFragment.kt @@ -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 = arrayListOf(); + private var _list: AnyAdapterView? = 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(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(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 {} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt index 4ef157f6..41578d00 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt @@ -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 { override val shouldShowTimeBar: Boolean get() = Settings.instance.subscriptions.progressBar + private var _subGroup: SubscriptionGroup? = null; + constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, LinearLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = 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(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(g); + if(g !is SubscriptionGroup.Add) + fragment.navigate(g); }; synchronized(_filterLock) { @@ -294,9 +303,15 @@ class SubscriptionsFeedFragment : MainFragment() { override fun filterResults(results: List): List { 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; diff --git a/app/src/main/java/com/futo/platformplayer/models/SubscriptionGroup.kt b/app/src/main/java/com/futo/platformplayer/models/SubscriptionGroup.kt index 1ec434f0..c46aa3b8 100644 --- a/app/src/main/java/com/futo/platformplayer/models/SubscriptionGroup.kt +++ b/app/src/main/java/com/futo/platformplayer/models/SubscriptionGroup.kt @@ -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 = 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("+"); + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt index 45e8e069..bcf68592 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -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(); + } } } diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt index 59e7e3be..f77e2fad 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt @@ -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 { 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(); + } } diff --git a/app/src/main/java/com/futo/platformplayer/views/AnyAdapterView.kt b/app/src/main/java/com/futo/platformplayer/views/AnyAdapterView.kt index 2ff1e01f..db295aa3 100644 --- a/app/src/main/java/com/futo/platformplayer/views/AnyAdapterView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/AnyAdapterView.kt @@ -46,9 +46,10 @@ class AnyAdapterView(view: RecyclerView, adapter: BaseAnyAdapter, where T : AnyAdapter.AnyViewHolder{ companion object { + /* inline fun > RecyclerView.asAny(list: List, orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null): AnyAdapterView { return asAny(ArrayList(list), orientation, reversed, onCreate); - } + }*/ inline fun > RecyclerView.asAny(list: ArrayList, orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null): AnyAdapterView { return AnyAdapterView(this, AnyAdapter.create(list, onCreate), orientation, reversed); } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/AnyAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/AnyAdapter.kt index 1db3a650..d5ca59f0 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/AnyAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/AnyAdapter.kt @@ -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, IT : ViewHolder> { cb(item); } + @SuppressLint("NotifyDataSetChanged") fun notifyContentChanged() { adapter.notifyDataSetChanged(); } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupBarViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupBarViewHolder.kt index 2b27e2b5..0f93a5eb 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupBarViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupBarViewHolder.kt @@ -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 { diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupListViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupListViewHolder.kt new file mode 100644 index 00000000..19fe8a30 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupListViewHolder.kt @@ -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( + 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(); + val onSettings = Event1(); + val onDelete = Event1(); + val onDragDrop = Event1(); + + 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"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/ImageVariableOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/ImageVariableOverlay.kt index 9fdcb2b4..ca577654 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/ImageVariableOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/ImageVariableOverlay.kt @@ -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; @@ -53,6 +62,7 @@ class ImageVariableOverlay: ConstraintLayout { ); private var _selected: ImageVariable? = null; + private var _selectedFile: String? = null; val onSelected = Event1(); 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(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(); } diff --git a/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt b/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt index 34f7ee5b..a456cba0 100644 --- a/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt +++ b/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt @@ -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? = null; private var _subGroups: AnyAdapterView private val _tagsContainer: LinearLayout; + private val _groups: ArrayList; + private var _group: SubscriptionGroup? = null; + val onClickChannel = Event1(); - val onClickGroup = Event1(); + val onToggleGroup = Event1(); val onHoldGroup = Event1(); + 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(R.id.recycler_creators).asAny(subscriptions, orientation = RecyclerView.HORIZONTAL) { it.onClick.subscribe { c -> onClickChannel.emit(c.channel); }; }; - val subgroups = StateSubscriptionGroups.instance.getSubscriptionGroups(); - _subGroups = findViewById(R.id.recycler_subgroups).asAny(subgroups, orientation = RecyclerView.HORIZONTAL) { - it.onClick.subscribe(onClickGroup::emit); - it.onClickLong.subscribe(onHoldGroup::emit); + _groups = ArrayList(getGroups()); + _subGroups = findViewById(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 { + 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(); diff --git a/app/src/main/res/drawable/background_primary_rounded_2dp.xml b/app/src/main/res/drawable/background_primary_rounded_2dp.xml new file mode 100644 index 00000000..1eafcfd9 --- /dev/null +++ b/app/src/main/res/drawable/background_primary_rounded_2dp.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_subscriptions_group.xml b/app/src/main/res/layout/fragment_subscriptions_group.xml index 76c1da2c..cf728852 100644 --- a/app/src/main/res/layout/fragment_subscriptions_group.xml +++ b/app/src/main/res/layout/fragment_subscriptions_group.xml @@ -26,19 +26,37 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="#AA000000"> - + android:orientation="horizontal"> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_subscription_group.xml b/app/src/main/res/layout/list_subscription_group.xml new file mode 100644 index 00000000..d7c1d7bc --- /dev/null +++ b/app/src/main/res/layout/list_subscription_group.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/overlay_image_variable.xml b/app/src/main/res/layout/overlay_image_variable.xml index b921b11a..b89d7fd4 100644 --- a/app/src/main/res/layout/overlay_image_variable.xml +++ b/app/src/main/res/layout/overlay_image_variable.xml @@ -26,8 +26,24 @@ + android:orientation="vertical" + android:gravity="center_horizontal"> + + + Live Chat Webview Switch to Audio in Background Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter + Groups + Show Subscription Groups + If subscription groups should be shown above your subscriptions to filter Preview Feed Items When the preview feedstyle is used, if items should auto-preview when scrolling over them Log Level