Show total downloaded content duration, Indicator how many subscriptions, save queue as playlist

This commit is contained in:
Kelvin 2025-02-12 18:43:15 +01:00
parent 8839d9f1c6
commit 83843f192d
8 changed files with 98 additions and 4 deletions

View File

@ -79,6 +79,36 @@ class UISlideOverlays {
return menu; return menu;
} }
fun showQueueOptionsOverlay(context: Context, container: ViewGroup) {
UISlideOverlays.showOverlay(container, "Queue options", null, {
}, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, {
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);
addPlaylistOverlay.onOK.subscribe {
val text = nameInput.text.trim()
if (text.isBlank()) {
return@subscribe;
}
addPlaylistOverlay.hide();
nameInput.deactivate();
nameInput.clear();
StatePlayer.instance.saveQueueAsPlaylist(text);
UIDialogs.appToast("Playlist [${text}] created");
};
addPlaylistOverlay.onCancel.subscribe {
nameInput.deactivate();
nameInput.clear();
};
addPlaylistOverlay.show();
nameInput.activate();
}, false));
}
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay { fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>(); val items = arrayListOf<View>();

View File

@ -10,6 +10,7 @@ import android.widget.EditText
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.Spinner import android.widget.Spinner
import android.widget.TextView
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -26,6 +27,7 @@ class CreatorsFragment : MainFragment() {
private var _overlayContainer: FrameLayout? = null; private var _overlayContainer: FrameLayout? = null;
private var _containerSearch: FrameLayout? = null; private var _containerSearch: FrameLayout? = null;
private var _editSearch: EditText? = null; private var _editSearch: EditText? = null;
private var _textMeta: TextView? = null;
private var _buttonClearSearch: ImageButton? = null private var _buttonClearSearch: ImageButton? = null
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -34,6 +36,7 @@ class CreatorsFragment : MainFragment() {
val editSearch: EditText = view.findViewById(R.id.edit_search); val editSearch: EditText = view.findViewById(R.id.edit_search);
val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search) val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
_editSearch = editSearch _editSearch = editSearch
_textMeta = view.findViewById(R.id.text_meta);
_buttonClearSearch = buttonClearSearch _buttonClearSearch = buttonClearSearch
buttonClearSearch.setOnClickListener { buttonClearSearch.setOnClickListener {
editSearch.text.clear() editSearch.text.clear()
@ -41,7 +44,11 @@ class CreatorsFragment : MainFragment() {
_buttonClearSearch?.visibility = View.INVISIBLE; _buttonClearSearch?.visibility = View.INVISIBLE;
} }
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)); val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)) { subs ->
_textMeta?.let {
it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}";
}
};
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) }; adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } } adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } }

View File

@ -22,6 +22,7 @@ import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.toHumanBytesSize import com.futo.platformplayer.toHumanBytesSize
import com.futo.platformplayer.toHumanDuration
import com.futo.platformplayer.views.AnyInsertedAdapterView import com.futo.platformplayer.views.AnyInsertedAdapterView
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
@ -215,7 +216,7 @@ class DownloadsFragment : MainFragment() {
_listDownloadedHeader.visibility = GONE; _listDownloadedHeader.visibility = GONE;
} else { } else {
_listDownloadedHeader.visibility = VISIBLE; _listDownloadedHeader.visibility = VISIBLE;
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()})"; _listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()}${if(downloaded.size > 0) ", ${downloaded.sumOf { it.duration }.toHumanDuration(false)}" else ""})";
} }
lastDownloads = downloaded; lastDownloads = downloaded;

View File

@ -13,6 +13,7 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo 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.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@ -130,6 +131,12 @@ class StatePlayer {
closeMediaSession(); closeMediaSession();
} }
fun saveQueueAsPlaylist(name: String){
val videos = _queue.toList();
val playlist = Playlist(name, videos.map { SerializedPlatformVideo.fromVideo(it) });
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
}
//Notifications //Notifications
fun hasMediaSession() : Boolean { fun hasMediaSession() : Boolean {
return MediaPlaybackService.getService() != null; return MediaPlaybackService.getService() != null;

View File

@ -16,6 +16,7 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
private lateinit var _sortedDataset: List<Subscription>; private lateinit var _sortedDataset: List<Subscription>;
private val _inflater: LayoutInflater; private val _inflater: LayoutInflater;
private val _confirmationMessage: String; private val _confirmationMessage: String;
private val _onDatasetChanged: ((List<Subscription>)->Unit)?;
var onClick = Event1<Subscription>(); var onClick = Event1<Subscription>();
var onSettings = Event1<Subscription>(); var onSettings = Event1<Subscription>();
@ -30,9 +31,10 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
updateDataset(); updateDataset();
} }
constructor(inflater: LayoutInflater, confirmationMessage: String) : super() { constructor(inflater: LayoutInflater, confirmationMessage: String, onDatasetChanged: ((List<Subscription>)->Unit)? = null) : super() {
_inflater = inflater; _inflater = inflater;
_confirmationMessage = confirmationMessage; _confirmationMessage = confirmationMessage;
_onDatasetChanged = onDatasetChanged;
StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper()) StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper())
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { updateDataset() } StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { updateDataset() }
@ -78,6 +80,8 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
.filter { (queryLower.isNullOrBlank() || it.channel.name.lowercase().contains(queryLower)) } .filter { (queryLower.isNullOrBlank() || it.channel.name.lowercase().contains(queryLower)) }
.toList(); .toList();
_onDatasetChanged?.invoke(_sortedDataset);
notifyDataSetChanged(); notifyDataSetChanged();
} }

View File

@ -2,16 +2,26 @@ package com.futo.platformplayer.views.overlays
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.views.lists.VideoListEditorView import com.futo.platformplayer.views.lists.VideoListEditorView
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
class QueueEditorOverlay : LinearLayout { class QueueEditorOverlay : LinearLayout {
private val _topbar : OverlayTopbar; private val _topbar : OverlayTopbar;
private val _editor : VideoListEditorView; private val _editor : VideoListEditorView;
private val _btnSettings: ImageView;
private val _overlayContainer: FrameLayout;
val onClose = Event0(); val onClose = Event0();
@ -19,6 +29,9 @@ class QueueEditorOverlay : LinearLayout {
inflate(context, R.layout.overlay_queue, this) inflate(context, R.layout.overlay_queue, this)
_topbar = findViewById(R.id.topbar); _topbar = findViewById(R.id.topbar);
_editor = findViewById(R.id.editor); _editor = findViewById(R.id.editor);
_btnSettings = findViewById(R.id.button_settings);
_overlayContainer = findViewById(R.id.overlay_container);
_topbar.onClose.subscribe(this, onClose::emit); _topbar.onClose.subscribe(this, onClose::emit);
_editor.onVideoOrderChanged.subscribe { StatePlayer.instance.setQueueWithExisting(it) } _editor.onVideoOrderChanged.subscribe { StatePlayer.instance.setQueueWithExisting(it) }
@ -28,6 +41,10 @@ class QueueEditorOverlay : LinearLayout {
} }
_editor.onVideoClicked.subscribe { v -> StatePlayer.instance.setQueuePosition(v) } _editor.onVideoClicked.subscribe { v -> StatePlayer.instance.setQueuePosition(v) }
_btnSettings.setOnClickListener {
handleSettings();
}
_topbar.setInfo(context.getString(R.string.queue), ""); _topbar.setInfo(context.getString(R.string.queue), "");
} }
@ -40,4 +57,8 @@ class QueueEditorOverlay : LinearLayout {
fun cleanup() { fun cleanup() {
_topbar.onClose.remove(this); _topbar.onClose.remove(this);
} }
fun handleSettings() {
UISlideOverlays.showQueueOptionsOverlay(context, _overlayContainer);
}
} }

View File

@ -16,7 +16,7 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="100dp" android:layout_height="110dp"
android:minHeight="0dp" android:minHeight="0dp"
app:layout_scrollFlags="scroll" app:layout_scrollFlags="scroll"
app:contentInsetStart="0dp" app:contentInsetStart="0dp"
@ -77,7 +77,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="20dp" android:paddingStart="20dp"
android:paddingEnd="20dp" /> android:paddingEnd="20dp" />
</LinearLayout> </LinearLayout>
<TextView
android:id="@+id/text_meta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="9dp"
android:textAlignment="center"
android:textColor="#333333"
android:text="0 creators" />
</LinearLayout> </LinearLayout>
</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>

View File

@ -21,5 +21,20 @@
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/topbar" app:layout_constraintTop_toBottomOf="@id/topbar"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toBottomOf="parent" />
<ImageView
android:id="@+id/button_settings"
android:background="@drawable/background_pill"
android:padding="5dp"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:srcCompat="@drawable/ic_settings" />
<FrameLayout
android:id="@+id/overlay_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>