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 43f16a97..402625f8 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 @@ -8,18 +8,25 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.content.res.Configuration +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.* +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.animation.doOnEnd +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.activities.MainActivity +import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.dp import com.futo.platformplayer.fragment.mainactivity.MainActivityFragment import com.futo.platformplayer.fragment.mainactivity.main.* @@ -27,6 +34,10 @@ import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePayment import com.futo.platformplayer.states.StateSubscriptions +import com.futo.platformplayer.views.AnyAdapterView +import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny +import com.futo.platformplayer.views.adapters.AnyAdapter +import com.futo.platformplayer.views.pills.RoundButton import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.math.floor @@ -69,9 +80,15 @@ class MenuBottomBarFragment : MainActivityFragment() { private val _inflater: LayoutInflater; private val _subscribedActivity: MainActivity?; + private val _containerMoreHeader: ConstraintLayout; + private val _toggleAirplaneMode: LinearLayout; + private val _togglePrivacy: LinearLayout; + private var _overlayMore: FrameLayout; private var _overlayMoreBackground: FrameLayout; - private var _layoutMoreButtons: LinearLayout; + private var _layoutMoreButtons: RecyclerView; + private val _layoutMoreButtonItems = arrayListOf(); + private var _layoutMoreButtonsAdapter: AnyAdapterView; private var _layoutBottomBarButtons: LinearLayout; private var _moreVisible = false; @@ -90,10 +107,71 @@ class MenuBottomBarFragment : MainActivityFragment() { _inflater = inflater; inflater.inflate(R.layout.fragment_overview_bottom_bar, this); + _containerMoreHeader = findViewById(R.id.container_more_options); + _toggleAirplaneMode = findViewById(R.id.container_toggle_airplane); + _togglePrivacy = findViewById(R.id.container_toggle_privacy); + + _toggleAirplaneMode.isVisible = false //TODO: Remove when airplane mode implemented + + StateApp.instance.airplaneModeChanged.subscribe { + if(!StateApp.instance.airplaneMode) + _toggleAirplaneMode.setBackgroundResource(R.drawable.background_menu_toggle) + else + _toggleAirplaneMode.setBackgroundResource(R.drawable.background_menu_toggle_active) + } + if(!StateApp.instance.airplaneMode) + _toggleAirplaneMode.setBackgroundResource(R.drawable.background_menu_toggle) + else + _toggleAirplaneMode.setBackgroundResource(R.drawable.background_menu_toggle_active) + _toggleAirplaneMode.setOnClickListener { + if(StateApp.instance.airplaneMode) { + StateApp.instance.setAirMode(false); + UIDialogs.appToast("Airplane mode disabled"); + } + else { + StateApp.instance.setAirMode(true); + UIDialogs.appToast("Airplane mode enabled"); + } + } + + StateApp.instance.privateModeChanged.subscribe { + if(!StateApp.instance.privateMode) + _togglePrivacy.setBackgroundResource(R.drawable.background_menu_toggle) + else + _togglePrivacy.setBackgroundResource(R.drawable.background_menu_toggle_active) + } + if(!StateApp.instance.privateMode) + _togglePrivacy.setBackgroundResource(R.drawable.background_menu_toggle) + else + _togglePrivacy.setBackgroundResource(R.drawable.background_menu_toggle_active) + _togglePrivacy.setOnClickListener { + if(StateApp.instance.privateMode) { + StateApp.instance.setPrivacyMode(false); + UIDialogs.appToast("Privacy mode disabled"); + } + else { + StateApp.instance.setPrivacyMode(true); + UIDialogs.appToast("Privacy mode enabled"); + } + } + _overlayMore = findViewById(R.id.more_overlay); _overlayMoreBackground = findViewById(R.id.more_overlay_background); _layoutMoreButtons = findViewById(R.id.more_menu_buttons); - _layoutBottomBarButtons = findViewById(R.id.bottom_bar_buttons) + _layoutBottomBarButtons = findViewById(R.id.bottom_bar_buttons); + + val totalWidthDp = resources.displayMetrics.widthPixels / resources.displayMetrics.density; + val columns = MenuButtonItemViewHolder.getAutoSizeColumns(totalWidthDp); + _layoutMoreButtonsAdapter = _layoutMoreButtons.asAny(_layoutMoreButtonItems, + RecyclerView.VERTICAL, false, { button -> + button.setAutoSize(totalWidthDp); + button.parentFragment = this@MenuBottomBarView._fragment; + button.onClick.subscribe { + setMoreVisible(false); + } + }) + val layoutManager = GridLayoutManager(context, columns, GridLayoutManager.VERTICAL, true); + _layoutMoreButtons.layoutManager = layoutManager; _overlayMoreBackground.setOnClickListener { setMoreVisible(false); }; @@ -120,6 +198,8 @@ class MenuBottomBarFragment : MainActivityFragment() { } private fun setMoreVisible(visible: Boolean) { + + //TODO: issues with these bools if (_moreVisibleAnimating) { return } @@ -128,9 +208,12 @@ class MenuBottomBarFragment : MainActivityFragment() { return } + +/* val height = _moreButtons.firstOrNull()?.let { it.height.toFloat() + (it.layoutParams as MarginLayoutParams).bottomMargin } ?: return + */ _moreVisibleAnimating = true val moreOverlayBackground = _overlayMoreBackground @@ -142,14 +225,17 @@ class MenuBottomBarFragment : MainActivityFragment() { moreOverlay.visibility = VISIBLE val animations = arrayListOf() animations.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 0.0f, 1.0f).setDuration(duration)) + animations.add(ObjectAnimator.ofFloat(_layoutMoreButtons, "alpha", 0.0f, 1.0f).setDuration(duration)) + animations.add(ObjectAnimator.ofFloat(_containerMoreHeader, "alpha", 0.0f, 1.0f).setDuration(duration)) _bottomButtons.find { it.definition.id == 99 }?.let { animations.add(ObjectAnimator.ofFloat(it, "alpha", 0.5f, 1.0f) .setDuration(duration)); } + animations.add(ObjectAnimator.ofFloat(_layoutMoreButtons, "translationY", resources.displayMetrics.heightPixels.toFloat(), 0.0f).setDuration(duration)) for ((index, button) in _moreButtons.withIndex()) { val i = _moreButtons.size - index - animations.add(ObjectAnimator.ofFloat(button, "translationY", height * staggerFactor * (i + 1), 0.0f).setDuration(duration)) + //animations.add(ObjectAnimator.ofFloat(button, "translationY", height * staggerFactor * (i + 1), 0.0f).setDuration(duration)) } val animatorSet = AnimatorSet() @@ -164,14 +250,21 @@ class MenuBottomBarFragment : MainActivityFragment() { animations .add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 1.0f, 0.0f) .setDuration(duration)) + animations + .add(ObjectAnimator.ofFloat(_layoutMoreButtons, "alpha", 1.0f, 0.0f) + .setDuration(duration)) + animations + .add(ObjectAnimator.ofFloat(_containerMoreHeader, "alpha", 1.0f, 0.0f) + .setDuration(duration)) _bottomButtons.find { it.definition.id == 99 }?.let { animations.add(ObjectAnimator.ofFloat(it, "alpha", 1.0f, 0.5f) .setDuration(duration)); } + animations.add(ObjectAnimator.ofFloat(_layoutMoreButtons, "translationY", 0.0f, resources.displayMetrics.heightPixels.toFloat()).setDuration(duration)) for ((index, button) in _moreButtons.withIndex()) { val i = _moreButtons.size - index - animations.add(ObjectAnimator.ofFloat(button, "translationY", 0.0f, height * staggerFactor * (i + 1)).setDuration(duration)) + //animations.add(ObjectAnimator.ofFloat(button, "translationY", 0.0f, height * staggerFactor * (i + 1)).setDuration(duration)) } val animatorSet = AnimatorSet() @@ -183,11 +276,12 @@ class MenuBottomBarFragment : MainActivityFragment() { animatorSet.playTogether(animations) animatorSet.start() } + } private fun updateBottomMenuButtons(buttons: MutableList, hasMore: Boolean) { if (hasMore) { - buttons.add(ButtonDefinition(99, R.drawable.ic_more, R.drawable.ic_more, R.string.more, canToggle = false, { false }, { setMoreVisible(true) })) + buttons.add(ButtonDefinition(99, R.drawable.ic_more, R.drawable.ic_more, R.string.more, canToggle = false, { false }, { setMoreVisible(!_moreVisible) })) } _bottomButtons.clear(); @@ -252,7 +346,9 @@ class MenuBottomBarFragment : MainActivityFragment() { insertedButtons++; } + val newButtons = mutableListOf(); for (data in buttons) { + /* val button = MenuButton(context, data, _fragment, true); button.setOnClickListener { updateMenuIcons() @@ -262,7 +358,12 @@ class MenuBottomBarFragment : MainActivityFragment() { _moreButtons.add(button); _layoutMoreButtons.addView(button); + */ + val buttonItem = MenuButtonItem(data); + newButtons.add(buttonItem); } + _layoutMoreButtonsAdapter.setData(newButtons); + _layoutMoreButtonsAdapter.notifyContentChanged(); } private fun updateMenuIcons() { @@ -350,6 +451,71 @@ class MenuBottomBarFragment : MainActivityFragment() { } + class MenuButtonItem(val def: ButtonDefinition); + class MenuButtonItemViewHolder(private val _viewGroup: ViewGroup): AnyAdapter.AnyViewHolder( + LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_menu_tile, + _viewGroup, false)) { + + val onClick = Event1(); + + val root: ConstraintLayout; + val imageIcon: ImageView; + val textName: TextView; + + + var button: MenuButtonItem? = null; + + var parentFragment: MenuBottomBarFragment? = null; + + init { + root = _view.findViewById(R.id.root); + imageIcon = _view.findViewById(R.id.image_icon); + textName = _view.findViewById(R.id.text_name); + + root.setOnClickListener { + button?.let { + it.def.action(parentFragment ?: return@let); + onClick.emit(it); + } + } + } + + + override fun bind(value: MenuButtonItem) { + button = value; + textName.text = _view.context.getString(value.def.string); + imageIcon.setImageResource(value.def.iconActive); + } + + + fun setWidth(dp: Int) { + root.updateLayoutParams { + this.width = (dp - 6).dp(_viewGroup.context.resources); + this.height = (dp - 6).dp(_viewGroup.context.resources); + } + imageIcon.updateLayoutParams { + this.width = (dp - 54).dp(_viewGroup.context.resources); + this.height = (dp - 54).dp(_viewGroup.context.resources); + } + } + + fun setAutoSize(totalWidth: Float) { + val dpWidth = totalWidth; + val columns = Math.max(((dpWidth) / viewWidthDp).toInt(), 1); + val remainder = dpWidth - columns * viewWidthDp; + val targetSize = viewWidthDp + (remainder / columns).toInt(); + setWidth(targetSize); + } + + companion object { + val viewWidthDp = 90; + fun getAutoSizeColumns(totalWidth: Float): Int { + val dpWidth = totalWidth; + val columns = Math.max(((dpWidth) / viewWidthDp).toInt(), 1); + return columns; + } + } + } class MenuButton: LinearLayout { val definition: ButtonDefinition; @@ -413,7 +579,9 @@ class MenuBottomBarFragment : MainActivityFragment() { } }), ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate(withHistory = false) }), - ButtonDefinition(12, R.drawable.ic_library, R.drawable.ic_library, R.string.library, canToggle = false, { it.currentMain is LibraryFragment }, { it.navigate(withHistory = false) }), + //if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) + ButtonDefinition(12, R.drawable.ic_library, R.drawable.ic_library, R.string.library, canToggle = false, { it.currentMain is LibraryFragment }, { it.navigate(withHistory = false) }) + ,//else null, ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate(withHistory = false) }), ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate(withHistory = false) }), ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate(withHistory = false) }), @@ -451,7 +619,7 @@ class MenuBottomBarFragment : MainActivityFragment() { //96 is reserved for privacy button //98 is reserved for buy button //99 is reserved for more button - ); + ).filterNotNull(); } data class ButtonDefinition( diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryArtistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryArtistFragment.kt index ea9f6c49..6f2e81f5 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryArtistFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryArtistFragment.kt @@ -14,6 +14,7 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.widget.AppCompatImageView +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle @@ -22,6 +23,7 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 +import com.bumptech.glide.Glide import com.futo.platformplayer.R import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UISlideOverlays @@ -359,7 +361,21 @@ class LibraryArtistFragment : MainFragment() { (_viewPager.adapter as ArtistViewPagerAdapter).artist = channel - _viewPager.adapter!!.notifyDataSetChanged() + _viewPager.adapter!!.notifyDataSetChanged(); + + val artistThumbnail = channel.getThumbnailOrAlbum(); + if(artistThumbnail != null) { + _creatorThumbnail.isVisible = true; + _creatorThumbnail.setThumbnail(channel.getThumbnailOrAlbum(), true, true); + Glide.with(_imageBanner) + .load(artistThumbnail) + .into(_imageBanner); + } + else { + _creatorThumbnail.isVisible = false; + Glide.with(_imageBanner).clear(_imageBanner); + } + this.channel = channel } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFilesFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFilesFragment.kt index 67c5bccd..fcef3098 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFilesFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFilesFragment.kt @@ -86,6 +86,7 @@ class LibraryFilesFragment : MainFragment() { } fun loadTop() { var initialDirectories = listOf(); + var path = ""; if(root == null) { initialDirectories = StateLibrary.instance.getFileDirectories(); if (initialDirectories.size == 0) { @@ -109,9 +110,10 @@ class LibraryFilesFragment : MainFragment() { it.isVisible = false; } initialDirectories = root?.getSubFiles() ?: listOf(); + path = root?.path ?: ""; } navStack.clear(); - val entry = FileStack("", initialDirectories); + val entry = FileStack(path, initialDirectories); navStack.add(entry); openDirectory(navStack.last()); fragment.topBar?.let { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt index 1d58331a..16c8df37 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.fragment.mainactivity.main import android.content.Intent import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import android.provider.MediaStore import android.util.AttributeSet @@ -11,11 +12,13 @@ import android.view.ViewGroup import android.widget.LinearLayout import android.widget.TextView import androidx.activity.result.contract.ActivityResultContracts +import androidx.collection.emptyLongSet import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.futo.platformplayer.R import com.futo.platformplayer.UIDialogs @@ -34,6 +37,7 @@ import com.futo.platformplayer.views.AnyInsertedAdapterView import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithViews import com.futo.platformplayer.views.LibrarySection +import com.futo.platformplayer.views.NoResultsView import com.futo.platformplayer.views.adapters.AnyAdapter import com.futo.platformplayer.views.adapters.InsertedViewAdapter import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder @@ -41,6 +45,9 @@ import com.futo.platformplayer.views.adapters.viewholders.ArtistTileViewHolder import com.futo.platformplayer.views.adapters.viewholders.FileViewHolder import com.futo.platformplayer.views.adapters.viewholders.LocalVideoTileViewHolder import com.futo.platformplayer.views.buttons.BigButton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import okhttp3.Dispatcher class LibraryFragment : MainFragment() { @@ -146,11 +153,12 @@ class LibraryFragment : MainFragment() { var sectionAlbums: LibrarySection; var sectionVideos: LibrarySection; var sectionFiles: LibrarySection; + var noContent: NoResultsView; //var buttonFiles: BigButton; val recycler: RecyclerView; - val adapterFiles: AnyInsertedAdapterView; + var adapterFiles: AnyInsertedAdapterView? = null; //var metaInfo: TextView; @@ -186,6 +194,9 @@ class LibraryFragment : MainFragment() { //buttonFiles = findViewById(R.id.button_files); //metaInfo = findViewById(R.id.meta_info); + noContent = NoResultsView(context, "No directories", "No directories have been added.\nAdd them using the (+) icon.", -1, listOf()); + noContent.isVisible = false; + this.allowMusic = allowMusic ?: false; this.allowVideo = allowVideo ?: false; @@ -195,14 +206,6 @@ class LibraryFragment : MainFragment() { else fragment.requestPermissionMusic(); }); - val adapterArtists = sectionArtists.getAnyAdapter({ - it.onClick.subscribe { - if(it != null) - fragment.navigate(it); - } - }); - val artists = StateLibrary.instance.getArtists(ArtistOrdering.TrackCount); - adapterArtists.setData(artists); sectionAlbums.setSection("Albums", { if(this.allowMusic) @@ -210,14 +213,6 @@ class LibraryFragment : MainFragment() { else fragment.requestPermissionMusic(); }); - val adapterAlbums = sectionAlbums.getAnyAdapter({ - it.onClick.subscribe { - if(it != null) - fragment.navigate(it); - } - }); - val albums = StateLibrary.instance.getAlbums(); - adapterAlbums.setData(albums); sectionVideos.setSection("Videos", { @@ -226,21 +221,118 @@ class LibraryFragment : MainFragment() { else fragment.requestPermissionVideo(); }); + + reloadLibraryUI(); + + + /* + buttonFiles.onClick.subscribe { + fragment.navigate() + } */ + //buttonFiles.setButtonEnabled(false); + setMusicPermissions(allowMusic ?: false); + setVideoPermissions(allowVideo ?: false); + } + + fun reloadFiles() { + val files = StateLibrary.instance.getFileDirectories(); + adapterFiles?.setData(files); + if(files.size == 0) { + noContent.isVisible = true; + } + else + noContent.isVisible = false; + } + + fun reloadLibraryUI() { + + val adapterAlbums = sectionAlbums.getAnyAdapter({ + it.onClick.subscribe { + if(it != null) + fragment.navigate(it); + } + }); + val adapterArtists = sectionArtists.getAnyAdapter({ + it.onClick.subscribe { + if(it != null) + fragment.navigate(it); + } + }); val adapterVideos = sectionVideos.getAnyAdapter({ it.onClick.subscribe { if(it != null) fragment.navigate(it); } }); - val videos = StateLibrary.instance.getRecentVideos(null, 20); - adapterVideos.setData(videos); + + if(this.allowMusic) { + val artists = StateLibrary.instance.getArtists(ArtistOrdering.TrackCount); + adapterArtists.setData(artists); + if (artists.size == 0) + sectionArtists.setEmpty( + "No artists", + "No artists were found on your device", + -1 + ); + else + sectionArtists.clearEmpty(); + } + else if(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + sectionAlbums.isVisible = false; + } + else { + sectionArtists.setEmpty( + "No Music Permissions", + "You have not granted music access permissions to Grayjay", + -1 + ); + } + + if(this.allowMusic) { + val albums = StateLibrary.instance.getAlbums(); + adapterAlbums.setData(albums); + if (albums.size == 0) + sectionAlbums.setEmpty("No albums", "No albums were found on your device", -1); + else + sectionAlbums.clearEmpty(); + } + else if(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + sectionArtists.isVisible = false; + } + else { + sectionAlbums.setEmpty( + "No Music Permissions", + "You have not granted music access permissions to Grayjay", + -1 + ); + } + + if(this.allowVideo) { + val videos = StateLibrary.instance.getRecentVideos(null, 20); + adapterVideos.setData(videos); + if (videos.size == 0) + sectionVideos.setEmpty("No videos", "No videos were found on your device", -1); + else + sectionVideos.clearEmpty(); + } + else if(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + sectionVideos.isVisible = false; + } + else { + sectionVideos.setEmpty( + "No Video Permissions", + "You have not granted video access permissions to Grayjay", + -1 + ); + } adapterFiles = recycler.asAnyWithViews( arrayListOf( sectionArtists, sectionAlbums, sectionVideos, - sectionFiles + sectionFiles, + noContent ), arrayListOf(View(context).apply { this.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 20.dp(resources)) }), RecyclerView.VERTICAL, false, { @@ -257,23 +349,8 @@ class LibraryFragment : MainFragment() { } ); reloadFiles(); - - - /* - buttonFiles.onClick.subscribe { - fragment.navigate() - } */ - //buttonFiles.setButtonEnabled(false); - setMusicPermissions(allowMusic ?: false); - setVideoPermissions(allowVideo ?: false); } - fun reloadFiles() { - val files = StateLibrary.instance.getFileDirectories(); - adapterFiles.setData(files); - } - - fun setMusicPermissions(access: Boolean) { allowMusic = access; sectionAlbums.setContentEmptyMessage(R.drawable.ic_library, "No mediastore permissions"); @@ -283,6 +360,10 @@ class LibraryFragment : MainFragment() { // if(!allowMusic) "You did not give access to local music, so these options are disabled" else null, // if(!allowVideo) "You did not give access to local videos, so these options are disabled" else null //).filterNotNull().joinToString("\n"); + + fragment.lifecycleScope.launch(Dispatchers.Main) { + reloadLibraryUI(); + } } fun setVideoPermissions(access: Boolean) { allowVideo = access; @@ -291,10 +372,22 @@ class LibraryFragment : MainFragment() { // if(!allowMusic) "You did not give access to local music, so these options are disabled" else null, // if(!allowVideo) "You did not give access to local videos, so these options are disabled" else null //).filterNotNull().joinToString("\n"); + // } + + fragment.lifecycleScope.launch(Dispatchers.Main) { + reloadLibraryUI(); + } } fun onShown() { + if(didShowAlpha) + return; + didShowAlpha = true; UIDialogs.appToast("Library is in alpha\nImprovements are coming to local media playback.") } + companion object { + var didShowAlpha: Boolean = false; + } } + } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index e27f81b7..009b85e9 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -1753,7 +1753,9 @@ class VideoDetailView : ConstraintLayout { } } catch (e: Throwable) { Logger.e(TAG, "Failed to get polycentric likes/dislikes.", e); - _rating.visibility = View.GONE; + fragment.lifecycleScope.launch(Dispatchers.Main) { + _rating.visibility = View.GONE; + } } } } 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 659077d2..f22f1835 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -68,6 +68,20 @@ class StateApp { val sessionId = UUID.randomUUID().toString(); + + var airplaneMode: Boolean = false + get(){ + return field; + } + private set(value) { + field = value; + } + val airplaneModeChanged = Event1(); + fun setAirMode(value: Boolean) { + airplaneMode = value; + airplaneModeChanged.emit(airplaneMode); + } + var privateMode: Boolean = false get(){ return field; diff --git a/app/src/main/java/com/futo/platformplayer/states/StateLibrary.kt b/app/src/main/java/com/futo/platformplayer/states/StateLibrary.kt index d7680452..2a5c4c8e 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateLibrary.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateLibrary.kt @@ -4,6 +4,7 @@ import android.content.ContentUris import android.content.Intent import android.database.Cursor import android.net.Uri +import android.os.Build import android.provider.MediaStore import android.provider.MediaStore.Audio.Artists import android.webkit.MimeTypeMap @@ -36,6 +37,8 @@ import java.io.File import java.time.Instant import java.time.OffsetDateTime import java.time.ZoneOffset +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap class StateLibrary { @@ -192,6 +195,8 @@ class StateLibrary { private var _cacheBucketNames: List? = null; fun getVideoBucketNames(): List { + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) + return listOf(); if(_cacheBucketNames != null) return _cacheBucketNames ?: listOf(); try { @@ -236,7 +241,6 @@ class StateLibrary { val PROJECTION_VIDEO = arrayOf( MediaStore.Video.Media._ID, MediaStore.Video.Media.DISPLAY_NAME, - MediaStore.Video.Media.AUTHOR, MediaStore.Video.Media.DATE_ADDED, MediaStore.Video.Media.MIME_TYPE, MediaStore.Video.Media.BUCKET_DISPLAY_NAME @@ -406,10 +410,10 @@ class StateLibrary { fun videoFromCursor(cursor: Cursor): IPlatformVideoDetails { val id = cursor.getString(0); val displayName = cursor.getString(1); - val author = cursor.getString(2); - val date = cursor.getLong(3); - val contentType = cursor.getString(4); - val category = cursor.getString(5); + val author = null;//cursor.getString(2); + val date = cursor.getLong(2); + val contentType = cursor.getString(3); + val category = cursor.getString(4); val idLong = id.toLongOrNull(); val contentUrl = if(idLong != null ) @@ -486,6 +490,10 @@ class Artist { return AdhocPager({ listOf() }, getTracksPager(idLong)); } + fun getThumbnailOrAlbum(): String? { + return thumbnail ?: tryGetArtistThumbnail(id.toLongOrNull()); + } + companion object { val ID_UNKNOWN = "UNKNOWN"; val PROJECTION: Array = arrayOf(Artists._ID, @@ -493,6 +501,20 @@ class Artist { Artists.NUMBER_OF_TRACKS, Artists.NUMBER_OF_ALBUMS); + val thumbnailCache = ConcurrentHashMap(); + + fun tryGetArtistThumbnail(artistId: Long?): String? { + if(artistId == null) + return null; + if(thumbnailCache.containsKey(artistId)) + return thumbnailCache.get(artistId); + else { + val album = Album.getArtistAlbumWithThumbnail(artistId); + thumbnailCache.put(artistId, album?.thumbnail ?: ""); + return album?.thumbnail; + } + } + fun fromCursor(cursor: Cursor): Artist { val id = cursor.getString(0); val artist = cursor.getString(1); @@ -538,8 +560,11 @@ class Artist { cursor.moveToFirst(); val list = mutableListOf() while(!cursor.isAfterLast) { - list.add(fromCursor(cursor)); + val artist = fromCursor(cursor); cursor.moveToNext(); + if(artist.name == "") + continue; //TODO: Better way of detecting unknown? + list.add(artist); } return@use list; } @@ -683,6 +708,26 @@ class Album { return@use list; } } + fun getArtistAlbumWithThumbnail(artistId: Long): Album? { + val resolver = StateApp.instance.contextOrNull?.contentResolver; + if(resolver == null) { + Logger.w(TAG, "Album contentResolver not found"); + return null; + } + val cursor = resolver?.query( + MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, PROJECTION, "${MediaStore.Audio.Media.ARTIST_ID} = ?", arrayOf(artistId.toString()), + MediaStore.Audio.Albums.ALBUM + " ASC") ?: return null; + return cursor.use { + cursor.moveToFirst(); + while(!cursor.isAfterLast) { + val album = fromCursor(cursor); + if(album.thumbnail != null) + return album + cursor.moveToNext(); + } + return@use null; + } + } } } diff --git a/app/src/main/java/com/futo/platformplayer/views/LibrarySection.kt b/app/src/main/java/com/futo/platformplayer/views/LibrarySection.kt index 176175be..ccaccbb4 100644 --- a/app/src/main/java/com/futo/platformplayer/views/LibrarySection.kt +++ b/app/src/main/java/com/futo/platformplayer/views/LibrarySection.kt @@ -22,13 +22,14 @@ class LibrarySection: ConstraintLayout { val imageNavigate: ImageView; val recycler: RecyclerView; + val noContent: NoResultsView; constructor(context: Context, attr: AttributeSet? = null) : super(context, attr) { inflate(context, R.layout.view_library_section, this); textName = findViewById(R.id.text_label) imageNavigate = findViewById(R.id.image_nav) recycler = findViewById(R.id.recycler_collection); - + noContent = findViewById(R.id.container_no_content); } fun setNavIcon(resId: Int) { @@ -46,4 +47,14 @@ class LibrarySection: ConstraintLayout { textName.text = title; imageNavigate.setOnClickListener { onOpen.invoke() }; } + + fun setEmpty(title: String, txt: String, iconId: Int) { + noContent.isVisible = true; + recycler.isVisible = false; + noContent.setText(title, txt, iconId); + } + fun clearEmpty() { + noContent.isVisible = false; + recycler.isVisible = true; + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/NoResultsView.kt b/app/src/main/java/com/futo/platformplayer/views/NoResultsView.kt index d3e710f5..5903f69a 100644 --- a/app/src/main/java/com/futo/platformplayer/views/NoResultsView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/NoResultsView.kt @@ -1,6 +1,7 @@ package com.futo.platformplayer.views import android.content.Context +import android.util.AttributeSet import android.view.View import android.widget.ImageView import android.widget.LinearLayout @@ -15,6 +16,13 @@ class NoResultsView: ConstraintLayout { val icon: ImageView; val containerExtraViews: LinearLayout; + constructor(context: Context, attributes: AttributeSet? = null) : super(context, attributes){ + inflate(context, R.layout.view_no_results, this); + textTitle = findViewById(R.id.text_title) + textCentered = findViewById(R.id.text_centered); + icon = findViewById(R.id.icon); + containerExtraViews = findViewById(R.id.container_extra_views); + } constructor(context: Context, title: String, text: String, iconId: Int, extraViews: List) : super(context) { inflate(context, R.layout.view_no_results, this); @@ -22,13 +30,21 @@ class NoResultsView: ConstraintLayout { textCentered = findViewById(R.id.text_centered); icon = findViewById(R.id.icon); containerExtraViews = findViewById(R.id.container_extra_views); + + setText(title, text, iconId, extraViews); + } + + + fun setText(title: String, text: String, iconId: Int = -1, extraViews: List? = null) { textTitle.text = title; textCentered.text = text; - icon.setImageResource(iconId); if(iconId < 0) icon.visibility = GONE; + else + icon.setImageResource(iconId); - for(view in extraViews) - containerExtraViews.addView(view); + if(extraViews != null) + for(view in extraViews) + containerExtraViews.addView(view); } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt index a7873328..9818694a 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt @@ -3,9 +3,7 @@ package com.futo.platformplayer.views.adapters import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import android.view.GestureDetector import android.view.LayoutInflater -import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.FrameLayout @@ -46,7 +44,7 @@ class CommentViewHolder : ViewHolder { private val _imageLikeIcon: ImageView; private val _textLikes: TextView; private val _imageDislikeIcon: ImageView; - private val _imageCopy: ImageView; + private val _buttonCopy: PillButton; private val _textDislikes: TextView; private val _buttonReplies: PillButton; private val _layoutRating: LinearLayout; @@ -70,7 +68,7 @@ class CommentViewHolder : ViewHolder { _textMetadata = itemView.findViewById(R.id.text_metadata); _textBody = itemView.findViewById(R.id.text_body); _imageLikeIcon = itemView.findViewById(R.id.image_like_icon); - _imageCopy = itemView.findViewById(R.id.image_copy); + _buttonCopy = itemView.findViewById(R.id.image_copy); _textLikes = itemView.findViewById(R.id.text_likes); _imageDislikeIcon = itemView.findViewById(R.id.image_dislike_icon); _textDislikes = itemView.findViewById(R.id.text_dislikes); @@ -105,7 +103,8 @@ class CommentViewHolder : ViewHolder { StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked) }; - _imageCopy.setOnLongClickListener { + _buttonCopy.setTransparant() + _buttonCopy.onClick.subscribe { val clipboard = viewGroup.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val text = comment?.message.orEmpty() val clip = ClipData.newPlainText("Comment", text) diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/ArtistTileViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/ArtistTileViewHolder.kt index 70fb0b4a..43eaea5e 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/ArtistTileViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/ArtistTileViewHolder.kt @@ -37,9 +37,10 @@ class ArtistTileViewHolder(val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder override fun bind(artist: Artist) { _artist = artist; _imageThumbnail?.let { - if (artist.thumbnail != null) + val thumbnail = artist.getThumbnailOrAlbum(); + if (thumbnail != null) Glide.with(it) - .load(artist.thumbnail) + .load(thumbnail) .placeholder(R.drawable.ic_artist) .into(it) else diff --git a/app/src/main/java/com/futo/platformplayer/views/pills/PillButton.kt b/app/src/main/java/com/futo/platformplayer/views/pills/PillButton.kt index 4ce1ecb1..a69bc736 100644 --- a/app/src/main/java/com/futo/platformplayer/views/pills/PillButton.kt +++ b/app/src/main/java/com/futo/platformplayer/views/pills/PillButton.kt @@ -7,11 +7,15 @@ import android.view.View import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.compose.ui.graphics.Color +import androidx.core.view.isVisible import com.futo.platformplayer.R import com.futo.platformplayer.constructs.Event0 +import com.futo.platformplayer.dp import com.futo.platformplayer.views.LoaderView class PillButton : LinearLayout { + val root: LinearLayout; val icon: ImageView; val text: TextView; val loaderView: LoaderView; @@ -23,6 +27,7 @@ class PillButton : LinearLayout { icon = findViewById(R.id.pill_icon); text = findViewById(R.id.pill_text); loaderView = findViewById(R.id.loader) + root = findViewById(R.id.root); val attrArr = context.obtainStyledAttributes(attrs, R.styleable.PillButton, 0, 0); val attrIconRef = attrArr.getResourceId(R.styleable.PillButton_pillIcon, -1); @@ -34,6 +39,13 @@ class PillButton : LinearLayout { val attrText = attrArr.getText(R.styleable.PillButton_pillText) ?: ""; text.text = attrText; + if(text.text.isNullOrBlank()) { + val dp6 = 6.dp(resources); + val dp7 = 7.dp(resources); + val dp12 = 12.dp(resources); + root.setPadding(dp7, dp6, dp7, dp7) + } + findViewById(R.id.root).setOnClickListener { if (_isLoading) { return@setOnClickListener @@ -43,6 +55,10 @@ class PillButton : LinearLayout { }; } + fun setTransparant() { + root.setBackgroundColor(0); + } + fun setLoading(loading: Boolean) { if (loading == _isLoading) { return diff --git a/app/src/main/res/drawable/background_menu.xml b/app/src/main/res/drawable/background_menu.xml new file mode 100644 index 00000000..f35f3b6e --- /dev/null +++ b/app/src/main/res/drawable/background_menu.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_menu_close.xml b/app/src/main/res/drawable/background_menu_close.xml new file mode 100644 index 00000000..d5689eb9 --- /dev/null +++ b/app/src/main/res/drawable/background_menu_close.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_menu_toggle.xml b/app/src/main/res/drawable/background_menu_toggle.xml new file mode 100644 index 00000000..91c9cd68 --- /dev/null +++ b/app/src/main/res/drawable/background_menu_toggle.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_menu_toggle_active.xml b/app/src/main/res/drawable/background_menu_toggle_active.xml new file mode 100644 index 00000000..32e54aa9 --- /dev/null +++ b/app/src/main/res/drawable/background_menu_toggle_active.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_flight.xml b/app/src/main/res/drawable/ic_flight.xml new file mode 100644 index 00000000..99f2fa1a --- /dev/null +++ b/app/src/main/res/drawable/ic_flight.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_artist.xml b/app/src/main/res/layout/fragment_artist.xml index 8110f62e..d727c042 100644 --- a/app/src/main/res/layout/fragment_artist.xml +++ b/app/src/main/res/layout/fragment_artist.xml @@ -60,7 +60,7 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + android:layout_marginStart="2dp" /> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_library_section.xml b/app/src/main/res/layout/view_library_section.xml index b2c26dd3..e849b980 100644 --- a/app/src/main/res/layout/view_library_section.xml +++ b/app/src/main/res/layout/view_library_section.xml @@ -41,6 +41,16 @@ android:layout_marginTop="10dp" android:orientation="horizontal" /> + +