Migrate to use SafeArgs

This commit is contained in:
birdbird 2022-08-04 12:35:53 +00:00 committed by Nite
parent f7b50d072d
commit a4919ef6e9
43 changed files with 1617 additions and 1430 deletions

View File

@ -17,6 +17,7 @@ buildscript {
classpath libs.kotlin
classpath libs.ktlintGradle
classpath libs.detekt
classpath libs.navigationSafeArgs
}
}

View File

@ -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")
}

View File

@ -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&lt;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>

View File

@ -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" }

View File

@ -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 {

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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
}
}

View File

@ -207,7 +207,11 @@ class ActiveServerProvider(
allowSelfSignedCertificate = false,
ldapSupport = false,
musicFolderId = "",
minimumApiVersion = null
minimumApiVersion = null,
bookmarkSupport = false,
podcastSupport = false,
shareSupport = false,
chatSupport = false
)
/**

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -37,7 +37,6 @@ class BookmarksFragment : TrackCollectionFragment() {
}
override fun getLiveData(
args: Bundle?,
refresh: Boolean
): LiveData<List<MusicDirectory.Child>> {
listModel.viewModelScope.launch(handler) {

View File

@ -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()
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
/**

View File

@ -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

View File

@ -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
)
}
}

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
)

View File

@ -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

View File

@ -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()

View File

@ -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 {

View File

@ -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

View File

@ -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?)

View File

@ -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

View File

@ -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()

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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" >