Even more library work

This commit is contained in:
Kelvin
2025-11-11 01:13:35 +01:00
parent 75ef7085eb
commit d5a696289b
23 changed files with 1040 additions and 134 deletions
@@ -8,6 +8,7 @@ import com.caoccao.javet.values.reference.V8ValueError
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValuePromise import com.caoccao.javet.values.reference.V8ValuePromise
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
@@ -388,3 +389,14 @@ suspend fun <T> Deferred<T>.awaitCancelConverted(): T {
throw ex; throw ex;
} }
} }
fun <T> IPager<T>.toList(): List<T> {
val list = this.getResults().toMutableList();
while(this.hasMorePages()) {
this.nextPage();
list.addAll(this.getResults());
}
return list.toList();
}
@@ -83,6 +83,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment.St
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
import com.futo.platformplayer.fragment.mainactivity.main.WebDetailFragment import com.futo.platformplayer.fragment.mainactivity.main.WebDetailFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.FilesTopBarFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
@@ -154,6 +155,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragTopBarNavigation: NavigationTopBarFragment; lateinit var _fragTopBarNavigation: NavigationTopBarFragment;
lateinit var _fragTopBarImport: ImportTopBarFragment; lateinit var _fragTopBarImport: ImportTopBarFragment;
lateinit var _fragTopBarAdd: AddTopBarFragment; lateinit var _fragTopBarAdd: AddTopBarFragment;
lateinit var _fragTopBarFiles: FilesTopBarFragment;
//Frags BotBar //Frags BotBar
lateinit var _fragBotBarMenu: MenuBottomBarFragment; lateinit var _fragBotBarMenu: MenuBottomBarFragment;
@@ -332,6 +334,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragTopBarNavigation = NavigationTopBarFragment.newInstance(); _fragTopBarNavigation = NavigationTopBarFragment.newInstance();
_fragTopBarImport = ImportTopBarFragment.newInstance(); _fragTopBarImport = ImportTopBarFragment.newInstance();
_fragTopBarAdd = AddTopBarFragment.newInstance(); _fragTopBarAdd = AddTopBarFragment.newInstance();
_fragTopBarFiles = FilesTopBarFragment.newInstance();
//BotBars //BotBars
_fragBotBarMenu = MenuBottomBarFragment.newInstance(); _fragBotBarMenu = MenuBottomBarFragment.newInstance();
@@ -508,7 +511,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragLibraryArtists.topBar = _fragTopBarNavigation; _fragLibraryArtists.topBar = _fragTopBarNavigation;
_fragLibraryArtist.topBar = _fragTopBarNavigation; _fragLibraryArtist.topBar = _fragTopBarNavigation;
_fragLibraryVideos.topBar = _fragTopBarNavigation; _fragLibraryVideos.topBar = _fragTopBarNavigation;
_fragLibraryFiles.topBar = _fragTopBarNavigation; _fragLibraryFiles.topBar = _fragTopBarFiles;
_fragBrowser.topBar = _fragTopBarNavigation; _fragBrowser.topBar = _fragTopBarNavigation;
@@ -1288,6 +1291,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
VideoDetailFragment::class -> _fragVideoDetail as T; VideoDetailFragment::class -> _fragVideoDetail as T;
MenuBottomBarFragment::class -> _fragBotBarMenu as T; MenuBottomBarFragment::class -> _fragBotBarMenu as T;
GeneralTopBarFragment::class -> _fragTopBarGeneral as T; GeneralTopBarFragment::class -> _fragTopBarGeneral as T;
FilesTopBarFragment::class -> _fragTopBarFiles as T;
SearchTopBarFragment::class -> _fragTopBarSearch as T; SearchTopBarFragment::class -> _fragTopBarSearch as T;
CreatorsFragment::class -> _fragMainSubscriptions as T; CreatorsFragment::class -> _fragMainSubscriptions as T;
CommentsFragment::class -> _fragMainComments as T; CommentsFragment::class -> _fragMainComments as T;
@@ -39,6 +39,7 @@ import java.time.OffsetDateTime
import kotlin.math.max import kotlin.math.max
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment { abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
protected val _feedRoot: FrameLayout;
protected val _recyclerResults: RecyclerView; protected val _recyclerResults: RecyclerView;
protected val _overlayContainer: FrameLayout; protected val _overlayContainer: FrameLayout;
protected val _swipeRefresh: SwipeRefreshLayout; protected val _swipeRefresh: SwipeRefreshLayout;
@@ -67,7 +68,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
private var _sortByOptions: List<String>? = null; private var _sortByOptions: List<String>? = null;
private var _activeTags: List<String>? = null; private var _activeTags: List<String>? = null;
private var _nextPageHandler: TaskHandler<TPager, List<TResult>>; private var _nextPageHandler: TaskHandler<TPager, Pair<TPager, List<TResult>>>;
val recyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>; val recyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>;
val fragment: TFragment; val fragment: TFragment;
@@ -80,6 +81,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
this.fragment = fragment; this.fragment = fragment;
inflater.inflate(R.layout.fragment_feed, this); inflater.inflate(R.layout.fragment_feed, this);
_feedRoot = findViewById(R.id.feed_root);
_textCentered = findViewById(R.id.text_centered); _textCentered = findViewById(R.id.text_centered);
_emptyPagerContainer = findViewById(R.id.empty_pager_container); _emptyPagerContainer = findViewById(R.id.empty_pager_container);
_progressBar = findViewById(R.id.progress_bar); _progressBar = findViewById(R.id.progress_bar);
@@ -135,23 +137,27 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
_toolbarContentView = findViewById(R.id.container_toolbar_content); _toolbarContentView = findViewById(R.id.container_toolbar_content);
_nextPageHandler = TaskHandler<TPager, List<TResult>>({fragment.lifecycleScope}, { _nextPageHandler = TaskHandler<TPager, Pair<TPager, List<TResult>>>({fragment.lifecycleScope}, {
if (it is IAsyncPager<*>) if (it is IAsyncPager<*>)
it.nextPageAsync(); it.nextPageAsync();
else else
it.nextPage(); it.nextPage();
processPagerExceptions(it); processPagerExceptions(it);
return@TaskHandler it.getResults(); return@TaskHandler Pair(it, it.getResults());
}).success { }).success {
val pager = it.first;
val results = it.second
setLoading(false); setLoading(false);
val posBefore = recyclerData.results.size; val posBefore = recyclerData.results.size;
val filteredResults = filterResults(it); val filteredResults = filterResults(results);
recyclerData.results.addAll(filteredResults); recyclerData.results.addAll(filteredResults);
recyclerData.resultsUnfiltered.addAll(it); recyclerData.resultsUnfiltered.addAll(results);
recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size); recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size);
ensureEnoughContentVisible(filteredResults) if(pager.hasMorePages())
ensureEnoughContentVisible(filteredResults)
}.exception<Throwable> { }.exception<Throwable> {
Logger.w(TAG, "Failed to load next page.", it); Logger.w(TAG, "Failed to load next page.", it);
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, { UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
@@ -390,6 +396,9 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
protected fun finishRefreshLayoutLoader() { protected fun finishRefreshLayoutLoader() {
_swipeRefresh.isRefreshing = false; _swipeRefresh.isRefreshing = false;
} }
protected fun disableRefreshLayout() {
_swipeRefresh.isEnabled = false;
}
fun clearResults(){ fun clearResults(){
setPager(EmptyPager<TResult>() as TPager); setPager(EmptyPager<TResult>() as TPager);
@@ -5,6 +5,9 @@ import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.UISlideOverlays import com.futo.platformplayer.UISlideOverlays
@@ -12,6 +15,8 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.structures.AdhocPager import com.futo.platformplayer.api.media.structures.AdhocPager
import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.dp
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
import com.futo.platformplayer.states.Album import com.futo.platformplayer.states.Album
import com.futo.platformplayer.states.StateLibrary import com.futo.platformplayer.states.StateLibrary
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
@@ -76,6 +81,11 @@ class LibraryAlbumFragment : MainFragment() {
StatePlayer.instance.setPlaylist(playlist, focus = true, shuffle = true); StatePlayer.instance.setPlaylist(playlist, focus = true, shuffle = true);
} }
} }
/*
_feedRoot.updateLayoutParams<LinearLayout.LayoutParams> {
this.setMargins(0,-50.dp(resources),0,0)
} */
} }
fun onShown(parameter: Any?) { fun onShown(parameter: Any?) {
@@ -95,6 +95,7 @@ class LibraryAlbumsFragment : MainFragment() {
} }
_toolbarContentView.addView(libraryTypeHeader); _toolbarContentView.addView(libraryTypeHeader);
disableRefreshLayout();
} }
fun onShown() { fun onShown() {
@@ -105,6 +106,11 @@ class LibraryAlbumsFragment : MainFragment() {
setPager(AdhocPager<Album>({ listOf(); }, initialAlbums)); setPager(AdhocPager<Album>({ listOf(); }, initialAlbums));
} }
override fun reload() {
super.reload();
finishRefreshLayoutLoader();
}
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<Album>): InsertedViewAdapterWithLoader<AlbumTileViewHolder> { override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<Album>): InsertedViewAdapterWithLoader<AlbumTileViewHolder> {
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(), return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
childCountGetter = { dataset.size }, childCountGetter = { dataset.size },
@@ -52,6 +52,7 @@ import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder
import com.futo.platformplayer.views.adapters.viewholders.TrackViewHolder
import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.subscriptions.SubscribeButton import com.futo.platformplayer.views.subscriptions.SubscribeButton
@@ -125,7 +126,7 @@ class LibraryArtistFragment : MainFragment() {
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {} private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
init { init {
inflater.inflate(R.layout.fragment_channel, this) inflater.inflate(R.layout.fragment_artist, this)
val tabs: TabLayout = findViewById(R.id.tabs) val tabs: TabLayout = findViewById(R.id.tabs)
val viewPager: ViewPager2 = findViewById(R.id.view_pager) val viewPager: ViewPager2 = findViewById(R.id.view_pager)
@@ -339,7 +340,7 @@ class LibraryArtistFragment : MainFragment() {
_buttonSubscriptionSettings.visibility = _buttonSubscriptionSettings.visibility =
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
_textChannel.text = channel.name _textChannel.text = channel.name
_textChannelSub.text = "" _textChannelSub.text = "${channel.countTracks} songs, ${channel.countAlbums} albums";
var supportsPlaylists = false; var supportsPlaylists = false;
val playlistPosition = 1 val playlistPosition = 1
@@ -474,20 +475,66 @@ class LibraryArtistFragment : MainFragment() {
_lastArtist = artist; _lastArtist = artist;
} }
} }
class ArtistContentView : ContentFeedView<LibraryArtistFragment> { class ArtistContentView : FeedView<LibraryArtistFragment, IPlatformContent, IPlatformVideo, IPager<IPlatformContent>, TrackViewHolder> {
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator; override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
protected var _artist: Artist? = null;
constructor(fragment: LibraryArtistFragment, inflater: LayoutInflater) : super(fragment, inflater) { constructor(fragment: LibraryArtistFragment, inflater: LayoutInflater) : super(fragment, inflater) {
} }
fun setArtist(artist: Artist) { fun setArtist(artist: Artist) {
this._artist = artist;
val tracks = artist.getAudioTracks(); val tracks = artist.getAudioTracks();
if(tracks.getResults().isEmpty()) if(tracks.getResults().isEmpty())
UIDialogs.appToast("No tracks found"); UIDialogs.appToast("No tracks found");
setPager(tracks); setPager(tracks);
} }
override fun filterResults(results: List<IPlatformContent>): List<IPlatformVideo> {
return results.filter { it is IPlatformVideo }.map { it as IPlatformVideo };
}
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<IPlatformVideo>): InsertedViewAdapterWithLoader<TrackViewHolder> {
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
childCountGetter = { dataset.size },
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
childViewHolderFactory = { viewGroup, _ ->
val holder = TrackViewHolder(viewGroup);
holder.onClick.subscribe { c ->
val playlist = _artist?.toPlaylist();
if (playlist != null) {
val index = playlist.videos.indexOf(c);
if (index == -1)
return@subscribe;
StatePlayer.instance.setPlaylist(playlist, index, true);
}
};
holder.onOptions.subscribe {
if(it is IPlatformVideo)
UISlideOverlays.showVideoOptionsOverlay(it, _overlayContainer);
}
return@InsertedViewAdapterWithLoader holder;
}
);
}
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
val glmResults = GridLayoutManager(context, 1)
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
rightMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
8.0f,
context.resources.displayMetrics
).toInt()
}
return glmResults
}
override fun updateSpanCount(){ } override fun updateSpanCount(){ }
@@ -97,6 +97,7 @@ class LibraryArtistsFragment : MainFragment() {
} }
_toolbarContentView.addView(libraryTypeHeader); _toolbarContentView.addView(libraryTypeHeader);
disableRefreshLayout();
} }
fun onShown() { fun onShown() {
@@ -18,12 +18,14 @@ import com.futo.platformplayer.api.media.structures.AdhocPager
import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.fragment.mainactivity.topbar.FilesTopBarFragment
import com.futo.platformplayer.states.FileEntry import com.futo.platformplayer.states.FileEntry
import com.futo.platformplayer.states.StateLibrary import com.futo.platformplayer.states.StateLibrary
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.NoResultsView import com.futo.platformplayer.views.NoResultsView
import com.futo.platformplayer.views.adapters.AnyAdapter import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.viewholders.FileViewHolder
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
class LibraryFilesFragment : MainFragment() { class LibraryFilesFragment : MainFragment() {
@@ -46,7 +48,7 @@ class LibraryFilesFragment : MainFragment() {
override fun onShownWithView(parameter: Any?, isBack: Boolean) { override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack); super.onShownWithView(parameter, isBack);
view?.onShown(); view?.onShown(parameter);
} }
override fun onDestroyMainView() { override fun onDestroyMainView() {
@@ -65,30 +67,51 @@ class LibraryFilesFragment : MainFragment() {
var buttonUp: BigButton? = null; var buttonUp: BigButton? = null;
var buttonAdd: BigButton? = null; var buttonAdd: BigButton? = null;
private var root: FileEntry? = null;
constructor(fragment: LibraryFilesFragment, inflater: LayoutInflater) : super(fragment, inflater) { constructor(fragment: LibraryFilesFragment, inflater: LayoutInflater) : super(fragment, inflater) {
} }
fun onShown() { fun onShown(parameter: Any? = null) {
this.root = if(parameter is FileEntry) parameter else null;
loadTop(); loadTop();
} }
fun loadTop() { fun loadTop() {
val initialDirectories = StateLibrary.instance.getFileDirectories(); var initialDirectories = listOf<FileEntry>();
if(initialDirectories.size == 0) { if(root == null) {
setEmptyPager(true); initialDirectories = StateLibrary.instance.getFileDirectories();
setPager(EmptyPager()); if (initialDirectories.size == 0) {
setEmptyPager(true);
setPager(EmptyPager());
buttonAdd?.let {
it.isVisible = false;
}
buttonUp?.let {
it.isVisible = false;
}
return;
} else
setEmptyPager(false);
}
else {
buttonAdd?.let { buttonAdd?.let {
it.isVisible = false; it.isVisible = false;
} }
buttonUp?.let { buttonUp?.let {
it.isVisible = false; it.isVisible = false;
} }
return; initialDirectories = root?.getSubFiles() ?: listOf();
} }
else
setEmptyPager(false);
navStack.clear(); navStack.clear();
navStack.add(FileStack("", initialDirectories)); val entry = FileStack("", initialDirectories);
navStack.add(entry);
openDirectory(navStack.last()); openDirectory(navStack.last());
fragment.topBar?.let {
if(it is FilesTopBarFragment) {
it.setUpNavigate(null);
it.setTitle(entry);
}
}
} }
fun leaveDirectory() { fun leaveDirectory() {
if(navStack.size > 1) { if(navStack.size > 1) {
@@ -101,6 +124,12 @@ class LibraryFilesFragment : MainFragment() {
if(addToStack) if(addToStack)
navStack.add(stack); navStack.add(stack);
fragment.topBar?.let {
if(it is FilesTopBarFragment) {
it.setTitle(stack);
}
}
buttonAdd?.let { buttonAdd?.let {
it.isVisible = navStack.size < 2 it.isVisible = navStack.size < 2
} }
@@ -109,6 +138,21 @@ class LibraryFilesFragment : MainFragment() {
} }
setPager(AdhocPager<FileEntry>({ listOf(); }, stack.files)); setPager(AdhocPager<FileEntry>({ listOf(); }, stack.files));
setLoading(false); setLoading(false);
fragment.topBar?.let {
if(it is FilesTopBarFragment) {
if(navStack.size > 1)
it.setUpNavigate{
leaveDirectory();
};
else it.setUpNavigate(null);
it.setTitle(stack);
}
}
}
fun setBack() {
fragment.topBar?.view
} }
override fun getEmptyPagerView(): View? { override fun getEmptyPagerView(): View? {
@@ -116,14 +160,15 @@ class LibraryFilesFragment : MainFragment() {
"To see files in Grayjay you have to add directories to view", "To see files in Grayjay you have to add directories to view",
R.drawable.ic_library, listOf( R.drawable.ic_library, listOf(
BigButton(context, "Add Directory", "Select a directory to add", R.drawable.ic_add, { BigButton(context, "Add Directory", "Select a directory to add", R.drawable.ic_add, {
StateLibrary.instance.addFileDirectory { StateLibrary.instance.addFileDirectory({
loadTop(); loadTop();
}; }, true);
}) })
)) ))
} }
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<FileEntry>): InsertedViewAdapterWithLoader<FileViewHolder> { override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<FileEntry>): InsertedViewAdapterWithLoader<FileViewHolder> {
/*
val buttonUp = BigButton(fragment.requireContext(), "Go up", "Go up a directory", R.drawable.ic_move_up) { val buttonUp = BigButton(fragment.requireContext(), "Go up", "Go up a directory", R.drawable.ic_move_up) {
if(navStack.size > 1) if(navStack.size > 1)
leaveDirectory(); leaveDirectory();
@@ -133,9 +178,10 @@ class LibraryFilesFragment : MainFragment() {
loadTop(); loadTop();
}; };
} }
this.buttonUp = buttonUp; */
this.buttonAdd = buttonAdd; //this.buttonUp = buttonUp;
return InsertedViewAdapterWithLoader(context, arrayListOf(buttonUp), arrayListOf(buttonAdd), //this.buttonAdd = buttonAdd;
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
childCountGetter = { dataset.size }, childCountGetter = { dataset.size },
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); }, childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
childViewHolderFactory = { viewGroup, _ -> childViewHolderFactory = { viewGroup, _ ->
@@ -185,51 +231,4 @@ class LibraryFilesFragment : MainFragment() {
val files: List<FileEntry> val files: List<FileEntry>
) )
class FileViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<FileEntry>(
LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_file,
_viewGroup, false)) {
val onClick = Event1<FileEntry?>();
val onDelete = Event1<FileEntry?>();
protected var _file: FileEntry? = null;
protected val _imageThumbnail: ImageView
protected val _buttonDelete: ImageButton;
protected val _textName: TextView
protected val _textMetadata: TextView
init {
_imageThumbnail = _view.findViewById(R.id.image_thumbnail);
_textName = _view.findViewById(R.id.text_name);
_textMetadata = _view.findViewById(R.id.text_metadata);
_buttonDelete = _view.findViewById(R.id.button_delete);
_view.setOnClickListener { onClick.emit(_file) };
_buttonDelete.setOnClickListener { onDelete.emit(_file) }
}
override fun bind(file: FileEntry) {
_file = file;
_imageThumbnail?.let {
if(file.isDirectory)
it.setImageResource(R.drawable.ic_library);
else {
Glide.with(it)
.load(file.thumbnail)
.placeholder(R.drawable.placeholder_channel_thumbnail)
.into(it)
}
};
_buttonDelete.isVisible = file.removable;
_textName.text = file.name;
if(file.isDirectory)
_textMetadata.text = "Directory";
else
_textMetadata.text = "";
}
}
} }
@@ -6,6 +6,7 @@ import android.os.Bundle
import android.provider.MediaStore import android.provider.MediaStore
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
@@ -37,6 +38,7 @@ import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.InsertedViewAdapter import com.futo.platformplayer.views.adapters.InsertedViewAdapter
import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder
import com.futo.platformplayer.views.adapters.viewholders.ArtistTileViewHolder 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.adapters.viewholders.LocalVideoTileViewHolder
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
@@ -146,7 +148,7 @@ class LibraryFragment : MainFragment() {
val recycler: RecyclerView; val recycler: RecyclerView;
val adapterFiles: AnyInsertedAdapterView<FileEntry,LibraryFilesFragment.FileViewHolder>; val adapterFiles: AnyInsertedAdapterView<FileEntry, FileViewHolder>;
//var metaInfo: TextView; //var metaInfo: TextView;
@@ -174,9 +176,9 @@ class LibraryFragment : MainFragment() {
this.setMargins(0,0, 0, 0); this.setMargins(0,0, 0, 0);
} }
sectionFiles.setSection("Directories") { sectionFiles.setSection("Directories") {
StateLibrary.instance.addFileDirectory { StateLibrary.instance.addFileDirectory({
reloadFiles(); reloadFiles();
} }, true)
} }
sectionFiles.setNavIcon(R.drawable.ic_add); sectionFiles.setNavIcon(R.drawable.ic_add);
//buttonFiles = findViewById<BigButton>(R.id.button_files); //buttonFiles = findViewById<BigButton>(R.id.button_files);
@@ -231,13 +233,15 @@ class LibraryFragment : MainFragment() {
val videos = StateLibrary.instance.getRecentVideos(null, 20); val videos = StateLibrary.instance.getRecentVideos(null, 20);
adapterVideos.setData(videos); adapterVideos.setData(videos);
adapterFiles = recycler.asAnyWithViews<FileEntry, LibraryFilesFragment.FileViewHolder>( adapterFiles = recycler.asAnyWithViews<FileEntry, FileViewHolder>(
arrayListOf( arrayListOf(
sectionArtists, sectionArtists,
sectionAlbums, sectionAlbums,
sectionVideos, sectionVideos,
sectionFiles sectionFiles
), arrayListOf(), RecyclerView.VERTICAL, false, { ),
arrayListOf(View(context).apply { this.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 20.dp(resources)) }),
RecyclerView.VERTICAL, false, {
it.onClick.subscribe { it.onClick.subscribe {
if(it != null) if(it != null)
fragment.navigate<LibraryFilesFragment>(it); fragment.navigate<LibraryFilesFragment>(it);
@@ -0,0 +1,210 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Context
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.LinearLayout.GONE
import android.widget.LinearLayout.VISIBLE
import android.widget.Spinner
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.widget.addTextChangedListener
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.UISlideOverlays
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.structures.AdhocPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.dp
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.Album
import com.futo.platformplayer.states.Artist
import com.futo.platformplayer.states.ArtistOrdering
import com.futo.platformplayer.states.FileEntry
import com.futo.platformplayer.states.StateLibrary
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.AnyInsertedAdapterView
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithViews
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.LibrarySection
import com.futo.platformplayer.views.LibraryTypeHeaderView
import com.futo.platformplayer.views.LibraryTypeHeaderView.SelectedType
import com.futo.platformplayer.views.PillV2
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
import com.futo.platformplayer.views.adapters.viewholders.AlbumTileViewHolder
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.adapters.viewholders.SelectablePlaylist
import com.futo.platformplayer.views.adapters.viewholders.TrackViewHolder
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.platform.PlatformIndicator
class LibrarySearchFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
var view: FragView? = null;
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = FragView(this);
this.view = view;
return view;
}
override fun onShown(parameter: Any?, isBack: Boolean) {
super.onShown(parameter, isBack)
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
view?.onShown();
}
override fun onDestroyMainView() {
view = null;
super.onDestroyMainView();
}
companion object {
fun newInstance() = LibrarySearchFragment().apply {}
}
class FragView: ConstraintLayout {
val fragment: LibrarySearchFragment;
val pillArtist: PillV2;
val pillAlbums: PillV2;
val pillSongs: PillV2;
val pills: List<PillV2>;
val recycler: RecyclerView;
val adapterArtists: AnyAdapterView<Artist, LibraryArtistsFragment.ArtistViewHolder>;
val adapterSongs: AnyAdapterView<IPlatformContent, TrackViewHolder>;
val adapterAlbums: AnyAdapterView<Album, AlbumTileViewHolder>;
constructor(fragment: LibrarySearchFragment) : super(fragment.requireContext()) {
inflate(context, R.layout.fragview_library, this);
this.fragment = fragment;
pillArtist = findViewById(R.id.pill_artist);
pillAlbums = findViewById(R.id.pill_albums);
pillSongs = findViewById(R.id.pill_songs);
pills = listOf(pillArtist, pillAlbums, pillSongs);
pillArtist.onClick.subscribe {
pills.forEach { it.setIsEnabled(false) };
pillArtist.setIsEnabled(true);
loadArtists();
}
pillAlbums.onClick.subscribe {
pills.forEach { it.setIsEnabled(false) };
pillAlbums.setIsEnabled(true);
loadAlbums();
}
pillSongs.onClick.subscribe {
pills.forEach { it.setIsEnabled(false) };
pillSongs.setIsEnabled(true);
loadSongs();
}
recycler = findViewById(R.id.recycler);
adapterArtists = recycler.asAny<Artist, LibraryArtistsFragment.ArtistViewHolder>(RecyclerView.VERTICAL, false, {
it.onClick.subscribe {
if(it != null)
fragment.navigate<LibraryArtistFragment>(it);
}
});
adapterAlbums = recycler.asAny<Album, AlbumTileViewHolder>(RecyclerView.VERTICAL, false, {
it.onClick.subscribe {
if(it != null)
fragment.navigate<LibraryAlbumFragment>(it);
}
});
adapterSongs = recycler.asAny<IPlatformContent, TrackViewHolder>(RecyclerView.VERTICAL, false, {
it.onClick.subscribe {
if(it != null && it is IPlatformVideo)
fragment.navigate<VideoDetailFragment>(it);
}
});
fragment.topBar?.let {
if(it is SearchTopBarFragment) {
it.onSearch.subscribe {
search(it);
}
}
}
pillArtist.setIsEnabled(true);
loadArtists();
}
fun loadArtists(){
recycler.adapter = adapterArtists.adapter.adapter;
}
fun loadAlbums() {
recycler.adapter = adapterAlbums.adapter.adapter;
}
fun loadSongs() {
recycler.adapter = adapterSongs.adapter.adapter;
}
fun search(str: String) {
if(recycler.adapter == adapterArtists.adapter.adapter) {
if(str.isNullOrBlank())
adapterArtists.adapter.setData(listOf());
else
adapterArtists.setData(StateLibrary.instance.searchArtists(str));
}
else if(recycler.adapter == adapterAlbums.adapter.adapter) {
if(str.isNullOrBlank())
adapterAlbums.adapter.setData(listOf());
else
adapterAlbums.setData(StateLibrary.instance.searchAlbums(str));
}
else if(recycler.adapter == adapterSongs.adapter.adapter) {
if(str.isNullOrBlank())
adapterSongs.adapter.setData(listOf());
else
adapterSongs.setData(StateLibrary.instance.searchTracks(str));
}
}
fun onShown() {
}
}
}
@@ -90,6 +90,7 @@ class LibraryVideosFragment : MainFragment() {
constructor(fragment: LibraryVideosFragment, inflater: LayoutInflater) : super(fragment, inflater) { constructor(fragment: LibraryVideosFragment, inflater: LayoutInflater) : super(fragment, inflater) {
initializeToolbarContent(); initializeToolbarContent();
disableRefreshLayout();
} }
fun onShown() { fun onShown() {
@@ -0,0 +1,129 @@
package com.futo.platformplayer.fragment.mainactivity.topbar
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatImageView
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFilesFragment
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.FileEntry
import com.futo.platformplayer.views.casting.CastButton
import com.futo.polycentric.core.PolycentricProfile
class FilesTopBarFragment : TopFragment() {
private var _buttonBack: ImageButton? = null;
private var _buttonCast: CastButton? = null;
private var _textTitle: TextView? = null;
private var _menuItems: LinearLayout? = null;
private var _upHandle: (()->Unit)? = null;
override fun onShown(parameter: Any?) {
setTitle(parameter);
setMenuItems(listOf());
}
override fun onHide() {
}
fun setTitle(parameter: Any? = null) {
if(parameter is IPlatformChannel) {
_textTitle?.text = parameter.name;
} else if(parameter is PlatformAuthorLink) {
_textTitle?.text = parameter.name;
} else if (parameter is Playlist) {
_textTitle?.text = parameter.name;
} else if (parameter is String) {
_textTitle?.text = parameter;
} else if (parameter is IPlatformClient) {
_textTitle?.text = parameter.name;
} else if (parameter is PolycentricProfile) {
_textTitle?.text = parameter.systemState.username;
} else if(parameter is FileEntry) {
val treePrefix = "content://com.android.externalstorage.documents/tree/";
if(parameter.path.startsWith(treePrefix)) {
_textTitle?.text = parameter.path.substring(treePrefix.length - 1).replace("%3A", " ").replace("%2F", "/");
}
else if(parameter.path.isNullOrBlank())
_textTitle?.text = parameter.name;
else
_textTitle?.text = parameter.path;
}
else if(parameter is LibraryFilesFragment.FileStack) {
val treePrefix = "content://com.android.externalstorage.documents/tree/";
if(parameter.path.startsWith(treePrefix)) {
_textTitle?.text = parameter.path.substring(treePrefix.length - 1).replace("%3A", " ").replace("%2F", "/");
}
else
_textTitle?.text = parameter.path;
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_files_top_bar, container, false);
val buttonBack: ImageButton = view.findViewById(R.id.button_back);
_textTitle = view.findViewById(R.id.text_title);
_menuItems = view.findViewById(R.id.menu_buttons)
buttonBack.setOnClickListener {
if(_upHandle != null)
_upHandle?.invoke();
else
closeSegment();
};
_buttonBack = buttonBack;
return view;
}
fun setUpNavigate(handle: (()->Unit)? = null) {
_upHandle = handle;
_buttonBack?.setImageResource(if(handle == null) R.drawable.ic_back_nav else R.drawable.ic_arrow_up);
}
override fun onDestroyView() {
super.onDestroyView()
_buttonBack?.setOnClickListener(null);
_buttonBack = null;
_buttonCast?.cleanup();
_buttonCast = null;
_textTitle = null;
}
fun setMenuItems(items: List<Pair<Int, ()->Unit>>) {
_menuItems?.removeAllViews();
val dp4 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4f, resources.displayMetrics).toInt();
val dp9 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 9f, resources.displayMetrics).toInt();
for(item in items) {
val compatImageItem = AppCompatImageView(requireContext());
compatImageItem.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT);
compatImageItem.setImageResource(item.first);
compatImageItem.setPadding(dp4, dp9, dp4, dp9);
compatImageItem.scaleType = ImageView.ScaleType.FIT_CENTER;
compatImageItem.setOnClickListener {
item.second.invoke();
};
_menuItems?.addView(compatImageItem);
}
}
companion object {
fun newInstance() = FilesTopBarFragment().apply { }
}
}
@@ -279,29 +279,52 @@ class StateApp {
}; };
} }
} }
fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, purpose: String? = null, path: Uri?, handle: (Uri?)->Unit) fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, purpose: String? = null, path: Uri?, handle: (Uri?)->Unit) {
return requestDirectoryAccess(activity, name, purpose, path, handle, false);
}
fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, purpose: String? = null, path: Uri?, handle: (Uri?)->Unit, skipDialog: Boolean = false)
{ {
if(activity is Context) if(activity is Context)
{ {
UIDialogs.showDialog(activity, R.drawable.ic_security, "Directory required for\n${name}", "Please select a directory for ${name}.\n${purpose}".trim(), null, 0, if(skipDialog) {
UIDialogs.Action("Cancel", {}), val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
UIDialogs.Action("Ok", { if(path != null)
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
if(path != null) intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path); .or(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION .or(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
.or(Intent.FLAG_GRANT_READ_URI_PERMISSION) .or(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
.or(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
.or(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); activity.launchForResult(intent, 99) {
if(it.resultCode == Activity.RESULT_OK) {
handle(it.data?.data);
}
else
UIDialogs.showDialogOk(context, R.drawable.ic_security_pred, "No access granted");
};
}
else {
UIDialogs.showDialog(activity, R.drawable.ic_security, "Directory required for\n${name}", "Please select a directory for ${name}.\n${purpose}".trim(), null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Ok", {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
if(path != null)
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
.or(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.or(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
.or(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
activity.launchForResult(intent, 99) {
if(it.resultCode == Activity.RESULT_OK) {
handle(it.data?.data);
}
else
UIDialogs.showDialogOk(context, R.drawable.ic_security_pred, "No access granted");
};
}, UIDialogs.ActionStyle.PRIMARY));
}
activity.launchForResult(intent, 99) {
if(it.resultCode == Activity.RESULT_OK) {
handle(it.data?.data);
}
else
UIDialogs.showDialogOk(context, R.drawable.ic_security_pred, "No access granted");
};
}, UIDialogs.ActionStyle.PRIMARY));
} }
} }
@@ -29,6 +29,7 @@ import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.Album.Companion.TAG import com.futo.platformplayer.states.Album.Companion.TAG
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.toList
import java.io.File import java.io.File
import java.time.Instant import java.time.Instant
import java.time.OffsetDateTime import java.time.OffsetDateTime
@@ -63,30 +64,50 @@ class StateLibrary {
_files.remove(path); _files.remove(path);
_files.save(); _files.save();
} }
fun addFileDirectory(onAdded: ((entry: FileEntry) -> Unit)? = null): Boolean { fun addFileDirectory(onAdded: ((entry: FileEntry) -> Unit)? = null, skipDialog: Boolean = false): Boolean {
if(!StateApp.instance.isMainActive) if(!StateApp.instance.isMainActive)
return false; return false;
val mainActivity = StateApp.instance.contextOrNull as MainActivity? ?: return false; val mainActivity = StateApp.instance.contextOrNull as MainActivity? ?: return false;
StateApp.instance.requestDirectoryAccess(mainActivity, "Select Directory", StateApp.instance.requestDirectoryAccess(mainActivity, "Select Directory",
"Select a directory you would like to make accessible to Grayjay", null, { "Select a directory you would like to make accessible to Grayjay", null, {
if(it != null) { if(it != null) {
mainActivity.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_WRITE_URI_PERMISSION.or(Intent.FLAG_GRANT_READ_URI_PERMISSION)); mainActivity.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_WRITE_URI_PERMISSION.or(Intent.FLAG_GRANT_READ_URI_PERMISSION));
try { try {
val file = DocumentFile.fromTreeUri(mainActivity, it) ?: return@requestDirectoryAccess; val file = DocumentFile.fromTreeUri(mainActivity, it) ?: return@requestDirectoryAccess;
val dir = FileEntry.fromFile(file); val dir = FileEntry.fromFile(file);
_files.add(dir.path); _files.add(dir.path);
_files.save(); _files.save();
onAdded?.invoke(dir); onAdded?.invoke(dir);
}
catch(ex: Throwable) {
Logger.e(TAG, "Something went wrong converting requested directory", ex);
}
} }
catch(ex: Throwable) { }, skipDialog);
Logger.e(TAG, "Something went wrong converting requested directory", ex);
}
}
});
return false; return false;
} }
fun searchTracks(str: String): List<IPlatformVideo> {
val resolver = StateApp.instance.contextOrNull?.contentResolver;
if(resolver == null) {
Logger.w(TAG, "Album contentResolver not found");
return listOf();
}
val cursor = resolver?.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, StateLibrary.PROJECTION_MEDIA,
"LOWER(" + MediaStore.Audio.Media.DISPLAY_NAME + ") LIKE ? ", arrayOf(str.trim().lowercase()),
null) ?: return listOf();
cursor.moveToFirst();
val list = mutableListOf<IPlatformVideo>()
while(!cursor.isAfterLast) {
list.add(StateLibrary.audioFromCursor(cursor));
cursor.moveToNext();
}
return list;
}
fun getAlbums(): List<Album> { fun getAlbums(): List<Album> {
return Album.getAlbums(); return Album.getAlbums();
} }
@@ -96,6 +117,10 @@ class StateLibrary {
return getAlbum(idLong); return getAlbum(idLong);
return null; return null;
} }
fun searchAlbums(str: String): List<Album> {
return Album.getAlbums("LOWER(" + MediaStore.Audio.Albums.ALBUM + ") LIKE ? ", arrayOf(str.trim().lowercase()));
}
fun getAlbum(id: Long): Album? { fun getAlbum(id: Long): Album? {
return Album.getAlbum(id); return Album.getAlbum(id);
} }
@@ -109,6 +134,10 @@ class StateLibrary {
return getArtist(idLong); return getArtist(idLong);
return null; return null;
} }
fun searchArtists(str: String): List<Artist> {
return Artist.getArtists(ArtistOrdering.TrackCount, "LOWER(" + MediaStore.Audio.Artists.ARTIST + ") LIKE ? ", arrayOf(str.trim().lowercase()));
}
fun getArtist(id: Long): Artist? { fun getArtist(id: Long): Artist? {
return Artist.getArtist(id); return Artist.getArtist(id);
} }
@@ -401,6 +430,10 @@ class Artist {
return Album.getArtistAlbums(id.toLongOrNull() ?: return listOf()); return Album.getArtistAlbums(id.toLongOrNull() ?: return listOf());
} }
fun toPlaylist(tracks: List<IPlatformVideo>? = null): Playlist {
return Playlist(name, tracks?.map { SerializedPlatformVideo.fromVideo(it) } ?: getAudioTracks().toList().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) })
}
fun getAudioTracks(): IPager<IPlatformContent> { fun getAudioTracks(): IPager<IPlatformContent> {
val idLong = id.toLongOrNull() ?: return EmptyPager(); val idLong = id.toLongOrNull() ?: return EmptyPager();
return AdhocPager({ listOf() }, getTracksPager(idLong)); return AdhocPager({ listOf() }, getTracksPager(idLong));
@@ -441,7 +474,7 @@ class Artist {
return null; return null;
return Artist.fromCursor(cursor); return Artist.fromCursor(cursor);
} }
fun getArtists(ordering: ArtistOrdering = ArtistOrdering.Alphabethic): List<Artist> { fun getArtists(ordering: ArtistOrdering = ArtistOrdering.Alphabethic, query: String? = null, args: Array<String>? = null): List<Artist> {
val ordering = when(ordering) { val ordering = when(ordering) {
ArtistOrdering.Alphabethic -> Artists.ARTIST + " ASC"; ArtistOrdering.Alphabethic -> Artists.ARTIST + " ASC";
ArtistOrdering.AlbumCount -> Artists.NUMBER_OF_ALBUMS + " DESC"; ArtistOrdering.AlbumCount -> Artists.NUMBER_OF_ALBUMS + " DESC";
@@ -450,8 +483,8 @@ class Artist {
} }
val cursor = StateApp.instance.contextOrNull?.contentResolver?.query(Artists.EXTERNAL_CONTENT_URI, PROJECTION, val cursor = StateApp.instance.contextOrNull?.contentResolver?.query(Artists.EXTERNAL_CONTENT_URI, PROJECTION,
null, query,
null, args,
ordering) ?: return listOf(); ordering) ?: return listOf();
cursor.moveToFirst(); cursor.moveToFirst();
val list = mutableListOf<Artist>() val list = mutableListOf<Artist>()
@@ -557,14 +590,14 @@ class Album {
return null; return null;
return fromCursor(cursor); return fromCursor(cursor);
} }
fun getAlbums(): List<Album> { fun getAlbums(query: String? = null, args: Array<String>? = null): List<Album> {
val resolver = StateApp.instance.contextOrNull?.contentResolver; val resolver = StateApp.instance.contextOrNull?.contentResolver;
if(resolver == null) { if(resolver == null) {
Logger.w(TAG, "Album contentResolver not found"); Logger.w(TAG, "Album contentResolver not found");
return listOf(); return listOf();
} }
val cursor = resolver?.query( val cursor = resolver?.query(
MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, PROJECTION, null, null, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, PROJECTION, query, args,
MediaStore.Audio.Albums.ALBUM + " ASC") ?: return listOf(); MediaStore.Audio.Albums.ALBUM + " ASC") ?: return listOf();
cursor.moveToFirst(); cursor.moveToFirst();
val list = mutableListOf<Album>() val list = mutableListOf<Album>()
@@ -7,6 +7,8 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.views.others.ToggleTagView
class PillV2: FrameLayout { class PillV2: FrameLayout {
@@ -17,6 +19,26 @@ class PillV2: FrameLayout {
val onClick = Event1<Boolean>(); val onClick = Event1<Boolean>();
constructor(context: Context, name: String, isActive: Boolean = false, action: (PillV2, Boolean)->Unit, actionLong: ((PillV2, Boolean)->Unit)? = null): super(context) {
inflate(context, R.layout.view_tag_v2, this);
root = findViewById(R.id.root);
text = findViewById(R.id.text_tag);
text.text = name;
setIsEnabled(isActive);
setOnClickListener {
setIsEnabled(!isToggled);
onClick.emit(isToggled);
action(this, isToggled);
}
if(actionLong != null)
setOnLongClickListener {
actionLong(this, this.isToggled);
return@setOnLongClickListener true;
}
}
constructor(context: Context, attr: AttributeSet? = null) : super(context, attr) { constructor(context: Context, attr: AttributeSet? = null) : super(context, attr) {
inflate(context, R.layout.view_tag_v2, this); inflate(context, R.layout.view_tag_v2, this);
root = findViewById(R.id.root); root = findViewById(R.id.root);
@@ -0,0 +1,62 @@
package com.futo.platformplayer.views.adapters.viewholders
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.states.FileEntry
import com.futo.platformplayer.views.adapters.AnyAdapter
class FileViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<FileEntry>(
LayoutInflater.from(_viewGroup.context).inflate(
R.layout.list_file,
_viewGroup, false)) {
val onClick = Event1<FileEntry?>();
val onDelete = Event1<FileEntry?>();
protected var _file: FileEntry? = null;
protected val _imageThumbnail: ImageView
protected val _buttonDelete: ImageButton;
protected val _textName: TextView
//protected val _textMetadata: TextView
init {
_imageThumbnail = _view.findViewById(R.id.image_thumbnail);
_textName = _view.findViewById(R.id.text_name);
//_textMetadata = _view.findViewById(R.id.text_metadata);
_buttonDelete = _view.findViewById(R.id.button_delete);
_view.setOnClickListener { onClick.emit(_file) };
_buttonDelete.setOnClickListener { onDelete.emit(_file) }
}
override fun bind(file: FileEntry) {
_file = file;
_imageThumbnail?.let {
if(file.isDirectory)
it.setImageResource(R.drawable.ic_library);
else {
Glide.with(it)
.load(file.thumbnail)
.placeholder(R.drawable.ic_music)
.into(it)
}
};
_buttonDelete.isVisible = file.removable;
_textName.text = file.name;
//if(file.isDirectory)
// _textMetadata.text = "Directory";
//else
// _textMetadata.text = "";
}
}
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M450,780L450,294.92L222.15,522.77L180,480L480,180L780,480L737.85,522.77L510,294.92L510,780L450,780Z"/>
</vector>
+190
View File
@@ -0,0 +1,190 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/channel_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:orientation="vertical"
android:background="@color/black">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="170dp"
android:background="@color/transparent"
app:elevation="0dp">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="@color/transparent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:toolbarId="@+id/toolbar">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="40dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/image_channel_banner"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/channel_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:background="@color/overlay">
<com.futo.platformplayer.views.others.CreatorThumbnail
android:id="@+id/creator_thumbnail"
android:background="@drawable/rounded_outline"
android:layout_width="1dp"
android:layout_height="35dp"
android:contentDescription="@string/cd_creator_thumbnail"
android:layout_marginStart="8dp"
android:scaleType="fitCenter"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_channel_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="14dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:maxLines="1"
android:ellipsize="end"
tools:text="CHANNEL NAME"
app:layout_constraintLeft_toRightOf="@id/creator_thumbnail"
app:layout_constraintBottom_toTopOf="@id/text_metadata"
app:layout_constraintRight_toLeftOf="@id/button_sub_settings" />
<TextView
android:id="@+id/text_metadata"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="14dp"
android:textColor="@color/gray_ac"
android:fontFamily="@font/inter_light"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:maxLines="1"
android:ellipsize="end"
tools:text="17 videos"
app:layout_constraintLeft_toRightOf="@id/creator_thumbnail"
app:layout_constraintRight_toLeftOf="@id/button_sub_settings"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/button_sub_settings"
android:layout_width="30dp"
android:layout_height="30dp"
android:contentDescription="@string/cd_button_settings"
android:layout_marginTop="3dp"
android:layout_marginRight="10dp"
android:scaleType="fitCenter"
app:layout_constraintTop_toTopOf="@id/button_subscribe"
app:layout_constraintRight_toLeftOf="@id/button_subscribe"
android:src="@drawable/ic_settings" />
<com.futo.platformplayer.views.subscriptions.SubscribeButton
android:id="@+id/button_subscribe"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/cd_button_subscribe"
android:layout_marginEnd="4dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="40dp"
android:minHeight="0dp"
app:contentInsetStart="0dp"
app:contentInsetEnd="0dp"
app:layout_collapseMode="pin"
android:layout_gravity="bottom">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
app:tabMode="scrollable"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:tabSelectedTextColor="@color/white"
app:tabTextColor="@color/gray_8c"
android:background="@drawable/tab_border"
app:tabIndicatorColor="@color/white"
android:textSize="12dp"
android:textColor="@color/gray_8c"
android:fontFamily="@font/inter_medium"
app:tabTextAppearance="@style/Theme.FutoVideo.TextAppearance.TabLayout" />
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<LinearLayout
android:id="@+id/channel_loading_overlay"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:background="#77000000"
android:gravity="center">
<ImageView
android:id="@+id/channel_loader_frag"
android:layout_width="80dp"
android:layout_height="80dp"
app:srcCompat="@drawable/ic_loader_animated"
android:layout_gravity="center"
android:alpha="0.7"
android:contentDescription="@string/loading" />
</LinearLayout>
<FrameLayout
android:id="@+id/overlay_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <FrameLayout
android:id="@+id/feed_root"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="47dp"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageButton
android:id="@+id/button_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:contentDescription="@string/cd_button_back"
android:paddingLeft="16dp"
android:paddingRight="8dp"
app:srcCompat="@drawable/ic_back_nav" />
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_marginLeft="10dp"
android:layout_height="wrap_content"
android:textSize="12dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_extra_light"
tools:text="FUTO"
android:maxLines="2" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/menu_buttons"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal">
</LinearLayout>
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="false"
android:background="@drawable/bottom_menu_border"
android:id="@+id/root"
android:clickable="true">
<ScrollView
android:id="@+id/container_tags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp"
android:orientation="horizontal">
<com.futo.platformplayer.views.PillV2
android:id="@+id/pill_songs"
app:pillV2Text="Songs"
android:layout_margin="3dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.futo.platformplayer.views.PillV2
android:id="@+id/pill_artist"
app:pillV2Text="Artist"
android:layout_margin="3dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.futo.platformplayer.views.PillV2
android:id="@+id/pill_albums"
app:pillV2Text="Albums"
android:layout_margin="3dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</ScrollView>
<TextView
android:id="@+id/text_metadata"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_margin="3dp"
android:gravity="center_vertical"
android:textSize="13sp"
android:textColor="#D0D0D0"
android:text="0 artists"
app:layout_constraintLeft_toLeftOf="@id/container_tags"
app:layout_constraintTop_toBottomOf="@id/container_tags"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/text_metadata"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
+34 -17
View File
@@ -4,41 +4,58 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="68dp"
android:orientation="vertical" android:orientation="vertical"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginBottom="5dp" android:layout_marginBottom="5dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:background="@drawable/background_16_round_4dp"
android:id="@+id/root" android:id="@+id/root"
android:clickable="true"> android:clickable="true">
<com.google.android.material.imageview.ShapeableImageView <LinearLayout
android:id="@+id/image_thumbnail" android:id="@+id/image_thumbnail_container"
android:layout_height="50dp" android:layout_width="wrap_content"
android:layout_width="50dp" android:layout_height="match_parent"
android:scaleType="centerCrop" android:layout_marginTop="8dp"
app:shapeAppearanceOverlay="@style/roundedCorners_4dp" android:layout_marginBottom="8dp"
app:srcCompat="@drawable/placeholder_video_thumbnail" android:layout_marginLeft="8dp"
android:background="@drawable/video_thumbnail_outline"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent" /> app:layout_constraintLeft_toLeftOf="parent"
android:background="@drawable/background_1b_round_6dp">
<ImageView
android:id="@+id/image_thumbnail"
android:alpha="0.4"
android:layout_height="34dp"
android:layout_width="34dp"
android:scaleType="centerCrop"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" />
</LinearLayout>
<TextView <TextView
android:id="@+id/text_name" android:id="@+id/text_name"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:textSize="13dp" android:textSize="15dp"
android:textColor="@color/white" android:textColor="@color/white"
android:fontFamily="@font/inter_light" android:fontFamily="@font/inter_regular"
tools:text="Legendary grant recipient: Marvin Wißfeld Very Long Title That is Long" tools:text="Legendary grant recipient: Marvin Wißfeld Very Long Title That is Long"
android:maxLines="2" android:maxLines="2"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail" app:layout_constraintLeft_toRightOf="@id/image_thumbnail_container"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/button_trash" app:layout_constraintRight_toLeftOf="@id/button_delete"
app:layout_constraintBottom_toTopOf="@id/text_metadata" app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="10dp" /> android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"/>
<!--
<TextView <TextView
android:id="@+id/text_metadata" android:id="@+id/text_metadata"
android:layout_width="0dp" android:layout_width="0dp"
@@ -53,7 +70,7 @@
app:layout_constraintLeft_toRightOf="@id/image_thumbnail" app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintRight_toLeftOf="@id/button_delete" app:layout_constraintRight_toLeftOf="@id/button_delete"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="10dp" /> android:layout_marginStart="12dp" />-->
<ImageButton <ImageButton
@@ -18,7 +18,7 @@
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
android:layout_marginLeft="10dp" android:layout_marginLeft="10dp"
android:textSize="20sp" android:textSize="17sp"
android:text="Albums" /> android:text="Albums" />
<ImageView <ImageView
android:id="@+id/image_nav" android:id="@+id/image_nav"