mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-14 16:37:16 +03:00
Migrate to use SafeArgs
This commit is contained in:
parent
f7b50d072d
commit
a4919ef6e9
@ -17,6 +17,7 @@ buildscript {
|
||||
classpath libs.kotlin
|
||||
classpath libs.ktlintGradle
|
||||
classpath libs.detekt
|
||||
classpath libs.navigationSafeArgs
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,8 @@ enum class AlbumListType(val typeName: String) {
|
||||
SORTED_BY_ARTIST("alphabeticalByArtist"),
|
||||
STARRED("starred"),
|
||||
BY_YEAR("byYear"),
|
||||
BY_GENRE("byGenre");
|
||||
BY_GENRE("byGenre"),
|
||||
BY_ARTIST("albumsByArtist");
|
||||
|
||||
override fun toString(): String {
|
||||
return typeName
|
||||
@ -35,6 +36,7 @@ enum class AlbumListType(val typeName: String) {
|
||||
in STARRED.typeName -> STARRED
|
||||
in BY_YEAR.typeName -> BY_YEAR
|
||||
in BY_GENRE.typeName -> BY_GENRE
|
||||
in BY_ARTIST.typeName -> BY_ARTIST
|
||||
else -> throw IllegalArgumentException("Unknown type: $typeName")
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,9 @@
|
||||
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID>
|
||||
<ID>LongMethod:PlaylistsFragment.kt$PlaylistsFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken, additionalId: String? )</ID>
|
||||
<ID>LongMethod:SharesFragment.kt$SharesFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, passedData: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) )</ID>
|
||||
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
|
||||
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f</ID>
|
||||
@ -19,7 +21,6 @@
|
||||
<ID>TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer.TaskQueue$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionThrown:Downloader.kt$Downloader.DownloadTask$throw RuntimeException( String.format( Locale.ROOT, "Download of '%s' was cancelled", downloadFile.track ) )</ID>
|
||||
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
||||
</CurrentIssues>
|
||||
|
@ -64,6 +64,7 @@ navigationUi = { module = "androidx.navigation:navigation-ui", versio
|
||||
navigationFragmentKtx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
|
||||
navigationUiKtx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
|
||||
navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" }
|
||||
navigationSafeArgs = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "navigation"}
|
||||
preferences = { module = "androidx.preference:preference", version.ref = "preferences" }
|
||||
media = { module = "androidx.media:media", version.ref = "media" }
|
||||
media3exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
|
||||
|
@ -1,6 +1,7 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: "androidx.navigation.safeargs.kotlin"
|
||||
apply from: "../gradle_scripts/code_quality.gradle"
|
||||
|
||||
android {
|
||||
|
@ -1,342 +0,0 @@
|
||||
package org.moire.ultrasonic.fragment;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider;
|
||||
import org.moire.ultrasonic.domain.Playlist;
|
||||
import org.moire.ultrasonic.service.MusicService;
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory;
|
||||
import org.moire.ultrasonic.service.OfflineException;
|
||||
import org.moire.ultrasonic.subsonic.DownloadHandler;
|
||||
import org.moire.ultrasonic.util.BackgroundTask;
|
||||
import org.moire.ultrasonic.util.CacheCleaner;
|
||||
import org.moire.ultrasonic.util.CancellationToken;
|
||||
import org.moire.ultrasonic.util.Constants;
|
||||
import org.moire.ultrasonic.util.LoadingTask;
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask;
|
||||
import org.moire.ultrasonic.util.Util;
|
||||
import org.moire.ultrasonic.view.PlaylistAdapter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import kotlin.Lazy;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
/**
|
||||
* Displays the playlists stored on the server
|
||||
*/
|
||||
public class PlaylistsFragment extends Fragment {
|
||||
|
||||
private SwipeRefreshLayout refreshPlaylistsListView;
|
||||
private ListView playlistsListView;
|
||||
private View emptyTextView;
|
||||
private PlaylistAdapter playlistAdapter;
|
||||
|
||||
private final Lazy<DownloadHandler> downloadHandler = inject(DownloadHandler.class);
|
||||
private CancellationToken cancellationToken;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
Util.applyTheme(this.getContext());
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.select_playlist, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
cancellationToken = new CancellationToken();
|
||||
|
||||
refreshPlaylistsListView = view.findViewById(R.id.select_playlist_refresh);
|
||||
playlistsListView = view.findViewById(R.id.select_playlist_list);
|
||||
|
||||
refreshPlaylistsListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
|
||||
{
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
load(true);
|
||||
}
|
||||
});
|
||||
|
||||
emptyTextView = view.findViewById(R.id.select_playlist_empty);
|
||||
playlistsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
Playlist playlist = (Playlist) parent.getItemAtPosition(position);
|
||||
|
||||
if (playlist == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.INTENT_ID, playlist.getId());
|
||||
bundle.putString(Constants.INTENT_PLAYLIST_ID, playlist.getId());
|
||||
bundle.putString(Constants.INTENT_PLAYLIST_NAME, playlist.getName());
|
||||
Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle);
|
||||
}
|
||||
});
|
||||
registerForContextMenu(playlistsListView);
|
||||
FragmentTitle.Companion.setTitle(this, R.string.playlist_label);
|
||||
|
||||
load(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
cancellationToken.cancel();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void load(final boolean refresh)
|
||||
{
|
||||
BackgroundTask<List<Playlist>> task = new FragmentBackgroundTask<List<Playlist>>(getActivity(), true, refreshPlaylistsListView, cancellationToken)
|
||||
{
|
||||
@Override
|
||||
protected List<Playlist> doInBackground() throws Throwable
|
||||
{
|
||||
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||
List<Playlist> playlists = musicService.getPlaylists(refresh);
|
||||
|
||||
if (!ActiveServerProvider.Companion.isOffline())
|
||||
new CacheCleaner().cleanPlaylists(playlists);
|
||||
return playlists;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done(List<Playlist> result)
|
||||
{
|
||||
playlistsListView.setAdapter(playlistAdapter = new PlaylistAdapter(getContext(), result));
|
||||
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
};
|
||||
task.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(@NotNull ContextMenu menu, @NotNull View view, ContextMenu.ContextMenuInfo menuInfo)
|
||||
{
|
||||
super.onCreateContextMenu(menu, view, menuInfo);
|
||||
|
||||
MenuInflater inflater = getActivity().getMenuInflater();
|
||||
if (ActiveServerProvider.Companion.isOffline()) inflater.inflate(R.menu.select_playlist_context_offline, menu);
|
||||
else inflater.inflate(R.menu.select_playlist_context, menu);
|
||||
|
||||
MenuItem downloadMenuItem = menu.findItem(R.id.playlist_menu_download);
|
||||
|
||||
if (downloadMenuItem != null)
|
||||
{
|
||||
downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem menuItem)
|
||||
{
|
||||
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
|
||||
if (info == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Playlist playlist = (Playlist) playlistsListView.getItemAtPosition(info.position);
|
||||
if (playlist == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Bundle bundle;
|
||||
int itemId = menuItem.getItemId();
|
||||
if (itemId == R.id.playlist_menu_pin) {
|
||||
downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), true, true, false, false, true, false, false);
|
||||
} else if (itemId == R.id.playlist_menu_unpin) {
|
||||
downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), false, false, false, false, true, false, true);
|
||||
} else if (itemId == R.id.playlist_menu_download) {
|
||||
downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), false, false, false, false, true, false, false);
|
||||
} else if (itemId == R.id.playlist_menu_play_now) {
|
||||
bundle = new Bundle();
|
||||
bundle.putString(Constants.INTENT_PLAYLIST_ID, playlist.getId());
|
||||
bundle.putString(Constants.INTENT_PLAYLIST_NAME, playlist.getName());
|
||||
bundle.putBoolean(Constants.INTENT_AUTOPLAY, true);
|
||||
Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle);
|
||||
} else if (itemId == R.id.playlist_menu_play_shuffled) {
|
||||
bundle = new Bundle();
|
||||
bundle.putString(Constants.INTENT_PLAYLIST_ID, playlist.getId());
|
||||
bundle.putString(Constants.INTENT_PLAYLIST_NAME, playlist.getName());
|
||||
bundle.putBoolean(Constants.INTENT_AUTOPLAY, true);
|
||||
bundle.putBoolean(Constants.INTENT_SHUFFLE, true);
|
||||
Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle);
|
||||
} else if (itemId == R.id.playlist_menu_delete) {
|
||||
deletePlaylist(playlist);
|
||||
} else if (itemId == R.id.playlist_info) {
|
||||
displayPlaylistInfo(playlist);
|
||||
} else if (itemId == R.id.playlist_update_info) {
|
||||
updatePlaylistInfo(playlist);
|
||||
} else {
|
||||
return super.onContextItemSelected(menuItem);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void deletePlaylist(final Playlist playlist)
|
||||
{
|
||||
new AlertDialog.Builder(getContext()).setIcon(R.drawable.ic_baseline_warning).setTitle(R.string.common_confirm).setMessage(getResources().getString(R.string.delete_playlist, playlist.getName())).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which)
|
||||
{
|
||||
new LoadingTask<Void>(getActivity(), refreshPlaylistsListView, cancellationToken)
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground() throws Throwable
|
||||
{
|
||||
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||
musicService.deletePlaylist(playlist.getId());
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done(Void result)
|
||||
{
|
||||
playlistAdapter.remove(playlist);
|
||||
playlistAdapter.notifyDataSetChanged();
|
||||
Util.toast(getContext(), getResources().getString(R.string.menu_deleted_playlist, playlist.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void error(Throwable error)
|
||||
{
|
||||
String msg;
|
||||
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_playlist_error, playlist.getName()), getErrorMessage(error));
|
||||
|
||||
Util.toast(getContext(), msg, false);
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
}).setNegativeButton(R.string.common_cancel, null).show();
|
||||
}
|
||||
|
||||
private void displayPlaylistInfo(final Playlist playlist)
|
||||
{
|
||||
final TextView textView = new TextView(getContext());
|
||||
textView.setPadding(5, 5, 5, 5);
|
||||
|
||||
final Spannable message = new SpannableString("Owner: " + playlist.getOwner() + "\nComments: " +
|
||||
((playlist.getComment() == null) ? "" : playlist.getComment()) +
|
||||
"\nSong Count: " + playlist.getSongCount() +
|
||||
((playlist.getPublic() == null) ? "" : ("\nPublic: " + playlist.getPublic()) + ((playlist.getCreated() == null) ? "" : ("\nCreation Date: " + playlist.getCreated().replace('T', ' ')))));
|
||||
|
||||
Linkify.addLinks(message, Linkify.WEB_URLS);
|
||||
textView.setText(message);
|
||||
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
new AlertDialog.Builder(getContext()).setTitle(playlist.getName()).setCancelable(true).setIcon(R.drawable.ic_baseline_info).setView(textView).show();
|
||||
}
|
||||
|
||||
private void updatePlaylistInfo(final Playlist playlist)
|
||||
{
|
||||
View dialogView = getLayoutInflater().inflate(R.layout.update_playlist, null);
|
||||
|
||||
if (dialogView == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
final EditText nameBox = dialogView.findViewById(R.id.get_playlist_name);
|
||||
final EditText commentBox = dialogView.findViewById(R.id.get_playlist_comment);
|
||||
final CheckBox publicBox = dialogView.findViewById(R.id.get_playlist_public);
|
||||
|
||||
nameBox.setText(playlist.getName());
|
||||
commentBox.setText(playlist.getComment());
|
||||
Boolean pub = playlist.getPublic();
|
||||
|
||||
if (pub == null)
|
||||
{
|
||||
publicBox.setEnabled(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
publicBox.setChecked(pub);
|
||||
}
|
||||
|
||||
AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext());
|
||||
|
||||
alertDialog.setIcon(R.drawable.ic_baseline_warning);
|
||||
alertDialog.setTitle(R.string.playlist_update_info);
|
||||
alertDialog.setView(dialogView);
|
||||
alertDialog.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which)
|
||||
{
|
||||
new LoadingTask<Void>(getActivity(), refreshPlaylistsListView, cancellationToken)
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground() throws Throwable
|
||||
{
|
||||
Editable nameBoxText = nameBox.getText();
|
||||
Editable commentBoxText = commentBox.getText();
|
||||
String name = nameBoxText != null ? nameBoxText.toString() : null;
|
||||
String comment = commentBoxText != null ? commentBoxText.toString() : null;
|
||||
|
||||
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||
musicService.updatePlaylist(playlist.getId(), name, comment, publicBox.isChecked());
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done(Void result)
|
||||
{
|
||||
load(true);
|
||||
Util.toast(getContext(), getResources().getString(R.string.playlist_updated_info, playlist.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void error(Throwable error)
|
||||
{
|
||||
String msg;
|
||||
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, playlist.getName()), getErrorMessage(error));
|
||||
|
||||
Util.toast(getContext(), msg, false);
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
});
|
||||
alertDialog.setNegativeButton(R.string.common_cancel, null);
|
||||
alertDialog.show();
|
||||
}
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
package org.moire.ultrasonic.fragment;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.domain.PodcastsChannel;
|
||||
import org.moire.ultrasonic.service.MusicService;
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory;
|
||||
import org.moire.ultrasonic.util.BackgroundTask;
|
||||
import org.moire.ultrasonic.util.CancellationToken;
|
||||
import org.moire.ultrasonic.util.Constants;
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask;
|
||||
import org.moire.ultrasonic.util.Util;
|
||||
import org.moire.ultrasonic.view.PodcastsChannelsAdapter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Displays the podcasts available on the server
|
||||
*/
|
||||
public class PodcastFragment extends Fragment {
|
||||
|
||||
private View emptyTextView;
|
||||
ListView channelItemsListView = null;
|
||||
private CancellationToken cancellationToken;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
Util.applyTheme(this.getContext());
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.podcasts, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
cancellationToken = new CancellationToken();
|
||||
swipeRefresh = view.findViewById(R.id.podcasts_refresh);
|
||||
swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
|
||||
{
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
load(view.getContext(), true);
|
||||
}
|
||||
});
|
||||
|
||||
FragmentTitle.Companion.setTitle(this, R.string.podcasts_label);
|
||||
|
||||
emptyTextView = view.findViewById(R.id.select_podcasts_empty);
|
||||
channelItemsListView = view.findViewById(R.id.podcasts_channels_items_list);
|
||||
channelItemsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
PodcastsChannel pc = (PodcastsChannel) parent.getItemAtPosition(position);
|
||||
if (pc == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.INTENT_PODCAST_CHANNEL_ID, pc.getId());
|
||||
Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle);
|
||||
}
|
||||
});
|
||||
|
||||
load(view.getContext(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
cancellationToken.cancel();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void load(final Context context, final boolean refresh)
|
||||
{
|
||||
BackgroundTask<List<PodcastsChannel>> task = new FragmentBackgroundTask<List<PodcastsChannel>>(getActivity(), true, swipeRefresh, cancellationToken)
|
||||
{
|
||||
@Override
|
||||
protected List<PodcastsChannel> doInBackground() throws Throwable
|
||||
{
|
||||
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||
return musicService.getPodcastsChannels(refresh);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done(List<PodcastsChannel> result)
|
||||
{
|
||||
channelItemsListView.setAdapter(new PodcastsChannelsAdapter(context, result));
|
||||
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
};
|
||||
task.execute();
|
||||
}
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
package org.moire.ultrasonic.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.domain.Genre;
|
||||
import org.moire.ultrasonic.service.MusicService;
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory;
|
||||
import org.moire.ultrasonic.util.BackgroundTask;
|
||||
import org.moire.ultrasonic.util.CancellationToken;
|
||||
import org.moire.ultrasonic.util.Constants;
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask;
|
||||
import org.moire.ultrasonic.util.Settings;
|
||||
import org.moire.ultrasonic.util.Util;
|
||||
import org.moire.ultrasonic.view.GenreAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Displays the available genres in the media library
|
||||
*/
|
||||
public class SelectGenreFragment extends Fragment {
|
||||
|
||||
private SwipeRefreshLayout refreshGenreListView;
|
||||
private ListView genreListView;
|
||||
private View emptyView;
|
||||
private CancellationToken cancellationToken;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
Util.applyTheme(this.getContext());
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.select_genre, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
cancellationToken = new CancellationToken();
|
||||
refreshGenreListView = view.findViewById(R.id.select_genre_refresh);
|
||||
genreListView = view.findViewById(R.id.select_genre_list);
|
||||
|
||||
refreshGenreListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
|
||||
{
|
||||
@Override
|
||||
public void onRefresh()
|
||||
{
|
||||
load(true);
|
||||
}
|
||||
});
|
||||
|
||||
genreListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
Genre genre = (Genre) parent.getItemAtPosition(position);
|
||||
|
||||
if (genre != null)
|
||||
{
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.INTENT_GENRE_NAME, genre.getName());
|
||||
bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, Settings.getMaxSongs());
|
||||
bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0);
|
||||
Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
emptyView = view.findViewById(R.id.select_genre_empty);
|
||||
registerForContextMenu(genreListView);
|
||||
|
||||
FragmentTitle.Companion.setTitle(this, R.string.main_genres_title);
|
||||
load(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
cancellationToken.cancel();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void load(final boolean refresh)
|
||||
{
|
||||
BackgroundTask<List<Genre>> task = new FragmentBackgroundTask<List<Genre>>(getActivity(), true, refreshGenreListView, cancellationToken)
|
||||
{
|
||||
@Override
|
||||
protected List<Genre> doInBackground()
|
||||
{
|
||||
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||
|
||||
List<Genre> genres = new ArrayList<>();
|
||||
|
||||
try
|
||||
{
|
||||
genres = musicService.getGenres(refresh);
|
||||
}
|
||||
catch (Exception x)
|
||||
{
|
||||
Timber.e(x, "Failed to load genres");
|
||||
}
|
||||
|
||||
return genres;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done(List<Genre> result)
|
||||
{
|
||||
emptyView.setVisibility(result == null || result.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
genreListView.setAdapter(new GenreAdapter(getContext(), result));
|
||||
}
|
||||
}
|
||||
};
|
||||
task.execute();
|
||||
}
|
||||
}
|
@ -1,331 +0,0 @@
|
||||
package org.moire.ultrasonic.fragment;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
|
||||
import org.moire.ultrasonic.domain.Share;
|
||||
import org.moire.ultrasonic.service.MusicService;
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory;
|
||||
import org.moire.ultrasonic.service.OfflineException;
|
||||
import org.moire.ultrasonic.subsonic.DownloadHandler;
|
||||
import org.moire.ultrasonic.util.BackgroundTask;
|
||||
import org.moire.ultrasonic.util.CancellationToken;
|
||||
import org.moire.ultrasonic.util.Constants;
|
||||
import org.moire.ultrasonic.util.LoadingTask;
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask;
|
||||
import org.moire.ultrasonic.util.TimeSpan;
|
||||
import org.moire.ultrasonic.util.TimeSpanPicker;
|
||||
import org.moire.ultrasonic.util.Util;
|
||||
import org.moire.ultrasonic.view.ShareAdapter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import kotlin.Lazy;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
/**
|
||||
* Displays the shares in the media library
|
||||
*/
|
||||
public class SharesFragment extends Fragment {
|
||||
|
||||
private SwipeRefreshLayout refreshSharesListView;
|
||||
private ListView sharesListView;
|
||||
private View emptyTextView;
|
||||
private ShareAdapter shareAdapter;
|
||||
|
||||
private final Lazy<DownloadHandler> downloadHandler = inject(DownloadHandler.class);
|
||||
private CancellationToken cancellationToken;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
Util.applyTheme(this.getContext());
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.select_share, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
cancellationToken = new CancellationToken();
|
||||
|
||||
refreshSharesListView = view.findViewById(R.id.select_share_refresh);
|
||||
sharesListView = view.findViewById(R.id.select_share_list);
|
||||
|
||||
refreshSharesListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
|
||||
{
|
||||
@Override
|
||||
public void onRefresh()
|
||||
{
|
||||
load(true);
|
||||
}
|
||||
});
|
||||
|
||||
emptyTextView = view.findViewById(R.id.select_share_empty);
|
||||
sharesListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
Share share = (Share) parent.getItemAtPosition(position);
|
||||
|
||||
if (share == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.INTENT_SHARE_ID, share.getId());
|
||||
bundle.putString(Constants.INTENT_SHARE_NAME, share.getName());
|
||||
Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle);
|
||||
}
|
||||
});
|
||||
registerForContextMenu(sharesListView);
|
||||
FragmentTitle.Companion.setTitle(this, R.string.button_bar_shares);
|
||||
|
||||
load(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
cancellationToken.cancel();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void load(final boolean refresh)
|
||||
{
|
||||
BackgroundTask<List<Share>> task = new FragmentBackgroundTask<List<Share>>(getActivity(), true, refreshSharesListView, cancellationToken)
|
||||
{
|
||||
@Override
|
||||
protected List<Share> doInBackground() throws Throwable
|
||||
{
|
||||
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||
return musicService.getShares(refresh);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done(List<Share> result)
|
||||
{
|
||||
sharesListView.setAdapter(shareAdapter = new ShareAdapter(getContext(), result));
|
||||
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
};
|
||||
task.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(@NotNull ContextMenu menu, @NotNull View view, ContextMenu.ContextMenuInfo menuInfo)
|
||||
{
|
||||
super.onCreateContextMenu(menu, view, menuInfo);
|
||||
|
||||
MenuInflater inflater = getActivity().getMenuInflater();
|
||||
inflater.inflate(R.menu.select_share_context, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem menuItem)
|
||||
{
|
||||
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
|
||||
if (info == null) return false;
|
||||
|
||||
Share share = (Share) sharesListView.getItemAtPosition(info.position);
|
||||
if (share == null || share.getId() == null) return false;
|
||||
|
||||
int itemId = menuItem.getItemId();
|
||||
if (itemId == R.id.share_menu_pin) {
|
||||
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), true, true, false, false, true, false, false);
|
||||
} else if (itemId == R.id.share_menu_unpin) {
|
||||
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, false, false, true, false, true);
|
||||
} else if (itemId == R.id.share_menu_download) {
|
||||
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, false, false, true, false, false);
|
||||
} else if (itemId == R.id.share_menu_play_now) {
|
||||
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, true, false, false, false, false);
|
||||
} else if (itemId == R.id.share_menu_play_shuffled) {
|
||||
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, true, true, false, false, false);
|
||||
} else if (itemId == R.id.share_menu_delete) {
|
||||
deleteShare(share);
|
||||
} else if (itemId == R.id.share_info) {
|
||||
displayShareInfo(share);
|
||||
} else if (itemId == R.id.share_update_info) {
|
||||
updateShareInfo(share);
|
||||
} else {
|
||||
return super.onContextItemSelected(menuItem);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void deleteShare(final Share share)
|
||||
{
|
||||
new AlertDialog.Builder(getContext()).setIcon(R.drawable.ic_baseline_warning).setTitle(R.string.common_confirm).setMessage(getResources().getString(R.string.delete_playlist, share.getName())).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which)
|
||||
{
|
||||
new LoadingTask<Void>(getActivity(), refreshSharesListView, cancellationToken)
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground() throws Throwable
|
||||
{
|
||||
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||
musicService.deleteShare(share.getId());
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done(Void result)
|
||||
{
|
||||
shareAdapter.remove(share);
|
||||
shareAdapter.notifyDataSetChanged();
|
||||
Util.toast(getContext(), getResources().getString(R.string.menu_deleted_share, share.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void error(Throwable error)
|
||||
{
|
||||
String msg;
|
||||
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_share_error, share.getName()), getErrorMessage(error));
|
||||
|
||||
Util.toast(getContext(), msg, false);
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
}).setNegativeButton(R.string.common_cancel, null).show();
|
||||
}
|
||||
|
||||
private void displayShareInfo(final Share share)
|
||||
{
|
||||
final TextView textView = new TextView(getContext());
|
||||
textView.setPadding(5, 5, 5, 5);
|
||||
|
||||
final Spannable message = new SpannableString("Owner: " + share.getUsername() +
|
||||
"\nComments: " + ((share.getDescription() == null) ? "" : share.getDescription()) +
|
||||
"\nURL: " + share.getUrl() +
|
||||
"\nEntry Count: " + share.getEntries().size() +
|
||||
"\nVisit Count: " + share.getVisitCount() +
|
||||
((share.getCreated() == null) ? "" : ("\nCreation Date: " + share.getCreated().replace('T', ' '))) +
|
||||
((share.getLastVisited() == null) ? "" : ("\nLast Visited Date: " + share.getLastVisited().replace('T', ' '))) +
|
||||
((share.getExpires() == null) ? "" : ("\nExpiration Date: " + share.getExpires().replace('T', ' '))));
|
||||
|
||||
Linkify.addLinks(message, Linkify.WEB_URLS);
|
||||
textView.setText(message);
|
||||
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
new AlertDialog.Builder(getContext()).setTitle("Share Details").setCancelable(true).setIcon(R.drawable.ic_baseline_info).setView(textView).show();
|
||||
}
|
||||
|
||||
private void updateShareInfo(final Share share)
|
||||
{
|
||||
View dialogView = getLayoutInflater().inflate(R.layout.share_details, null);
|
||||
if (dialogView == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
final EditText shareDescription = dialogView.findViewById(R.id.share_description);
|
||||
final TimeSpanPicker timeSpanPicker = dialogView.findViewById(R.id.date_picker);
|
||||
|
||||
shareDescription.setText(share.getDescription());
|
||||
|
||||
CheckBox hideDialogCheckBox = dialogView.findViewById(R.id.hide_dialog);
|
||||
CheckBox saveAsDefaultsCheckBox = dialogView.findViewById(R.id.save_as_defaults);
|
||||
CheckBox noExpirationCheckBox = dialogView.findViewById(R.id.timeSpanDisableCheckBox);
|
||||
|
||||
noExpirationCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean b)
|
||||
{
|
||||
timeSpanPicker.setEnabled(!b);
|
||||
}
|
||||
});
|
||||
|
||||
noExpirationCheckBox.setChecked(true);
|
||||
|
||||
timeSpanPicker.setTimeSpanDisableText(getResources().getText(R.string.no_expiration));
|
||||
|
||||
hideDialogCheckBox.setVisibility(View.GONE);
|
||||
saveAsDefaultsCheckBox.setVisibility(View.GONE);
|
||||
|
||||
AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext());
|
||||
|
||||
alertDialog.setIcon(R.drawable.ic_baseline_warning);
|
||||
alertDialog.setTitle(R.string.playlist_update_info);
|
||||
alertDialog.setView(dialogView);
|
||||
alertDialog.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which)
|
||||
{
|
||||
new LoadingTask<Void>(getActivity(), refreshSharesListView, cancellationToken)
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground() throws Throwable
|
||||
{
|
||||
long millis = timeSpanPicker.getTimeSpan().getTotalMilliseconds();
|
||||
|
||||
if (millis > 0)
|
||||
{
|
||||
millis = TimeSpan.getCurrentTime().add(millis).getTotalMilliseconds();
|
||||
}
|
||||
|
||||
Editable shareDescriptionText = shareDescription.getText();
|
||||
String description = shareDescriptionText != null ? shareDescriptionText.toString() : null;
|
||||
|
||||
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||
musicService.updateShare(share.getId(), description, millis);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done(Void result)
|
||||
{
|
||||
load(true);
|
||||
Util.toast(getContext(), getResources().getString(R.string.playlist_updated_info, share.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void error(Throwable error)
|
||||
{
|
||||
String msg;
|
||||
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, share.getName()), getErrorMessage(error));
|
||||
|
||||
Util.toast(getContext(), msg, false);
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
});
|
||||
|
||||
alertDialog.setNegativeButton(R.string.common_cancel, null);
|
||||
alertDialog.show();
|
||||
}
|
||||
}
|
@ -44,6 +44,7 @@ import com.google.android.material.navigation.NavigationView
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.moire.ultrasonic.NavigationGraphDirections
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
@ -76,6 +77,9 @@ class NavigationActivity : AppCompatActivity() {
|
||||
private var bookmarksMenuItem: MenuItem? = null
|
||||
private var sharesMenuItem: MenuItem? = null
|
||||
private var podcastsMenuItem: MenuItem? = null
|
||||
private var playlistsMenuItem: MenuItem? = null
|
||||
private var downloadsMenuItem: MenuItem? = null
|
||||
|
||||
private var nowPlayingView: FragmentContainerView? = null
|
||||
private var nowPlayingHidden = false
|
||||
private var navigationView: NavigationView? = null
|
||||
@ -274,15 +278,25 @@ class NavigationActivity : AppCompatActivity() {
|
||||
private fun setupNavigationMenu(navController: NavController) {
|
||||
navigationView?.setupWithNavController(navController)
|
||||
|
||||
// The exit menu is handled here manually
|
||||
val exitItem: MenuItem? = navigationView?.menu?.findItem(R.id.menu_exit)
|
||||
exitItem?.setOnMenuItemClickListener { item ->
|
||||
if (item.itemId == R.id.menu_exit) {
|
||||
setResult(Constants.RESULT_CLOSE_ALL)
|
||||
mediaPlayerController.stopJukeboxService()
|
||||
finish()
|
||||
exit()
|
||||
// The fragments which expect SafeArgs need to be navigated to with SafeArgs (even when
|
||||
// they are empty)!
|
||||
navigationView?.setNavigationItemSelectedListener {
|
||||
when (it.itemId) {
|
||||
R.id.mediaLibraryFragment -> {
|
||||
navController.navigate(NavigationGraphDirections.toMediaLibrary())
|
||||
}
|
||||
R.id.bookmarksFragment -> {
|
||||
navController.navigate(NavigationGraphDirections.toBookmarks())
|
||||
}
|
||||
R.id.menu_exit -> {
|
||||
setResult(Constants.RESULT_CLOSE_ALL)
|
||||
mediaPlayerController.stopJukeboxService()
|
||||
finish()
|
||||
exit()
|
||||
}
|
||||
else -> navController.navigate(it.itemId)
|
||||
}
|
||||
drawerLayout?.closeDrawer(GravityCompat.START)
|
||||
true
|
||||
}
|
||||
|
||||
@ -290,6 +304,9 @@ class NavigationActivity : AppCompatActivity() {
|
||||
bookmarksMenuItem = navigationView?.menu?.findItem(R.id.bookmarksFragment)
|
||||
sharesMenuItem = navigationView?.menu?.findItem(R.id.sharesFragment)
|
||||
podcastsMenuItem = navigationView?.menu?.findItem(R.id.podcastFragment)
|
||||
playlistsMenuItem = navigationView?.menu?.findItem(R.id.playlistsFragment)
|
||||
downloadsMenuItem = navigationView?.menu?.findItem(R.id.downloadsFragment)
|
||||
|
||||
selectServerButton =
|
||||
navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server)
|
||||
selectServerButton?.setOnClickListener {
|
||||
@ -457,17 +474,17 @@ class NavigationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun setMenuForServerCapabilities() {
|
||||
if (ActiveServerProvider.isOffline()) {
|
||||
chatMenuItem?.isVisible = false
|
||||
bookmarksMenuItem?.isVisible = false
|
||||
sharesMenuItem?.isVisible = false
|
||||
podcastsMenuItem?.isVisible = false
|
||||
return
|
||||
}
|
||||
val isOnline = !ActiveServerProvider.isOffline()
|
||||
val activeServer = activeServerProvider.getActiveServer()
|
||||
|
||||
// Note: Offline capabilities are defined in ActiveServerProvider, OFFLINE_DB.
|
||||
// If you add Offline support for some of these features you need
|
||||
// to switch the boolean to true there.
|
||||
chatMenuItem?.isVisible = activeServer.chatSupport != false
|
||||
bookmarksMenuItem?.isVisible = activeServer.bookmarkSupport != false
|
||||
sharesMenuItem?.isVisible = activeServer.shareSupport != false
|
||||
podcastsMenuItem?.isVisible = activeServer.podcastSupport != false
|
||||
playlistsMenuItem?.isVisible = isOnline
|
||||
downloadsMenuItem?.isVisible = isOnline
|
||||
}
|
||||
}
|
||||
|
@ -207,7 +207,11 @@ class ActiveServerProvider(
|
||||
allowSelfSignedCertificate = false,
|
||||
ldapSupport = false,
|
||||
musicFolderId = "",
|
||||
minimumApiVersion = null
|
||||
minimumApiVersion = null,
|
||||
bookmarkSupport = false,
|
||||
podcastSupport = false,
|
||||
shareSupport = false,
|
||||
chatSupport = false
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -1,23 +1,28 @@
|
||||
/*
|
||||
* AlbumListFragment.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
@file:Suppress("NAME_SHADOWING")
|
||||
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.launch
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.adapters.AlbumRowBinder
|
||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||
import org.moire.ultrasonic.domain.Album
|
||||
import org.moire.ultrasonic.model.AlbumListModel
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
|
||||
/**
|
||||
* Displays a list of Albums from the media library
|
||||
@ -39,33 +44,58 @@ class AlbumListFragment : EntryListFragment<Album>() {
|
||||
*/
|
||||
override val refreshOnCreation: Boolean = false
|
||||
|
||||
private val navArgs: AlbumListFragmentArgs by navArgs()
|
||||
|
||||
/**
|
||||
* The central function to pass a query to the model and return a LiveData object
|
||||
*/
|
||||
override fun getLiveData(
|
||||
args: Bundle?,
|
||||
refresh: Boolean
|
||||
): LiveData<List<Album>> {
|
||||
if (args == null) throw IllegalArgumentException("Required arguments are missing")
|
||||
fetchAlbums(refresh)
|
||||
|
||||
val refresh2 = args.getBoolean(Constants.INTENT_REFRESH) || refresh
|
||||
val append = args.getBoolean(Constants.INTENT_APPEND)
|
||||
|
||||
return listModel.getAlbumList(refresh2 or append, refreshListView!!, args)
|
||||
return listModel.list
|
||||
}
|
||||
|
||||
private fun fetchAlbums(refresh: Boolean = navArgs.refresh, append: Boolean = navArgs.append) {
|
||||
val refresh = navArgs.refresh || refresh
|
||||
|
||||
listModel.viewModelScope.launch(handler) {
|
||||
refreshListView?.isRefreshing = true
|
||||
|
||||
if (navArgs.type == AlbumListType.BY_ARTIST) {
|
||||
listModel.getAlbumsOfArtist(
|
||||
refresh = navArgs.refresh,
|
||||
id = navArgs.id!!,
|
||||
name = navArgs.title
|
||||
)
|
||||
} else {
|
||||
listModel.getAlbums(
|
||||
albumListType = navArgs.type,
|
||||
size = navArgs.size,
|
||||
offset = navArgs.offset,
|
||||
append = append,
|
||||
refresh = refresh or append
|
||||
)
|
||||
}
|
||||
refreshListView?.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make generic
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setTitle(navArgs.title)
|
||||
|
||||
// Attach our onScrollListener
|
||||
listView = view.findViewById<RecyclerView>(recyclerViewId).apply {
|
||||
val scrollListener = object : EndlessScrollListener(viewManager) {
|
||||
override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
|
||||
// Triggered only when new data needs to be appended to the list
|
||||
// Add whatever code is needed to append new items to the bottom of the list
|
||||
val appendArgs = getArgumentsClone()
|
||||
appendArgs.putBoolean(Constants.INTENT_APPEND, true)
|
||||
getLiveData(appendArgs)
|
||||
fetchAlbums(append = true)
|
||||
}
|
||||
}
|
||||
addOnScrollListener(scrollListener)
|
||||
@ -83,11 +113,12 @@ class AlbumListFragment : EntryListFragment<Album>() {
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Album) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(Constants.INTENT_ID, item.id)
|
||||
bundle.putBoolean(Constants.INTENT_IS_ALBUM, item.isDirectory)
|
||||
bundle.putString(Constants.INTENT_NAME, item.title)
|
||||
bundle.putString(Constants.INTENT_PARENT_ID, item.parent)
|
||||
findNavController().navigate(R.id.trackCollectionFragment, bundle)
|
||||
val action = AlbumListFragmentDirections.albumListToTrackCollection(
|
||||
item.id,
|
||||
isAlbum = item.isDirectory,
|
||||
name = item.title,
|
||||
parentId = item.parent
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,25 @@
|
||||
/*
|
||||
* ArtistListFragment.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.adapters.ArtistRowBinder
|
||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||
import org.moire.ultrasonic.domain.Index
|
||||
import org.moire.ultrasonic.model.ArtistListModel
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
|
||||
/**
|
||||
* Displays the list of Artists or Indexes (folders) from the media library
|
||||
@ -29,16 +36,18 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
|
||||
*/
|
||||
override val mainLayout = R.layout.list_layout_generic
|
||||
|
||||
private val navArgs: ArtistListFragmentArgs by navArgs()
|
||||
|
||||
/**
|
||||
* The central function to pass a query to the model and return a LiveData object
|
||||
*/
|
||||
override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<ArtistOrIndex>> {
|
||||
val refresh2 = args?.getBoolean(Constants.INTENT_REFRESH) ?: false || refresh
|
||||
return listModel.getItems(refresh2, refreshListView!!)
|
||||
override fun getLiveData(refresh: Boolean): LiveData<List<ArtistOrIndex>> {
|
||||
return listModel.getItems(navArgs.refresh || refresh, refreshListView!!)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setTitle(navArgs.title)
|
||||
|
||||
viewAdapter.register(
|
||||
ArtistRowBinder(
|
||||
@ -55,29 +64,24 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
|
||||
* If we are showing artists, we need to go to AlbumList
|
||||
*/
|
||||
override fun onItemClick(item: ArtistOrIndex) {
|
||||
Companion.onItemClick(item, findNavController())
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun onItemClick(item: ArtistOrIndex, navController: NavController) {
|
||||
val bundle = Bundle()
|
||||
|
||||
// Common arguments
|
||||
bundle.putString(Constants.INTENT_ID, item.id)
|
||||
bundle.putString(Constants.INTENT_NAME, item.name)
|
||||
bundle.putString(Constants.INTENT_PARENT_ID, item.id)
|
||||
bundle.putBoolean(Constants.INTENT_ARTIST, (item is Artist))
|
||||
|
||||
// Check type
|
||||
if (item is Index) {
|
||||
navController.navigate(R.id.artistsListToTrackCollection, bundle)
|
||||
} else {
|
||||
bundle.putString(Constants.INTENT_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST)
|
||||
bundle.putString(Constants.INTENT_ALBUM_LIST_TITLE, item.name)
|
||||
bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, 1000)
|
||||
bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0)
|
||||
navController.navigate(R.id.artistsListToAlbumsList, bundle)
|
||||
}
|
||||
// Check type
|
||||
val action = if (item is Index) {
|
||||
ArtistListFragmentDirections.artistsListToTrackCollection(
|
||||
id = item.id,
|
||||
name = item.name,
|
||||
parentId = item.id,
|
||||
isArtist = (item is Artist)
|
||||
)
|
||||
} else {
|
||||
ArtistListFragmentDirections.artistsListToAlbumsList(
|
||||
type = AlbumListType.BY_ARTIST,
|
||||
id = item.id,
|
||||
title = item.name,
|
||||
size = 1000,
|
||||
offset = 0
|
||||
)
|
||||
}
|
||||
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,6 @@ class BookmarksFragment : TrackCollectionFragment() {
|
||||
}
|
||||
|
||||
override fun getLiveData(
|
||||
args: Bundle?,
|
||||
refresh: Boolean
|
||||
): LiveData<List<MusicDirectory.Child>> {
|
||||
listModel.viewModelScope.launch(handler) {
|
||||
|
@ -41,7 +41,7 @@ class DownloadsFragment : MultiListFragment<Track>() {
|
||||
/**
|
||||
* The central function to pass a query to the model and return a LiveData object
|
||||
*/
|
||||
override fun getLiveData(args: Bundle?, refresh: Boolean): LiveData<List<Track>> {
|
||||
override fun getLiveData(refresh: Boolean): LiveData<List<Track>> {
|
||||
return listModel.getList()
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,10 @@
|
||||
/*
|
||||
* EditServerFragment.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
|
@ -24,7 +24,7 @@ abstract class EndlessScrollListener : RecyclerView.OnScrollListener {
|
||||
|
||||
// Sets the starting page index
|
||||
private val startingPageIndex = 0
|
||||
var thisManager: RecyclerView.LayoutManager
|
||||
private var thisManager: RecyclerView.LayoutManager
|
||||
|
||||
constructor(layoutManager: LinearLayoutManager) {
|
||||
thisManager = layoutManager
|
||||
|
@ -1,3 +1,10 @@
|
||||
/*
|
||||
* EntryListFragment.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
@ -13,7 +20,6 @@ import org.moire.ultrasonic.domain.GenericEntry
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
|
||||
/**
|
||||
@ -26,9 +32,9 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
||||
/**
|
||||
* Whether to show the folder selector
|
||||
*/
|
||||
fun showFolderHeader(): Boolean {
|
||||
return listModel.showSelectFolderHeader(arguments) &&
|
||||
!listModel.isOffline() && !Settings.shouldUseId3Tags
|
||||
private fun showFolderHeader(): Boolean {
|
||||
return listModel.showSelectFolderHeader() && !listModel.isOffline() &&
|
||||
!Settings.shouldUseId3Tags
|
||||
}
|
||||
|
||||
override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean {
|
||||
@ -38,12 +44,14 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
||||
}
|
||||
|
||||
override fun onItemClick(item: T) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(Constants.INTENT_ID, item.id)
|
||||
bundle.putString(Constants.INTENT_NAME, item.name)
|
||||
bundle.putString(Constants.INTENT_PARENT_ID, item.id)
|
||||
bundle.putBoolean(Constants.INTENT_ARTIST, (item is Artist))
|
||||
findNavController().navigate(R.id.trackCollectionFragment, bundle)
|
||||
val action = EntryListFragmentDirections.entryListToTrackCollection(
|
||||
id = item.id,
|
||||
name = item.name,
|
||||
parentId = item.id,
|
||||
isArtist = (item is Artist),
|
||||
)
|
||||
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@ -59,7 +67,7 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
||||
currentSetting.musicFolderId = it
|
||||
serverSettingsModel.updateItem(currentSetting)
|
||||
}
|
||||
listModel.refresh(refreshListView!!, arguments)
|
||||
listModel.refresh(refreshListView!!)
|
||||
}
|
||||
|
||||
viewAdapter.register(
|
||||
@ -71,7 +79,7 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
||||
* What to do when the list has changed
|
||||
*/
|
||||
override val defaultObserver: (List<T>) -> Unit = {
|
||||
emptyView.isVisible = it.isEmpty() && !(refreshListView?.isRefreshing?:false)
|
||||
emptyView.isVisible = it.isEmpty() && !(refreshListView?.isRefreshing ?: false)
|
||||
|
||||
if (showFolderHeader()) {
|
||||
val list = mutableListOf<Identifiable>(folderHeader)
|
||||
@ -92,12 +100,11 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
||||
)
|
||||
|
||||
listModel.musicFolders.observe(
|
||||
viewLifecycleOwner,
|
||||
{
|
||||
header.folders = it
|
||||
viewAdapter.notifyItemChanged(0)
|
||||
}
|
||||
)
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
header.folders = it
|
||||
viewAdapter.notifyItemChanged(0)
|
||||
}
|
||||
|
||||
header
|
||||
}
|
||||
|
@ -1,3 +1,10 @@
|
||||
/*
|
||||
* MainFragment.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
@ -7,12 +14,12 @@ import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.databinding.MainBinding
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
|
||||
@ -118,7 +125,7 @@ class MainFragment : Fragment(), KoinComponent {
|
||||
musicTitle.isVisible = true
|
||||
artistsButton.isVisible = true
|
||||
albumsButton.isVisible = isOnline || useId3Offline
|
||||
genresButton.isVisible = true
|
||||
genresButton.isVisible = isOnline
|
||||
|
||||
// Songs
|
||||
songsTitle.isVisible = true
|
||||
@ -143,35 +150,35 @@ class MainFragment : Fragment(), KoinComponent {
|
||||
|
||||
private fun setupClickListener() {
|
||||
albumsNewestButton.setOnClickListener {
|
||||
showAlbumList("newest", R.string.main_albums_newest)
|
||||
showAlbumList(AlbumListType.NEWEST, R.string.main_albums_newest)
|
||||
}
|
||||
|
||||
albumsRandomButton.setOnClickListener {
|
||||
showAlbumList("random", R.string.main_albums_random)
|
||||
showAlbumList(AlbumListType.RANDOM, R.string.main_albums_random)
|
||||
}
|
||||
|
||||
albumsHighestButton.setOnClickListener {
|
||||
showAlbumList("highest", R.string.main_albums_highest)
|
||||
showAlbumList(AlbumListType.HIGHEST, R.string.main_albums_highest)
|
||||
}
|
||||
|
||||
albumsRecentButton.setOnClickListener {
|
||||
showAlbumList("recent", R.string.main_albums_recent)
|
||||
showAlbumList(AlbumListType.RECENT, R.string.main_albums_recent)
|
||||
}
|
||||
|
||||
albumsFrequentButton.setOnClickListener {
|
||||
showAlbumList("frequent", R.string.main_albums_frequent)
|
||||
showAlbumList(AlbumListType.FREQUENT, R.string.main_albums_frequent)
|
||||
}
|
||||
|
||||
albumsStarredButton.setOnClickListener {
|
||||
showAlbumList(Constants.STARRED, R.string.main_albums_starred)
|
||||
showAlbumList(AlbumListType.STARRED, R.string.main_albums_starred)
|
||||
}
|
||||
|
||||
albumsAlphaByNameButton.setOnClickListener {
|
||||
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_alphaByName)
|
||||
showAlbumList(AlbumListType.SORTED_BY_NAME, R.string.main_albums_alphaByName)
|
||||
}
|
||||
|
||||
albumsAlphaByArtistButton.setOnClickListener {
|
||||
showAlbumList("alphabeticalByArtist", R.string.main_albums_alphaByArtist)
|
||||
showAlbumList(AlbumListType.SORTED_BY_ARTIST, R.string.main_albums_alphaByArtist)
|
||||
}
|
||||
|
||||
songsStarredButton.setOnClickListener {
|
||||
@ -183,7 +190,7 @@ class MainFragment : Fragment(), KoinComponent {
|
||||
}
|
||||
|
||||
albumsButton.setOnClickListener {
|
||||
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_title)
|
||||
showAlbumList(AlbumListType.SORTED_BY_NAME, R.string.main_albums_title)
|
||||
}
|
||||
|
||||
randomSongsButton.setOnClickListener {
|
||||
@ -200,45 +207,47 @@ class MainFragment : Fragment(), KoinComponent {
|
||||
}
|
||||
|
||||
private fun showStarredSongs() {
|
||||
val bundle = Bundle()
|
||||
bundle.putInt(Constants.INTENT_STARRED, 1)
|
||||
Navigation.findNavController(requireView()).navigate(R.id.mainToTrackCollection, bundle)
|
||||
val action = MainFragmentDirections.mainToTrackCollection(
|
||||
getStarred = true,
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
private fun showRandomSongs() {
|
||||
val bundle = Bundle()
|
||||
bundle.putInt(Constants.INTENT_RANDOM, 1)
|
||||
bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, Settings.maxSongs)
|
||||
Navigation.findNavController(requireView()).navigate(R.id.mainToTrackCollection, bundle)
|
||||
val action = MainFragmentDirections.mainToTrackCollection(
|
||||
getRandom = true,
|
||||
size = Settings.maxSongs
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
private fun showArtists() {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(
|
||||
Constants.INTENT_ALBUM_LIST_TITLE,
|
||||
requireContext().resources.getString(R.string.main_artists_title)
|
||||
val action = MainFragmentDirections.mainToArtistList(
|
||||
title = requireContext().resources.getString(R.string.main_artists_title)
|
||||
)
|
||||
Navigation.findNavController(requireView()).navigate(R.id.mainToArtistList, bundle)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
private fun showAlbumList(type: String, titleIndex: Int) {
|
||||
val bundle = Bundle()
|
||||
private fun showAlbumList(type: AlbumListType, titleIndex: Int) {
|
||||
val title = requireContext().resources.getString(titleIndex, "")
|
||||
bundle.putString(Constants.INTENT_ALBUM_LIST_TYPE, type)
|
||||
bundle.putString(Constants.INTENT_ALBUM_LIST_TITLE, title)
|
||||
bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, Settings.maxAlbums)
|
||||
bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0)
|
||||
Navigation.findNavController(requireView()).navigate(R.id.mainToAlbumList, bundle)
|
||||
val action = MainFragmentDirections.mainToAlbumList(
|
||||
type = type,
|
||||
title = title,
|
||||
size = Settings.maxAlbums,
|
||||
offset = 0
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
private fun showGenres() {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.mainToSelectGenre)
|
||||
findNavController().navigate(R.id.mainToSelectGenre)
|
||||
}
|
||||
|
||||
private fun showVideos() {
|
||||
val bundle = Bundle()
|
||||
bundle.putInt(Constants.INTENT_VIDEOS, 1)
|
||||
Navigation.findNavController(requireView()).navigate(R.id.mainToTrackCollection, bundle)
|
||||
val action = MainFragmentDirections.mainToTrackCollection(
|
||||
getVideos = true,
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -8,6 +8,8 @@
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
@ -22,6 +24,7 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.moire.ultrasonic.R
|
||||
@ -32,7 +35,7 @@ import org.moire.ultrasonic.model.GenericListModel
|
||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.CommunicationError
|
||||
import org.moire.ultrasonic.util.Util
|
||||
|
||||
/**
|
||||
@ -67,12 +70,12 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
|
||||
* The LiveData containing the list provided by the model
|
||||
* Implement this as a getter
|
||||
*/
|
||||
internal lateinit var liveDataItems: LiveData<List<T>>
|
||||
private lateinit var liveDataItems: LiveData<List<T>>
|
||||
|
||||
/**
|
||||
* The central function to pass a query to the model and return a LiveData object
|
||||
*/
|
||||
open fun getLiveData(args: Bundle? = null, refresh: Boolean = false): LiveData<List<T>> {
|
||||
open fun getLiveData(refresh: Boolean = false): LiveData<List<T>> {
|
||||
return MutableLiveData()
|
||||
}
|
||||
|
||||
@ -94,6 +97,16 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
|
||||
*/
|
||||
open val refreshOnCreation: Boolean = true
|
||||
|
||||
/**
|
||||
* The default Exception Handler for Coroutines
|
||||
*/
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
CommunicationError.handleError(exception, context)
|
||||
}
|
||||
refreshListView?.isRefreshing = false
|
||||
}
|
||||
|
||||
open fun setTitle(title: String?) {
|
||||
if (title == null) {
|
||||
FragmentTitle.setTitle(
|
||||
@ -118,17 +131,14 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Set the title if available
|
||||
setTitle(arguments?.getString(Constants.INTENT_ALBUM_LIST_TITLE))
|
||||
|
||||
// Setup refresh handler
|
||||
refreshListView = view.findViewById(refreshListId)
|
||||
refreshListView?.setOnRefreshListener {
|
||||
listModel.refresh(refreshListView!!, arguments)
|
||||
listModel.refresh(refreshListView!!)
|
||||
}
|
||||
|
||||
// Populate the LiveData. This starts an API request in most cases
|
||||
liveDataItems = getLiveData(arguments, refreshOnCreation)
|
||||
liveDataItems = getLiveData(refreshOnCreation)
|
||||
|
||||
// Link view to display text if the list is empty
|
||||
emptyView = view.findViewById(emptyViewId)
|
||||
@ -165,16 +175,4 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
|
||||
abstract fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean
|
||||
|
||||
abstract fun onItemClick(item: T)
|
||||
|
||||
fun getArgumentsClone(): Bundle {
|
||||
var bundle: Bundle
|
||||
|
||||
try {
|
||||
bundle = arguments?.clone() as Bundle
|
||||
} catch (ignored: Exception) {
|
||||
bundle = Bundle()
|
||||
}
|
||||
|
||||
return bundle
|
||||
}
|
||||
}
|
||||
|
@ -17,15 +17,16 @@ import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import java.lang.Exception
|
||||
import kotlin.math.abs
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.moire.ultrasonic.NavigationGraphDirections
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util.applyTheme
|
||||
import org.moire.ultrasonic.util.Util.getNotificationImageSize
|
||||
@ -35,7 +36,6 @@ import timber.log.Timber
|
||||
/**
|
||||
* Contains the mini-now playing information box displayed at the bottom of the screen
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class NowPlayingFragment : Fragment() {
|
||||
|
||||
private var downX = 0f
|
||||
@ -107,21 +107,13 @@ class NowPlayingFragment : Fragment() {
|
||||
nowPlayingArtist!!.text = artist
|
||||
|
||||
nowPlayingAlbumArtImage!!.setOnClickListener {
|
||||
val bundle = Bundle()
|
||||
|
||||
if (Settings.shouldUseId3Tags) {
|
||||
bundle.putBoolean(Constants.INTENT_IS_ALBUM, true)
|
||||
bundle.putString(Constants.INTENT_ID, file.albumId)
|
||||
} else {
|
||||
bundle.putBoolean(Constants.INTENT_IS_ALBUM, false)
|
||||
bundle.putString(Constants.INTENT_ID, file.parent)
|
||||
}
|
||||
|
||||
bundle.putString(Constants.INTENT_NAME, file.album)
|
||||
bundle.putString(Constants.INTENT_NAME, file.album)
|
||||
|
||||
Navigation.findNavController(requireActivity(), R.id.nav_host_fragment)
|
||||
.navigate(R.id.trackCollectionFragment, bundle)
|
||||
val id3 = Settings.shouldUseId3Tags
|
||||
val action = NavigationGraphDirections.toTrackCollection(
|
||||
isAlbum = id3,
|
||||
id = if (id3) file.albumId else file.parent,
|
||||
name = file.album
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,10 @@
|
||||
/*
|
||||
* OnBackPressedHandler.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
/**
|
||||
|
@ -43,6 +43,7 @@ import androidx.media3.common.Player
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.session.SessionResult
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@ -160,7 +161,7 @@ class PlayerFragment :
|
||||
private val hollowStar = R.drawable.ic_star_hollow
|
||||
private val fullStar = R.drawable.ic_star_full
|
||||
|
||||
internal val viewAdapter: BaseAdapter<Identifiable> by lazy {
|
||||
private val viewAdapter: BaseAdapter<Identifiable> by lazy {
|
||||
BaseAdapter()
|
||||
}
|
||||
|
||||
@ -205,7 +206,7 @@ class PlayerFragment :
|
||||
fiveStar5ImageView = view.findViewById(R.id.song_five_star_5)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "DEPRECATION")
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
cancellationToken = CancellationToken()
|
||||
@ -353,15 +354,6 @@ class PlayerFragment :
|
||||
|
||||
registerForContextMenu(playlistView)
|
||||
|
||||
if (arguments != null && requireArguments().getBoolean(
|
||||
Constants.INTENT_SHUFFLE,
|
||||
false
|
||||
)
|
||||
) {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
mediaPlayerController.isShufflePlayEnabled = true
|
||||
}
|
||||
|
||||
visualizerViewLayout.isVisible = false
|
||||
VisualizerController.get().observe(
|
||||
requireActivity()
|
||||
@ -641,11 +633,14 @@ class PlayerFragment :
|
||||
if (track == null) return false
|
||||
|
||||
if (Settings.shouldUseId3Tags) {
|
||||
PlayerFragmentDirections.playerToSelectAlbum(
|
||||
id = track.artistId,
|
||||
name = track.artist,
|
||||
parentId = track.artistId,
|
||||
isArtist = true,
|
||||
)
|
||||
bundle = Bundle()
|
||||
bundle.putString(Constants.INTENT_ID, track.artistId)
|
||||
bundle.putString(Constants.INTENT_NAME, track.artist)
|
||||
bundle.putString(Constants.INTENT_PARENT_ID, track.artistId)
|
||||
bundle.putBoolean(Constants.INTENT_ARTIST, true)
|
||||
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(R.id.playerToSelectAlbum, bundle)
|
||||
}
|
||||
@ -655,18 +650,19 @@ class PlayerFragment :
|
||||
if (track == null) return false
|
||||
|
||||
val albumId = if (Settings.shouldUseId3Tags) track.albumId else track.parent
|
||||
bundle = Bundle()
|
||||
bundle.putString(Constants.INTENT_ID, albumId)
|
||||
bundle.putString(Constants.INTENT_NAME, track.album)
|
||||
bundle.putString(Constants.INTENT_PARENT_ID, track.parent)
|
||||
bundle.putBoolean(Constants.INTENT_IS_ALBUM, true)
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(R.id.playerToSelectAlbum, bundle)
|
||||
|
||||
val action = PlayerFragmentDirections.playerToSelectAlbum(
|
||||
id = albumId,
|
||||
name = track.album,
|
||||
parentId = track.parent,
|
||||
isAlbum = true
|
||||
)
|
||||
|
||||
findNavController().navigate(action)
|
||||
return true
|
||||
}
|
||||
R.id.menu_lyrics -> {
|
||||
if (track == null) return false
|
||||
|
||||
bundle = Bundle()
|
||||
bundle.putString(Constants.INTENT_ARTIST, track.artist)
|
||||
bundle.putString(Constants.INTENT_TITLE, track.title)
|
||||
@ -815,7 +811,12 @@ class PlayerFragment :
|
||||
val playlistEntry = item.toTrack()
|
||||
tracks.add(playlistEntry)
|
||||
}
|
||||
shareHandler.createShare(this, tracks, null, cancellationToken)
|
||||
shareHandler.createShare(
|
||||
this,
|
||||
tracks = tracks,
|
||||
swipe = null,
|
||||
cancellationToken = cancellationToken,
|
||||
)
|
||||
return true
|
||||
}
|
||||
R.id.menu_item_share_song -> {
|
||||
@ -824,7 +825,12 @@ class PlayerFragment :
|
||||
val tracks: MutableList<Track?> = ArrayList()
|
||||
tracks.add(currentSong)
|
||||
|
||||
shareHandler.createShare(this, tracks, null, cancellationToken)
|
||||
shareHandler.createShare(
|
||||
this,
|
||||
tracks,
|
||||
swipe = null,
|
||||
cancellationToken = cancellationToken
|
||||
)
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
|
@ -1,3 +1,10 @@
|
||||
/*
|
||||
* SearchFragment.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.app.SearchManager
|
||||
@ -11,7 +18,6 @@ import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.launch
|
||||
@ -24,6 +30,7 @@ import org.moire.ultrasonic.adapters.DividerBinder
|
||||
import org.moire.ultrasonic.adapters.MoreButtonBinder
|
||||
import org.moire.ultrasonic.adapters.MoreButtonBinder.MoreButton
|
||||
import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||
import org.moire.ultrasonic.domain.Album
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||
@ -47,6 +54,8 @@ import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Initiates a search on the media library and displays the results
|
||||
*
|
||||
* TODO: Move to SafeArgs
|
||||
*/
|
||||
class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
private var searchResult: SearchResult? = null
|
||||
@ -266,33 +275,37 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
}
|
||||
|
||||
private fun onArtistSelected(item: ArtistOrIndex) {
|
||||
val bundle = Bundle()
|
||||
|
||||
// Common arguments
|
||||
bundle.putString(Constants.INTENT_ID, item.id)
|
||||
bundle.putString(Constants.INTENT_NAME, item.name)
|
||||
bundle.putString(Constants.INTENT_PARENT_ID, item.id)
|
||||
bundle.putBoolean(Constants.INTENT_ARTIST, (item is Artist))
|
||||
|
||||
// Check type
|
||||
if (item is Index) {
|
||||
findNavController().navigate(R.id.searchToTrackCollection, bundle)
|
||||
// Create action based on type
|
||||
val action = if (item is Index) {
|
||||
SearchFragmentDirections.searchToTrackCollection(
|
||||
id = item.id,
|
||||
name = item.name,
|
||||
parentId = item.id,
|
||||
isArtist = (item is Artist)
|
||||
)
|
||||
} else {
|
||||
bundle.putString(Constants.INTENT_ALBUM_LIST_TYPE, Constants.ALBUMS_OF_ARTIST)
|
||||
bundle.putString(Constants.INTENT_ALBUM_LIST_TITLE, item.name)
|
||||
bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, 1000)
|
||||
bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0)
|
||||
findNavController().navigate(R.id.searchToAlbumsList, bundle)
|
||||
SearchFragmentDirections.searchToAlbumsList(
|
||||
type = AlbumListType.BY_ARTIST,
|
||||
id = item.id,
|
||||
title = item.name,
|
||||
size = 1000,
|
||||
offset = 0
|
||||
)
|
||||
}
|
||||
|
||||
// Lets go!
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
private fun onAlbumSelected(album: Album, autoplay: Boolean) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(Constants.INTENT_ID, album.id)
|
||||
bundle.putString(Constants.INTENT_NAME, album.title)
|
||||
bundle.putBoolean(Constants.INTENT_IS_ALBUM, album.isDirectory)
|
||||
bundle.putBoolean(Constants.INTENT_AUTOPLAY, autoplay)
|
||||
Navigation.findNavController(requireView()).navigate(R.id.searchToTrackCollection, bundle)
|
||||
|
||||
val action = SearchFragmentDirections.searchToTrackCollection(
|
||||
id = album.id,
|
||||
name = album.title,
|
||||
autoPlay = autoplay,
|
||||
isAlbum = true
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
private fun onSongSelected(song: Track, append: Boolean) {
|
||||
@ -366,7 +379,8 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
autoPlay = true,
|
||||
playNext = false,
|
||||
shuffle = false,
|
||||
songs = songs
|
||||
songs = songs,
|
||||
playlistName = null
|
||||
)
|
||||
}
|
||||
R.id.song_menu_play_next -> {
|
||||
@ -378,7 +392,8 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
autoPlay = false,
|
||||
playNext = true,
|
||||
shuffle = false,
|
||||
songs = songs
|
||||
songs = songs,
|
||||
playlistName = null
|
||||
)
|
||||
}
|
||||
R.id.song_menu_play_last -> {
|
||||
@ -390,7 +405,8 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
autoPlay = false,
|
||||
playNext = false,
|
||||
shuffle = false,
|
||||
songs = songs
|
||||
songs = songs,
|
||||
playlistName = null
|
||||
)
|
||||
}
|
||||
R.id.song_menu_pin -> {
|
||||
@ -431,7 +447,13 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
}
|
||||
R.id.song_menu_share -> {
|
||||
songs.add(item)
|
||||
shareHandler.createShare(this, songs, searchRefresh, cancellationToken!!)
|
||||
shareHandler.createShare(
|
||||
fragment = this,
|
||||
tracks = songs,
|
||||
swipe = searchRefresh,
|
||||
cancellationToken = cancellationToken!!,
|
||||
additionalId = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,8 +8,6 @@
|
||||
package org.moire.ultrasonic.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
@ -19,11 +17,11 @@ import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import java.util.Collections
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.moire.ultrasonic.R
|
||||
@ -45,9 +43,7 @@ import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
||||
import org.moire.ultrasonic.subsonic.ShareHandler
|
||||
import org.moire.ultrasonic.subsonic.VideoPlayer
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.CommunicationError
|
||||
import org.moire.ultrasonic.util.ConfirmationDialog
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
@ -93,6 +89,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
*/
|
||||
override val mainLayout: Int = R.layout.list_layout_track
|
||||
|
||||
private val navArgs: TrackCollectionFragmentArgs by navArgs()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
cancellationToken = CancellationToken()
|
||||
@ -102,7 +100,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
// Setup refresh handler
|
||||
refreshListView = view.findViewById(refreshListId)
|
||||
refreshListView?.setOnRefreshListener {
|
||||
getLiveData(arguments, true)
|
||||
handleRefresh()
|
||||
}
|
||||
|
||||
setupButtons(view)
|
||||
@ -155,6 +153,10 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
}
|
||||
}
|
||||
|
||||
internal open fun handleRefresh() {
|
||||
getLiveData(true)
|
||||
}
|
||||
|
||||
internal open fun setupButtons(view: View) {
|
||||
selectButton = view.findViewById(R.id.select_album_select)
|
||||
playNowButton = view.findViewById(R.id.select_album_play_now)
|
||||
@ -178,7 +180,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
downloadHandler.download(
|
||||
this@TrackCollectionFragment, append = true,
|
||||
save = false, autoPlay = false, playNext = true, shuffle = false,
|
||||
songs = getSelectedSongs()
|
||||
songs = getSelectedSongs(),
|
||||
playlistName = navArgs.playlistName
|
||||
)
|
||||
}
|
||||
|
||||
@ -219,13 +222,6 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
}
|
||||
}
|
||||
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
CommunicationError.handleError(exception, context)
|
||||
}
|
||||
refreshListView?.isRefreshing = false
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
playAllButton = menu.findItem(R.id.select_album_play_all)
|
||||
@ -254,7 +250,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
} else if (itemId == R.id.menu_item_share) {
|
||||
shareHandler.createShare(
|
||||
this, getSelectedSongs(),
|
||||
refreshListView, cancellationToken!!
|
||||
refreshListView, cancellationToken!!,
|
||||
navArgs.id
|
||||
)
|
||||
return true
|
||||
}
|
||||
@ -274,7 +271,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
if (selectedSongs.isNotEmpty()) {
|
||||
downloadHandler.download(
|
||||
this, append, false, !append, playNext = false,
|
||||
shuffle = false, songs = selectedSongs
|
||||
shuffle = false, songs = selectedSongs, null
|
||||
)
|
||||
} else {
|
||||
playAll(false, append)
|
||||
@ -304,10 +301,10 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
}
|
||||
}
|
||||
|
||||
val isArtist = arguments?.getBoolean(Constants.INTENT_ARTIST, false) ?: false
|
||||
val id = arguments?.getString(Constants.INTENT_ID)
|
||||
val isArtist = navArgs.isArtist
|
||||
val id = navArgs.id
|
||||
|
||||
if (hasSubFolders && id != null) {
|
||||
if (hasSubFolders) {
|
||||
downloadHandler.downloadRecursively(
|
||||
fragment = this,
|
||||
id = id,
|
||||
@ -328,7 +325,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
autoPlay = !append,
|
||||
playNext = false,
|
||||
shuffle = shuffle,
|
||||
songs = getAllSongs()
|
||||
songs = getAllSongs(),
|
||||
playlistName = navArgs.playlistName
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -465,7 +463,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
}
|
||||
}
|
||||
|
||||
val listSize = arguments?.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) ?: 0
|
||||
val listSize = navArgs.size
|
||||
|
||||
// Hide select button for video lists and singular selection lists
|
||||
selectButton!!.isVisible = !allVideos && viewAdapter.hasMultipleSelection() && songCount > 0
|
||||
@ -475,9 +473,9 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
moreButton!!.visibility = View.GONE
|
||||
} else {
|
||||
moreButton!!.visibility = View.VISIBLE
|
||||
if ((arguments?.getInt(Constants.INTENT_RANDOM, 0) ?: 0) > 0) {
|
||||
if (navArgs.getRandom) {
|
||||
moreRandomTracks()
|
||||
} else if ((arguments?.getString(Constants.INTENT_GENRE_NAME, "") ?: "") != "") {
|
||||
} else if (navArgs.genreName != null) {
|
||||
moreSongsForGenre()
|
||||
}
|
||||
}
|
||||
@ -488,9 +486,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
|
||||
enableButtons()
|
||||
|
||||
val isAlbumList = arguments?.containsKey(
|
||||
Constants.INTENT_ALBUM_LIST_TYPE
|
||||
) ?: false
|
||||
val isAlbumList = (navArgs.albumListType != null)
|
||||
|
||||
playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos
|
||||
shareButtonVisible = !isOffline() && songCount > 0
|
||||
@ -499,7 +495,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
shareButton?.isVisible = shareButtonVisible
|
||||
|
||||
if (songCount > 0 && listModel.showHeader) {
|
||||
val intentAlbumName = arguments?.getString(Constants.INTENT_NAME, "")
|
||||
val intentAlbumName = navArgs.name
|
||||
val albumHeader = AlbumHeader(it, intentAlbumName)
|
||||
val mixedList: MutableList<Identifiable> = mutableListOf(albumHeader)
|
||||
mixedList.addAll(entryList)
|
||||
@ -508,11 +504,11 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
viewAdapter.submitList(entryList)
|
||||
}
|
||||
|
||||
val playAll = arguments?.getBoolean(Constants.INTENT_AUTOPLAY, false) ?: false
|
||||
val playAll = navArgs.autoPlay
|
||||
|
||||
if (playAll && songCount > 0) {
|
||||
playAll(
|
||||
arguments?.getBoolean(Constants.INTENT_SHUFFLE, false) ?: false,
|
||||
navArgs.shuffle,
|
||||
false
|
||||
)
|
||||
}
|
||||
@ -522,37 +518,30 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
Timber.i("Processed list")
|
||||
}
|
||||
|
||||
private fun moreSongsForGenre(args: Bundle = requireArguments()) {
|
||||
private fun moreSongsForGenre() {
|
||||
moreButton!!.setOnClickListener {
|
||||
val theGenre = args.getString(Constants.INTENT_GENRE_NAME)
|
||||
val size = args.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0)
|
||||
val theOffset = args.getInt(
|
||||
Constants.INTENT_ALBUM_LIST_OFFSET, 0
|
||||
) + size
|
||||
val bundle = Bundle()
|
||||
bundle.putString(Constants.INTENT_GENRE_NAME, theGenre)
|
||||
bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, size)
|
||||
bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, theOffset)
|
||||
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(R.id.trackCollectionFragment, bundle)
|
||||
val action = TrackCollectionFragmentDirections.loadMoreTracks(
|
||||
genreName = navArgs.genreName,
|
||||
size = navArgs.size,
|
||||
offset = navArgs.offset + navArgs.size
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun moreRandomTracks() {
|
||||
val listSize = arguments?.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0) ?: 0
|
||||
|
||||
val listSize = navArgs.size
|
||||
|
||||
moreButton!!.setOnClickListener {
|
||||
val offset = requireArguments().getInt(
|
||||
Constants.INTENT_ALBUM_LIST_OFFSET, 0
|
||||
) + listSize
|
||||
val bundle = Bundle()
|
||||
bundle.putInt(Constants.INTENT_RANDOM, 1)
|
||||
bundle.putInt(Constants.INTENT_ALBUM_LIST_SIZE, listSize)
|
||||
bundle.putInt(Constants.INTENT_ALBUM_LIST_OFFSET, offset)
|
||||
Navigation.findNavController(requireView()).navigate(
|
||||
R.id.trackCollectionFragment, bundle
|
||||
val offset = navArgs.offset + listSize
|
||||
|
||||
val action = TrackCollectionFragmentDirections.loadMoreTracks(
|
||||
getRandom = true,
|
||||
size = listSize,
|
||||
offset = offset
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
}
|
||||
|
||||
@ -576,27 +565,25 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun getLiveData(
|
||||
args: Bundle?,
|
||||
refresh: Boolean
|
||||
): LiveData<List<MusicDirectory.Child>> {
|
||||
Timber.i("Starting gathering track collection data...")
|
||||
if (args == null) return listModel.currentList
|
||||
val id = args.getString(Constants.INTENT_ID)
|
||||
val isAlbum = args.getBoolean(Constants.INTENT_IS_ALBUM, false)
|
||||
val name = args.getString(Constants.INTENT_NAME)
|
||||
val playlistId = args.getString(Constants.INTENT_PLAYLIST_ID)
|
||||
val podcastChannelId = args.getString(Constants.INTENT_PODCAST_CHANNEL_ID)
|
||||
val playlistName = args.getString(Constants.INTENT_PLAYLIST_NAME)
|
||||
val shareId = args.getString(Constants.INTENT_SHARE_ID)
|
||||
val shareName = args.getString(Constants.INTENT_SHARE_NAME)
|
||||
val genreName = args.getString(Constants.INTENT_GENRE_NAME)
|
||||
val id = navArgs.id
|
||||
val isAlbum = navArgs.isAlbum
|
||||
val name = navArgs.name
|
||||
val playlistId = navArgs.playlistId
|
||||
val podcastChannelId = navArgs.podcastChannelId
|
||||
val playlistName = navArgs.playlistName
|
||||
val shareId = navArgs.shareId
|
||||
val shareName = navArgs.shareName
|
||||
val genreName = navArgs.genreName
|
||||
|
||||
val getStarredTracks = args.getInt(Constants.INTENT_STARRED, 0)
|
||||
val getVideos = args.getInt(Constants.INTENT_VIDEOS, 0)
|
||||
val getRandomTracks = args.getInt(Constants.INTENT_RANDOM, 0)
|
||||
val albumListSize = args.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0)
|
||||
val albumListOffset = args.getInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0)
|
||||
val refresh2 = args.getBoolean(Constants.INTENT_REFRESH, true) || refresh
|
||||
val getStarredTracks = navArgs.getStarred
|
||||
val getVideos = navArgs.getVideos
|
||||
val getRandomTracks = navArgs.getRandom
|
||||
val albumListSize = navArgs.size
|
||||
val albumListOffset = navArgs.offset
|
||||
val refresh2 = navArgs.refresh || refresh
|
||||
|
||||
listModel.viewModelScope.launch(handler) {
|
||||
refreshListView?.isRefreshing = true
|
||||
@ -613,13 +600,13 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
} else if (genreName != null) {
|
||||
setTitle(genreName)
|
||||
listModel.getSongsForGenre(genreName, albumListSize, albumListOffset)
|
||||
} else if (getStarredTracks != 0) {
|
||||
} else if (getStarredTracks) {
|
||||
setTitle(getString(R.string.main_songs_starred))
|
||||
listModel.getStarred()
|
||||
} else if (getVideos != 0) {
|
||||
} else if (getVideos) {
|
||||
setTitle(R.string.main_videos)
|
||||
listModel.getVideos(refresh2)
|
||||
} else if (getRandomTracks != 0) {
|
||||
} else if (getRandomTracks) {
|
||||
setTitle(R.string.main_songs_random)
|
||||
listModel.getRandom(albumListSize)
|
||||
} else {
|
||||
@ -659,7 +646,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
autoPlay = false,
|
||||
playNext = true,
|
||||
shuffle = false,
|
||||
songs = songs
|
||||
songs = songs,
|
||||
playlistName = navArgs.playlistName
|
||||
)
|
||||
}
|
||||
R.id.song_menu_play_last -> {
|
||||
@ -681,8 +669,11 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
R.id.song_menu_share -> {
|
||||
if (item is Track) {
|
||||
shareHandler.createShare(
|
||||
this, listOf(item), refreshListView,
|
||||
cancellationToken!!
|
||||
this,
|
||||
tracks = listOf(item),
|
||||
swipe = refreshListView,
|
||||
cancellationToken = cancellationToken!!,
|
||||
additionalId = navArgs.id
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -706,15 +697,13 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||
override fun onItemClick(item: MusicDirectory.Child) {
|
||||
when {
|
||||
item.isDirectory -> {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(Constants.INTENT_ID, item.id)
|
||||
bundle.putBoolean(Constants.INTENT_IS_ALBUM, item.isDirectory)
|
||||
bundle.putString(Constants.INTENT_NAME, item.title)
|
||||
bundle.putString(Constants.INTENT_PARENT_ID, item.parent)
|
||||
Navigation.findNavController(requireView()).navigate(
|
||||
R.id.trackCollectionFragment,
|
||||
bundle
|
||||
val action = TrackCollectionFragmentDirections.loadMoreTracks(
|
||||
id = item.id,
|
||||
isAlbum = true,
|
||||
name = item.title,
|
||||
parentId = item.parent
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
item is Track && item.isVideo -> {
|
||||
VideoPlayer.playVideo(requireContext(), item)
|
||||
|
@ -0,0 +1,345 @@
|
||||
/*
|
||||
* PlaylistsFragment.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment.legacy
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.util.Linkify
|
||||
import android.view.ContextMenu
|
||||
import android.view.ContextMenu.ContextMenuInfo
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import java.util.Locale
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.domain.Playlist
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.service.OfflineException
|
||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
||||
import org.moire.ultrasonic.util.BackgroundTask
|
||||
import org.moire.ultrasonic.util.CacheCleaner
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
||||
import org.moire.ultrasonic.util.LoadingTask
|
||||
import org.moire.ultrasonic.util.Util.applyTheme
|
||||
import org.moire.ultrasonic.util.Util.toast
|
||||
import org.moire.ultrasonic.view.PlaylistAdapter
|
||||
|
||||
/**
|
||||
* Displays the playlists stored on the server
|
||||
*
|
||||
* TODO: This file has been converted from Java, but not modernized yet.
|
||||
*/
|
||||
class PlaylistsFragment : Fragment() {
|
||||
private var refreshPlaylistsListView: SwipeRefreshLayout? = null
|
||||
private var playlistsListView: ListView? = null
|
||||
private var emptyTextView: View? = null
|
||||
private var playlistAdapter: PlaylistAdapter? = null
|
||||
private val downloadHandler = inject<DownloadHandler>(
|
||||
DownloadHandler::class.java
|
||||
)
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
applyTheme(this.context)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.select_playlist, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
cancellationToken = CancellationToken()
|
||||
refreshPlaylistsListView = view.findViewById(R.id.select_playlist_refresh)
|
||||
playlistsListView = view.findViewById(R.id.select_playlist_list)
|
||||
refreshPlaylistsListView!!.setOnRefreshListener { load(true) }
|
||||
emptyTextView = view.findViewById(R.id.select_playlist_empty)
|
||||
playlistsListView!!.setOnItemClickListener { parent, _, position, _ ->
|
||||
val (id1, name) = parent.getItemAtPosition(position) as Playlist
|
||||
|
||||
val action = PlaylistsFragmentDirections.playlistsToTrackCollection(
|
||||
id = id1,
|
||||
playlistId = id1,
|
||||
name = name,
|
||||
playlistName = name,
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
registerForContextMenu(playlistsListView!!)
|
||||
setTitle(this, R.string.playlist_label)
|
||||
load(false)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cancellationToken!!.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun load(refresh: Boolean) {
|
||||
val task: BackgroundTask<List<Playlist>> =
|
||||
object : FragmentBackgroundTask<List<Playlist>>(
|
||||
activity, true, refreshPlaylistsListView, cancellationToken
|
||||
) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): List<Playlist> {
|
||||
val musicService = getMusicService()
|
||||
val playlists = musicService.getPlaylists(refresh)
|
||||
if (!isOffline()) CacheCleaner().cleanPlaylists(playlists)
|
||||
return playlists
|
||||
}
|
||||
|
||||
override fun done(result: List<Playlist>) {
|
||||
playlistsListView!!.adapter =
|
||||
PlaylistAdapter(context, result).also { playlistAdapter = it }
|
||||
emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
task.execute()
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) {
|
||||
super.onCreateContextMenu(menu, view, menuInfo)
|
||||
val inflater = requireActivity().menuInflater
|
||||
if (isOffline()) inflater.inflate(
|
||||
R.menu.select_playlist_context_offline,
|
||||
menu
|
||||
) else inflater.inflate(R.menu.select_playlist_context, menu)
|
||||
val downloadMenuItem = menu.findItem(R.id.playlist_menu_download)
|
||||
if (downloadMenuItem != null) {
|
||||
downloadMenuItem.isVisible = !isOffline()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContextItemSelected(menuItem: MenuItem): Boolean {
|
||||
val info = menuItem.menuInfo as AdapterContextMenuInfo
|
||||
val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist
|
||||
when (menuItem.itemId) {
|
||||
R.id.playlist_menu_pin -> {
|
||||
downloadHandler.value.downloadPlaylist(
|
||||
this,
|
||||
id = playlist.id,
|
||||
name = playlist.name,
|
||||
save = true,
|
||||
append = true,
|
||||
autoplay = false,
|
||||
shuffle = false,
|
||||
background = true,
|
||||
playNext = false,
|
||||
unpin = false
|
||||
)
|
||||
}
|
||||
R.id.playlist_menu_unpin -> {
|
||||
downloadHandler.value.downloadPlaylist(
|
||||
this,
|
||||
id = playlist.id,
|
||||
name = playlist.name,
|
||||
save = false,
|
||||
append = false,
|
||||
autoplay = false,
|
||||
shuffle = false,
|
||||
background = true,
|
||||
playNext = false,
|
||||
unpin = true
|
||||
)
|
||||
}
|
||||
R.id.playlist_menu_download -> {
|
||||
downloadHandler.value.downloadPlaylist(
|
||||
this,
|
||||
id = playlist.id,
|
||||
name = playlist.name,
|
||||
save = false,
|
||||
append = false,
|
||||
autoplay = false,
|
||||
shuffle = false,
|
||||
background = true,
|
||||
playNext = false,
|
||||
unpin = false
|
||||
)
|
||||
}
|
||||
R.id.playlist_menu_play_now -> {
|
||||
val action = PlaylistsFragmentDirections.playlistsToTrackCollection(
|
||||
playlistId = playlist.id,
|
||||
playlistName = playlist.name,
|
||||
autoPlay = true
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
R.id.playlist_menu_play_shuffled -> {
|
||||
val action = PlaylistsFragmentDirections.playlistsToTrackCollection(
|
||||
playlistId = playlist.id,
|
||||
playlistName = playlist.name,
|
||||
autoPlay = true,
|
||||
shuffle = true
|
||||
)
|
||||
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
R.id.playlist_menu_delete -> {
|
||||
deletePlaylist(playlist)
|
||||
}
|
||||
R.id.playlist_info -> {
|
||||
displayPlaylistInfo(playlist)
|
||||
}
|
||||
R.id.playlist_update_info -> {
|
||||
updatePlaylistInfo(playlist)
|
||||
}
|
||||
else -> {
|
||||
return super.onContextItemSelected(menuItem)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun deletePlaylist(playlist: Playlist) {
|
||||
AlertDialog.Builder(context).setIcon(R.drawable.ic_baseline_warning)
|
||||
.setTitle(R.string.common_confirm).setMessage(
|
||||
resources.getString(R.string.delete_playlist, playlist.name)
|
||||
).setPositiveButton(R.string.common_ok) { _, _ ->
|
||||
object : LoadingTask<Any?>(activity, refreshPlaylistsListView, cancellationToken) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): Any? {
|
||||
val musicService = getMusicService()
|
||||
musicService.deletePlaylist(playlist.id)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Any?) {
|
||||
playlistAdapter!!.remove(playlist)
|
||||
playlistAdapter!!.notifyDataSetChanged()
|
||||
toast(
|
||||
context,
|
||||
resources.getString(R.string.menu_deleted_playlist, playlist.name)
|
||||
)
|
||||
}
|
||||
|
||||
override fun error(error: Throwable) {
|
||||
val msg: String =
|
||||
if (error is OfflineException || error is ApiNotSupportedException)
|
||||
getErrorMessage(
|
||||
error
|
||||
) else String.format(
|
||||
Locale.ROOT,
|
||||
"%s %s",
|
||||
resources.getString(
|
||||
R.string.menu_deleted_playlist_error,
|
||||
playlist.name
|
||||
),
|
||||
getErrorMessage(error)
|
||||
)
|
||||
toast(context, msg, false)
|
||||
}
|
||||
}.execute()
|
||||
}.setNegativeButton(R.string.common_cancel, null).show()
|
||||
}
|
||||
|
||||
private fun displayPlaylistInfo(playlist: Playlist) {
|
||||
val textView = TextView(context)
|
||||
textView.setPadding(5, 5, 5, 5)
|
||||
val message: Spannable = SpannableString(
|
||||
"""
|
||||
Owner: ${playlist.owner}
|
||||
Comments: ${playlist.comment}
|
||||
Song Count: ${playlist.songCount}
|
||||
""".trimIndent() +
|
||||
if (playlist.public == null) "" else """
|
||||
|
||||
Public: ${playlist.public}
|
||||
""".trimIndent() + """
|
||||
|
||||
Creation Date: ${playlist.created.replace('T', ' ')}
|
||||
""".trimIndent()
|
||||
)
|
||||
Linkify.addLinks(message, Linkify.WEB_URLS)
|
||||
textView.text = message
|
||||
textView.movementMethod = LinkMovementMethod.getInstance()
|
||||
AlertDialog.Builder(context).setTitle(playlist.name).setCancelable(true)
|
||||
.setIcon(R.drawable.ic_baseline_info).setView(textView).show()
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun updatePlaylistInfo(playlist: Playlist) {
|
||||
val dialogView = layoutInflater.inflate(R.layout.update_playlist, null) ?: return
|
||||
val nameBox = dialogView.findViewById<EditText>(R.id.get_playlist_name)
|
||||
val commentBox = dialogView.findViewById<EditText>(R.id.get_playlist_comment)
|
||||
val publicBox = dialogView.findViewById<CheckBox>(R.id.get_playlist_public)
|
||||
nameBox.setText(playlist.name)
|
||||
commentBox.setText(playlist.comment)
|
||||
val pub = playlist.public
|
||||
if (pub == null) {
|
||||
publicBox.isEnabled = false
|
||||
} else {
|
||||
publicBox.isChecked = pub
|
||||
}
|
||||
val alertDialog = AlertDialog.Builder(context)
|
||||
alertDialog.setIcon(R.drawable.ic_baseline_warning)
|
||||
alertDialog.setTitle(R.string.playlist_update_info)
|
||||
alertDialog.setView(dialogView)
|
||||
alertDialog.setPositiveButton(R.string.common_ok) { _, _ ->
|
||||
object : LoadingTask<Any?>(activity, refreshPlaylistsListView, cancellationToken) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): Any? {
|
||||
val nameBoxText = nameBox.text
|
||||
val commentBoxText = commentBox.text
|
||||
val name = nameBoxText?.toString()
|
||||
val comment = commentBoxText?.toString()
|
||||
val musicService = getMusicService()
|
||||
musicService.updatePlaylist(playlist.id, name, comment, publicBox.isChecked)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Any?) {
|
||||
load(true)
|
||||
toast(
|
||||
context,
|
||||
resources.getString(R.string.playlist_updated_info, playlist.name)
|
||||
)
|
||||
}
|
||||
|
||||
override fun error(error: Throwable) {
|
||||
val msg: String =
|
||||
if (error is OfflineException || error is ApiNotSupportedException)
|
||||
getErrorMessage(
|
||||
error
|
||||
) else String.format(
|
||||
Locale.ROOT,
|
||||
"%s %s",
|
||||
resources.getString(
|
||||
R.string.playlist_updated_info_error,
|
||||
playlist.name
|
||||
),
|
||||
getErrorMessage(error)
|
||||
)
|
||||
toast(context, msg, false)
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
alertDialog.setNegativeButton(R.string.common_cancel, null)
|
||||
alertDialog.show()
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* PodcastFragment.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment.legacy
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ListView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.PodcastsChannel
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.BackgroundTask
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
||||
import org.moire.ultrasonic.util.Util.applyTheme
|
||||
import org.moire.ultrasonic.view.PodcastsChannelsAdapter
|
||||
|
||||
/**
|
||||
* Displays the podcasts available on the server
|
||||
*
|
||||
* TODO: This file has been converted from Java, but not modernized yet.
|
||||
*/
|
||||
class PodcastFragment : Fragment() {
|
||||
private var emptyTextView: View? = null
|
||||
var channelItemsListView: ListView? = null
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
private var swipeRefresh: SwipeRefreshLayout? = null
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
applyTheme(this.context)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.podcasts, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
cancellationToken = CancellationToken()
|
||||
swipeRefresh = view.findViewById(R.id.podcasts_refresh)
|
||||
swipeRefresh!!.setOnRefreshListener { load(view.context, true) }
|
||||
setTitle(this, R.string.podcasts_label)
|
||||
emptyTextView = view.findViewById(R.id.select_podcasts_empty)
|
||||
channelItemsListView = view.findViewById(R.id.podcasts_channels_items_list)
|
||||
channelItemsListView!!.setOnItemClickListener { parent, _, position, _ ->
|
||||
val (id) = parent.getItemAtPosition(position) as PodcastsChannel
|
||||
val action = PodcastFragmentDirections.podcastToTrackCollection(
|
||||
podcastChannelId = id
|
||||
)
|
||||
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
load(view.context, false)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cancellationToken!!.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun load(context: Context, refresh: Boolean) {
|
||||
val task: BackgroundTask<List<PodcastsChannel>> =
|
||||
object : FragmentBackgroundTask<List<PodcastsChannel>>(
|
||||
activity, true, swipeRefresh, cancellationToken
|
||||
) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): List<PodcastsChannel> {
|
||||
val musicService = getMusicService()
|
||||
return musicService.getPodcastsChannels(refresh)
|
||||
}
|
||||
|
||||
override fun done(result: List<PodcastsChannel>) {
|
||||
channelItemsListView!!.adapter = PodcastsChannelsAdapter(context, result)
|
||||
emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
task.execute()
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* SelectGenreFragment.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment.legacy
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ListView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.Genre
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.BackgroundTask
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
||||
import org.moire.ultrasonic.util.Settings.maxSongs
|
||||
import org.moire.ultrasonic.util.Util.applyTheme
|
||||
import org.moire.ultrasonic.view.GenreAdapter
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Displays the available genres in the media library
|
||||
*
|
||||
* TODO: This file has been converted from Java, but not modernized yet.
|
||||
*/
|
||||
class SelectGenreFragment : Fragment() {
|
||||
private var refreshGenreListView: SwipeRefreshLayout? = null
|
||||
private var genreListView: ListView? = null
|
||||
private var emptyView: View? = null
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
applyTheme(this.context)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.select_genre, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
cancellationToken = CancellationToken()
|
||||
refreshGenreListView = view.findViewById(R.id.select_genre_refresh)
|
||||
genreListView = view.findViewById(R.id.select_genre_list)
|
||||
refreshGenreListView!!.setOnRefreshListener { load(true) }
|
||||
genreListView!!.setOnItemClickListener { parent: AdapterView<*>,
|
||||
_: View?,
|
||||
position: Int,
|
||||
_: Long ->
|
||||
val genre = parent.getItemAtPosition(position) as Genre
|
||||
|
||||
val action = SelectGenreFragmentDirections.selectGenreToTrackCollection(
|
||||
genreName = genre.name,
|
||||
size = maxSongs,
|
||||
offset = 0
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
emptyView = view.findViewById(R.id.select_genre_empty)
|
||||
registerForContextMenu(genreListView!!)
|
||||
setTitle(this, R.string.main_genres_title)
|
||||
load(false)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cancellationToken!!.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun load(refresh: Boolean) {
|
||||
val task: BackgroundTask<List<Genre>> = object : FragmentBackgroundTask<List<Genre>>(
|
||||
activity, true, refreshGenreListView, cancellationToken
|
||||
) {
|
||||
override fun doInBackground(): List<Genre> {
|
||||
val musicService = getMusicService()
|
||||
var genres: List<Genre> = ArrayList()
|
||||
try {
|
||||
genres = musicService.getGenres(refresh)
|
||||
} catch (all: Exception) {
|
||||
Timber.e(all, "Failed to load genres")
|
||||
}
|
||||
return genres
|
||||
}
|
||||
|
||||
override fun done(result: List<Genre>) {
|
||||
emptyView!!.isVisible = result.isEmpty()
|
||||
genreListView!!.adapter = GenreAdapter(context, result)
|
||||
}
|
||||
}
|
||||
task.execute()
|
||||
}
|
||||
}
|
@ -0,0 +1,364 @@
|
||||
/*
|
||||
* SharesFragment.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.fragment.legacy
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.util.Linkify
|
||||
import android.view.ContextMenu
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import java.util.Locale
|
||||
import org.koin.java.KoinJavaComponent
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
|
||||
import org.moire.ultrasonic.domain.Share
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.service.OfflineException
|
||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
||||
import org.moire.ultrasonic.util.BackgroundTask
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
||||
import org.moire.ultrasonic.util.LoadingTask
|
||||
import org.moire.ultrasonic.util.TimeSpan
|
||||
import org.moire.ultrasonic.util.TimeSpanPicker
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import org.moire.ultrasonic.view.ShareAdapter
|
||||
|
||||
/**
|
||||
* Displays the shares in the media library
|
||||
*
|
||||
* TODO: This file has been converted from Java, but not modernized yet.
|
||||
*/
|
||||
class SharesFragment : Fragment() {
|
||||
private var refreshSharesListView: SwipeRefreshLayout? = null
|
||||
private var sharesListView: ListView? = null
|
||||
private var emptyTextView: View? = null
|
||||
private var shareAdapter: ShareAdapter? = null
|
||||
private val downloadHandler = KoinJavaComponent.inject<DownloadHandler>(
|
||||
DownloadHandler::class.java
|
||||
)
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Util.applyTheme(this.context)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.select_share, container, false)
|
||||
}
|
||||
|
||||
@Suppress("NAME_SHADOWING")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
cancellationToken = CancellationToken()
|
||||
refreshSharesListView = view.findViewById(R.id.select_share_refresh)
|
||||
sharesListView = view.findViewById(R.id.select_share_list)
|
||||
refreshSharesListView!!.setOnRefreshListener { load(true) }
|
||||
emptyTextView = view.findViewById(R.id.select_share_empty)
|
||||
sharesListView!!.onItemClickListener = AdapterView.OnItemClickListener {
|
||||
parent, _, position, _ ->
|
||||
val share = parent.getItemAtPosition(position) as Share
|
||||
|
||||
val action = SharesFragmentDirections.sharesToTrackCollection(
|
||||
shareId = share.id,
|
||||
shareName = share.name
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
registerForContextMenu(sharesListView!!)
|
||||
FragmentTitle.setTitle(this, R.string.button_bar_shares)
|
||||
load(false)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cancellationToken!!.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun load(refresh: Boolean) {
|
||||
val task: BackgroundTask<List<Share>> = object : FragmentBackgroundTask<List<Share>>(
|
||||
activity, true, refreshSharesListView, cancellationToken
|
||||
) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): List<Share> {
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
return musicService.getShares(refresh)
|
||||
}
|
||||
|
||||
override fun done(result: List<Share>) {
|
||||
sharesListView!!.adapter = ShareAdapter(context, result).also { shareAdapter = it }
|
||||
emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
task.execute()
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(
|
||||
menu: ContextMenu,
|
||||
view: View,
|
||||
menuInfo: ContextMenu.ContextMenuInfo?
|
||||
) {
|
||||
super.onCreateContextMenu(menu, view, menuInfo)
|
||||
val inflater = requireActivity().menuInflater
|
||||
inflater.inflate(R.menu.select_share_context, menu)
|
||||
}
|
||||
|
||||
override fun onContextItemSelected(menuItem: MenuItem): Boolean {
|
||||
val info = menuItem.menuInfo as AdapterView.AdapterContextMenuInfo
|
||||
val share = sharesListView!!.getItemAtPosition(info.position) as Share
|
||||
when (menuItem.itemId) {
|
||||
R.id.share_menu_pin -> {
|
||||
downloadHandler.value.downloadShare(
|
||||
this,
|
||||
share.id,
|
||||
share.name,
|
||||
save = true,
|
||||
append = true,
|
||||
autoplay = false,
|
||||
shuffle = false,
|
||||
background = true,
|
||||
playNext = false,
|
||||
unpin = false
|
||||
)
|
||||
}
|
||||
R.id.share_menu_unpin -> {
|
||||
downloadHandler.value.downloadShare(
|
||||
this,
|
||||
share.id,
|
||||
share.name,
|
||||
save = false,
|
||||
append = false,
|
||||
autoplay = false,
|
||||
shuffle = false,
|
||||
background = true,
|
||||
playNext = false,
|
||||
unpin = true
|
||||
)
|
||||
}
|
||||
R.id.share_menu_download -> {
|
||||
downloadHandler.value.downloadShare(
|
||||
this,
|
||||
share.id,
|
||||
share.name,
|
||||
save = false,
|
||||
append = false,
|
||||
autoplay = false,
|
||||
shuffle = false,
|
||||
background = true,
|
||||
playNext = false,
|
||||
unpin = false
|
||||
)
|
||||
}
|
||||
R.id.share_menu_play_now -> {
|
||||
downloadHandler.value.downloadShare(
|
||||
this,
|
||||
share.id,
|
||||
share.name,
|
||||
save = false,
|
||||
append = false,
|
||||
autoplay = true,
|
||||
shuffle = false,
|
||||
background = false,
|
||||
playNext = false,
|
||||
unpin = false
|
||||
)
|
||||
}
|
||||
R.id.share_menu_play_shuffled -> {
|
||||
downloadHandler.value.downloadShare(
|
||||
this,
|
||||
share.id,
|
||||
share.name,
|
||||
save = false,
|
||||
append = false,
|
||||
autoplay = true,
|
||||
shuffle = true,
|
||||
background = false,
|
||||
playNext = false,
|
||||
unpin = false
|
||||
)
|
||||
}
|
||||
R.id.share_menu_delete -> {
|
||||
deleteShare(share)
|
||||
}
|
||||
R.id.share_info -> {
|
||||
displayShareInfo(share)
|
||||
}
|
||||
R.id.share_update_info -> {
|
||||
updateShareInfo(share)
|
||||
}
|
||||
else -> {
|
||||
return super.onContextItemSelected(menuItem)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun deleteShare(share: Share) {
|
||||
AlertDialog.Builder(context).setIcon(R.drawable.ic_baseline_warning)
|
||||
.setTitle(R.string.common_confirm).setMessage(
|
||||
resources.getString(R.string.delete_playlist, share.name)
|
||||
).setPositiveButton(R.string.common_ok) { _, _ ->
|
||||
object : LoadingTask<Any?>(activity, refreshSharesListView, cancellationToken) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): Any? {
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
musicService.deleteShare(share.id)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Any?) {
|
||||
shareAdapter!!.remove(share)
|
||||
shareAdapter!!.notifyDataSetChanged()
|
||||
Util.toast(
|
||||
context,
|
||||
resources.getString(R.string.menu_deleted_share, share.name)
|
||||
)
|
||||
}
|
||||
|
||||
override fun error(error: Throwable) {
|
||||
val msg: String =
|
||||
if (error is OfflineException || error is ApiNotSupportedException) {
|
||||
getErrorMessage(
|
||||
error
|
||||
)
|
||||
} else {
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"%s %s",
|
||||
resources.getString(
|
||||
R.string.menu_deleted_share_error,
|
||||
share.name
|
||||
),
|
||||
getErrorMessage(error)
|
||||
)
|
||||
}
|
||||
Util.toast(context, msg, false)
|
||||
}
|
||||
}.execute()
|
||||
}.setNegativeButton(R.string.common_cancel, null).show()
|
||||
}
|
||||
|
||||
private fun displayShareInfo(share: Share) {
|
||||
val textView = TextView(context)
|
||||
textView.setPadding(5, 5, 5, 5)
|
||||
val message: Spannable = SpannableString(
|
||||
"""
|
||||
Owner: ${share.username}
|
||||
Comments: ${if (share.description == null) "" else share.description}
|
||||
URL: ${share.url}
|
||||
Entry Count: ${share.getEntries().size}
|
||||
Visit Count: ${share.visitCount}
|
||||
""".trimIndent() +
|
||||
(
|
||||
if (share.created == null) "" else """
|
||||
|
||||
Creation Date: ${share.created!!.replace('T', ' ')}
|
||||
""".trimIndent()
|
||||
) +
|
||||
(
|
||||
if (share.lastVisited == null) "" else """
|
||||
|
||||
Last Visited Date: ${share.lastVisited!!.replace('T', ' ')}
|
||||
""".trimIndent()
|
||||
) +
|
||||
if (share.expires == null) "" else """
|
||||
|
||||
Expiration Date: ${share.expires!!.replace('T', ' ')}
|
||||
""".trimIndent()
|
||||
)
|
||||
Linkify.addLinks(message, Linkify.WEB_URLS)
|
||||
textView.text = message
|
||||
textView.movementMethod = LinkMovementMethod.getInstance()
|
||||
AlertDialog.Builder(context).setTitle("Share Details").setCancelable(true)
|
||||
.setIcon(R.drawable.ic_baseline_info).setView(textView).show()
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun updateShareInfo(share: Share) {
|
||||
val dialogView = layoutInflater.inflate(R.layout.share_details, null) ?: return
|
||||
val shareDescription = dialogView.findViewById<EditText>(R.id.share_description)
|
||||
val timeSpanPicker = dialogView.findViewById<TimeSpanPicker>(R.id.date_picker)
|
||||
shareDescription.setText(share.description)
|
||||
val hideDialogCheckBox = dialogView.findViewById<CheckBox>(R.id.hide_dialog)
|
||||
val saveAsDefaultsCheckBox = dialogView.findViewById<CheckBox>(R.id.save_as_defaults)
|
||||
val noExpirationCheckBox = dialogView.findViewById<CheckBox>(R.id.timeSpanDisableCheckBox)
|
||||
noExpirationCheckBox.setOnCheckedChangeListener { _, b ->
|
||||
timeSpanPicker.isEnabled = !b
|
||||
}
|
||||
noExpirationCheckBox.isChecked = true
|
||||
timeSpanPicker.setTimeSpanDisableText(resources.getText(R.string.no_expiration))
|
||||
hideDialogCheckBox.visibility = View.GONE
|
||||
saveAsDefaultsCheckBox.visibility = View.GONE
|
||||
val alertDialog = AlertDialog.Builder(context)
|
||||
alertDialog.setIcon(R.drawable.ic_baseline_warning)
|
||||
alertDialog.setTitle(R.string.playlist_update_info)
|
||||
alertDialog.setView(dialogView)
|
||||
alertDialog.setPositiveButton(R.string.common_ok) { _, _ ->
|
||||
object : LoadingTask<Any?>(activity, refreshSharesListView, cancellationToken) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): Any? {
|
||||
var millis = timeSpanPicker.timeSpan.totalMilliseconds
|
||||
if (millis > 0) {
|
||||
millis = TimeSpan.getCurrentTime().add(millis).totalMilliseconds
|
||||
}
|
||||
val shareDescriptionText = shareDescription.text
|
||||
val description = shareDescriptionText?.toString()
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
musicService.updateShare(share.id, description, millis)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Any?) {
|
||||
load(true)
|
||||
Util.toast(
|
||||
context,
|
||||
resources.getString(R.string.playlist_updated_info, share.name)
|
||||
)
|
||||
}
|
||||
|
||||
override fun error(error: Throwable) {
|
||||
val msg: String
|
||||
msg = if (error is OfflineException || error is ApiNotSupportedException) {
|
||||
getErrorMessage(
|
||||
error
|
||||
)
|
||||
} else {
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"%s %s",
|
||||
resources.getString(R.string.playlist_updated_info_error, share.name),
|
||||
getErrorMessage(error)
|
||||
)
|
||||
}
|
||||
Util.toast(context, msg, false)
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
alertDialog.setNegativeButton(R.string.common_cancel, null)
|
||||
alertDialog.show()
|
||||
}
|
||||
}
|
@ -1,124 +1,104 @@
|
||||
/*
|
||||
* AlbumListModel.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.model
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||
import org.moire.ultrasonic.domain.Album
|
||||
import org.moire.ultrasonic.service.MusicService
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
|
||||
class AlbumListModel(application: Application) : GenericListModel(application) {
|
||||
|
||||
val list: MutableLiveData<List<Album>> = MutableLiveData()
|
||||
var lastType: String? = null
|
||||
private var lastType: AlbumListType? = null
|
||||
private var loadedUntil: Int = 0
|
||||
|
||||
fun getAlbumList(
|
||||
refresh: Boolean,
|
||||
swipe: SwipeRefreshLayout,
|
||||
args: Bundle
|
||||
): LiveData<List<Album>> {
|
||||
// Don't reload the data if navigating back to the view that was active before.
|
||||
// This way, we keep the scroll position
|
||||
val albumListType = args.getString(Constants.INTENT_ALBUM_LIST_TYPE)!!
|
||||
|
||||
if (refresh || list.value?.isEmpty() != false || albumListType != lastType) {
|
||||
lastType = albumListType
|
||||
backgroundLoadFromServer(refresh, swipe, args)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
private fun getAlbumsOfArtist(
|
||||
musicService: MusicService,
|
||||
suspend fun getAlbumsOfArtist(
|
||||
refresh: Boolean,
|
||||
id: String,
|
||||
name: String?
|
||||
) {
|
||||
list.postValue(musicService.getAlbumsOfArtist(id, name, refresh))
|
||||
withContext(Dispatchers.IO) {
|
||||
val service = MusicServiceFactory.getMusicService()
|
||||
list.postValue(service.getAlbumsOfArtist(id, name, refresh))
|
||||
}
|
||||
}
|
||||
|
||||
override fun load(
|
||||
isOffline: Boolean,
|
||||
useId3Tags: Boolean,
|
||||
musicService: MusicService,
|
||||
refresh: Boolean,
|
||||
args: Bundle
|
||||
@Suppress("NAME_SHADOWING")
|
||||
suspend fun getAlbums(
|
||||
albumListType: AlbumListType,
|
||||
size: Int = 0,
|
||||
offset: Int = 0,
|
||||
append: Boolean = false,
|
||||
refresh: Boolean
|
||||
) {
|
||||
super.load(isOffline, useId3Tags, musicService, refresh, args)
|
||||
|
||||
val albumListType = args.getString(Constants.INTENT_ALBUM_LIST_TYPE)!!
|
||||
val size = args.getInt(Constants.INTENT_ALBUM_LIST_SIZE, 0)
|
||||
var offset = args.getInt(Constants.INTENT_ALBUM_LIST_OFFSET, 0)
|
||||
val append = args.getBoolean(Constants.INTENT_APPEND, false)
|
||||
|
||||
val musicDirectory: List<Album>
|
||||
val musicFolderId = if (showSelectFolderHeader(args)) {
|
||||
activeServerProvider.getActiveServer().musicFolderId
|
||||
} else {
|
||||
null
|
||||
// Don't reload the data if navigating back to the view that was active before.
|
||||
// This way, we keep the scroll position
|
||||
if ((!refresh && list.value?.isEmpty() == false && albumListType == lastType)) {
|
||||
return
|
||||
}
|
||||
lastType = albumListType
|
||||
|
||||
// If we are refreshing the random list, we want to avoid items moving across the screen,
|
||||
// by clearing the list first
|
||||
if (refresh && !append && albumListType == "random") {
|
||||
list.postValue(listOf())
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
val service = MusicServiceFactory.getMusicService()
|
||||
var offset = offset
|
||||
|
||||
// Handle the logic for endless scrolling:
|
||||
// If appending the existing list, set the offset from where to load
|
||||
if (append) offset += (size + loadedUntil)
|
||||
val musicDirectory: List<Album>
|
||||
val musicFolderId = if (showSelectFolderHeader()) {
|
||||
activeServerProvider.getActiveServer().musicFolderId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (albumListType == Constants.ALBUMS_OF_ARTIST) {
|
||||
return getAlbumsOfArtist(
|
||||
musicService,
|
||||
refresh,
|
||||
args.getString(Constants.INTENT_ID, ""),
|
||||
args.getString(Constants.INTENT_NAME, "")
|
||||
)
|
||||
}
|
||||
// If we are refreshing the random list, we want to avoid items moving across the screen,
|
||||
// by clearing the list first
|
||||
if (refresh && !append && albumListType == AlbumListType.RANDOM) {
|
||||
list.postValue(listOf())
|
||||
}
|
||||
|
||||
val type = AlbumListType.fromName(albumListType)
|
||||
// Handle the logic for endless scrolling:
|
||||
// If appending the existing list, set the offset from where to load
|
||||
if (append) offset += (size + loadedUntil)
|
||||
|
||||
if (useId3Tags) {
|
||||
musicDirectory =
|
||||
musicService.getAlbumList2(
|
||||
type, size,
|
||||
musicDirectory = if (Settings.shouldUseId3Tags) {
|
||||
service.getAlbumList2(
|
||||
albumListType, size,
|
||||
offset, musicFolderId
|
||||
)
|
||||
} else {
|
||||
musicDirectory = musicService.getAlbumList(
|
||||
type, size,
|
||||
offset, musicFolderId
|
||||
)
|
||||
} else {
|
||||
service.getAlbumList(
|
||||
albumListType, size,
|
||||
offset, musicFolderId
|
||||
)
|
||||
}
|
||||
|
||||
currentListIsSortable = isCollectionSortable(albumListType)
|
||||
|
||||
if (append && list.value != null) {
|
||||
val newList = ArrayList<Album>()
|
||||
newList.addAll(list.value!!)
|
||||
newList.addAll(musicDirectory)
|
||||
list.postValue(newList)
|
||||
} else {
|
||||
list.postValue(musicDirectory)
|
||||
}
|
||||
|
||||
loadedUntil = offset
|
||||
}
|
||||
|
||||
currentListIsSortable = isCollectionSortable(type)
|
||||
|
||||
if (append && list.value != null) {
|
||||
val newList = ArrayList<Album>()
|
||||
newList.addAll(list.value!!)
|
||||
newList.addAll(musicDirectory)
|
||||
list.postValue(newList)
|
||||
} else {
|
||||
list.postValue(musicDirectory)
|
||||
}
|
||||
|
||||
loadedUntil = offset
|
||||
}
|
||||
|
||||
override fun showSelectFolderHeader(args: Bundle?): Boolean {
|
||||
if (args == null) return false
|
||||
|
||||
// TODO: Use proper type here
|
||||
val albumListType = args.getString(Constants.INTENT_ALBUM_LIST_TYPE)!!
|
||||
|
||||
val isAlphabetical = (albumListType == AlbumListType.SORTED_BY_NAME.typeName) ||
|
||||
(albumListType == AlbumListType.SORTED_BY_ARTIST.typeName)
|
||||
override fun showSelectFolderHeader(): Boolean {
|
||||
val isAlphabetical = (lastType == AlbumListType.SORTED_BY_NAME) ||
|
||||
(lastType == AlbumListType.SORTED_BY_ARTIST)
|
||||
|
||||
return !isOffline() && !Settings.shouldUseId3Tags && isAlphabetical
|
||||
}
|
||||
|
@ -7,7 +7,6 @@
|
||||
package org.moire.ultrasonic.model
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
@ -38,10 +37,9 @@ class ArtistListModel(application: Application) : GenericListModel(application)
|
||||
isOffline: Boolean,
|
||||
useId3Tags: Boolean,
|
||||
musicService: MusicService,
|
||||
refresh: Boolean,
|
||||
args: Bundle
|
||||
refresh: Boolean
|
||||
) {
|
||||
super.load(isOffline, useId3Tags, musicService, refresh, args)
|
||||
super.load(isOffline, useId3Tags, musicService, refresh)
|
||||
|
||||
val musicFolderId = activeServer.musicFolderId
|
||||
|
||||
@ -54,7 +52,7 @@ class ArtistListModel(application: Application) : GenericListModel(application)
|
||||
artists.postValue(result.toMutableList().sortedWith(comparator))
|
||||
}
|
||||
|
||||
override fun showSelectFolderHeader(args: Bundle?): Boolean {
|
||||
override fun showSelectFolderHeader(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,14 @@
|
||||
/*
|
||||
* GenericListModel.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.model
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
@ -41,7 +47,7 @@ open class GenericListModel(application: Application) :
|
||||
|
||||
val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData(listOf())
|
||||
|
||||
open fun showSelectFolderHeader(args: Bundle?): Boolean {
|
||||
open fun showSelectFolderHeader(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -55,8 +61,8 @@ open class GenericListModel(application: Application) :
|
||||
/**
|
||||
* Refreshes the cached items from the server
|
||||
*/
|
||||
fun refresh(swipe: SwipeRefreshLayout, bundle: Bundle?) {
|
||||
backgroundLoadFromServer(true, swipe, bundle ?: Bundle())
|
||||
fun refresh(swipe: SwipeRefreshLayout) {
|
||||
backgroundLoadFromServer(true, swipe)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,12 +70,11 @@ open class GenericListModel(application: Application) :
|
||||
*/
|
||||
fun backgroundLoadFromServer(
|
||||
refresh: Boolean,
|
||||
swipe: SwipeRefreshLayout,
|
||||
bundle: Bundle = Bundle()
|
||||
swipe: SwipeRefreshLayout
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
swipe.isRefreshing = true
|
||||
loadFromServer(refresh, swipe, bundle)
|
||||
loadFromServer(refresh, swipe)
|
||||
swipe.isRefreshing = false
|
||||
}
|
||||
}
|
||||
@ -77,18 +82,22 @@ open class GenericListModel(application: Application) :
|
||||
/**
|
||||
* Calls the load() function with error handling
|
||||
*/
|
||||
suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout, bundle: Bundle) =
|
||||
private suspend fun loadFromServer(
|
||||
refresh: Boolean,
|
||||
swipe: SwipeRefreshLayout
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
val isOffline = ActiveServerProvider.isOffline()
|
||||
val useId3Tags = Settings.shouldUseId3Tags
|
||||
|
||||
try {
|
||||
load(isOffline, useId3Tags, musicService, refresh, bundle)
|
||||
load(isOffline, useId3Tags, musicService, refresh)
|
||||
} catch (all: Exception) {
|
||||
handleException(all, swipe.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleException(exception: Exception, context: Context) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
@ -103,12 +112,11 @@ open class GenericListModel(application: Application) :
|
||||
isOffline: Boolean,
|
||||
useId3Tags: Boolean,
|
||||
musicService: MusicService,
|
||||
refresh: Boolean,
|
||||
args: Bundle
|
||||
refresh: Boolean
|
||||
) {
|
||||
// Update the list of available folders if enabled
|
||||
@Suppress("ComplexCondition")
|
||||
if (showSelectFolderHeader(args) && !isOffline && !useId3Tags && refresh) {
|
||||
if (showSelectFolderHeader() && !isOffline && !useId3Tags && refresh) {
|
||||
musicFolders.postValue(
|
||||
musicService.getMusicFolders(refresh)
|
||||
)
|
||||
|
@ -1,14 +1,12 @@
|
||||
package org.moire.ultrasonic.model
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.moire.ultrasonic.domain.SearchCriteria
|
||||
import org.moire.ultrasonic.domain.SearchResult
|
||||
import org.moire.ultrasonic.fragment.SearchFragment
|
||||
import org.moire.ultrasonic.service.MusicService
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
|
||||
@ -16,16 +14,6 @@ class SearchListModel(application: Application) : GenericListModel(application)
|
||||
|
||||
var searchResult: MutableLiveData<SearchResult?> = MutableLiveData()
|
||||
|
||||
override fun load(
|
||||
isOffline: Boolean,
|
||||
useId3Tags: Boolean,
|
||||
musicService: MusicService,
|
||||
refresh: Boolean,
|
||||
args: Bundle
|
||||
) {
|
||||
super.load(isOffline, useId3Tags, musicService, refresh, args)
|
||||
}
|
||||
|
||||
suspend fun search(query: String) {
|
||||
val maxArtists = Settings.maxArtists
|
||||
val maxAlbums = Settings.maxAlbums
|
||||
|
@ -1,17 +0,0 @@
|
||||
|
||||
|
||||
|
||||
UI:
|
||||
[x] Display tracks
|
||||
[x] On selection: Translate Tracks to MediaItems
|
||||
[x] Move playlist val to Controller: Keep it around for easier migration!!
|
||||
[x] Also make a LRU Cache to help with translation between MediaItem and DownloadFile
|
||||
[x] Hand MediaItems to Service
|
||||
[] If wanted also hand them to Downloader.kt
|
||||
[x] Service plays MediaItem through OkHttp
|
||||
[x] UI needs to receive info from service
|
||||
[x] Create a Cache Layer
|
||||
[] Translate AutoMediaBrowserService
|
||||
[] Add new shuffle icon....
|
||||
|
||||
convertToPlaybackStateCompatState()
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CachedMusicService.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
@ -31,7 +31,6 @@ import org.moire.ultrasonic.domain.SearchResult
|
||||
import org.moire.ultrasonic.domain.Share
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.domain.UserInfo
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.LRUCache
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.TimeLimitedCache
|
||||
@ -393,7 +392,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||
var result = cachedGenres.get()
|
||||
if (result == null) {
|
||||
result = musicService.getGenres(refresh)
|
||||
cachedGenres.set(result!!)
|
||||
cachedGenres.set(result)
|
||||
}
|
||||
|
||||
val sorted = result.toMutableList()
|
||||
@ -443,7 +442,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||
override fun getVideos(refresh: Boolean): MusicDirectory? {
|
||||
checkSettingsChanged()
|
||||
var cache =
|
||||
if (refresh) null else cachedMusicDirectories[Constants.INTENT_VIDEOS]
|
||||
if (refresh) null else cachedMusicDirectories[CACHE_KEY_VIDEOS]
|
||||
var dir = cache?.get()
|
||||
if (dir == null) {
|
||||
dir = musicService.getVideos(refresh)
|
||||
@ -451,7 +450,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||
Settings.directoryCacheTime.toLong(), TimeUnit.SECONDS
|
||||
)
|
||||
cache.set(dir)
|
||||
cachedMusicDirectories.put(Constants.INTENT_VIDEOS, cache)
|
||||
cachedMusicDirectories.put(CACHE_KEY_VIDEOS, cache)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
@ -493,6 +492,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||
|
||||
companion object {
|
||||
private const val MUSIC_DIR_CACHE_SIZE = 100
|
||||
const val CACHE_KEY_VIDEOS = "VIDEOS"
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -231,7 +231,7 @@ class MediaPlayerController(
|
||||
|
||||
private fun publishPlaybackState() {
|
||||
val newState = RxBus.StateWithTrack(
|
||||
track = currentMediaItem?.let { it.toTrack() },
|
||||
track = currentMediaItem?.toTrack(),
|
||||
index = currentMediaItemIndex,
|
||||
isPlaying = isPlaying,
|
||||
state = playbackState
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* MusicService.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
@ -36,7 +36,7 @@ interface MusicService {
|
||||
fun isLicenseValid(): Boolean
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getGenres(refresh: Boolean): List<Genre>?
|
||||
fun getGenres(refresh: Boolean): List<Genre>
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun star(id: String?, albumId: String?, artistId: String?)
|
||||
|
@ -376,7 +376,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun getGenres(refresh: Boolean): List<Genre>? {
|
||||
override fun getGenres(refresh: Boolean): List<Genre> {
|
||||
throw OfflineException("Getting Genres not available in offline mode")
|
||||
}
|
||||
|
||||
@ -572,7 +572,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
title = name
|
||||
|
||||
val albumArt = FileUtil.getAlbumArtFile(this)
|
||||
if (albumArt != null && File(albumArt).exists()) {
|
||||
if (File(albumArt).exists()) {
|
||||
coverArt = albumArt
|
||||
}
|
||||
}
|
||||
@ -628,10 +628,11 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
if (string == null) return null
|
||||
|
||||
val slashIndex = string.indexOf('/')
|
||||
if (slashIndex > 0)
|
||||
return string.substring(0, slashIndex).toIntOrNull()
|
||||
|
||||
return if (slashIndex > 0)
|
||||
string.substring(0, slashIndex).toIntOrNull()
|
||||
else
|
||||
return string.toIntOrNull()
|
||||
string.toIntOrNull()
|
||||
}
|
||||
|
||||
/*
|
||||
@ -642,10 +643,10 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
|
||||
val duration: Long? = string.toLongOrNull()
|
||||
|
||||
if (duration != null)
|
||||
return TimeUnit.MILLISECONDS.toSeconds(duration).toInt()
|
||||
return if (duration != null)
|
||||
TimeUnit.MILLISECONDS.toSeconds(duration).toInt()
|
||||
else
|
||||
return null
|
||||
null
|
||||
}
|
||||
|
||||
// TODO: Simplify this deeply nested and complicated function
|
||||
|
@ -574,7 +574,7 @@ open class RESTMusicService(
|
||||
@Throws(Exception::class)
|
||||
override fun getGenres(
|
||||
refresh: Boolean
|
||||
): List<Genre>? {
|
||||
): List<Genre> {
|
||||
val response = API.getGenres().execute().throwOnFailure()
|
||||
|
||||
return response.body()!!.genresList.toDomainEntityList()
|
||||
|
@ -1,3 +1,10 @@
|
||||
/*
|
||||
* DownloadHandler.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.subsonic
|
||||
|
||||
import android.app.Activity
|
||||
@ -11,7 +18,6 @@ import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
|
||||
import org.moire.ultrasonic.util.ModalBackgroundTask
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
@ -35,6 +41,7 @@ class DownloadHandler(
|
||||
playNext: Boolean,
|
||||
shuffle: Boolean,
|
||||
songs: List<Track>,
|
||||
playlistName: String?,
|
||||
) {
|
||||
val onValid = Runnable {
|
||||
// TODO: The logic here is different than in the controller...
|
||||
@ -52,9 +59,7 @@ class DownloadHandler(
|
||||
shuffle,
|
||||
insertionMode
|
||||
)
|
||||
val playlistName: String? = fragment.arguments?.getString(
|
||||
Constants.INTENT_PLAYLIST_NAME
|
||||
)
|
||||
|
||||
if (playlistName != null) {
|
||||
mediaPlayerController.suggestedPlaylistName = playlistName
|
||||
}
|
||||
|
@ -1,3 +1,10 @@
|
||||
/*
|
||||
* ShareHandler.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.subsonic
|
||||
|
||||
import android.app.AlertDialog
|
||||
@ -20,7 +27,6 @@ import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.BackgroundTask
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.ShareDetails
|
||||
@ -42,31 +48,12 @@ class ShareHandler(val context: Context) {
|
||||
private var textViewExpiration: TextView? = null
|
||||
private val pattern = Pattern.compile(":")
|
||||
|
||||
fun createShare(
|
||||
fragment: Fragment,
|
||||
tracks: List<Track?>?,
|
||||
swipe: SwipeRefreshLayout?,
|
||||
cancellationToken: CancellationToken
|
||||
) {
|
||||
val askForDetails = Settings.shouldAskForShareDetails
|
||||
val shareDetails = ShareDetails()
|
||||
shareDetails.Entries = tracks
|
||||
if (askForDetails) {
|
||||
showDialog(fragment, shareDetails, swipe, cancellationToken)
|
||||
} else {
|
||||
shareDetails.Description = Settings.defaultShareDescription
|
||||
shareDetails.Expiration = TimeSpan.getCurrentTime().add(
|
||||
Settings.defaultShareExpirationInMillis
|
||||
).totalMilliseconds
|
||||
share(fragment, shareDetails, swipe, cancellationToken)
|
||||
}
|
||||
}
|
||||
|
||||
fun share(
|
||||
fragment: Fragment,
|
||||
shareDetails: ShareDetails,
|
||||
swipe: SwipeRefreshLayout?,
|
||||
cancellationToken: CancellationToken
|
||||
cancellationToken: CancellationToken,
|
||||
additionalId: String?
|
||||
) {
|
||||
val task: BackgroundTask<Share?> = object : FragmentBackgroundTask<Share?>(
|
||||
fragment.requireActivity(),
|
||||
@ -80,7 +67,7 @@ class ShareHandler(val context: Context) {
|
||||
|
||||
if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1) return null
|
||||
if (shareDetails.Entries.isEmpty()) {
|
||||
fragment.arguments?.getString(Constants.INTENT_ID).ifNotNull {
|
||||
additionalId.ifNotNull {
|
||||
ids.add(it)
|
||||
}
|
||||
} else {
|
||||
@ -144,11 +131,33 @@ class ShareHandler(val context: Context) {
|
||||
task.execute()
|
||||
}
|
||||
|
||||
fun createShare(
|
||||
fragment: Fragment,
|
||||
tracks: List<Track?>?,
|
||||
swipe: SwipeRefreshLayout?,
|
||||
cancellationToken: CancellationToken,
|
||||
additionalId: String? = null
|
||||
) {
|
||||
val askForDetails = Settings.shouldAskForShareDetails
|
||||
val shareDetails = ShareDetails()
|
||||
shareDetails.Entries = tracks
|
||||
if (askForDetails) {
|
||||
showDialog(fragment, shareDetails, swipe, cancellationToken, additionalId)
|
||||
} else {
|
||||
shareDetails.Description = Settings.defaultShareDescription
|
||||
shareDetails.Expiration = TimeSpan.getCurrentTime().add(
|
||||
Settings.defaultShareExpirationInMillis
|
||||
).totalMilliseconds
|
||||
share(fragment, shareDetails, swipe, cancellationToken, additionalId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDialog(
|
||||
fragment: Fragment,
|
||||
shareDetails: ShareDetails,
|
||||
swipe: SwipeRefreshLayout?,
|
||||
cancellationToken: CancellationToken
|
||||
cancellationToken: CancellationToken,
|
||||
additionalId: String?
|
||||
) {
|
||||
val layout = LayoutInflater.from(fragment.context).inflate(R.layout.share_details, null)
|
||||
|
||||
@ -205,7 +214,7 @@ class ShareHandler(val context: Context) {
|
||||
Settings.shareOnServer = shareDetails.ShareOnServer
|
||||
}
|
||||
|
||||
share(fragment, shareDetails, swipe, cancellationToken)
|
||||
share(fragment, shareDetails, swipe, cancellationToken, additionalId)
|
||||
}
|
||||
|
||||
builder.setNegativeButton(R.string.common_cancel) { dialog, _ ->
|
||||
@ -216,9 +225,7 @@ class ShareHandler(val context: Context) {
|
||||
builder.setCancelable(true)
|
||||
|
||||
timeSpanPicker!!.setTimeSpanDisableText(context.resources.getString(R.string.no_expiration))
|
||||
noExpirationCheckBox!!.setOnCheckedChangeListener {
|
||||
_,
|
||||
b ->
|
||||
noExpirationCheckBox!!.setOnCheckedChangeListener { _, b ->
|
||||
timeSpanPicker!!.isEnabled = !b
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Constants.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
@ -15,32 +15,13 @@ object Constants {
|
||||
const val REST_PROTOCOL_VERSION = "1.7.0"
|
||||
const val REST_CLIENT_ID = "Ultrasonic"
|
||||
|
||||
// Names for intent extras.
|
||||
const val INTENT_ID = "subsonic.id"
|
||||
const val INTENT_NAME = "subsonic.name"
|
||||
// Legacy names for intent extras, in those fragments which don't use SafeArgs yet.
|
||||
const val INTENT_ARTIST = "subsonic.artist"
|
||||
const val INTENT_TITLE = "subsonic.title"
|
||||
const val INTENT_AUTOPLAY = "subsonic.playall"
|
||||
const val INTENT_QUERY = "subsonic.query"
|
||||
const val INTENT_PLAYLIST_ID = "subsonic.playlist.id"
|
||||
const val INTENT_PODCAST_CHANNEL_ID = "subsonic.podcastChannel.id"
|
||||
const val INTENT_PARENT_ID = "subsonic.parent.id"
|
||||
const val INTENT_PLAYLIST_NAME = "subsonic.playlist.name"
|
||||
const val INTENT_SHARE_ID = "subsonic.share.id"
|
||||
const val INTENT_SHARE_NAME = "subsonic.share.name"
|
||||
const val INTENT_ALBUM_LIST_TYPE = "subsonic.albumlisttype"
|
||||
const val INTENT_ALBUM_LIST_TITLE = "subsonic.albumlisttitle"
|
||||
const val INTENT_ALBUM_LIST_SIZE = "subsonic.albumlistsize"
|
||||
const val INTENT_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset"
|
||||
const val INTENT_SHUFFLE = "subsonic.shuffle"
|
||||
const val INTENT_REFRESH = "subsonic.refresh"
|
||||
const val INTENT_STARRED = "subsonic.starred"
|
||||
const val INTENT_RANDOM = "subsonic.random"
|
||||
const val INTENT_GENRE_NAME = "subsonic.genre"
|
||||
const val INTENT_IS_ALBUM = "subsonic.isalbum"
|
||||
const val INTENT_VIDEOS = "subsonic.videos"
|
||||
const val INTENT_SHOW_PLAYER = "subsonic.showplayer"
|
||||
const val INTENT_APPEND = "subsonic.append"
|
||||
|
||||
// Names for Intent Actions
|
||||
const val CMD_PROCESS_KEYCODE = "org.moire.ultrasonic.CMD_PROCESS_KEYCODE"
|
||||
@ -62,8 +43,5 @@ object Constants {
|
||||
|
||||
const val FILENAME_PLAYLIST_SER = "downloadstate.ser"
|
||||
const val ALBUM_ART_FILE = "folder.jpeg"
|
||||
const val STARRED = "starred"
|
||||
const val ALPHABETICAL_BY_NAME = "alphabeticalByName"
|
||||
const val ALBUMS_OF_ARTIST = "albumsOfArtist"
|
||||
const val RESULT_CLOSE_ALL = 1337
|
||||
}
|
||||
|
@ -3,6 +3,13 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/navigation_graph"
|
||||
app:startDestination="@id/mainFragment">
|
||||
|
||||
<action android:id="@+id/toTrackCollection"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
<action android:id="@+id/toBookmarks"
|
||||
app:destination="@id/bookmarksFragment" />
|
||||
<action android:id="@+id/toMediaLibrary"
|
||||
app:destination="@id/mediaLibraryFragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/mainFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.MainFragment"
|
||||
@ -19,6 +26,9 @@
|
||||
<action
|
||||
android:id="@+id/mainToSelectGenre"
|
||||
app:destination="@id/selectGenreFragment" />
|
||||
<action
|
||||
android:id="@+id/toTrackCollection"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/mediaLibraryFragment"
|
||||
@ -31,6 +41,14 @@
|
||||
android:id="@+id/artistsListToTrackCollection"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/nowPlayingFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.NowPlayingFragment"
|
||||
android:label="@string/button_bar.now_playing">
|
||||
<action
|
||||
android:id="@+id/toTrackCollection"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/artistListFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.ArtistListFragment" >
|
||||
@ -40,21 +58,150 @@
|
||||
<action
|
||||
android:id="@+id/artistsListToTrackCollection"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
<argument
|
||||
android:name="refresh"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
<argument
|
||||
android:name="title"
|
||||
app:argType="string"
|
||||
app:nullable="true"
|
||||
android:defaultValue="@null"
|
||||
/>
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/trackCollectionFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.TrackCollectionFragment" >
|
||||
<argument
|
||||
android:name="id"
|
||||
app:nullable="true"
|
||||
android:defaultValue="@null"
|
||||
app:argType="string" />
|
||||
<argument
|
||||
android:name="isAlbum"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
android:defaultValue="false"/>
|
||||
<argument
|
||||
android:name="isArtist"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false"/>
|
||||
<argument
|
||||
android:name="getRandom"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
<argument
|
||||
android:name="getStarred"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
<argument
|
||||
android:name="getVideos"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
<argument
|
||||
android:name="autoPlay"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
<argument
|
||||
android:name="shuffle"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
<argument
|
||||
android:name="refresh"
|
||||
android:defaultValue="true"
|
||||
app:argType="boolean" />
|
||||
<argument android:name="name"
|
||||
app:argType="string"
|
||||
android:defaultValue="@null"
|
||||
app:nullable="true"/>
|
||||
<argument android:name="parentId"
|
||||
app:argType="string"
|
||||
android:defaultValue="@null"
|
||||
app:nullable="true"/>
|
||||
<argument android:name="genreName"
|
||||
app:argType="string"
|
||||
android:defaultValue="@null"
|
||||
app:nullable="true"/>
|
||||
<argument android:name="shareId"
|
||||
app:argType="string"
|
||||
android:defaultValue="@null"
|
||||
app:nullable="true"/>
|
||||
<argument android:name="playlistId"
|
||||
app:argType="string"
|
||||
android:defaultValue="@null"
|
||||
app:nullable="true"/>
|
||||
<argument android:name="playlistName"
|
||||
app:argType="string"
|
||||
android:defaultValue="@null"
|
||||
app:nullable="true"/>
|
||||
<argument android:name="shareName"
|
||||
app:argType="string"
|
||||
android:defaultValue="@null"
|
||||
app:nullable="true"/>
|
||||
<argument android:name="podcastChannelId"
|
||||
app:argType="string"
|
||||
android:defaultValue="@null"
|
||||
app:nullable="true"/>
|
||||
<argument
|
||||
android:name="albumListType"
|
||||
app:argType="string"
|
||||
app:nullable="true"
|
||||
android:defaultValue="@null"/>
|
||||
<argument
|
||||
android:name="size"
|
||||
app:argType="integer"
|
||||
android:defaultValue="0"
|
||||
/>
|
||||
<argument
|
||||
android:name="offset"
|
||||
app:argType="integer"
|
||||
android:defaultValue="0"
|
||||
/>
|
||||
<action
|
||||
android:id="@+id/loadMoreTracks"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/albumListFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.AlbumListFragment" >
|
||||
<argument
|
||||
android:name="type"
|
||||
app:argType="org.moire.ultrasonic.api.subsonic.models.AlbumListType"
|
||||
/>
|
||||
<argument
|
||||
android:name="title"
|
||||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
<argument
|
||||
android:name="size"
|
||||
app:argType="integer"
|
||||
/>
|
||||
<argument
|
||||
android:name="offset"
|
||||
app:argType="integer"
|
||||
/>
|
||||
<argument
|
||||
android:name="append"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
<argument
|
||||
android:name="refresh"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
<argument
|
||||
android:name="id"
|
||||
android:defaultValue="@null"
|
||||
app:nullable="true"
|
||||
app:argType="string" />
|
||||
<action
|
||||
android:id="@+id/albumListToTrackCollection"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/entryListFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.EntryListFragment" >
|
||||
<action
|
||||
android:id="@+id/entryListToTrackCollection"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/searchFragment"
|
||||
@ -68,9 +215,9 @@
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/playlistsFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.PlaylistsFragment" >
|
||||
android:name="org.moire.ultrasonic.fragment.legacy.PlaylistsFragment" >
|
||||
<action
|
||||
android:id="@+id/playlistsToSelectAlbum"
|
||||
android:id="@+id/playlistsToTrackCollection"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
@ -78,9 +225,9 @@
|
||||
android:name="org.moire.ultrasonic.fragment.DownloadsFragment" />
|
||||
<fragment
|
||||
android:id="@+id/sharesFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.SharesFragment" >
|
||||
android:name="org.moire.ultrasonic.fragment.legacy.SharesFragment" >
|
||||
<action
|
||||
android:id="@+id/sharesToSelectAlbum"
|
||||
android:id="@+id/sharesToTrackCollection"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
@ -91,9 +238,9 @@
|
||||
android:name="org.moire.ultrasonic.fragment.ChatFragment" />
|
||||
<fragment
|
||||
android:id="@+id/podcastFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.PodcastFragment" >
|
||||
android:name="org.moire.ultrasonic.fragment.legacy.PodcastFragment" >
|
||||
<action
|
||||
android:id="@+id/podcastToSelectAlbum"
|
||||
android:id="@+id/podcastToTrackCollection"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
@ -105,7 +252,11 @@
|
||||
android:name="org.moire.ultrasonic.fragment.AboutFragment" />
|
||||
<fragment
|
||||
android:id="@+id/selectGenreFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.SelectGenreFragment" />
|
||||
android:name="org.moire.ultrasonic.fragment.legacy.SelectGenreFragment">
|
||||
<action
|
||||
android:id="@+id/selectGenreToTrackCollection"
|
||||
app:destination="@id/trackCollectionFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/playerFragment"
|
||||
android:name="org.moire.ultrasonic.fragment.PlayerFragment" >
|
||||
|
Loading…
x
Reference in New Issue
Block a user