diff --git a/dependencies.gradle b/dependencies.gradle
index be338ccc..cffa0219 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -38,6 +38,7 @@ ext.versions = [
robolectric : "4.4",
dexter : "6.1.2",
timber : "4.7.1",
+ fastScroll : "2.0.1",
]
ext.gradlePlugins = [
@@ -77,6 +78,7 @@ ext.other = [
picasso : "com.squareup.picasso:picasso:$versions.picasso",
dexter : "com.karumi:dexter:$versions.dexter",
timber : "com.jakewharton.timber:timber:$versions.timber",
+ fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll",
]
ext.testing = [
diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle
index 479eb980..415ea07a 100644
--- a/ultrasonic/build.gradle
+++ b/ultrasonic/build.gradle
@@ -77,6 +77,7 @@ dependencies {
implementation other.koinAndroid
implementation other.koinViewModel
implementation other.okhttpLogging
+ implementation other.fastScroll
kapt androidSupport.room
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectArtistActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectArtistActivity.java
deleted file mode 100644
index 1f48b79e..00000000
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectArtistActivity.java
+++ /dev/null
@@ -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 .
-
- 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 = inject(ActiveServerProvider.class);
- private Lazy 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 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 task = new TabActivityBackgroundTask(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 artists = new ArrayList(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
- {
- @Override
- protected void onPostExecute(String[] result)
- {
- super.onPostExecute(result);
- }
-
- @Override
- protected String[] doInBackground(Void... params)
- {
- refresh();
- return null;
- }
- }
-}
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java
index 7b1fe14a..c6e97ba0 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java
@@ -63,6 +63,7 @@ public class SettingsFragment extends PreferenceFragment
private CheckBoxPreference lockScreenEnabled;
private CheckBoxPreference sendBluetoothNotifications;
private CheckBoxPreference sendBluetoothAlbumArt;
+ private CheckBoxPreference showArtistPicture;
private ListPreference viewRefresh;
private ListPreference imageLoaderConcurrency;
private EditTextPreference sharingDefaultDescription;
@@ -121,6 +122,7 @@ public class SettingsFragment extends PreferenceFragment
resumeOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE);
pauseOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE);
debugLogToFile = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE);
+ showArtistPicture = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE);
sharingDefaultGreeting.setText(Util.getShareGreeting(getActivity()));
setupClearSearchPreference();
@@ -176,6 +178,9 @@ public class SettingsFragment extends PreferenceFragment
setImageLoaderConcurrency(Integer.parseInt(sharedPreferences.getString(key, "5")));
} else if (Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE.equals(key)) {
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 {
debugLogToFile.setSummary("");
}
+
+ if (Util.getShouldUseId3Tags(getActivity())) showArtistPicture.setEnabled(true);
+ else showArtistPicture.setEnabled(false);
}
private static void setImageLoaderConcurrency(int concurrency) {
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java
index 498b9742..3bd56c61 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java
@@ -20,18 +20,7 @@ package org.moire.ultrasonic.util;
import android.app.Activity;
import android.os.Handler;
-import timber.log.Timber;
-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;
+import org.moire.ultrasonic.service.CommunicationErrorHandler;
/**
* @author Sindre Mehus
@@ -65,41 +54,13 @@ public abstract class BackgroundTask implements ProgressListener
protected void error(Throwable error)
{
- Timber.w(error);
- new ErrorDialog(activity, getErrorMessage(error), false);
+ CommunicationErrorHandler.Companion.handleError(error, activity);
}
- protected String getErrorMessage(Throwable error) {
- if (error instanceof IOException && !Util.isNetworkConnected(activity)) {
- return activity.getResources().getString(R.string.background_task_no_network);
- } 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();
- }
+ protected String getErrorMessage(Throwable error)
+ {
+ return CommunicationErrorHandler.Companion.getErrorMessage(error, activity);
+ }
@Override
public abstract void updateProgress(final String message);
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java
index 1f2b1b08..8e5b0925 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java
@@ -114,6 +114,7 @@ public final class Constants
public static final String PREFERENCES_KEY_DOWNLOAD_TRANSITION = "transitionToDownloadOnPlay";
public static final String PREFERENCES_KEY_INCREMENT_TIME = "incrementTime";
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_CHAT_REFRESH_INTERVAL = "chatRefreshInterval";
public static final String PREFERENCES_KEY_DIRECTORY_CACHE_TIME = "directoryCacheTime";
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java
index 74202148..d45c67b2 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java
@@ -13,23 +13,16 @@ public interface ImageLoader {
void stopImageLoader();
- void loadAvatarImage(
- View view,
- String username,
- boolean large,
- int size,
- boolean crossFade,
- boolean highQuality
- );
+ void loadAvatarImage(View view, String username, boolean large, int size, boolean crossFade,
+ boolean highQuality);
- void loadImage(
- View view,
- MusicDirectory.Entry entry,
- boolean large,
- int size,
- boolean crossFade,
- boolean highQuality
- );
+ void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size,
+ boolean crossFade, boolean highQuality);
+
+ void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size,
+ boolean crossFade, boolean highQuality, int defaultResourceId);
+
+ void cancel(String coverArt);
Bitmap getImageBitmap(String username, int size);
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java
index 3684c6b8..e6370f39 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java
@@ -38,6 +38,8 @@ import org.moire.ultrasonic.service.MusicServiceFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
+import java.util.SortedSet;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -165,25 +167,24 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
}
@Override
- public void loadImage(
- View view,
- MusicDirectory.Entry entry,
- boolean large,
- int size,
- boolean crossFade,
- boolean highQuality
- ) {
+ public void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size,
+ boolean crossFade, boolean highQuality) {
+ loadImage(view, entry, large, size, crossFade, highQuality, -1);
+ }
+
+ public void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size,
+ boolean crossFade, boolean highQuality, int defaultResourceId) {
view.invalidate();
if (entry == null) {
- setUnknownImage(view, large);
+ setUnknownImage(view, large, defaultResourceId);
return;
}
String coverArt = entry.getCoverArt();
if (TextUtils.isEmpty(coverArt)) {
- setUnknownImage(view, large);
+ setUnknownImage(view, large, defaultResourceId);
return;
}
@@ -198,11 +199,21 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
return;
}
- setUnknownImage(view, large);
+ setUnknownImage(view, large, defaultResourceId);
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) {
return String.format("%s:%d", coverArtId, size);
}
@@ -330,13 +341,18 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
}
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) {
setImageBitmap(view, null, largeUnknownImage, false);
} else {
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) {
- ((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 highQuality;
- Task(
- View view,
- MusicDirectory.Entry entry,
- int size,
- boolean saveToFile,
- boolean crossFade,
- boolean highQuality
- ) {
+ Task(View view, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean crossFade, boolean highQuality) {
this.view = view;
this.entry = entry;
this.username = null;
@@ -399,14 +408,7 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
handler = new Handler();
}
- Task(
- View view,
- String username,
- int size,
- boolean saveToFile,
- boolean crossFade,
- boolean highQuality
- ) {
+ Task(View view, String username, int size, boolean saveToFile, boolean crossFade, boolean highQuality) {
this.view = view;
this.entry = null;
this.username = username;
@@ -421,9 +423,9 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
try {
MusicService musicService = MusicServiceFactory.getMusicService(view.getContext());
final boolean isAvatar = this.username != null && this.entry == null;
- final Bitmap bitmap = this.entry != null
- ? musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null)
- : musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null);
+ final Bitmap bitmap = this.entry != null ?
+ musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) :
+ musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null);
if (bitmap == null) {
Timber.d("Found empty album art.");
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java
index 7a8c4281..75c96683 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java
@@ -54,6 +54,7 @@ import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.activity.MainActivity;
import org.moire.ultrasonic.activity.SettingsActivity;
+import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.*;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
@@ -1181,6 +1182,15 @@ public class Util
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)
{
SharedPreferences preferences = getPreferences(context);
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistListModel.kt
new file mode 100644
index 00000000..a12e6a43
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistListModel.kt
@@ -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 .
+
+ 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> = MutableLiveData()
+ private val artists: MutableLiveData> = MutableLiveData()
+
+ /**
+ * Retrieves the available Artists in a LiveData
+ */
+ fun getArtists(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData> {
+ backgroundLoadFromServer(refresh, swipe)
+ return artists
+ }
+
+ /**
+ * Retrieves the available Music Folders in a LiveData
+ */
+ fun getMusicFolders(): LiveData> {
+ 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 =
+ 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)
+ }
+ }
+ }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistRowAdapter.kt
new file mode 100644
index 00000000..28105012
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/ArtistRowAdapter.kt
@@ -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 .
+
+ 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,
+ 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(), SectionedAdapter {
+
+ /**
+ * Sets the data to be displayed in the RecyclerView
+ */
+ fun setData(data: List) {
+ 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
+ }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/SelectArtistActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/SelectArtistActivity.kt
new file mode 100644
index 00000000..87a4c4c8
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/SelectArtistActivity.kt
@@ -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 .
+
+ 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? = 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(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(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): 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
+ }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt
index fba4b146..42ac9b6b 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt
@@ -4,9 +4,11 @@ package org.moire.ultrasonic.di
import kotlin.math.abs
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.android.ext.koin.androidContext
+import org.koin.android.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.moire.ultrasonic.BuildConfig
+import org.moire.ultrasonic.activity.ArtistListModel
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
@@ -71,4 +73,6 @@ val musicServiceModule = module {
}
single { SubsonicImageLoader(androidContext(), get()) }
+
+ viewModel { ArtistListModel(get(), androidContext()) }
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt
index d7381164..cec72778 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt
@@ -7,6 +7,7 @@ import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist
fun APIArtist.toDomainEntity(): Artist = Artist(
id = this@toDomainEntity.id,
+ coverArt = this@toDomainEntity.coverArt,
name = this@toDomainEntity.name
)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt
new file mode 100644
index 00000000..39a72dcf
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt
@@ -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 .
+
+ 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
+ }
+ }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt
index 90f33a17..6d0d2b99 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt
@@ -26,18 +26,32 @@ class SubsonicImageLoaderProxy(
size: Int,
crossFade: 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 unknownImageId =
+ if (defaultResourceId == -1) R.drawable.unknown_album
+ else defaultResourceId
if (id != null &&
view != null &&
view is ImageView
) {
val request = ImageRequest.CoverArt(
- id,
- view,
- placeHolderDrawableRes = R.drawable.unknown_album,
- errorDrawableRes = R.drawable.unknown_album
+ id, view,
+ placeHolderDrawableRes = unknownImageId,
+ errorDrawableRes = unknownImageId
)
subsonicImageLoader.load(request)
}
@@ -56,8 +70,7 @@ class SubsonicImageLoaderProxy(
view is ImageView
) {
val request = ImageRequest.Avatar(
- username,
- view,
+ username, view,
placeHolderDrawableRes = R.drawable.ic_contact_picture,
errorDrawableRes = R.drawable.ic_contact_picture
)
diff --git a/ultrasonic/src/main/res/drawable/line.xml b/ultrasonic/src/main/res/drawable/line.xml
new file mode 100644
index 00000000..e3f2eaac
--- /dev/null
+++ b/ultrasonic/src/main/res/drawable/line.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/drawable/line_drawable.xml b/ultrasonic/src/main/res/drawable/line_drawable.xml
new file mode 100644
index 00000000..4f79f470
--- /dev/null
+++ b/ultrasonic/src/main/res/drawable/line_drawable.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/drawable/thumb.xml b/ultrasonic/src/main/res/drawable/thumb.xml
new file mode 100644
index 00000000..078a12c1
--- /dev/null
+++ b/ultrasonic/src/main/res/drawable/thumb.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/drawable/thumb_drawable.xml b/ultrasonic/src/main/res/drawable/thumb_drawable.xml
new file mode 100644
index 00000000..a93b8b83
--- /dev/null
+++ b/ultrasonic/src/main/res/drawable/thumb_drawable.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/layout/artist_list_item.xml b/ultrasonic/src/main/res/layout/artist_list_item.xml
index 2891750e..26a4d7a1 100644
--- a/ultrasonic/src/main/res/layout/artist_list_item.xml
+++ b/ultrasonic/src/main/res/layout/artist_list_item.xml
@@ -1,11 +1,56 @@
-
\ No newline at end of file
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/layout/select_artist.xml b/ultrasonic/src/main/res/layout/select_artist.xml
index 43a1a8ae..455f8531 100644
--- a/ultrasonic/src/main/res/layout/select_artist.xml
+++ b/ultrasonic/src/main/res/layout/select_artist.xml
@@ -2,21 +2,33 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ a:orientation="vertical">
+ a:id="@+id/select_artist_refresh"
+ a:layout_width="fill_parent"
+ a:layout_height="0dip"
+ a:layout_weight="1.0">
-
+
diff --git a/ultrasonic/src/main/res/layout/select_artist_header.xml b/ultrasonic/src/main/res/layout/select_artist_header.xml
index a42df88b..7a56076e 100644
--- a/ultrasonic/src/main/res/layout/select_artist_header.xml
+++ b/ultrasonic/src/main/res/layout/select_artist_header.xml
@@ -7,7 +7,10 @@
a:orientation="horizontal"
a:paddingBottom="2dip"
a:paddingLeft="6dp"
- a:paddingTop="2dip" >
+ a:paddingTop="2dip"
+ a:background="?android:attr/selectableItemBackground"
+ a:clickable="true"
+ a:focusable="true">
Feltételezi, hogy a legfelső szintű mappa az előadó neve.
Böngészés ID3 Tag használatával
ID3 Tag módszer használata a fájlredszer alapú mód helyett.
+ Előadó képének megjelenítése
+ Az előadó listában megjeleníti a képeket, amennyiben elérhetőek
Videó
Videólejátszó
Nézet frissítési gyakorisága
diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml
index 53b796ef..019cc0cd 100644
--- a/ultrasonic/src/main/res/values/strings.xml
+++ b/ultrasonic/src/main/res/values/strings.xml
@@ -316,6 +316,8 @@
Assume top-level folder is the name of the album artist
Browse Using ID3 Tags
Use ID3 tag methods instead of file system based methods
+ Show artist picture in artist list
+ Displays the artist picture in the artist list if available
Video
Video player
View Refresh
diff --git a/ultrasonic/src/main/res/values/styles.xml b/ultrasonic/src/main/res/values/styles.xml
index 9334649d..65012f6d 100644
--- a/ultrasonic/src/main/res/values/styles.xml
+++ b/ultrasonic/src/main/res/values/styles.xml
@@ -35,6 +35,11 @@
- center_vertical
+
+
diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml
index 0101ea77..2f451ad2 100644
--- a/ultrasonic/src/main/res/xml/settings.xml
+++ b/ultrasonic/src/main/res/xml/settings.xml
@@ -64,6 +64,11 @@
a:key="useId3Tags"
a:summary="@string/settings.use_id3_summary"
a:title="@string/settings.use_id3"/>
+