Merge branch 'nitehu-refactor/artist_list' into develop

This commit is contained in:
Óscar García Amor 2020-11-29 11:11:30 +01:00
commit b471b9cad0
No known key found for this signature in database
GPG Key ID: E18B2370D3D566EE
27 changed files with 840 additions and 494 deletions

View File

@ -38,6 +38,7 @@ ext.versions = [
robolectric : "4.4", robolectric : "4.4",
dexter : "6.1.2", dexter : "6.1.2",
timber : "4.7.1", timber : "4.7.1",
fastScroll : "2.0.1",
] ]
ext.gradlePlugins = [ ext.gradlePlugins = [
@ -77,6 +78,7 @@ ext.other = [
picasso : "com.squareup.picasso:picasso:$versions.picasso", picasso : "com.squareup.picasso:picasso:$versions.picasso",
dexter : "com.karumi:dexter:$versions.dexter", dexter : "com.karumi:dexter:$versions.dexter",
timber : "com.jakewharton.timber:timber:$versions.timber", timber : "com.jakewharton.timber:timber:$versions.timber",
fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll",
] ]
ext.testing = [ ext.testing = [

View File

@ -77,6 +77,7 @@ dependencies {
implementation other.koinAndroid implementation other.koinAndroid
implementation other.koinViewModel implementation other.koinViewModel
implementation other.okhttpLogging implementation other.okhttpLogging
implementation other.fastScroll
kapt androidSupport.room kapt androidSupport.room

View File

@ -1,374 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.data.ServerSetting;
import org.moire.ultrasonic.domain.Artist;
import org.moire.ultrasonic.domain.Indexes;
import org.moire.ultrasonic.domain.MusicFolder;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.ArtistAdapter;
import java.util.ArrayList;
import java.util.List;
import kotlin.Lazy;
import static org.koin.android.viewmodel.compat.ViewModelCompat.viewModel;
import static org.koin.java.KoinJavaComponent.inject;
public class SelectArtistActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener
{
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
private Lazy<ServerSettingsModel> serverSettingsModel = viewModel(this, ServerSettingsModel.class);
private static final int MENU_GROUP_MUSIC_FOLDER = 10;
private SwipeRefreshLayout refreshArtistListView;
private ListView artistListView;
private View folderButton;
private TextView folderName;
private List<MusicFolder> musicFolders;
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.select_artist);
refreshArtistListView = findViewById(R.id.select_artist_refresh);
artistListView = findViewById(R.id.select_artist_list);
refreshArtistListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
artistListView.setOnItemClickListener(this);
folderButton = LayoutInflater.from(this).inflate(R.layout.select_artist_header, artistListView, false);
if (folderButton != null)
{
folderName = (TextView) folderButton.findViewById(R.id.select_artist_folder_2);
}
if (!ActiveServerProvider.Companion.isOffline(this) && !Util.getShouldUseId3Tags(this))
{
artistListView.addHeaderView(folderButton);
}
registerForContextMenu(artistListView);
String title = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE);
if (title == null)
{
setActionBarSubtitle(ActiveServerProvider.Companion.isOffline(this) ? R.string.music_library_label_offline : R.string.music_library_label);
}
else
{
setActionBarSubtitle(title);
}
View browseMenuItem = findViewById(R.id.menu_browse);
menuDrawer.setActiveView(browseMenuItem);
musicFolders = null;
load();
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
super.onCreateOptionsMenu(menu);
return true;
}
private void refresh()
{
finish();
Intent intent = getIntent();
String title = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, title);
intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
startActivityForResultWithoutTransition(this, intent);
}
private void selectFolder()
{
folderButton.showContextMenu();
}
private void load()
{
BackgroundTask<Indexes> task = new TabActivityBackgroundTask<Indexes>(this, true)
{
@Override
protected Indexes doInBackground() throws Throwable
{
boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
MusicService musicService = MusicServiceFactory.getMusicService(SelectArtistActivity.this);
boolean isOffline = ActiveServerProvider.Companion.isOffline(SelectArtistActivity.this);
boolean useId3Tags = Util.getShouldUseId3Tags(SelectArtistActivity.this);
if (!isOffline && !useId3Tags)
{
musicFolders = musicService.getMusicFolders(refresh, SelectArtistActivity.this, this);
}
String musicFolderId = activeServerProvider.getValue().getActiveServer().getMusicFolderId();
return !isOffline && useId3Tags ? musicService.getArtists(refresh, SelectArtistActivity.this, this) : musicService.getIndexes(musicFolderId, refresh, SelectArtistActivity.this, this);
}
@Override
protected void done(Indexes result)
{
if (result != null)
{
List<Artist> artists = new ArrayList<Artist>(result.getShortcuts().size() + result.getArtists().size());
artists.addAll(result.getShortcuts());
artists.addAll(result.getArtists());
artistListView.setAdapter(new ArtistAdapter(SelectArtistActivity.this, artists));
}
// Display selected music folder
if (musicFolders != null)
{
String musicFolderId = activeServerProvider.getValue().getActiveServer().getMusicFolderId();
if (musicFolderId == null || musicFolderId.equals(""))
{
if (folderName != null)
{
folderName.setText(R.string.select_artist_all_folders);
}
}
else
{
for (MusicFolder musicFolder : musicFolders)
{
if (musicFolder.getId().equals(musicFolderId))
{
if (folderName != null)
{
folderName.setText(musicFolder.getName());
}
break;
}
}
}
}
}
};
task.execute();
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (view == folderButton)
{
selectFolder();
}
else
{
Artist artist = (Artist) parent.getItemAtPosition(position);
if (artist != null)
{
Intent intent = new Intent(this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
intent.putExtra(Constants.INTENT_EXTRA_NAME_PARENT_ID, artist.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_ARTIST, true);
startActivityForResultWithoutTransition(this, intent);
}
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
if (artistListView.getItemAtPosition(info.position) instanceof Artist)
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.select_artist_context, menu);
}
else if (info.position == 0)
{
String musicFolderId = activeServerProvider.getValue().getActiveServer().getMusicFolderId();
MenuItem menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders);
if (musicFolderId == null || musicFolderId.isEmpty())
{
menuItem.setChecked(true);
}
if (musicFolders != null)
{
for (int i = 0; i < musicFolders.size(); i++)
{
MusicFolder musicFolder = musicFolders.get(i);
menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, musicFolder.getName());
if (musicFolder.getId().equals(musicFolderId))
{
menuItem.setChecked(true);
}
}
}
menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true);
}
MenuItem downloadMenuItem = menu.findItem(R.id.artist_menu_download);
if (downloadMenuItem != null)
{
downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(this));
}
}
@Override
public boolean onContextItemSelected(MenuItem menuItem)
{
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
if (info == null)
{
return true;
}
Artist artist = (Artist) artistListView.getItemAtPosition(info.position);
if (artist != null)
{
switch (menuItem.getItemId())
{
case R.id.artist_menu_play_now:
downloadRecursively(artist.getId(), false, false, true, false, false, false, false, true);
break;
case R.id.artist_menu_play_next:
downloadRecursively(artist.getId(), false, false, true, true, false, true, false, true);
break;
case R.id.artist_menu_play_last:
downloadRecursively(artist.getId(), false, true, false, false, false, false, false, true);
break;
case R.id.artist_menu_pin:
downloadRecursively(artist.getId(), true, true, false, false, false, false, false, true);
break;
case R.id.artist_menu_unpin:
downloadRecursively(artist.getId(), false, false, false, false, false, false, true, true);
break;
case R.id.artist_menu_download:
downloadRecursively(artist.getId(), false, false, false, false, true, false, false, true);
break;
default:
return super.onContextItemSelected(menuItem);
}
}
else if (info.position == 0)
{
MusicFolder selectedFolder = menuItem.getItemId() == -1 ? null : musicFolders.get(menuItem.getItemId());
String musicFolderId = selectedFolder == null ? null : selectedFolder.getId();
String musicFolderName = selectedFolder == null ? getString(R.string.select_artist_all_folders) : selectedFolder.getName();
if (!ActiveServerProvider.Companion.isOffline(this)) {
ServerSetting currentSetting = activeServerProvider.getValue().getActiveServer();
currentSetting.setMusicFolderId(musicFolderId);
serverSettingsModel.getValue().updateItem(currentSetting);
}
folderName.setText(musicFolderName);
refresh();
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
menuDrawer.toggleMenu();
return true;
case R.id.main_shuffle:
Intent intent = new Intent(this, DownloadActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
startActivityForResultWithoutTransition(this, intent);
return true;
}
return false;
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
refresh();
return null;
}
}
}

View File

@ -63,6 +63,7 @@ public class SettingsFragment extends PreferenceFragment
private CheckBoxPreference lockScreenEnabled; private CheckBoxPreference lockScreenEnabled;
private CheckBoxPreference sendBluetoothNotifications; private CheckBoxPreference sendBluetoothNotifications;
private CheckBoxPreference sendBluetoothAlbumArt; private CheckBoxPreference sendBluetoothAlbumArt;
private CheckBoxPreference showArtistPicture;
private ListPreference viewRefresh; private ListPreference viewRefresh;
private ListPreference imageLoaderConcurrency; private ListPreference imageLoaderConcurrency;
private EditTextPreference sharingDefaultDescription; private EditTextPreference sharingDefaultDescription;
@ -121,6 +122,7 @@ public class SettingsFragment extends PreferenceFragment
resumeOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE); resumeOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE);
pauseOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE); pauseOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE);
debugLogToFile = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE); debugLogToFile = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE);
showArtistPicture = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE);
sharingDefaultGreeting.setText(Util.getShareGreeting(getActivity())); sharingDefaultGreeting.setText(Util.getShareGreeting(getActivity()));
setupClearSearchPreference(); setupClearSearchPreference();
@ -176,6 +178,9 @@ public class SettingsFragment extends PreferenceFragment
setImageLoaderConcurrency(Integer.parseInt(sharedPreferences.getString(key, "5"))); setImageLoaderConcurrency(Integer.parseInt(sharedPreferences.getString(key, "5")));
} else if (Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE.equals(key)) { } else if (Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE.equals(key)) {
setDebugLogToFile(sharedPreferences.getBoolean(key, false)); setDebugLogToFile(sharedPreferences.getBoolean(key, false));
} else if (Constants.PREFERENCES_KEY_ID3_TAGS.equals(key)) {
if (sharedPreferences.getBoolean(key, false)) showArtistPicture.setEnabled(true);
else showArtistPicture.setEnabled(false);
} }
} }
@ -427,6 +432,9 @@ public class SettingsFragment extends PreferenceFragment
} else { } else {
debugLogToFile.setSummary(""); debugLogToFile.setSummary("");
} }
if (Util.getShouldUseId3Tags(getActivity())) showArtistPicture.setEnabled(true);
else showArtistPicture.setEnabled(false);
} }
private static void setImageLoaderConcurrency(int concurrency) { private static void setImageLoaderConcurrency(int concurrency) {

View File

@ -20,18 +20,7 @@ package org.moire.ultrasonic.util;
import android.app.Activity; import android.app.Activity;
import android.os.Handler; import android.os.Handler;
import timber.log.Timber; import org.moire.ultrasonic.service.CommunicationErrorHandler;
import com.fasterxml.jackson.core.JsonParseException;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.service.SubsonicRESTException;
import org.moire.ultrasonic.subsonic.RestErrorMapper;
import javax.net.ssl.SSLException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
/** /**
* @author Sindre Mehus * @author Sindre Mehus
@ -65,41 +54,13 @@ public abstract class BackgroundTask<T> implements ProgressListener
protected void error(Throwable error) protected void error(Throwable error)
{ {
Timber.w(error); CommunicationErrorHandler.Companion.handleError(error, activity);
new ErrorDialog(activity, getErrorMessage(error), false);
} }
protected String getErrorMessage(Throwable error) { protected String getErrorMessage(Throwable error)
if (error instanceof IOException && !Util.isNetworkConnected(activity)) { {
return activity.getResources().getString(R.string.background_task_no_network); return CommunicationErrorHandler.Companion.getErrorMessage(error, activity);
} else if (error instanceof FileNotFoundException) { }
return activity.getResources().getString(R.string.background_task_not_found);
} else if (error instanceof JsonParseException) {
return activity.getResources().getString(R.string.background_task_parse_error);
} else if (error instanceof SSLException) {
if (error.getCause() instanceof CertificateException &&
error.getCause().getCause() instanceof CertPathValidatorException) {
return activity.getResources()
.getString(R.string.background_task_ssl_cert_error,
error.getCause().getCause().getMessage());
} else {
return activity.getResources().getString(R.string.background_task_ssl_error);
}
} else if (error instanceof ApiNotSupportedException) {
return activity.getResources().getString(R.string.background_task_unsupported_api,
((ApiNotSupportedException) error).getServerApiVersion());
} else if (error instanceof IOException) {
return activity.getResources().getString(R.string.background_task_network_error);
} else if (error instanceof SubsonicRESTException) {
return RestErrorMapper.getLocalizedErrorMessage((SubsonicRESTException) error, activity);
}
String message = error.getMessage();
if (message != null) {
return message;
}
return error.getClass().getSimpleName();
}
@Override @Override
public abstract void updateProgress(final String message); public abstract void updateProgress(final String message);

View File

@ -114,6 +114,7 @@ public final class Constants
public static final String PREFERENCES_KEY_DOWNLOAD_TRANSITION = "transitionToDownloadOnPlay"; public static final String PREFERENCES_KEY_DOWNLOAD_TRANSITION = "transitionToDownloadOnPlay";
public static final String PREFERENCES_KEY_INCREMENT_TIME = "incrementTime"; public static final String PREFERENCES_KEY_INCREMENT_TIME = "incrementTime";
public static final String PREFERENCES_KEY_ID3_TAGS = "useId3Tags"; public static final String PREFERENCES_KEY_ID3_TAGS = "useId3Tags";
public static final String PREFERENCES_KEY_SHOW_ARTIST_PICTURE = "showArtistPicture";
public static final String PREFERENCES_KEY_TEMP_LOSS = "tempLoss"; public static final String PREFERENCES_KEY_TEMP_LOSS = "tempLoss";
public static final String PREFERENCES_KEY_CHAT_REFRESH_INTERVAL = "chatRefreshInterval"; public static final String PREFERENCES_KEY_CHAT_REFRESH_INTERVAL = "chatRefreshInterval";
public static final String PREFERENCES_KEY_DIRECTORY_CACHE_TIME = "directoryCacheTime"; public static final String PREFERENCES_KEY_DIRECTORY_CACHE_TIME = "directoryCacheTime";

View File

@ -13,23 +13,16 @@ public interface ImageLoader {
void stopImageLoader(); void stopImageLoader();
void loadAvatarImage( void loadAvatarImage(View view, String username, boolean large, int size, boolean crossFade,
View view, boolean highQuality);
String username,
boolean large,
int size,
boolean crossFade,
boolean highQuality
);
void loadImage( void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size,
View view, boolean crossFade, boolean highQuality);
MusicDirectory.Entry entry,
boolean large, void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size,
int size, boolean crossFade, boolean highQuality, int defaultResourceId);
boolean crossFade,
boolean highQuality void cancel(String coverArt);
);
Bitmap getImageBitmap(String username, int size); Bitmap getImageBitmap(String username, int size);

View File

@ -38,6 +38,8 @@ import org.moire.ultrasonic.service.MusicServiceFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.SortedSet;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -165,25 +167,24 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
} }
@Override @Override
public void loadImage( public void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size,
View view, boolean crossFade, boolean highQuality) {
MusicDirectory.Entry entry, loadImage(view, entry, large, size, crossFade, highQuality, -1);
boolean large, }
int size,
boolean crossFade, public void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size,
boolean highQuality boolean crossFade, boolean highQuality, int defaultResourceId) {
) {
view.invalidate(); view.invalidate();
if (entry == null) { if (entry == null) {
setUnknownImage(view, large); setUnknownImage(view, large, defaultResourceId);
return; return;
} }
String coverArt = entry.getCoverArt(); String coverArt = entry.getCoverArt();
if (TextUtils.isEmpty(coverArt)) { if (TextUtils.isEmpty(coverArt)) {
setUnknownImage(view, large); setUnknownImage(view, large, defaultResourceId);
return; return;
} }
@ -198,11 +199,21 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
return; return;
} }
setUnknownImage(view, large); setUnknownImage(view, large, defaultResourceId);
queue.offer(new Task(view, entry, size, large, crossFade, highQuality)); queue.offer(new Task(view, entry, size, large, crossFade, highQuality));
} }
public void cancel(String coverArt) {
for (Object taskObject : queue.toArray()) {
Task task = (Task)taskObject;
if ((task.entry.getCoverArt() != null) && (coverArt.compareTo(task.entry.getCoverArt()) == 0)) {
queue.remove(taskObject);
break;
}
}
}
private static String getKey(String coverArtId, int size) { private static String getKey(String coverArtId, int size) {
return String.format("%s:%d", coverArtId, size); return String.format("%s:%d", coverArtId, size);
} }
@ -330,13 +341,18 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
} }
private void setUnknownImage(View view, boolean large) { private void setUnknownImage(View view, boolean large) {
setUnknownImage(view, large, -1);
}
private void setUnknownImage(View view, boolean large, int resId) {
if (resId == -1) resId = R.drawable.unknown_album;
if (large) { if (large) {
setImageBitmap(view, null, largeUnknownImage, false); setImageBitmap(view, null, largeUnknownImage, false);
} else { } else {
if (view instanceof TextView) { if (view instanceof TextView) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_album, 0, 0, 0); ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(resId, 0, 0, 0);
} else if (view instanceof ImageView) { } else if (view instanceof ImageView) {
((ImageView) view).setImageResource(R.drawable.unknown_album); ((ImageView) view).setImageResource(resId);
} }
} }
} }
@ -381,14 +397,7 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
private final boolean crossFade; private final boolean crossFade;
private final boolean highQuality; private final boolean highQuality;
Task( Task(View view, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean crossFade, boolean highQuality) {
View view,
MusicDirectory.Entry entry,
int size,
boolean saveToFile,
boolean crossFade,
boolean highQuality
) {
this.view = view; this.view = view;
this.entry = entry; this.entry = entry;
this.username = null; this.username = null;
@ -399,14 +408,7 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
handler = new Handler(); handler = new Handler();
} }
Task( Task(View view, String username, int size, boolean saveToFile, boolean crossFade, boolean highQuality) {
View view,
String username,
int size,
boolean saveToFile,
boolean crossFade,
boolean highQuality
) {
this.view = view; this.view = view;
this.entry = null; this.entry = null;
this.username = username; this.username = username;
@ -421,9 +423,9 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
try { try {
MusicService musicService = MusicServiceFactory.getMusicService(view.getContext()); MusicService musicService = MusicServiceFactory.getMusicService(view.getContext());
final boolean isAvatar = this.username != null && this.entry == null; final boolean isAvatar = this.username != null && this.entry == null;
final Bitmap bitmap = this.entry != null final Bitmap bitmap = this.entry != null ?
? musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) :
: musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null); musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null);
if (bitmap == null) { if (bitmap == null) {
Timber.d("Found empty album art."); Timber.d("Found empty album art.");

View File

@ -54,6 +54,7 @@ import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.DownloadActivity; import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.activity.MainActivity; import org.moire.ultrasonic.activity.MainActivity;
import org.moire.ultrasonic.activity.SettingsActivity; import org.moire.ultrasonic.activity.SettingsActivity;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.*; import org.moire.ultrasonic.domain.*;
import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
@ -1181,6 +1182,15 @@ public class Util
return preferences.getBoolean(Constants.PREFERENCES_KEY_ID3_TAGS, false); return preferences.getBoolean(Constants.PREFERENCES_KEY_ID3_TAGS, false);
} }
public static boolean getShouldShowArtistPicture(Context context)
{
SharedPreferences preferences = getPreferences(context);
boolean isOffline = ActiveServerProvider.Companion.isOffline(context);
boolean isId3Enabled = preferences.getBoolean(Constants.PREFERENCES_KEY_ID3_TAGS, false);
boolean shouldShowArtistPicture = preferences.getBoolean(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE, false);
return (!isOffline) && isId3Enabled && shouldShowArtistPicture;
}
public static int getChatRefreshInterval(Context context) public static int getChatRefreshInterval(Context context)
{ {
SharedPreferences preferences = getPreferences(context); SharedPreferences preferences = getPreferences(context);

View File

@ -0,0 +1,109 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2020 (C) Jozsef Varga
*/
package org.moire.ultrasonic.activity
import android.content.Context
import android.os.Handler
import android.os.Looper
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.service.CommunicationErrorHandler
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.Util
/**
* Provides ViewModel which contains the list of available Artists
*/
class ArtistListModel(
private val activeServerProvider: ActiveServerProvider,
private val context: Context
) : ViewModel() {
private val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData()
private val artists: MutableLiveData<List<Artist>> = MutableLiveData()
/**
* Retrieves the available Artists in a LiveData
*/
fun getArtists(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<Artist>> {
backgroundLoadFromServer(refresh, swipe)
return artists
}
/**
* Retrieves the available Music Folders in a LiveData
*/
fun getMusicFolders(): LiveData<List<MusicFolder>> {
return musicFolders
}
/**
* Refreshes the cached Artists from the server
*/
fun refresh(swipe: SwipeRefreshLayout) {
backgroundLoadFromServer(true, swipe)
}
private fun backgroundLoadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) {
viewModelScope.launch {
swipe.isRefreshing = true
loadFromServer(refresh, swipe)
swipe.isRefreshing = false
}
}
private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) =
withContext(Dispatchers.IO) {
val musicService = MusicServiceFactory.getMusicService(context)
val isOffline = ActiveServerProvider.isOffline(context)
val useId3Tags = Util.getShouldUseId3Tags(context)
try {
if (!isOffline && !useId3Tags) {
musicFolders.postValue(
musicService.getMusicFolders(refresh, context, null)
)
}
val musicFolderId = activeServerProvider.getActiveServer().musicFolderId
val result = if (!isOffline && useId3Tags)
musicService.getArtists(refresh, context, null)
else musicService.getIndexes(musicFolderId, refresh, context, null)
val retrievedArtists: MutableList<Artist> =
ArrayList(result.shortcuts.size + result.artists.size)
retrievedArtists.addAll(result.shortcuts)
retrievedArtists.addAll(result.artists)
artists.postValue(retrievedArtists)
} catch (exception: Exception) {
Handler(Looper.getMainLooper()).post {
CommunicationErrorHandler.handleError(exception, swipe.context)
}
}
}
}

View File

@ -0,0 +1,194 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2020 (C) Jozsef Varga
*/
package org.moire.ultrasonic.activity
import android.view.LayoutInflater
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter
import java.text.Collator
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.ImageLoader
import org.moire.ultrasonic.util.Util
/**
* Creates a Row in a RecyclerView which contains the details of an Artist
*/
class ArtistRowAdapter(
private var artistList: List<Artist>,
private var folderName: String,
private var shouldShowHeader: Boolean,
val onArtistClick: (Artist) -> Unit,
val onContextMenuClick: (MenuItem, Artist) -> Boolean,
val onFolderClick: (view: View) -> Unit,
private val imageLoader: ImageLoader
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), SectionedAdapter {
/**
* Sets the data to be displayed in the RecyclerView
*/
fun setData(data: List<Artist>) {
artistList = data.sortedWith(compareBy(Collator.getInstance()) { t -> t.name })
notifyDataSetChanged()
}
/**
* Sets the name of the folder to be displayed n the Header (first) row
*/
fun setFolderName(name: String) {
folderName = name
notifyDataSetChanged()
}
/**
* Holds the view properties of an Artist row
*/
class ArtistViewHolder(
itemView: View
) : RecyclerView.ViewHolder(itemView) {
var section: TextView = itemView.findViewById(R.id.row_section)
var textView: TextView = itemView.findViewById(R.id.row_artist_name)
var layout: RelativeLayout = itemView.findViewById(R.id.row_artist_layout)
var coverArt: ImageView = itemView.findViewById(R.id.artist_coverart)
var coverArtId: String? = null
}
/**
* Holds the view properties of the Header row
*/
class HeaderViewHolder(
itemView: View
) : RecyclerView.ViewHolder(itemView) {
var folderName: TextView = itemView.findViewById(R.id.select_artist_folder_2)
var layout: LinearLayout = itemView.findViewById(R.id.select_artist_folder)
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder {
if (viewType == TYPE_ITEM) {
val row = LayoutInflater.from(parent.context)
.inflate(R.layout.artist_list_item, parent, false)
return ArtistViewHolder(row)
}
val header = LayoutInflater.from(parent.context)
.inflate(R.layout.select_artist_header, parent, false)
return HeaderViewHolder(header)
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if ((holder is ArtistViewHolder) && (holder.coverArtId != null)) {
imageLoader.cancel(holder.coverArtId)
}
super.onViewRecycled(holder)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ArtistViewHolder) {
val listPosition = if (shouldShowHeader) position - 1 else position
holder.textView.text = artistList[listPosition].name
holder.section.text = getSectionForArtist(listPosition)
holder.layout.setOnClickListener { onArtistClick(artistList[listPosition]) }
holder.layout.setOnLongClickListener { view -> createPopupMenu(view, listPosition) }
holder.coverArtId = artistList[listPosition].coverArt
if (Util.getShouldShowArtistPicture(holder.coverArt.context)) {
holder.coverArt.visibility = View.VISIBLE
imageLoader.loadImage(
holder.coverArt,
MusicDirectory.Entry().apply { coverArt = holder.coverArtId },
false, 0, false, true, R.drawable.ic_contact_picture
)
} else {
holder.coverArt.visibility = View.GONE
}
} else if (holder is HeaderViewHolder) {
holder.folderName.text = folderName
holder.layout.setOnClickListener { onFolderClick(holder.layout) }
}
}
override fun getItemCount() = if (shouldShowHeader) artistList.size + 1 else artistList.size
override fun getItemViewType(position: Int): Int {
return if (position == 0 && shouldShowHeader) TYPE_HEADER else TYPE_ITEM
}
override fun getSectionName(position: Int): String {
var listPosition = if (shouldShowHeader) position - 1 else position
// Show the first artist's initial in the popup when the list is
// scrolled up to the "Select Folder" row
if (listPosition < 0) listPosition = 0
return getSectionFromName(artistList[listPosition].name ?: " ")
}
private fun getSectionForArtist(artistPosition: Int): String {
if (artistPosition == 0)
return getSectionFromName(artistList[artistPosition].name ?: " ")
val previousArtistSection = getSectionFromName(
artistList[artistPosition - 1].name ?: " "
)
val currentArtistSection = getSectionFromName(
artistList[artistPosition].name ?: " "
)
return if (previousArtistSection == currentArtistSection) "" else currentArtistSection
}
private fun getSectionFromName(name: String): String {
var section = name.first().toUpperCase()
if (!section.isLetter()) section = '#'
return section.toString()
}
private fun createPopupMenu(view: View, position: Int): Boolean {
val popup = PopupMenu(view.context, view)
val inflater: MenuInflater = popup.menuInflater
inflater.inflate(R.menu.select_artist_context, popup.menu)
val downloadMenuItem = popup.menu.findItem(R.id.artist_menu_download)
downloadMenuItem?.isVisible = !isOffline(view.context)
popup.setOnMenuItemClickListener { menuItem ->
onContextMenuClick(menuItem, artistList[position])
}
popup.show()
return true
}
companion object {
private const val TYPE_HEADER = 0
private const val TYPE_ITEM = 1
}
}

View File

@ -0,0 +1,215 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2020 (C) Jozsef Varga
*/
package org.moire.ultrasonic.activity
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.widget.PopupMenu
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.koin.android.ext.android.inject
import org.koin.android.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Util
/**
* Displays the available Artists in a list
*/
class SelectArtistActivity : SubsonicTabActivity() {
private val activeServerProvider: ActiveServerProvider by inject()
private val serverSettingsModel: ServerSettingsModel by viewModel()
private val artistListModel: ArtistListModel by viewModel()
private var refreshArtistListView: SwipeRefreshLayout? = null
private var artistListView: RecyclerView? = null
private var musicFolders: List<MusicFolder>? = null
private lateinit var viewManager: RecyclerView.LayoutManager
private lateinit var viewAdapter: ArtistRowAdapter
/**
* Called when the activity is first created.
*/
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.select_artist)
refreshArtistListView = findViewById(R.id.select_artist_refresh)
refreshArtistListView!!.setOnRefreshListener {
artistListModel.refresh(refreshArtistListView!!)
}
val shouldShowHeader = (!isOffline(this) && !Util.getShouldUseId3Tags(this))
val title = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE)
if (title == null) {
setActionBarSubtitle(
if (isOffline(this)) R.string.music_library_label_offline
else R.string.music_library_label
)
} else {
actionBarSubtitle = title
}
val browseMenuItem = findViewById<View>(R.id.menu_browse)
menuDrawer.setActiveView(browseMenuItem)
musicFolders = null
val refresh = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false)
artistListModel.getMusicFolders()
.observe(
this,
Observer { changedFolders ->
if (changedFolders != null) {
musicFolders = changedFolders
viewAdapter.setFolderName(getMusicFolderName(changedFolders))
}
}
)
val artists = artistListModel.getArtists(refresh, refreshArtistListView!!)
artists.observe(
this, Observer { changedArtists -> viewAdapter.setData(changedArtists) }
)
viewManager = LinearLayoutManager(this)
viewAdapter = ArtistRowAdapter(
artists.value ?: listOf(),
getText(R.string.select_artist_all_folders).toString(),
shouldShowHeader,
{ artist -> onItemClick(artist) },
{ menuItem, artist -> onArtistMenuItemSelected(menuItem, artist) },
{ view -> onFolderClick(view) },
imageLoader
)
artistListView = findViewById<RecyclerView>(R.id.select_artist_list).apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
menuDrawer.toggleMenu()
return true
}
R.id.main_shuffle -> {
val intent = Intent(this, DownloadActivity::class.java)
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true)
startActivityForResultWithoutTransition(this, intent)
return true
}
}
return false
}
private fun getMusicFolderName(musicFolders: List<MusicFolder>): String {
val musicFolderId = activeServerProvider.getActiveServer().musicFolderId
if (musicFolderId != null && musicFolderId != "") {
for ((id, name) in musicFolders) {
if (id == musicFolderId) {
return name
}
}
}
return getText(R.string.select_artist_all_folders).toString()
}
private fun onItemClick(artist: Artist) {
val intent = Intent(this, SelectAlbumActivity::class.java)
intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, artist.id)
intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, artist.name)
intent.putExtra(Constants.INTENT_EXTRA_NAME_PARENT_ID, artist.id)
intent.putExtra(Constants.INTENT_EXTRA_NAME_ARTIST, true)
startActivityForResultWithoutTransition(this, intent)
}
private fun onFolderClick(view: View) {
val popup = PopupMenu(this, view)
val musicFolderId = activeServerProvider.getActiveServer().musicFolderId
var menuItem = popup.menu.add(
MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders
)
if (musicFolderId == null || musicFolderId.isEmpty()) {
menuItem.isChecked = true
}
if (musicFolders != null) {
for (i in musicFolders!!.indices) {
val (id, name) = musicFolders!![i]
menuItem = popup.menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, name)
if (id == musicFolderId) {
menuItem.isChecked = true
}
}
}
popup.menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true)
popup.setOnMenuItemClickListener { item -> onFolderMenuItemSelected(item) }
popup.show()
}
private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean {
when (menuItem.itemId) {
R.id.artist_menu_play_now ->
downloadRecursively(artist.id, false, false, true, false, false, false, false, true)
R.id.artist_menu_play_next ->
downloadRecursively(artist.id, false, false, true, true, false, true, false, true)
R.id.artist_menu_play_last ->
downloadRecursively(artist.id, false, true, false, false, false, false, false, true)
R.id.artist_menu_pin ->
downloadRecursively(artist.id, true, true, false, false, false, false, false, true)
R.id.artist_menu_unpin ->
downloadRecursively(artist.id, false, false, false, false, false, false, true, true)
R.id.artist_menu_download ->
downloadRecursively(artist.id, false, false, false, false, true, false, false, true)
}
return true
}
private fun onFolderMenuItemSelected(menuItem: MenuItem): Boolean {
val selectedFolder = if (menuItem.itemId == -1) null else musicFolders!![menuItem.itemId]
val musicFolderId = selectedFolder?.id
val musicFolderName = selectedFolder?.name
?: getString(R.string.select_artist_all_folders)
if (!isOffline(this)) {
val currentSetting = activeServerProvider.getActiveServer()
currentSetting.musicFolderId = musicFolderId
serverSettingsModel.updateItem(currentSetting)
}
viewAdapter.setFolderName(musicFolderName)
artistListModel.refresh(refreshArtistListView!!)
return true
}
companion object {
private const val MENU_GROUP_MUSIC_FOLDER = 10
}
}

View File

@ -4,9 +4,11 @@ package org.moire.ultrasonic.di
import kotlin.math.abs import kotlin.math.abs
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.BuildConfig
import org.moire.ultrasonic.activity.ArtistListModel
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
@ -71,4 +73,6 @@ val musicServiceModule = module {
} }
single { SubsonicImageLoader(androidContext(), get()) } single { SubsonicImageLoader(androidContext(), get()) }
viewModel { ArtistListModel(get(), androidContext()) }
} }

View File

@ -7,6 +7,7 @@ import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist
fun APIArtist.toDomainEntity(): Artist = Artist( fun APIArtist.toDomainEntity(): Artist = Artist(
id = this@toDomainEntity.id, id = this@toDomainEntity.id,
coverArt = this@toDomainEntity.coverArt,
name = this@toDomainEntity.name name = this@toDomainEntity.name
) )

View File

@ -0,0 +1,85 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2020 (C) Jozsef Varga
*/
package org.moire.ultrasonic.service
import android.app.AlertDialog
import android.content.Context
import com.fasterxml.jackson.core.JsonParseException
import java.io.FileNotFoundException
import java.io.IOException
import java.security.cert.CertPathValidatorException
import java.security.cert.CertificateException
import javax.net.ssl.SSLException
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
import org.moire.ultrasonic.subsonic.getLocalizedErrorMessage
import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
* Contains helper functions to handle the exceptions
* thrown during the communication with a Subsonic server
*/
class CommunicationErrorHandler {
companion object {
fun handleError(error: Throwable?, context: Context) {
Timber.w(error)
AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.error_label)
.setMessage(getErrorMessage(error!!, context))
.setCancelable(true)
.setPositiveButton(R.string.common_ok) { _, _ -> }
.create().show()
}
fun getErrorMessage(error: Throwable, context: Context): String {
if (error is IOException && !Util.isNetworkConnected(context)) {
return context.resources.getString(R.string.background_task_no_network)
} else if (error is FileNotFoundException) {
return context.resources.getString(R.string.background_task_not_found)
} else if (error is JsonParseException) {
return context.resources.getString(R.string.background_task_parse_error)
} else if (error is SSLException) {
return if (
error.cause is CertificateException &&
error.cause?.cause is CertPathValidatorException
) {
context.resources
.getString(
R.string.background_task_ssl_cert_error, error.cause?.cause?.message
)
} else {
context.resources.getString(R.string.background_task_ssl_error)
}
} else if (error is ApiNotSupportedException) {
return context.resources.getString(
R.string.background_task_unsupported_api, error.serverApiVersion
)
} else if (error is IOException) {
return context.resources.getString(R.string.background_task_network_error)
} else if (error is SubsonicRESTException) {
return error.getLocalizedErrorMessage(context)
}
val message = error.message
return message ?: error.javaClass.simpleName
}
}
}

View File

@ -26,18 +26,32 @@ class SubsonicImageLoaderProxy(
size: Int, size: Int,
crossFade: Boolean, crossFade: Boolean,
highQuality: Boolean highQuality: Boolean
) {
return loadImage(view, entry, large, size, crossFade, highQuality, -1)
}
override fun loadImage(
view: View?,
entry: MusicDirectory.Entry?,
large: Boolean,
size: Int,
crossFade: Boolean,
highQuality: Boolean,
defaultResourceId: Int
) { ) {
val id = entry?.coverArt val id = entry?.coverArt
val unknownImageId =
if (defaultResourceId == -1) R.drawable.unknown_album
else defaultResourceId
if (id != null && if (id != null &&
view != null && view != null &&
view is ImageView view is ImageView
) { ) {
val request = ImageRequest.CoverArt( val request = ImageRequest.CoverArt(
id, id, view,
view, placeHolderDrawableRes = unknownImageId,
placeHolderDrawableRes = R.drawable.unknown_album, errorDrawableRes = unknownImageId
errorDrawableRes = R.drawable.unknown_album
) )
subsonicImageLoader.load(request) subsonicImageLoader.load(request)
} }
@ -56,8 +70,7 @@ class SubsonicImageLoaderProxy(
view is ImageView view is ImageView
) { ) {
val request = ImageRequest.Avatar( val request = ImageRequest.Avatar(
username, username, view,
view,
placeHolderDrawableRes = R.drawable.ic_contact_picture, placeHolderDrawableRes = R.drawable.ic_contact_picture,
errorDrawableRes = R.drawable.ic_contact_picture errorDrawableRes = R.drawable.ic_contact_picture
) )

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/darker_gray" />
<padding
android:top="10dp"
android:left="10dp"
android:right="10dp"
android:bottom="10dp"/>
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true"
android:drawable="@drawable/line"/>
<item
android:drawable="@drawable/line"/>
</selector>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:topLeftRadius="44dp"
android:topRightRadius="44dp"
android:bottomRightRadius="44dp"
android:bottomLeftRadius="44dp" />
<padding
android:paddingLeft="22dp"
android:paddingRight="22dp" />
<solid android:color="@color/selected_color_dark" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true"
android:drawable="@drawable/thumb"/>
<item
android:drawable="@drawable/thumb"/>
</selector>

View File

@ -1,11 +1,56 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:a="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:id="@android:id/text1" xmlns:app="http://schemas.android.com/apk/res-auto"
a:drawablePadding="6dip" a:id="@+id/row_artist_layout"
a:layout_width="fill_parent" a:layout_height="wrap_content"
a:layout_height="wrap_content" a:layout_width="match_parent"
a:textAppearance="?android:attr/textAppearanceMedium" a:background="?android:attr/selectableItemBackground"
a:gravity="center_vertical" a:clickable="true"
a:paddingLeft="3dip" a:focusable="true">
a:paddingRight="3dip"
a:minHeight="50dip"/> <TextView
a:id="@+id/row_section"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:gravity="center_horizontal|center_vertical"
a:minWidth="56dip"
a:minHeight="56dip"
a:paddingStart="8dip"
a:paddingLeft="8dip"
a:paddingEnd="8dip"
a:paddingRight="8dip"
a:text="A"
a:textAppearance="?android:attr/textAppearanceLarge"
a:textColor="@color/cyan" />
<com.google.android.material.imageview.ShapeableImageView
a:id="@+id/artist_coverart"
a:layout_width="40dp"
a:layout_height="40dp"
a:layout_gravity="center_horizontal|center_vertical"
a:layout_marginTop="8dp"
a:layout_marginStart="2dp"
a:layout_marginLeft="2dp"
a:layout_marginEnd="10dp"
a:layout_marginRight="10dp"
a:layout_toEndOf="@+id/row_section"
a:layout_toRightOf="@+id/row_section"
a:scaleType="fitCenter"
a:src="@drawable/ic_contact_picture"
app:shapeAppearanceOverlay="@style/roundedImageView" />
<TextView
a:id="@+id/row_artist_name"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:layout_toEndOf="@+id/artist_coverart"
a:layout_toRightOf="@+id/artist_coverart"
a:drawablePadding="6dip"
a:gravity="center_vertical"
a:minHeight="56dip"
a:paddingLeft="3dip"
a:paddingRight="3dip"
a:layout_marginRight="12dp"
a:layout_marginEnd="12dp"
a:textAppearance="?android:attr/textAppearanceMedium" />
</RelativeLayout>

View File

@ -2,21 +2,33 @@
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="fill_parent" a:layout_width="fill_parent"
a:layout_height="fill_parent" a:layout_height="fill_parent"
a:orientation="vertical" > xmlns:app="http://schemas.android.com/apk/res-auto"
a:orientation="vertical">
<include layout="@layout/tab_progress" /> <include layout="@layout/tab_progress" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android" a:id="@+id/select_artist_refresh"
android:id="@+id/select_artist_refresh" a:layout_width="fill_parent"
android:layout_width="fill_parent" a:layout_height="0dip"
android:layout_height="0dip" a:layout_weight="1.0">
android:layout_weight="1.0">
<ListView <com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
android:id="@+id/select_artist_list" a:id="@+id/select_artist_list"
android:layout_width="match_parent" a:layout_width="match_parent"
android:layout_height="match_parent" /> a:layout_height="match_parent"
a:paddingTop="8dp"
a:paddingBottom="8dp"
a:clipToPadding="false"
app:fastScrollAutoHide="true"
app:fastScrollAutoHideDelay="2000"
app:fastScrollPopupTextSize="28sp"
app:fastScrollPopupBackgroundSize="42dp"
app:fastScrollPopupBgColor="@color/cyan"
app:fastScrollPopupTextColor="@android:color/primary_text_dark"
app:fastScrollPopupPosition="adjacent"
app:fastScrollTrackColor="@color/dividerColor"
app:fastScrollThumbColor="@color/cyan" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -7,7 +7,10 @@
a:orientation="horizontal" a:orientation="horizontal"
a:paddingBottom="2dip" a:paddingBottom="2dip"
a:paddingLeft="6dp" a:paddingLeft="6dp"
a:paddingTop="2dip" > a:paddingTop="2dip"
a:background="?android:attr/selectableItemBackground"
a:clickable="true"
a:focusable="true">
<ImageView <ImageView
a:layout_width="wrap_content" a:layout_width="wrap_content"

View File

@ -313,6 +313,8 @@
<string name="settings.use_folder_for_album_artist_summary">Feltételezi, hogy a legfelső szintű mappa az előadó neve.</string> <string name="settings.use_folder_for_album_artist_summary">Feltételezi, hogy a legfelső szintű mappa az előadó neve.</string>
<string name="settings.use_id3">Böngészés ID3 Tag használatával</string> <string name="settings.use_id3">Böngészés ID3 Tag használatával</string>
<string name="settings.use_id3_summary">ID3 Tag módszer használata a fájlredszer alapú mód helyett.</string> <string name="settings.use_id3_summary">ID3 Tag módszer használata a fájlredszer alapú mód helyett.</string>
<string name="settings.show_artist_picture">Előadó képének megjelenítése</string>
<string name="settings.show_artist_picture_summary">Az előadó listában megjeleníti a képeket, amennyiben elérhetőek</string>
<string name="settings.video_title">Videó</string> <string name="settings.video_title">Videó</string>
<string name="settings.video_player">Videólejátszó</string> <string name="settings.video_player">Videólejátszó</string>
<string name="settings.view_refresh">Nézet frissítési gyakorisága</string> <string name="settings.view_refresh">Nézet frissítési gyakorisága</string>

View File

@ -316,6 +316,8 @@
<string name="settings.use_folder_for_album_artist_summary">Assume top-level folder is the name of the album artist</string> <string name="settings.use_folder_for_album_artist_summary">Assume top-level folder is the name of the album artist</string>
<string name="settings.use_id3">Browse Using ID3 Tags</string> <string name="settings.use_id3">Browse Using ID3 Tags</string>
<string name="settings.use_id3_summary">Use ID3 tag methods instead of file system based methods</string> <string name="settings.use_id3_summary">Use ID3 tag methods instead of file system based methods</string>
<string name="settings.show_artist_picture">Show artist picture in artist list</string>
<string name="settings.show_artist_picture_summary">Displays the artist picture in the artist list if available</string>
<string name="settings.video_title">Video</string> <string name="settings.video_title">Video</string>
<string name="settings.video_player">Video player</string> <string name="settings.video_player">Video player</string>
<string name="settings.view_refresh">View Refresh</string> <string name="settings.view_refresh">View Refresh</string>

View File

@ -35,6 +35,11 @@
<item name="android:gravity">center_vertical</item> <item name="android:gravity">center_vertical</item>
</style> </style>
<style name="roundedImageView" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">8dp</item>
</style>
<attr name="star_hollow" format="reference"/> <attr name="star_hollow" format="reference"/>
<attr name="star_full" format="reference"/> <attr name="star_full" format="reference"/>
<attr name="about" format="reference"/> <attr name="about" format="reference"/>

View File

@ -64,6 +64,11 @@
a:key="useId3Tags" a:key="useId3Tags"
a:summary="@string/settings.use_id3_summary" a:summary="@string/settings.use_id3_summary"
a:title="@string/settings.use_id3"/> a:title="@string/settings.use_id3"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="showArtistPicture"
a:summary="@string/settings.show_artist_picture_summary"
a:title="@string/settings.show_artist_picture"/>
<CheckBoxPreference <CheckBoxPreference
a:defaultValue="true" a:defaultValue="true"
a:key="mediaButtons" a:key="mediaButtons"