diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt index 2e21e1115..8818aeeb9 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt @@ -10,4 +10,6 @@ import kotlinx.parcelize.Parcelize class ArtistWithAlbumsID3( @SerializedName("album") var albums: List? = null, + @SerializedName("appearsOn") + var appearsOn: List? = null, ) : ArtistID3(), Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java index 3509175f4..f872b403f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java @@ -3,7 +3,6 @@ import android.content.Context; import android.content.Intent; import android.content.res.Configuration; -import android.graphics.Rect; import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.NetworkInfo; @@ -11,12 +10,16 @@ import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; -import android.util.Log; +import android.view.Gravity; import android.view.View; -import android.view.ViewGroup; +import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.core.splashscreen.SplashScreen; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; +import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; import androidx.media3.common.MediaItem; @@ -48,6 +51,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.color.DynamicColors; +import com.google.android.material.navigation.NavigationView; import com.google.common.util.concurrent.MoreExecutors; import java.util.Objects; @@ -63,9 +67,12 @@ public class MainActivity extends BaseActivity { private FragmentManager fragmentManager; private NavHostFragment navHostFragment; private BottomNavigationView bottomNavigationView; + private FrameLayout bottomNavigationViewFrame; public NavController navController; + private DrawerLayout drawerLayout; + private NavigationView navigationView; private BottomSheetBehavior bottomSheetBehavior; - private boolean isLandscape = false; + public boolean isLandscape = false; private AssetLinkNavigator assetLinkNavigator; private AssetLinkUtil.AssetLink pendingAssetLink; @@ -111,6 +118,7 @@ protected void onStart() { protected void onResume() { super.onResume(); pingServer(); + toggleNavigationDrawerLockOnOrientationChange(); } @Override @@ -148,14 +156,8 @@ public void init() { goToLogin(); } - // Set bottom navigation height - if (isLandscape) { - ViewGroup.LayoutParams layoutParams = bottomNavigationView.getLayoutParams(); - Rect windowRect = new Rect(); - bottomNavigationView.getWindowVisibleDisplayFrame(windowRect); - layoutParams.width = windowRect.height(); - bottomNavigationView.setLayoutParams(layoutParams); - } + toggleNavigationDrawerLockOnOrientationChange(); + } // BOTTOM SHEET/NAVIGATION @@ -259,8 +261,12 @@ private void animateBottomNavigation(float slideOffset, int navigationHeight) { private void initNavigation() { bottomNavigationView = findViewById(R.id.bottom_navigation); + bottomNavigationViewFrame = findViewById(R.id.bottom_navigation_frame); navHostFragment = (NavHostFragment) fragmentManager.findFragmentById(R.id.nav_host_fragment); navController = Objects.requireNonNull(navHostFragment).getNavController(); + // This is the lateral slide-in drawer + drawerLayout = findViewById(R.id.drawer_layout); + navigationView = findViewById(R.id.nav_view); /* * In questo modo intercetto il cambio schermata tramite navbar e se il bottom sheet รจ aperto, @@ -277,16 +283,90 @@ private void initNavigation() { }); NavigationUI.setupWithNavController(bottomNavigationView, navController); + NavigationUI.setupWithNavController(navigationView, navController); } public void setBottomNavigationBarVisibility(boolean visibility) { if (visibility) { bottomNavigationView.setVisibility(View.VISIBLE); + bottomNavigationViewFrame.setVisibility(View.VISIBLE); } else { bottomNavigationView.setVisibility(View.GONE); + bottomNavigationViewFrame.setVisibility(View.GONE); + } + } + + public void toggleBottomNavigationBarVisibilityOnOrientationChange() { + // Ignore orientation change, bottom navbar always hidden + if (Preferences.getHideBottomNavbarOnPortrait()) { + setBottomNavigationBarVisibility(false); + setPortraitPlayerBottomSheetPeekHeight(56); + setSystemBarsVisibility(!isLandscape); + return; + } + + if (!isLandscape) { + // Show app navbar + show system bars + setPortraitPlayerBottomSheetPeekHeight(136); + setBottomNavigationBarVisibility(true); + setSystemBarsVisibility(true); + } else { + // Hide app navbar + hide system bars + setPortraitPlayerBottomSheetPeekHeight(56); + setBottomNavigationBarVisibility(false); + setSystemBarsVisibility(false); + } + } + + public void setNavigationDrawerLock(boolean locked) { + int mode = locked + ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED + : DrawerLayout.LOCK_MODE_UNLOCKED; + drawerLayout.setDrawerLockMode(mode); + } + + public void toggleNavigationDrawerLockOnOrientationChange() { + // Ignore orientation check, drawer always unlocked + if (Preferences.getEnableDrawerOnPortrait()) { + setNavigationDrawerLock(false); + return; + } + if (!isLandscape) { + setNavigationDrawerLock(true); + } else { + setNavigationDrawerLock(false); } } + public void setSystemBarsVisibility(boolean visibility) { + WindowInsetsControllerCompat insetsController; + View decorView = getWindow().getDecorView(); + insetsController = new WindowInsetsControllerCompat(getWindow(), decorView); + + if (visibility) { + WindowCompat.setDecorFitsSystemWindows(getWindow(), true); + insetsController.show(WindowInsetsCompat.Type.navigationBars()); + insetsController.show(WindowInsetsCompat.Type.statusBars()); + insetsController.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_DEFAULT); + } else { + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + insetsController.hide(WindowInsetsCompat.Type.navigationBars()); + insetsController.hide(WindowInsetsCompat.Type.statusBars()); + insetsController.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + } + + private void setPortraitPlayerBottomSheetPeekHeight(int peekHeight) { + FrameLayout bottomSheet = findViewById(R.id.player_bottom_sheet); + BottomSheetBehavior behavior = + BottomSheetBehavior.from(bottomSheet); + + int newPeekPx = (int) (peekHeight * getResources().getDisplayMetrics().density); + behavior.setPeekHeight(newPeekPx); + } + private void initService() { MediaManager.check(getMediaBrowserListenableFuture()); @@ -570,4 +650,4 @@ private void playDownloadedMedia(Intent intent) { MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCarouselAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCarouselAdapter.java new file mode 100644 index 000000000..5e06d853b --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCarouselAdapter.java @@ -0,0 +1,86 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemAlbumCarouselBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.util.Constants; + +import java.util.Collections; +import java.util.List; + +public class AlbumCarouselAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + private List albums; + private boolean showArtist; + + public AlbumCarouselAdapter(ClickCallback click, boolean showArtist) { + this.click = click; + this.albums = Collections.emptyList(); + this.showArtist = showArtist; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemAlbumCarouselBinding view = ItemAlbumCarouselBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + AlbumID3 album = albums.get(position); + + holder.item.albumNameLabel.setText(album.getName()); + holder.item.artistNameLabel.setText(album.getArtist()); + holder.item.artistNameLabel.setVisibility(showArtist ? View.VISIBLE : View.GONE); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), album.getCoverArtId(), CustomGlideRequest.ResourceType.Album) + .build() + .into(holder.item.albumCoverImageView); + } + + @Override + public int getItemCount() { + return albums.size(); + } + + public void setItems(List albums) { + this.albums = albums; + notifyDataSetChanged(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemAlbumCarouselBinding item; + + ViewHolder(ItemAlbumCarouselBinding item) { + super(item.getRoot()); + this.item = item; + + itemView.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ALBUM_OBJECT, albums.get(getBindingAdapterPosition())); + click.onAlbumClick(bundle); + }); + + itemView.setOnLongClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ALBUM_OBJECT, albums.get(getBindingAdapterPosition())); + click.onAlbumLongClick(bundle); + return true; + }); + + item.albumNameLabel.setSelected(true); + item.artistNameLabel.setSelected(true); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistCarouselAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistCarouselAdapter.java new file mode 100644 index 000000000..802725ba7 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistCarouselAdapter.java @@ -0,0 +1,80 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemArtistCarouselBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.util.Constants; + +import java.util.Collections; +import java.util.List; + +public class ArtistCarouselAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + private List artists; + + public ArtistCarouselAdapter(ClickCallback click) { + this.click = click; + this.artists = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemArtistCarouselBinding view = ItemArtistCarouselBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + ArtistID3 artist = artists.get(position); + + holder.item.artistNameLabel.setText(artist.getName()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), artist.getCoverArtId(), CustomGlideRequest.ResourceType.Artist) + .build() + .into(holder.item.artistCoverImageView); + } + + @Override + public int getItemCount() { + return artists.size(); + } + + public void setItems(List artists) { + this.artists = artists; + notifyDataSetChanged(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemArtistCarouselBinding item; + + ViewHolder(ItemArtistCarouselBinding item) { + super(item.getRoot()); + this.item = item; + + itemView.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artists.get(getBindingAdapterPosition())); + click.onArtistClick(bundle); + }); + + itemView.setOnLongClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artists.get(getBindingAdapterPosition())); + click.onArtistLongClick(bundle); + return true; + }); + + item.artistNameLabel.setSelected(true); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java index 38dc9b10b..574825bc9 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java @@ -93,6 +93,10 @@ private void init() { albumListPageViewModel.artist = requireArguments().getParcelable(Constants.ARTIST_OBJECT); albumListPageViewModel.title = Constants.ALBUM_FROM_ARTIST; bind.pageTitleLabel.setText(albumListPageViewModel.artist.getName()); + } else if (requireArguments().getParcelableArrayList(Constants.ALBUMS_OBJECT) != null) { + albumListPageViewModel.albums = requireArguments().getParcelableArrayList(Constants.ALBUMS_OBJECT); + albumListPageViewModel.title = requireArguments().getString(Constants.ALBUM_LIST_TITLE, ""); + bind.pageTitleLabel.setText(albumListPageViewModel.title); } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java index e995d209f..ccf4f6679 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java @@ -37,7 +37,8 @@ import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.ui.activity.MainActivity; -import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter; +import com.cappielloantonio.tempo.ui.adapter.AlbumCarouselAdapter; +import com.cappielloantonio.tempo.ui.adapter.ArtistCarouselAdapter; import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter; import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; import com.cappielloantonio.tempo.util.Constants; @@ -59,8 +60,11 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { private PlaybackViewModel playbackViewModel; private SongHorizontalAdapter songHorizontalAdapter; - private AlbumCatalogueAdapter albumCatalogueAdapter; - private ArtistCatalogueAdapter artistCatalogueAdapter; + private AlbumCarouselAdapter mainAlbumAdapter; + private AlbumCarouselAdapter epAdapter; + private AlbumCarouselAdapter singleAdapter; + private AlbumCarouselAdapter appearsOnAdapter; + private ArtistCarouselAdapter similarArtistAdapter; private ListenableFuture mediaBrowserListenableFuture; @@ -84,7 +88,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, initArtistInfo(); initPlayButtons(); initTopSongsView(); - initAlbumsView(); + initCategorizedAlbumsView(); initSimilarArtistsView(); return view; @@ -118,6 +122,7 @@ public void onDestroyView() { private void init(View view) { artistPageViewModel.setArtist(requireArguments().getParcelable(Constants.ARTIST_OBJECT)); + artistPageViewModel.fetchCategorizedAlbums(getViewLifecycleOwner()); bind.mostStreamedSongTextViewClickable.setOnClickListener(v -> { Bundle bundle = new Bundle(); @@ -275,40 +280,95 @@ private void initTopSongsView() { if (songs == null) { if (bind != null) bind.artistPageTopSongsSector.setVisibility(View.GONE); } else { - if (bind != null) + if (bind != null) { bind.artistPageTopSongsSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE); - songHorizontalAdapter.setItems(songs); + bind.mostStreamedSongTextViewClickable.setVisibility(songs.size() > 3 ? View.VISIBLE : View.GONE); + } + songHorizontalAdapter.setItems(songs.stream().limit(3).collect(java.util.stream.Collectors.toList())); reapplyPlayback(); } }); } - private void initAlbumsView() { - bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount)); - bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false)); - bind.albumsRecyclerView.setHasFixedSize(true); + private void initCategorizedAlbumsView() { + // Main Albums + bind.mainAlbumsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.mainAlbumsRecyclerView.setHasFixedSize(true); + mainAlbumAdapter = new AlbumCarouselAdapter(this, false); + bind.mainAlbumsRecyclerView.setAdapter(mainAlbumAdapter); + artistPageViewModel.getMainAlbums().observe(getViewLifecycleOwner(), albums -> { + if (bind != null) { + bind.artistPageMainAlbumsSector.setVisibility(albums != null && !albums.isEmpty() ? View.VISIBLE : View.GONE); + if (albums != null) { + bind.mainAlbumsSeeAllTextView.setVisibility(albums.size() > 5 ? View.VISIBLE : View.GONE); + mainAlbumAdapter.setItems(albums); + bind.mainAlbumsSeeAllTextView.setOnClickListener(v -> navigateToAlbumList(getString(R.string.artist_page_title_album_section), albums)); + } + } + }); + + // EPs + bind.epsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.epsRecyclerView.setHasFixedSize(true); + epAdapter = new AlbumCarouselAdapter(this, false); + bind.epsRecyclerView.setAdapter(epAdapter); + artistPageViewModel.getEPs().observe(getViewLifecycleOwner(), albums -> { + if (bind != null) { + bind.artistPageEpsSector.setVisibility(albums != null && !albums.isEmpty() ? View.VISIBLE : View.GONE); + if (albums != null) { + bind.epsSeeAllTextView.setVisibility(albums.size() > 5 ? View.VISIBLE : View.GONE); + epAdapter.setItems(albums); + bind.epsSeeAllTextView.setOnClickListener(v -> navigateToAlbumList(getString(R.string.artist_page_title_ep_section), albums)); + } + } + }); - albumCatalogueAdapter = new AlbumCatalogueAdapter(this, false); - bind.albumsRecyclerView.setAdapter(albumCatalogueAdapter); + // Singles + bind.singlesRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.singlesRecyclerView.setHasFixedSize(true); + singleAdapter = new AlbumCarouselAdapter(this, false); + bind.singlesRecyclerView.setAdapter(singleAdapter); + artistPageViewModel.getSingles().observe(getViewLifecycleOwner(), albums -> { + if (bind != null) { + bind.artistPageSinglesSector.setVisibility(albums != null && !albums.isEmpty() ? View.VISIBLE : View.GONE); + if (albums != null) { + bind.singlesSeeAllTextView.setVisibility(albums.size() > 5 ? View.VISIBLE : View.GONE); + singleAdapter.setItems(albums); + bind.singlesSeeAllTextView.setOnClickListener(v -> navigateToAlbumList(getString(R.string.artist_page_title_single_section), albums)); + } + } + }); - artistPageViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> { - if (albums == null) { - if (bind != null) bind.artistPageAlbumsSector.setVisibility(View.GONE); - } else { - if (bind != null) - bind.artistPageAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); - albumCatalogueAdapter.setItems(albums); + // Appears On + bind.appearsOnRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.appearsOnRecyclerView.setHasFixedSize(true); + appearsOnAdapter = new AlbumCarouselAdapter(this, true); // Show artist name for Appears On + bind.appearsOnRecyclerView.setAdapter(appearsOnAdapter); + artistPageViewModel.getAppearsOn().observe(getViewLifecycleOwner(), albums -> { + if (bind != null) { + bind.artistPageAppearsOnSector.setVisibility(albums != null && !albums.isEmpty() ? View.VISIBLE : View.GONE); + if (albums != null) { + bind.appearsOnSeeAllTextView.setVisibility(albums.size() > 5 ? View.VISIBLE : View.GONE); + appearsOnAdapter.setItems(albums); + bind.appearsOnSeeAllTextView.setOnClickListener(v -> navigateToAlbumList(getString(R.string.artist_page_title_appears_on_section), albums)); + } } }); } + private void navigateToAlbumList(String title, List albums) { + Bundle bundle = new Bundle(); + bundle.putString(Constants.ALBUM_LIST_TITLE, title); + bundle.putParcelableArrayList(Constants.ALBUMS_OBJECT, new ArrayList<>(albums)); + Navigation.findNavController(requireView()).navigate(R.id.albumListPageFragment, bundle); + } + private void initSimilarArtistsView() { - bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount)); - bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false)); + bind.similarArtistsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); bind.similarArtistsRecyclerView.setHasFixedSize(true); - artistCatalogueAdapter = new ArtistCatalogueAdapter(this); - bind.similarArtistsRecyclerView.setAdapter(artistCatalogueAdapter); + similarArtistAdapter = new ArtistCarouselAdapter(this); + bind.similarArtistsRecyclerView.setAdapter(similarArtistAdapter); artistPageViewModel.getArtistInfo(artistPageViewModel.getArtist().getId()).observe(getViewLifecycleOwner(), artist -> { if (artist == null) { @@ -323,7 +383,7 @@ private void initSimilarArtistsView() { artists.addAll(artist.getSimilarArtists()); } - artistCatalogueAdapter.setItems(artists); + similarArtistAdapter.setItems(artists); } }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java index 12ca24349..72d1e0f30 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java @@ -83,7 +83,7 @@ public void onStart() { super.onStart(); initializeMediaBrowser(); - activity.setBottomNavigationBarVisibility(true); + activity.toggleBottomNavigationBarVisibilityOnOrientationChange(); activity.setBottomSheetVisibility(true); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt index ac6608c73..56f6419bd 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt @@ -21,10 +21,12 @@ import com.cappielloantonio.tempo.R import com.cappielloantonio.tempo.service.EqualizerManager import com.cappielloantonio.tempo.service.BaseMediaService import com.cappielloantonio.tempo.service.MediaService +import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.util.Preferences class EqualizerFragment : Fragment() { + private lateinit var activity: MainActivity private var equalizerManager: EqualizerManager? = null private lateinit var eqBandsContainer: LinearLayout private lateinit var eqSwitch: Switch @@ -33,6 +35,13 @@ class EqualizerFragment : Fragment() { private val bandSeekBars = mutableListOf() private var receiverRegistered = false + + @OptIn(UnstableApi::class) + override fun onAttach(context: Context) { + super.onAttach(context) + activity = requireActivity() as MainActivity + } + private val equalizerUpdatedReceiver = object : BroadcastReceiver() { @OptIn(UnstableApi::class) override fun onReceive(context: Context?, intent: Intent?) { @@ -73,8 +82,13 @@ class EqualizerFragment : Fragment() { ) receiverRegistered = true } + activity.setBottomNavigationBarVisibility(false) + activity.setBottomSheetVisibility(false) + activity.setNavigationDrawerLock(true) + activity.setSystemBarsVisibility(!activity.isLandscape) } + @OptIn(UnstableApi::class) override fun onStop() { super.onStop() requireActivity().unbindService(connection) @@ -87,6 +101,8 @@ class EqualizerFragment : Fragment() { } receiverRegistered = false } + + activity.setBottomSheetVisibility(true); } override fun onCreateView( diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java index a0d0380c3..7697c01f7 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java @@ -53,7 +53,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat public void onStart() { super.onStart(); - activity.setBottomNavigationBarVisibility(true); + activity.toggleBottomNavigationBarVisibilityOnOrientationChange(); activity.setBottomSheetVisibility(true); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java index b50ee60f9..daa5d4121 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java @@ -87,7 +87,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat public void onStart() { super.onStart(); initializeMediaBrowser(); - activity.setBottomNavigationBarVisibility(true); + activity.toggleBottomNavigationBarVisibilityOnOrientationChange(); } @Override diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java new file mode 100644 index 000000000..e770976a3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java @@ -0,0 +1,603 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.audiofx.AudioEffect; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.text.InputFilter; +import android.text.InputType; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.os.LocaleListCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.NavController; +import androidx.navigation.NavOptions; +import androidx.navigation.fragment.NavHostFragment; +import androidx.preference.EditTextPreference; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.SwitchPreference; + +import com.cappielloantonio.tempo.BuildConfig; +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.helper.ThemeHelper; +import com.cappielloantonio.tempo.interfaces.DialogClickCallback; +import com.cappielloantonio.tempo.interfaces.ScanCallback; +import com.cappielloantonio.tempo.service.EqualizerManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog; +import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog; +import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog; +import com.cappielloantonio.tempo.ui.dialog.StarredArtistSyncDialog; +import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog; +import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.ExternalAudioReader; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.util.UIUtil; +import com.cappielloantonio.tempo.viewmodel.SettingViewModel; + +import java.util.Locale; +import java.util.Map; + +@OptIn(markerClass = UnstableApi.class) +public class SettingsContainerFragment extends PreferenceFragmentCompat { + + private static final String TAG = "SettingsFragment"; + private MainActivity activity; + + private SettingViewModel settingViewModel; + + private ActivityResultLauncher directoryPickerLauncher; + + private MediaService.LocalBinder mediaServiceBinder; + private boolean isServiceBound = false; + private ActivityResultLauncher equalizerResultLauncher; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + equalizerResultLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> {} + ); + + if (!BuildConfig.FLAVOR.equals("tempus")) { + PreferenceCategory githubUpdateCategory = findPreference("settings_github_update_category_key"); + if (githubUpdateCategory != null) { + getPreferenceScreen().removePreference(githubUpdateCategory); + } + } + + directoryPickerLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK) { + Intent data = result.getData(); + if (data != null) { + Uri uri = data.getData(); + if (uri != null) { + requireContext().getContentResolver().takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ); + + Preferences.setDownloadDirectoryUri(uri.toString()); + ExternalAudioReader.refreshCache(); + Toast.makeText(requireContext(), R.string.settings_download_folder_set, Toast.LENGTH_SHORT).show(); + checkDownloadDirectory(); + } + } + } + }); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + View view = super.onCreateView(inflater, container, savedInstanceState); + settingViewModel = new ViewModelProvider(requireActivity()).get(SettingViewModel.class); + + if (view != null) { + getListView().setPadding(0, 0, 0, (int) getResources().getDimension(R.dimen.global_padding_bottom)); + } + + return view; + } + + @Override + public void onStart() { + super.onStart(); + activity.setBottomNavigationBarVisibility(false); + activity.setBottomSheetVisibility(false); + } + + @Override + public void onResume() { + super.onResume(); + + checkSystemEqualizer(); + checkCacheStorage(); + checkStorage(); + checkDownloadDirectory(); + + setStreamingCacheSize(); + setAppLanguage(); + setVersion(); + setNetorkPingTimeoutBase(); + + actionLogout(); + actionScan(); + actionSyncStarredAlbums(); + actionSyncStarredTracks(); + actionSyncStarredArtists(); + actionChangeStreamingCacheStorage(); + actionChangeDownloadStorage(); + actionSetDownloadDirectory(); + actionDeleteDownloadStorage(); + actionKeepScreenOn(); + actionAutoDownloadLyrics(); + actionMiniPlayerHeart(); + + bindMediaService(); + actionAppEqualizer(); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.global_preferences, rootKey); + ListPreference themePreference = findPreference(Preferences.THEME); + if (themePreference != null) { + themePreference.setOnPreferenceChangeListener( + (preference, newValue) -> { + String themeOption = (String) newValue; + ThemeHelper.applyTheme(themeOption); + return true; + }); + } + } + + private void checkSystemEqualizer() { + Preference equalizer = findPreference("system_equalizer"); + + if (equalizer == null) return; + + Intent intent = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL); + + if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) { + equalizer.setOnPreferenceClickListener(preference -> { + equalizerResultLauncher.launch(intent); + return true; + }); + } else { + equalizer.setVisible(false); + } + } + + private void checkCacheStorage() { + Preference storage = findPreference("streaming_cache_storage"); + + if (storage == null) return; + + try { + if (requireContext().getExternalFilesDirs(null)[1] == null) { + storage.setVisible(false); + } else { + storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button); + } + } catch (Exception exception) { + storage.setVisible(false); + } + } + + private void checkStorage() { + Preference storage = findPreference("download_storage"); + + if (storage == null) return; + + try { + if (requireContext().getExternalFilesDirs(null)[1] == null) { + storage.setVisible(false); + } else { + int pref = Preferences.getDownloadStoragePreference(); + if (pref == 0) { + storage.setSummary(R.string.download_storage_internal_dialog_negative_button); + } else if (pref == 1) { + storage.setSummary(R.string.download_storage_external_dialog_positive_button); + } else { + storage.setSummary(R.string.download_storage_directory_dialog_neutral_button); + } + } + } catch (Exception exception) { + storage.setVisible(false); + } + } + + private void checkDownloadDirectory() { + Preference storage = findPreference("download_storage"); + Preference directory = findPreference("set_download_directory"); + + if (directory == null) return; + + String current = Preferences.getDownloadDirectoryUri(); + if (current != null) { + if (storage != null) storage.setVisible(false); + directory.setVisible(true); + directory.setIcon(R.drawable.ic_close); + directory.setTitle(R.string.settings_clear_download_folder); + directory.setSummary(current); + } else { + if (storage != null) storage.setVisible(true); + if (Preferences.getDownloadStoragePreference() == 2) { + directory.setVisible(true); + directory.setIcon(R.drawable.ic_folder); + directory.setTitle(R.string.settings_set_download_folder); + directory.setSummary(R.string.settings_choose_download_folder); + } else { + directory.setVisible(false); + } + } + } + + private void setNetorkPingTimeoutBase() { + EditTextPreference networkPingTimeoutBase = findPreference("network_ping_timeout_base"); + + if (networkPingTimeoutBase != null) { + networkPingTimeoutBase.setSummaryProvider(EditTextPreference.SimpleSummaryProvider.getInstance()); + networkPingTimeoutBase.setOnBindEditTextListener(editText -> { + editText.setInputType(InputType.TYPE_CLASS_NUMBER); + editText.setFilters(new InputFilter[]{ (source, start, end, dest, dstart, dend) -> { + for (int i = start; i < end; i++) { + if (!Character.isDigit(source.charAt(i))) { + return ""; + } + } + return null; + }}); + }); + + networkPingTimeoutBase.setOnPreferenceChangeListener((preference, newValue) -> { + String input = (String) newValue; + return input != null && !input.isEmpty(); + }); + } + } + + private void setStreamingCacheSize() { + ListPreference streamingCachePreference = findPreference("streaming_cache_size"); + + if (streamingCachePreference != null) { + streamingCachePreference.setSummaryProvider(new Preference.SummaryProvider() { + @Nullable + @Override + public CharSequence provideSummary(@NonNull ListPreference preference) { + CharSequence entry = preference.getEntry(); + + if (entry == null) return null; + + long currentSizeMb = DownloadUtil.getStreamingCacheSize(requireActivity()) / (1024 * 1024); + + return getString(R.string.settings_summary_streaming_cache_size, entry, String.valueOf(currentSizeMb)); + } + }); + } + } + + private void setAppLanguage() { + ListPreference localePref = (ListPreference) findPreference("language"); + + Map locales = UIUtil.getLangPreferenceDropdownEntries(requireContext()); + + CharSequence[] entries = locales.keySet().toArray(new CharSequence[locales.size()]); + CharSequence[] entryValues = locales.values().toArray(new CharSequence[locales.size()]); + + localePref.setEntries(entries); + localePref.setEntryValues(entryValues); + + String value = localePref.getValue(); + if ("default".equals(value)) { + localePref.setSummary(requireContext().getString(R.string.settings_system_language)); + } else { + localePref.setSummary(Locale.forLanguageTag(value).getDisplayName()); + } + + localePref.setOnPreferenceChangeListener((preference, newValue) -> { + if ("default".equals(newValue)) { + AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList()); + preference.setSummary(requireContext().getString(R.string.settings_system_language)); + } else { + LocaleListCompat appLocale = LocaleListCompat.forLanguageTags((String) newValue); + AppCompatDelegate.setApplicationLocales(appLocale); + preference.setSummary(Locale.forLanguageTag((String) newValue).getDisplayName()); + } + return true; + }); + } + + private void setVersion() { + findPreference("version").setSummary(BuildConfig.VERSION_NAME); + } + + private void actionLogout() { + findPreference("logout").setOnPreferenceClickListener(preference -> { + activity.quit(); + return true; + }); + } + + private void actionScan() { + findPreference("scan_library").setOnPreferenceClickListener(preference -> { + settingViewModel.launchScan(new ScanCallback() { + @Override + public void onError(Exception exception) { + findPreference("scan_library").setSummary(exception.getMessage()); + } + + @Override + public void onSuccess(boolean isScanning, long count) { + findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count)); + if (isScanning) getScanStatus(); + } + }); + + return true; + }); + } + + private void actionSyncStarredTracks() { + findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + StarredSyncDialog dialog = new StarredSyncDialog(() -> { + ((SwitchPreference)preference).setChecked(false); + }); + dialog.show(activity.getSupportFragmentManager(), null); + } + } + return true; + }); + } + + private void actionSyncStarredAlbums() { + findPreference("sync_starred_albums_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + StarredAlbumSyncDialog dialog = new StarredAlbumSyncDialog(() -> { + ((SwitchPreference)preference).setChecked(false); + }); + dialog.show(activity.getSupportFragmentManager(), null); + } + } + return true; + }); + } + + private void actionSyncStarredArtists() { + findPreference("sync_starred_artists_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + StarredArtistSyncDialog dialog = new StarredArtistSyncDialog(() -> { + ((SwitchPreference)preference).setChecked(false); + }); + dialog.show(activity.getSupportFragmentManager(), null); + } + } + return true; + }); + } + + private void actionChangeStreamingCacheStorage() { + findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> { + StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() { + @Override + public void onPositiveClick() { + findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_external_dialog_positive_button); + } + + @Override + public void onNegativeClick() { + findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_internal_dialog_negative_button); + } + }); + dialog.show(activity.getSupportFragmentManager(), null); + return true; + }); + } + + private void actionChangeDownloadStorage() { + findPreference("download_storage").setOnPreferenceClickListener(preference -> { + DownloadStorageDialog dialog = new DownloadStorageDialog(new DialogClickCallback() { + @Override + public void onPositiveClick() { + findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button); + checkDownloadDirectory(); + } + + @Override + public void onNegativeClick() { + findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button); + checkDownloadDirectory(); + } + + @Override + public void onNeutralClick() { + findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button); + checkDownloadDirectory(); + } + }); + dialog.show(activity.getSupportFragmentManager(), null); + return true; + }); + } + + private void actionSetDownloadDirectory() { + Preference pref = findPreference("set_download_directory"); + if (pref != null) { + pref.setOnPreferenceClickListener(preference -> { + String current = Preferences.getDownloadDirectoryUri(); + + if (current != null) { + Preferences.setDownloadDirectoryUri(null); + Preferences.setDownloadStoragePreference(0); + ExternalAudioReader.refreshCache(); + Toast.makeText(requireContext(), R.string.settings_download_folder_cleared, Toast.LENGTH_SHORT).show(); + checkStorage(); + checkDownloadDirectory(); + } else { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + directoryPickerLauncher.launch(intent); + } + return true; + }); + } + } + + private void actionDeleteDownloadStorage() { + findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> { + DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog(); + dialog.show(activity.getSupportFragmentManager(), null); + return true; + }); + } + + private void actionMiniPlayerHeart() { + SwitchPreference preference = findPreference("mini_shuffle_button_visibility"); + if (preference == null) { + return; + } + + preference.setChecked(Preferences.showShuffleInsteadOfHeart()); + preference.setOnPreferenceChangeListener((pref, newValue) -> { + if (newValue instanceof Boolean) { + Preferences.setShuffleInsteadOfHeart((Boolean) newValue); + } + return true; + }); + } + + private void actionAutoDownloadLyrics() { + SwitchPreference preference = findPreference("auto_download_lyrics"); + if (preference == null) { + return; + } + + preference.setChecked(Preferences.isAutoDownloadLyricsEnabled()); + preference.setOnPreferenceChangeListener((pref, newValue) -> { + if (newValue instanceof Boolean) { + Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue); + } + return true; + }); + } + + private void getScanStatus() { + settingViewModel.getScanStatus(new ScanCallback() { + @Override + public void onError(Exception exception) { + findPreference("scan_library").setSummary(exception.getMessage()); + } + + @Override + public void onSuccess(boolean isScanning, long count) { + findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count)); + if (isScanning) getScanStatus(); + } + }); + } + + private void actionKeepScreenOn() { + findPreference("always_on_display").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + return true; + }); + } + + private final ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mediaServiceBinder = (MediaService.LocalBinder) service; + isServiceBound = true; + checkEqualizerBands(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mediaServiceBinder = null; + isServiceBound = false; + } + }; + + private void bindMediaService() { + Intent intent = new Intent(requireActivity(), MediaService.class); + intent.setAction(MediaService.ACTION_BIND_EQUALIZER); + requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + isServiceBound = true; + } + + private void checkEqualizerBands() { + if (mediaServiceBinder != null) { + EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager(); + short numBands = eqManager.getNumberOfBands(); + Preference appEqualizer = findPreference("app_equalizer"); + if (appEqualizer != null) { + appEqualizer.setVisible(numBands > 0); + } + } + } + + private void actionAppEqualizer() { + Preference appEqualizer = findPreference("app_equalizer"); + if (appEqualizer != null) { + appEqualizer.setOnPreferenceClickListener(preference -> { + NavController navController = NavHostFragment.findNavController(this); + NavOptions navOptions = new NavOptions.Builder() + .setLaunchSingleTop(true) + .setPopUpTo(R.id.equalizerFragment, true) + .build(); + activity.setBottomNavigationBarVisibility(true); + activity.setBottomSheetVisibility(true); + navController.navigate(R.id.equalizerFragment, null, navOptions); + return true; + }); + } + } + + @Override + public void onPause() { + super.onPause(); + if (isServiceBound) { + requireActivity().unbindService(serviceConnection); + isServiceBound = false; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java index aab615936..c1399d750 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java @@ -1,5 +1,7 @@ package com.cappielloantonio.tempo.ui.fragment; +import static com.google.android.material.internal.ViewUtils.hideKeyboard; + import android.app.Activity; import android.content.Context; import android.content.ComponentName; @@ -22,8 +24,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; +import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.Toolbar; import androidx.core.os.LocaleListCompat; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; import androidx.media3.common.util.UnstableApi; import androidx.navigation.NavController; @@ -38,6 +45,8 @@ import com.cappielloantonio.tempo.BuildConfig; import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentAlbumCatalogueBinding; +import com.cappielloantonio.tempo.databinding.FragmentSettingsBinding; import com.cappielloantonio.tempo.helper.ThemeHelper; import com.cappielloantonio.tempo.interfaces.DialogClickCallback; import com.cappielloantonio.tempo.interfaces.ScanCallback; @@ -59,70 +68,46 @@ import java.util.Locale; import java.util.Map; -@OptIn(markerClass = UnstableApi.class) -public class SettingsFragment extends PreferenceFragmentCompat { - private static final String TAG = "SettingsFragment"; +public class SettingsFragment extends Fragment { private MainActivity activity; - private SettingViewModel settingViewModel; - - private ActivityResultLauncher equalizerResultLauncher; - private ActivityResultLauncher directoryPickerLauncher; - - private MediaService.LocalBinder mediaServiceBinder; - private boolean isServiceBound = false; + private FragmentSettingsBinding bind; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - equalizerResultLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> {} - ); + activity = (MainActivity) getActivity(); - if (!BuildConfig.FLAVOR.equals("tempus")) { - PreferenceCategory githubUpdateCategory = findPreference("settings_github_update_category_key"); - if (githubUpdateCategory != null) { - getPreferenceScreen().removePreference(githubUpdateCategory); - } - } + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + bind = FragmentSettingsBinding.inflate(inflater,container,false); + View view = bind.getRoot(); - directoryPickerLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == Activity.RESULT_OK) { - Intent data = result.getData(); - if (data != null) { - Uri uri = data.getData(); - if (uri != null) { - requireContext().getContentResolver().takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ); + initAppBar(); + + return view; - Preferences.setDownloadDirectoryUri(uri.toString()); - ExternalAudioReader.refreshCache(); - Toast.makeText(requireContext(), R.string.settings_download_folder_set, Toast.LENGTH_SHORT).show(); - checkDownloadDirectory(); - } - } - } - }); } @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - activity = (MainActivity) getActivity(); + public void onViewCreated(@NonNull View view, + @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); - View view = super.onCreateView(inflater, container, savedInstanceState); - settingViewModel = new ViewModelProvider(requireActivity()).get(SettingViewModel.class); + // Add the PreferenceFragment only the first time + if (savedInstanceState == null) { + SettingsContainerFragment prefFragment = new SettingsContainerFragment(); - if (view != null) { - getListView().setPadding(0, 0, 0, (int) getResources().getDimension(R.dimen.global_padding_bottom)); + // Use the child fragment manager so the PreferenceFragment is scoped to this fragment + getChildFragmentManager() + .beginTransaction() + .replace(R.id.settings_container, prefFragment) + .setReorderingAllowed(true) // optional but recommended + .commit(); } - - return view; } @Override @@ -130,37 +115,8 @@ public void onStart() { super.onStart(); activity.setBottomNavigationBarVisibility(false); activity.setBottomSheetVisibility(false); - } - - @Override - public void onResume() { - super.onResume(); - - checkSystemEqualizer(); - checkCacheStorage(); - checkStorage(); - checkDownloadDirectory(); - - setStreamingCacheSize(); - setAppLanguage(); - setVersion(); - setNetorkPingTimeoutBase(); - - actionLogout(); - actionScan(); - actionSyncStarredAlbums(); - actionSyncStarredTracks(); - actionSyncStarredArtists(); - actionChangeStreamingCacheStorage(); - actionChangeDownloadStorage(); - actionSetDownloadDirectory(); - actionDeleteDownloadStorage(); - actionKeepScreenOn(); - actionAutoDownloadLyrics(); - actionMiniPlayerHeart(); - - bindMediaService(); - actionAppEqualizer(); + activity.setNavigationDrawerLock(true); + activity.setSystemBarsVisibility(!activity.isLandscape); } @Override @@ -169,440 +125,9 @@ public void onStop() { activity.setBottomSheetVisibility(true); } - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - setPreferencesFromResource(R.xml.global_preferences, rootKey); - ListPreference themePreference = findPreference(Preferences.THEME); - if (themePreference != null) { - themePreference.setOnPreferenceChangeListener( - (preference, newValue) -> { - String themeOption = (String) newValue; - ThemeHelper.applyTheme(themeOption); - return true; - }); - } - } - - private void checkSystemEqualizer() { - Preference equalizer = findPreference("system_equalizer"); - - if (equalizer == null) return; - - Intent intent = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL); - - if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) { - equalizer.setOnPreferenceClickListener(preference -> { - equalizerResultLauncher.launch(intent); - return true; - }); - } else { - equalizer.setVisible(false); - } - } - - private void checkCacheStorage() { - Preference storage = findPreference("streaming_cache_storage"); - - if (storage == null) return; - - try { - if (requireContext().getExternalFilesDirs(null)[1] == null) { - storage.setVisible(false); - } else { - storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button); - } - } catch (Exception exception) { - storage.setVisible(false); - } - } - - private void checkStorage() { - Preference storage = findPreference("download_storage"); - - if (storage == null) return; - - try { - if (requireContext().getExternalFilesDirs(null)[1] == null) { - storage.setVisible(false); - } else { - int pref = Preferences.getDownloadStoragePreference(); - if (pref == 0) { - storage.setSummary(R.string.download_storage_internal_dialog_negative_button); - } else if (pref == 1) { - storage.setSummary(R.string.download_storage_external_dialog_positive_button); - } else { - storage.setSummary(R.string.download_storage_directory_dialog_neutral_button); - } - } - } catch (Exception exception) { - storage.setVisible(false); - } - } - - private void checkDownloadDirectory() { - Preference storage = findPreference("download_storage"); - Preference directory = findPreference("set_download_directory"); - - if (directory == null) return; - - String current = Preferences.getDownloadDirectoryUri(); - if (current != null) { - if (storage != null) storage.setVisible(false); - directory.setVisible(true); - directory.setIcon(R.drawable.ic_close); - directory.setTitle(R.string.settings_clear_download_folder); - directory.setSummary(current); - } else { - if (storage != null) storage.setVisible(true); - if (Preferences.getDownloadStoragePreference() == 2) { - directory.setVisible(true); - directory.setIcon(R.drawable.ic_folder); - directory.setTitle(R.string.settings_set_download_folder); - directory.setSummary(R.string.settings_choose_download_folder); - } else { - directory.setVisible(false); - } - } - } - - private void setNetorkPingTimeoutBase() { - EditTextPreference networkPingTimeoutBase = findPreference("network_ping_timeout_base"); - - if (networkPingTimeoutBase != null) { - networkPingTimeoutBase.setSummaryProvider(EditTextPreference.SimpleSummaryProvider.getInstance()); - networkPingTimeoutBase.setOnBindEditTextListener(editText -> { - editText.setInputType(InputType.TYPE_CLASS_NUMBER); - editText.setFilters(new InputFilter[]{ (source, start, end, dest, dstart, dend) -> { - for (int i = start; i < end; i++) { - if (!Character.isDigit(source.charAt(i))) { - return ""; - } - } - return null; - }}); - }); - - networkPingTimeoutBase.setOnPreferenceChangeListener((preference, newValue) -> { - String input = (String) newValue; - return input != null && !input.isEmpty(); - }); - } - } - - private void setStreamingCacheSize() { - ListPreference streamingCachePreference = findPreference("streaming_cache_size"); - - if (streamingCachePreference != null) { - streamingCachePreference.setSummaryProvider(new Preference.SummaryProvider() { - @Nullable - @Override - public CharSequence provideSummary(@NonNull ListPreference preference) { - CharSequence entry = preference.getEntry(); - - if (entry == null) return null; - - long currentSizeMb = DownloadUtil.getStreamingCacheSize(requireActivity()) / (1024 * 1024); - - return getString(R.string.settings_summary_streaming_cache_size, entry, String.valueOf(currentSizeMb)); - } - }); - } - } - - private void setAppLanguage() { - ListPreference localePref = (ListPreference) findPreference("language"); - - Map locales = UIUtil.getLangPreferenceDropdownEntries(requireContext()); - - CharSequence[] entries = locales.keySet().toArray(new CharSequence[locales.size()]); - CharSequence[] entryValues = locales.values().toArray(new CharSequence[locales.size()]); - - localePref.setEntries(entries); - localePref.setEntryValues(entryValues); - - String value = localePref.getValue(); - if ("default".equals(value)) { - localePref.setSummary(requireContext().getString(R.string.settings_system_language)); - } else { - localePref.setSummary(Locale.forLanguageTag(value).getDisplayName()); - } - - localePref.setOnPreferenceChangeListener((preference, newValue) -> { - if ("default".equals(newValue)) { - AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList()); - preference.setSummary(requireContext().getString(R.string.settings_system_language)); - } else { - LocaleListCompat appLocale = LocaleListCompat.forLanguageTags((String) newValue); - AppCompatDelegate.setApplicationLocales(appLocale); - preference.setSummary(Locale.forLanguageTag((String) newValue).getDisplayName()); - } - return true; + private void initAppBar() { + bind.settingsToolbar.setNavigationOnClickListener(v -> { + activity.navController.navigateUp(); }); } - - private void setVersion() { - findPreference("version").setSummary(BuildConfig.VERSION_NAME); - } - - private void actionLogout() { - findPreference("logout").setOnPreferenceClickListener(preference -> { - activity.quit(); - return true; - }); - } - - private void actionScan() { - findPreference("scan_library").setOnPreferenceClickListener(preference -> { - settingViewModel.launchScan(new ScanCallback() { - @Override - public void onError(Exception exception) { - findPreference("scan_library").setSummary(exception.getMessage()); - } - - @Override - public void onSuccess(boolean isScanning, long count) { - findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count)); - if (isScanning) getScanStatus(); - } - }); - - return true; - }); - } - - private void actionSyncStarredTracks() { - findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue instanceof Boolean) { - if ((Boolean) newValue) { - StarredSyncDialog dialog = new StarredSyncDialog(() -> { - ((SwitchPreference)preference).setChecked(false); - }); - dialog.show(activity.getSupportFragmentManager(), null); - } - } - return true; - }); - } - - private void actionSyncStarredAlbums() { - findPreference("sync_starred_albums_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue instanceof Boolean) { - if ((Boolean) newValue) { - StarredAlbumSyncDialog dialog = new StarredAlbumSyncDialog(() -> { - ((SwitchPreference)preference).setChecked(false); - }); - dialog.show(activity.getSupportFragmentManager(), null); - } - } - return true; - }); - } - - private void actionSyncStarredArtists() { - findPreference("sync_starred_artists_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue instanceof Boolean) { - if ((Boolean) newValue) { - StarredArtistSyncDialog dialog = new StarredArtistSyncDialog(() -> { - ((SwitchPreference)preference).setChecked(false); - }); - dialog.show(activity.getSupportFragmentManager(), null); - } - } - return true; - }); - } - - private void actionChangeStreamingCacheStorage() { - findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> { - StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() { - @Override - public void onPositiveClick() { - findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_external_dialog_positive_button); - } - - @Override - public void onNegativeClick() { - findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_internal_dialog_negative_button); - } - }); - dialog.show(activity.getSupportFragmentManager(), null); - return true; - }); - } - - private void actionChangeDownloadStorage() { - findPreference("download_storage").setOnPreferenceClickListener(preference -> { - DownloadStorageDialog dialog = new DownloadStorageDialog(new DialogClickCallback() { - @Override - public void onPositiveClick() { - findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button); - checkDownloadDirectory(); - } - - @Override - public void onNegativeClick() { - findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button); - checkDownloadDirectory(); - } - - @Override - public void onNeutralClick() { - findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button); - checkDownloadDirectory(); - } - }); - dialog.show(activity.getSupportFragmentManager(), null); - return true; - }); - } - - private void actionSetDownloadDirectory() { - Preference pref = findPreference("set_download_directory"); - if (pref != null) { - pref.setOnPreferenceClickListener(preference -> { - String current = Preferences.getDownloadDirectoryUri(); - - if (current != null) { - Preferences.setDownloadDirectoryUri(null); - Preferences.setDownloadStoragePreference(0); - ExternalAudioReader.refreshCache(); - Toast.makeText(requireContext(), R.string.settings_download_folder_cleared, Toast.LENGTH_SHORT).show(); - checkStorage(); - checkDownloadDirectory(); - } else { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION - | Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - directoryPickerLauncher.launch(intent); - } - return true; - }); - } - } - - private void actionDeleteDownloadStorage() { - findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> { - DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog(); - dialog.show(activity.getSupportFragmentManager(), null); - return true; - }); - } - - private void actionMiniPlayerHeart() { - SwitchPreference preference = findPreference("mini_shuffle_button_visibility"); - if (preference == null) { - return; - } - - preference.setChecked(Preferences.showShuffleInsteadOfHeart()); - preference.setOnPreferenceChangeListener((pref, newValue) -> { - if (newValue instanceof Boolean) { - Preferences.setShuffleInsteadOfHeart((Boolean) newValue); - } - return true; - }); - } - - private void actionAutoDownloadLyrics() { - SwitchPreference preference = findPreference("auto_download_lyrics"); - if (preference == null) { - return; - } - - preference.setChecked(Preferences.isAutoDownloadLyricsEnabled()); - preference.setOnPreferenceChangeListener((pref, newValue) -> { - if (newValue instanceof Boolean) { - Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue); - } - return true; - }); - } - - private void getScanStatus() { - settingViewModel.getScanStatus(new ScanCallback() { - @Override - public void onError(Exception exception) { - findPreference("scan_library").setSummary(exception.getMessage()); - } - - @Override - public void onSuccess(boolean isScanning, long count) { - findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count)); - if (isScanning) getScanStatus(); - } - }); - } - - private void actionKeepScreenOn() { - findPreference("always_on_display").setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue instanceof Boolean) { - if ((Boolean) newValue) { - activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - } - return true; - }); - } - - private final ServiceConnection serviceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - mediaServiceBinder = (MediaService.LocalBinder) service; - isServiceBound = true; - checkEqualizerBands(); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - mediaServiceBinder = null; - isServiceBound = false; - } - }; - - private void bindMediaService() { - Intent intent = new Intent(requireActivity(), MediaService.class); - intent.setAction(MediaService.ACTION_BIND_EQUALIZER); - requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); - isServiceBound = true; - } - - private void checkEqualizerBands() { - if (mediaServiceBinder != null) { - EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager(); - short numBands = eqManager.getNumberOfBands(); - Preference appEqualizer = findPreference("app_equalizer"); - if (appEqualizer != null) { - appEqualizer.setVisible(numBands > 0); - } - } - } - - private void actionAppEqualizer() { - Preference appEqualizer = findPreference("app_equalizer"); - if (appEqualizer != null) { - appEqualizer.setOnPreferenceClickListener(preference -> { - NavController navController = NavHostFragment.findNavController(this); - NavOptions navOptions = new NavOptions.Builder() - .setLaunchSingleTop(true) - .setPopUpTo(R.id.equalizerFragment, true) - .build(); - activity.setBottomNavigationBarVisibility(true); - activity.setBottomSheetVisibility(true); - navController.navigate(R.id.equalizerFragment, null, navOptions); - return true; - }); - } - } - - @Override - public void onPause() { - super.onPause(); - if (isServiceBound) { - requireActivity().unbindService(serviceConnection); - isServiceBound = false; - } - } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt index 7d2224edf..baa8dbf62 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt @@ -18,6 +18,8 @@ object Constants { const val MUSIC_DIRECTORY_OBJECT = "MUSIC_DIRECTORY_OBJECT" const val MUSIC_INDEX_OBJECT = "MUSIC_DIRECTORY_OBJECT" const val MUSIC_DIRECTORY_ID = "MUSIC_DIRECTORY_ID" + const val ALBUMS_OBJECT = "ALBUMS_OBJECT" + const val ALBUM_LIST_TITLE = "ALBUM_LIST_TITLE" const val ALBUM_RECENTLY_PLAYED = "ALBUM_RECENTLY_PLAYED" const val ALBUM_MOST_PLAYED = "ALBUM_MOST_PLAYED" diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt index a95c84ae1..47c30cc00 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt @@ -30,6 +30,8 @@ object Preferences { private const val IMAGE_CACHE_SIZE = "image_cache_size" private const val STREAMING_CACHE_SIZE = "streaming_cache_size" private const val LANDSCAPE_ITEMS_PER_ROW = "landscape_items_per_row" + private const val ENABLE_DRAWER_ON_PORTRAIT = "enable_drawer_on_portrait" + private const val HIDE_BOTTOM_NAVBAR_ON_PORTRAIT = "hide_bottom_navbar_on_portrait" private const val IMAGE_SIZE = "image_size" private const val MAX_BITRATE_WIFI = "max_bitrate_wifi" private const val MAX_BITRATE_MOBILE = "max_bitrate_mobile" @@ -310,6 +312,16 @@ object Preferences { return App.getInstance().preferences.getString(LANDSCAPE_ITEMS_PER_ROW, "4")!!.toInt() } + @JvmStatic + fun getEnableDrawerOnPortrait(): Boolean { + return App.getInstance().preferences.getBoolean(ENABLE_DRAWER_ON_PORTRAIT, false) + } + + @JvmStatic + fun getHideBottomNavbarOnPortrait(): Boolean { + return App.getInstance().preferences.getBoolean(HIDE_BOTTOM_NAVBAR_ON_PORTRAIT, false) + } + @JvmStatic fun getImageSize(): Int { return App.getInstance().preferences.getString(IMAGE_SIZE, "-1")!!.toInt() diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumListPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumListPageViewModel.java index 8e88cec85..4d2c7c329 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumListPageViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumListPageViewModel.java @@ -23,6 +23,7 @@ public class AlbumListPageViewModel extends AndroidViewModel { public String title; public ArtistID3 artist; + public List albums; private MutableLiveData> albumList; @@ -34,6 +35,9 @@ public AlbumListPageViewModel(@NonNull Application application) { } public LiveData> getAlbumList(LifecycleOwner owner) { + if (albums != null) { + return new MutableLiveData<>(albums); + } albumList = new MutableLiveData<>(new ArrayList<>()); switch (title) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java index f2b7d9e6a..36af0af4f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java @@ -8,6 +8,7 @@ import androidx.annotation.OptIn; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; import androidx.media3.common.util.UnstableApi; import com.cappielloantonio.tempo.model.Download; @@ -24,8 +25,10 @@ import com.cappielloantonio.tempo.util.NetworkUtil; import com.cappielloantonio.tempo.util.Preferences; +import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; public class ArtistPageViewModel extends AndroidViewModel { @@ -35,6 +38,11 @@ public class ArtistPageViewModel extends AndroidViewModel { private ArtistID3 artist; + private final MutableLiveData> singles = new MutableLiveData<>(); + private final MutableLiveData> eps = new MutableLiveData<>(); + private final MutableLiveData> mainAlbums = new MutableLiveData<>(); + private final MutableLiveData> appearsOn = new MutableLiveData<>(); + public ArtistPageViewModel(@NonNull Application application) { super(application); @@ -43,6 +51,65 @@ public ArtistPageViewModel(@NonNull Application application) { favoriteRepository = new FavoriteRepository(); } + public void fetchCategorizedAlbums(androidx.lifecycle.LifecycleOwner owner) { + artistRepository.getArtist(artist.getId()).observe(owner, artistWithAlbums -> { + if (artistWithAlbums != null && artistWithAlbums instanceof com.cappielloantonio.tempo.subsonic.models.ArtistWithAlbumsID3) { + com.cappielloantonio.tempo.subsonic.models.ArtistWithAlbumsID3 fullArtist = (com.cappielloantonio.tempo.subsonic.models.ArtistWithAlbumsID3) artistWithAlbums; + + List allAlbums = fullArtist.getAlbums(); + if (allAlbums != null) { + allAlbums.sort(Comparator.comparing(AlbumID3::getYear).reversed()); + + mainAlbums.setValue(allAlbums.stream() + .filter(a -> + isType(a, "album") && Objects.equals(a.getArtistId(), artist.getId())) + .collect(Collectors.toList())); + + singles.setValue(allAlbums.stream() + .filter(a -> + isType(a, "single") && Objects.equals(a.getArtistId(), artist.getId())) + .collect(Collectors.toList())); + + eps.setValue(allAlbums.stream() + .filter(a -> + isType(a, "ep") && Objects.equals(a.getArtistId(), artist.getId())) + .collect(Collectors.toList())); + } + if (allAlbums != null) { + allAlbums.sort(Comparator.comparing(AlbumID3::getYear).reversed()); + + appearsOn.setValue(allAlbums.stream() + .filter(a -> !Objects.equals(a.getArtistId(), artist.getId())) + .collect(Collectors.toList()) + ); + } + } + }); + } + + private boolean isType(AlbumID3 album, String targetType) { + if (album.getReleaseTypes() != null && !album.getReleaseTypes().isEmpty()) { + return album.getReleaseTypes().contains(targetType); + } + // Fallback to song count if releaseTypes is not available + int songCount = album.getSongCount() != null ? album.getSongCount() : 0; + switch (targetType) { + case "single": + return songCount >= 1 && songCount <= 2; + case "ep": + return songCount >= 3 && songCount <= 7; + case "album": + return songCount >= 8; + default: + return false; + } + } + + public LiveData> getSingles() { return singles; } + public LiveData> getEPs() { return eps; } + public LiveData> getMainAlbums() { return mainAlbums; } + public LiveData> getAppearsOn() { return appearsOn; } + public LiveData> getAlbumList() { return albumRepository.getArtistAlbums(artist.getId()); } diff --git a/app/src/main/res/drawable/ic_albums.xml b/app/src/main/res/drawable/ic_albums.xml new file mode 100644 index 000000000..4c1c697df --- /dev/null +++ b/app/src/main/res/drawable/ic_albums.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_artists.xml b/app/src/main/res/drawable/ic_artists.xml new file mode 100644 index 000000000..03d8308f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_artists.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_genres.xml b/app/src/main/res/drawable/ic_genres.xml new file mode 100644 index 000000000..3e7b66795 --- /dev/null +++ b/app/src/main/res/drawable/ic_genres.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_playlist.xml b/app/src/main/res/drawable/ic_playlist.xml new file mode 100644 index 000000000..08225cc90 --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist.xml @@ -0,0 +1,33 @@ + + + + + + diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index 581aa4ce7..1b6d2e9c7 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -1,16 +1,15 @@ - + android:background="?attr/colorSurface"> + android:layout_height="match_parent"> - - - + + - - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 01edf7f5f..15a8d99cd 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,16 +1,16 @@ - + android:background="?attr/colorSurface"> + + android:layout_height="match_parent"> + + + + + + - - + diff --git a/app/src/main/res/layout/fragment_artist_page.xml b/app/src/main/res/layout/fragment_artist_page.xml index 6712dd2e9..b2ebc4fe2 100644 --- a/app/src/main/res/layout/fragment_artist_page.xml +++ b/app/src/main/res/layout/fragment_artist_page.xml @@ -54,7 +54,7 @@ android:clipToPadding="false" android:orientation="vertical" android:paddingTop="18dp" - android:paddingBottom="@dimen/global_padding_bottom"> + android:paddingBottom="120dp"> - + + + + + + + + android:paddingEnd="16dp" /> + + + + + + + + + + + android:paddingEnd="16dp" /> + + + + + + + + + + + + + + + + + + + + + + + + + android:paddingEnd="16dp" /> diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 3e8d99309..c5a98dc4a 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -1,6 +1,22 @@ - \ No newline at end of file + android:layout_height="match_parent"> + + + + + diff --git a/app/src/main/res/layout/item_album_carousel.xml b/app/src/main/res/layout/item_album_carousel.xml new file mode 100644 index 000000000..3f202989f --- /dev/null +++ b/app/src/main/res/layout/item_album_carousel.xml @@ -0,0 +1,45 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_artist_carousel.xml b/app/src/main/res/layout/item_artist_carousel.xml new file mode 100644 index 000000000..336ede173 --- /dev/null +++ b/app/src/main/res/layout/item_artist_carousel.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/nav_drawer_header.xml b/app/src/main/res/layout/nav_drawer_header.xml new file mode 100644 index 000000000..b6ff546c5 --- /dev/null +++ b/app/src/main/res/layout/nav_drawer_header.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/nav_drawer.xml b/app/src/main/res/menu/nav_drawer.xml new file mode 100644 index 000000000..a01dfcedf --- /dev/null +++ b/app/src/main/res/menu/nav_drawer.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index c6aba5abb..d234d5440 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -220,6 +220,10 @@ + Switch layout More like this Albums + EPs + Singles + Appears On More Biography - Most Streamed Songs + Top Songs See all Ignore Don\'t ask again @@ -384,6 +387,10 @@ If enabled, show the shuffle button, remove the heart in the mini player Show radio If enabled, show the radio section. Restart the app for it to take full effect. + Enable drawer on portrait + Unlocks the lateral landscape menu drawer on portrait. The changes will take effect on restart. + Hide bottom navbar on portrait + Increases vertical space by removing the bottom navbar. The changes will take effect on restart. Auto download lyrics Automatically save lyrics when they are available so they can be shown while offline. Set replay gain mode diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index aa08c1514..b598c2541 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -68,4 +68,8 @@ ?attr/colorErrorContainer ?attr/colorOnErrorContainer + + \ No newline at end of file diff --git a/app/src/main/res/xml/global_preferences.xml b/app/src/main/res/xml/global_preferences.xml index 4ee034e53..cdb875a9f 100644 --- a/app/src/main/res/xml/global_preferences.xml +++ b/app/src/main/res/xml/global_preferences.xml @@ -54,6 +54,16 @@ android:defaultValue="false" android:key="always_on_display" /> + + + +