Merge branch 'nitehu-refactor/artist_list' into develop
This commit is contained in:
commit
b471b9cad0
|
@ -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 = [
|
||||
|
|
|
@ -77,6 +77,7 @@ dependencies {
|
|||
implementation other.koinAndroid
|
||||
implementation other.koinViewModel
|
||||
implementation other.okhttpLogging
|
||||
implementation other.fastScroll
|
||||
|
||||
kapt androidSupport.room
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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,40 +54,12 @@ public abstract class BackgroundTask<T> 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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()) }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,11 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:a="http://schemas.android.com/apk/res/android"
|
||||
a:id="@android:id/text1"
|
||||
a:drawablePadding="6dip"
|
||||
<RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
a:id="@+id/row_artist_layout"
|
||||
a:layout_height="wrap_content"
|
||||
a:layout_width="match_parent"
|
||||
a:background="?android:attr/selectableItemBackground"
|
||||
a:clickable="true"
|
||||
a:focusable="true">
|
||||
|
||||
<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:textAppearance="?android:attr/textAppearanceMedium"
|
||||
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:minHeight="50dip"/>
|
||||
a:layout_marginRight="12dp"
|
||||
a:layout_marginEnd="12dp"
|
||||
a:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
</RelativeLayout>
|
|
@ -2,21 +2,33 @@
|
|||
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
||||
a:layout_width="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" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/select_artist_refresh"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="0dip"
|
||||
android:layout_weight="1.0">
|
||||
a:id="@+id/select_artist_refresh"
|
||||
a:layout_width="fill_parent"
|
||||
a:layout_height="0dip"
|
||||
a:layout_weight="1.0">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/select_artist_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
<com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
||||
a:id="@+id/select_artist_list"
|
||||
a:layout_width="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>
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
||||
<ImageView
|
||||
a:layout_width="wrap_content"
|
||||
|
|
|
@ -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_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.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_player">Videólejátszó</string>
|
||||
<string name="settings.view_refresh">Nézet frissítési gyakorisága</string>
|
||||
|
|
|
@ -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_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.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_player">Video player</string>
|
||||
<string name="settings.view_refresh">View Refresh</string>
|
||||
|
|
|
@ -35,6 +35,11 @@
|
|||
<item name="android:gravity">center_vertical</item>
|
||||
</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_full" format="reference"/>
|
||||
<attr name="about" format="reference"/>
|
||||
|
|
|
@ -64,6 +64,11 @@
|
|||
a:key="useId3Tags"
|
||||
a:summary="@string/settings.use_id3_summary"
|
||||
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
|
||||
a:defaultValue="true"
|
||||
a:key="mediaButtons"
|
||||
|
|
Loading…
Reference in New Issue