Migrated SelectArtistActivity to Kotlin
Updated SelectArtistActivity to use RecyclerView Changed old progress display to use SwipeRefreshLayout's spinner Added alphabetic side index Enabled RecyclerView's FastScroll
This commit is contained in:
parent
6aa29d54fe
commit
34a6413f10
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<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
|
||||
public abstract void updateProgress(final String message);
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
fun getArtists(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<Artist>> {
|
||||
backgroundLoadFromServer(refresh, swipe)
|
||||
return artists
|
||||
}
|
||||
|
||||
fun getMusicFolders(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<MusicFolder>> {
|
||||
backgroundLoadFromServer(refresh, swipe)
|
||||
return musicFolders
|
||||
}
|
||||
|
||||
fun refresh(swipe: SwipeRefreshLayout) {
|
||||
backgroundLoadFromServer(true, swipe)
|
||||
}
|
||||
|
||||
private fun backgroundLoadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) {
|
||||
swipe.isRefreshing = true
|
||||
viewModelScope.launch {
|
||||
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,141 @@
|
|||
/*
|
||||
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.LinearLayout
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.domain.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
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
fun setData(data: List<Artist>) {
|
||||
artistList = data.sortedBy { t -> t.name }
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun setFolderName(name: String) {
|
||||
folderName = name
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 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) }
|
||||
} 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
|
||||
}
|
||||
|
||||
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,211 @@
|
|||
/*
|
||||
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
|
||||
|
||||
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(refresh, refreshArtistListView!!)
|
||||
.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) }
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import org.koin.android.ext.koin.androidContext
|
|||
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 +72,5 @@ val musicServiceModule = module {
|
|||
}
|
||||
|
||||
single { SubsonicImageLoader(androidContext(), get()) }
|
||||
single { ArtistListModel(get(), get()) }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,16 @@
|
|||
<?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: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,37 @@
|
|||
<?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"
|
||||
a:layout_width="fill_parent"
|
||||
a:layout_height="wrap_content"
|
||||
a:textAppearance="?android:attr/textAppearanceMedium"
|
||||
a:gravity="center_vertical"
|
||||
a:paddingLeft="3dip"
|
||||
a:paddingRight="3dip"
|
||||
a:minHeight="50dip"/>
|
||||
<RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
||||
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="72dip"
|
||||
a:minHeight="50dip"
|
||||
a:paddingStart="16dip"
|
||||
a:paddingLeft="16dip"
|
||||
a:paddingEnd="32dip"
|
||||
a:paddingRight="32dip"
|
||||
a:text="A"
|
||||
a:textAppearance="?android:attr/textAppearanceLarge"
|
||||
a:textColor="@color/cyan" />
|
||||
|
||||
<TextView
|
||||
a:id="@+id/row_artist_name"
|
||||
a:layout_width="fill_parent"
|
||||
a:layout_height="wrap_content"
|
||||
a:layout_toEndOf="@+id/row_section"
|
||||
a:layout_toRightOf="@+id/row_section"
|
||||
a:drawablePadding="6dip"
|
||||
a:gravity="center_vertical"
|
||||
a:minHeight="50dip"
|
||||
a:paddingLeft="3dip"
|
||||
a:paddingRight="3dip"
|
||||
a:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
</RelativeLayout>
|
|
@ -2,21 +2,29 @@
|
|||
<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" />
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
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:fastScrollEnabled="true"
|
||||
app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
|
||||
app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
|
||||
app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
|
||||
app:fastScrollVerticalTrackDrawable="@drawable/line_drawable" />
|
||||
|
||||
</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"
|
||||
|
|
Loading…
Reference in New Issue