diff options
215 files changed, 5355 insertions, 2150 deletions
diff --git a/ServerProxy b/ServerProxy -Subproject 6ad30486eda6f517b8d306d90a213972e52f471 +Subproject 7c35351b74ba87750acb37dc57f402d0e3bc437 diff --git a/app/build.gradle b/app/build.gradle index 12cd09fa..bbbbee04 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,18 +1,27 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 22 - buildToolsVersion "22.0.0" + compileSdkVersion 23 + buildToolsVersion "23.0.3" + useLibrary 'org.apache.http.legacy' defaultConfig { applicationId "github.daneren2005.dsub" minSdkVersion 14 targetSdkVersion 22 + versionCode 185 + versionName '5.2.2' + setProperty("archivesBaseName", "DSub $versionName") } buildTypes { release { minifyEnabled true proguardFiles 'proguard.cfg' + zipAlignEnabled true + } + fix { + minifyEnabled true + proguardFiles 'proguard.cfg' } } @@ -34,16 +43,16 @@ android { dependencies { compile project(':Server Proxy') compile fileTree(include: ['*.jar'], dir: 'libs') - compile 'com.android.support:support-v4:22.2.+' - compile 'com.android.support:appcompat-v7:22.2.+' - compile 'com.android.support:mediarouter-v7:22.2.+' - compile 'com.android.support:recyclerview-v7:22.2.+' - compile 'com.android.support:design:22.2.+' + compile 'com.android.support:support-v4:23.4.+' + compile 'com.android.support:appcompat-v7:23.4.+' + compile 'com.android.support:mediarouter-v7:23.4.+' + compile 'com.android.support:recyclerview-v7:23.4.+' + compile 'com.android.support:design:23.4.+' compile 'com.google.android.gms:play-services-cast:8.1.0' compile 'com.sothree.slidinguppanel:library:3.0.0' compile 'de.hdodenhof:circleimageview:1.2.1' - compile group: 'org.fourthline.cling', name: 'cling-core', version:'2.0.1' - compile group: 'org.fourthline.cling', name: 'cling-support', version:'2.0.1' + compile group: 'org.fourthline.cling', name: 'cling-core', version:'2.1.0' + compile group: 'org.fourthline.cling', name: 'cling-support', version:'2.1.0' compile group: 'org.eclipse.jetty', name: 'jetty-server', version:'8.1.16.v20140903' compile group: 'org.eclipse.jetty', name: 'jetty-servlet', version:'8.1.16.v20140903' compile group: 'org.eclipse.jetty', name: 'jetty-client', version:'8.1.16.v20140903' diff --git a/app/libs/CWAC-AdapterWrapper.jar b/app/libs/CWAC-AdapterWrapper.jar Binary files differdeleted file mode 100644 index 692fe4d3..00000000 --- a/app/libs/CWAC-AdapterWrapper.jar +++ /dev/null diff --git a/app/libs/CWAC-EndlessAdapter.jar b/app/libs/CWAC-EndlessAdapter.jar Binary files differdeleted file mode 100644 index ec20d936..00000000 --- a/app/libs/CWAC-EndlessAdapter.jar +++ /dev/null diff --git a/app/proguard.cfg b/app/proguard.cfg index 8e1a0a0a..a18ae91a 100644 --- a/app/proguard.cfg +++ b/app/proguard.cfg @@ -44,6 +44,7 @@ } -keep class android.support.v7.app.MediaRouteButton { *; } +-keep class android.support.v7.widget.SearchView { *; } -dontwarn android.support.** diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index be941740..999fb150 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,10 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="github.daneren2005.dsub" - android:installLocation="internalOnly" - android:versionCode="167" - android:versionName="5.1.2"> - + android:installLocation="internalOnly"> + <instrumentation android:name="android.test.InstrumentationTestRunner" android:targetPackage="github.daneren2005.dsub" android:label="Tests" /> @@ -30,8 +28,7 @@ <uses-feature android:name="android.hardware.bluetooth" android:required="false" /> <uses-feature android:name="android.hardware.microphone" android:required="false" /> <uses-feature android:name="android.hardware.wifi" android:required="false" /> - - <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="22"/> + <uses-feature android:name="android.software.leanback" android:required="false"/> <supports-screens android:anyDensity="true" android:xlargeScreens="true" android:largeScreens="true" android:normalScreens="true" android:smallScreens="true"/> @@ -44,7 +41,7 @@ <activity android:name="github.daneren2005.dsub.activity.SubsonicFragmentActivity" android:configChanges="orientation|keyboardHidden" - android:launchMode="standard"> + android:launchMode="singleTask"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> diff --git a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java index a58f169b..ab846ba0 100644 --- a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java +++ b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicActivity.java @@ -33,7 +33,6 @@ import android.os.Environment; import android.os.Handler; import android.support.design.widget.NavigationView; import android.support.v7.app.ActionBarDrawerToggle; -import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.widget.DrawerLayout; @@ -67,7 +66,9 @@ import java.util.List; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.ServerInfo; +import github.daneren2005.dsub.fragments.AdminFragment; import github.daneren2005.dsub.fragments.SubsonicFragment; +import github.daneren2005.dsub.fragments.UserFragment; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.HeadphoneListenerService; import github.daneren2005.dsub.service.MusicService; @@ -189,17 +190,16 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte } protected void createCustomActionBarView() { - View customActionbar = getLayoutInflater().inflate(R.layout.actionbar_spinner, null); - actionBarSpinner = (Spinner)customActionbar.findViewById(R.id.spinner); - if(Util.getThemeRes(this) == R.style.Theme_DSub_Light_No_Actionbar) { - actionBarSpinner.setBackgroundResource(R.drawable.abc_spinner_mtrl_am_alpha); + actionBarSpinner = (Spinner) getLayoutInflater().inflate(R.layout.actionbar_spinner, null); + if((this instanceof SubsonicFragmentActivity || this instanceof SettingsActivity) && (Util.getPreferences(this).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true) || Util.getThemeRes(this) != R.style.Theme_DSub_Light_No_Color)) { + actionBarSpinner.setBackgroundDrawable(DrawableTint.getTintedDrawableFromColor(this, R.drawable.abc_spinner_mtrl_am_alpha, android.R.color.white)); } spinnerAdapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item); spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); actionBarSpinner.setOnItemSelectedListener(this); actionBarSpinner.setAdapter(spinnerAdapter); - getSupportActionBar().setCustomView(customActionbar); + getSupportActionBar().setCustomView(actionBarSpinner); } @Override @@ -538,7 +538,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { int top = spinnerAdapter.getCount() - 1; if(position < top) { - for(int i = top; i > position; i--) { + for(int i = top; i > position && i >= 0; i--) { removeCurrent(); } } @@ -752,6 +752,11 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte recreateSpinner(); } public void removeCurrent() { + // Don't try to remove current if there is no backstack to remove from + if(backStack.isEmpty()) { + return; + } + if(currentFragment != null) { currentFragment.setPrimaryFragment(false); } @@ -819,7 +824,11 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte removeCurrent(); } - currentFragment.invalidate(); + if(currentFragment instanceof UserFragment || currentFragment instanceof AdminFragment) { + restart(false); + } else { + currentFragment.invalidate(); + } populateTabs(); } @@ -853,6 +862,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte spinnerAdapter.notifyDataSetChanged(); actionBarSpinner.setSelection(spinnerAdapter.getCount() - 1); if(!isTv()) { + getSupportActionBar().setDisplayShowTitleEnabled(false); getSupportActionBar().setDisplayShowCustomEnabled(true); } @@ -862,6 +872,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte getSupportActionBar().setDisplayHomeAsUpEnabled(true); } } else if(!isTv()) { + getSupportActionBar().setDisplayShowTitleEnabled(true); getSupportActionBar().setTitle(currentFragment.getTitle()); getSupportActionBar().setDisplayShowCustomEnabled(false); drawerToggle.setDrawerIndicatorEnabled(true); @@ -869,10 +880,19 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte } protected void restart() { - Intent intent = new Intent(this, ((Object) this).getClass()); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + restart(true); + } + protected void restart(boolean resumePosition) { + Intent intent = new Intent(this, this.getClass()); intent.putExtras(getIntent()); - intent.putExtra(Constants.FRAGMENT_POSITION, lastSelectedPosition); + if(resumePosition) { + intent.putExtra(Constants.FRAGMENT_POSITION, lastSelectedPosition); + } else { + String fragmentType = Util.openToTab(this); + intent.putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, fragmentType); + intent.putExtra(Constants.FRAGMENT_POSITION, getDrawerItemId(fragmentType)); + } + finish(); Util.startActivityWithoutTransition(this, intent); } @@ -1044,6 +1064,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte UserUtil.seedCurrentUser(this); this.updateDrawerHeader(); + drawer.closeDrawers(); } private void showOfflineSyncDialog(final int scrobbleCount, final int starsCount) { @@ -1125,6 +1146,10 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte } public int getDrawerItemId(String fragmentType) { + if(fragmentType == null) { + return R.id.drawer_home; + } + switch(fragmentType) { case "Home": return R.id.drawer_home; diff --git a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java index 87a67e80..ca6dd168 100644 --- a/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java +++ b/app/src/main/java/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java @@ -93,6 +93,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo private SlidingUpPanelLayout slideUpPanel; private SlidingUpPanelLayout.PanelSlideListener panelSlideListener; + private boolean isPanelClosing = false; private NowPlayingFragment nowPlayingFragment; private SubsonicFragment secondaryFragment; private Toolbar mainToolbar; @@ -106,6 +107,10 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo private long lastBackPressTime = 0; private DownloadFile currentPlaying; private PlayerState currentState; + private ImageButton previousButton; + private ImageButton nextButton; + private ImageButton rewindButton; + private ImageButton fastforwardButton; @Override public void onCreate(Bundle savedInstanceState) { @@ -153,8 +158,19 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo if(item != null) { item.setChecked(true); } + } else { + lastSelectedPosition = getDrawerItemId(fragmentType); } + currentFragment = getNewFragment(fragmentType); + if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_ID)) { + Bundle currentArguments = currentFragment.getArguments(); + if(currentArguments == null) { + currentArguments = new Bundle(); + } + currentArguments.putString(Constants.INTENT_EXTRA_NAME_ID, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID)); + currentFragment.setArguments(currentArguments); + } currentFragment.setPrimaryFragment(true); getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, currentFragment, currentFragment.getSupportTag() + "").commit(); @@ -187,24 +203,34 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo @Override public void onPanelCollapsed(View panel) { - bottomBar.setVisibility(View.VISIBLE); - nowPlayingToolbar.setVisibility(View.GONE); - nowPlayingFragment.setPrimaryFragment(false); - setSupportActionBar(mainToolbar); - recreateSpinner(); + isPanelClosing = false; + if(bottomBar.getVisibility() == View.GONE) { + bottomBar.setVisibility(View.VISIBLE); + nowPlayingToolbar.setVisibility(View.GONE); + nowPlayingFragment.setPrimaryFragment(false); + setSupportActionBar(mainToolbar); + recreateSpinner(); + } } @Override public void onPanelExpanded(View panel) { + isPanelClosing = false; currentFragment.stopActionMode(); // Disable custom view before switching getSupportActionBar().setDisplayShowCustomEnabled(false); + getSupportActionBar().setDisplayShowTitleEnabled(true); bottomBar.setVisibility(View.GONE); nowPlayingToolbar.setVisibility(View.VISIBLE); setSupportActionBar(nowPlayingToolbar); - nowPlayingFragment.setPrimaryFragment(true); + + if(secondaryFragment == null) { + nowPlayingFragment.setPrimaryFragment(true); + } else { + secondaryFragment.setPrimaryFragment(true); + } drawerToggle.setDrawerIndicatorEnabled(false); getSupportActionBar().setDisplayHomeAsUpEnabled(true); @@ -230,6 +256,8 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo openNowPlaying(); } }, 200); + + getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD); } bottomBar = findViewById(R.id.bottom_bar); @@ -248,7 +276,25 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo trans.commit(); } - ImageButton previousButton = (ImageButton) findViewById(R.id.download_previous); + rewindButton = (ImageButton) findViewById(R.id.download_rewind); + rewindButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask<Void>(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if (getDownloadService() == null) { + return null; + } + + getDownloadService().rewind(); + return null; + } + }.execute(); + } + }); + + previousButton = (ImageButton) findViewById(R.id.download_previous); previousButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -286,7 +332,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo } }); - ImageButton nextButton = (ImageButton) findViewById(R.id.download_next); + nextButton = (ImageButton) findViewById(R.id.download_next); nextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -303,6 +349,24 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo }.execute(); } }); + + fastforwardButton = (ImageButton) findViewById(R.id.download_fastforward); + fastforwardButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask<Void>(SubsonicFragmentActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if (getDownloadService() == null) { + return null; + } + + getDownloadService().fastForward(); + return null; + } + }.execute(); + } + }); } @Override @@ -330,17 +394,16 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo super.onNewIntent(intent); if(currentFragment != null && intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY) != null) { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + closeNowPlaying(); + } + if(currentFragment instanceof SearchFragment) { String query = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY); boolean autoplay = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false); - boolean requestsearch = intent.getBooleanExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, false); if (query != null) { ((SearchFragment)currentFragment).search(query, autoplay); - } else { - if (requestsearch) { - onSearchRequested(); - } } getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_QUERY); } else { @@ -349,7 +412,14 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo SearchFragment fragment = new SearchFragment(); replaceFragment(fragment, fragment.getSupportTag()); } + } else if(intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, false)) { + if(slideUpPanel.getPanelState() != SlidingUpPanelLayout.PanelState.EXPANDED) { + openNowPlaying(); + } } else { + if(slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + closeNowPlaying(); + } setIntent(intent); } if(drawer != null) { @@ -402,6 +472,9 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo public void onSaveInstanceState(Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); savedInstanceState.putString(Constants.MAIN_NOW_PLAYING, nowPlayingFragment.getTag()); + if(secondaryFragment != null) { + savedInstanceState.putString(Constants.MAIN_NOW_PLAYING_SECONDARY, secondaryFragment.getTag()); + } savedInstanceState.putInt(Constants.MAIN_SLIDE_PANEL_STATE, slideUpPanel.getPanelState().hashCode()); } @Override @@ -411,6 +484,19 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo String id = savedInstanceState.getString(Constants.MAIN_NOW_PLAYING); FragmentManager fm = getSupportFragmentManager(); nowPlayingFragment = (NowPlayingFragment) fm.findFragmentByTag(id); + + String secondaryId = savedInstanceState.getString(Constants.MAIN_NOW_PLAYING_SECONDARY); + if(secondaryId != null) { + secondaryFragment = (SubsonicFragment) fm.findFragmentByTag(secondaryId); + + nowPlayingFragment.setPrimaryFragment(false); + secondaryFragment.setPrimaryFragment(true); + + FragmentTransaction trans = getSupportFragmentManager().beginTransaction(); + trans.hide(nowPlayingFragment); + trans.commit(); + } + if(drawerToggle != null && backStack.size() > 0) { drawerToggle.setDrawerIndicatorEnabled(false); } @@ -472,7 +558,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo @Override public void replaceFragment(SubsonicFragment fragment, int tag, boolean replaceCurrent) { - if(slideUpPanel != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED) { + if(slideUpPanel != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED && !isPanelClosing) { secondaryFragment = fragment; nowPlayingFragment.setPrimaryFragment(false); secondaryFragment.setPrimaryFragment(true); @@ -562,6 +648,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo @Override public void closeNowPlaying() { slideUpPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + isPanelClosing = true; } private SubsonicFragment getNewFragment(String fragmentType) { @@ -851,13 +938,26 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo } getImageLoader().loadImage(coverArtView, song, false, height, false); } + + if(currentPlaying != null && currentPlaying.getSong() != null && (currentPlaying.getSong().isPodcast() || currentPlaying.getSong().isAudioBook())) { + previousButton.setVisibility(View.GONE); + nextButton.setVisibility(View.GONE); + + rewindButton.setVisibility(View.VISIBLE); + fastforwardButton.setVisibility(View.VISIBLE); + } else { + previousButton.setVisibility(View.VISIBLE); + nextButton.setVisibility(View.VISIBLE); + + rewindButton.setVisibility(View.GONE); + fastforwardButton.setVisibility(View.GONE); + } } @Override public void onSongsChanged(List<DownloadFile> songs, DownloadFile currentPlaying, int currentPlayingIndex) { if(this.currentPlaying != currentPlaying || this.currentPlaying == null) { onSongChanged(currentPlaying, currentPlayingIndex); - onMetadataUpdate(currentPlaying != null ? currentPlaying.getSong() : null, DownloadService.METADATA_UPDATED_ALL); } } @@ -875,7 +975,21 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo } @Override - public void onMetadataUpdate(MusicDirectory.Entry entry, int fieldChange) { + public void onMetadataUpdate(MusicDirectory.Entry song, int fieldChange) { + if(song != null && coverArtView != null && fieldChange == DownloadService.METADATA_UPDATED_COVER_ART) { + int height = coverArtView.getHeight(); + if (height <= 0) { + int[] attrs = new int[]{R.attr.actionBarSize}; + TypedArray typedArray = this.obtainStyledAttributes(attrs); + height = typedArray.getDimensionPixelSize(0, 0); + typedArray.recycle(); + } + getImageLoader().loadImage(coverArtView, song, false, height, false); + // We need to update it immediately since it won't update if updater is not running for it + if(nowPlayingFragment != null && slideUpPanel.getPanelState() == SlidingUpPanelLayout.PanelState.COLLAPSED) { + nowPlayingFragment.onMetadataUpdate(song, fieldChange); + } + } } } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java index 5feaa482..5ed79e82 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/ArtistAdapter.java @@ -23,27 +23,32 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import java.io.Serializable; import java.util.List; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.MusicFolder; import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.ArtistView; import github.daneren2005.dsub.view.FastScroller; +import github.daneren2005.dsub.view.SongView; import github.daneren2005.dsub.view.UpdateView; -public class ArtistAdapter extends SectionAdapter<Artist> implements FastScroller.BubbleTextGetter { +public class ArtistAdapter extends SectionAdapter<Serializable> implements FastScroller.BubbleTextGetter { + public static int VIEW_TYPE_SONG = 3; public static int VIEW_TYPE_ARTIST = 4; private List<MusicFolder> musicFolders; private OnMusicFolderChanged onMusicFolderChanged; - public ArtistAdapter(Context context, List<Artist> artists, OnItemClickedListener listener) { + public ArtistAdapter(Context context, List<Serializable> artists, OnItemClickedListener listener) { this(context, artists, null, listener, null); } - public ArtistAdapter(Context context, List<Artist> artists, List<MusicFolder> musicFolders, OnItemClickedListener onItemClickedListener, OnMusicFolderChanged onMusicFolderChanged) { + public ArtistAdapter(Context context, List<Serializable> artists, List<MusicFolder> musicFolders, OnItemClickedListener onItemClickedListener, OnMusicFolderChanged onMusicFolderChanged) { super(context, artists); this.musicFolders = musicFolders; this.onItemClickedListener = onItemClickedListener; @@ -92,7 +97,7 @@ public class ArtistAdapter extends SectionAdapter<Artist> implements FastScrolle return new UpdateView.UpdateViewHolder(header, false); } @Override - public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header) { + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, int sectionIndex) { TextView folderName = (TextView) holder.getView().findViewById(R.id.select_artist_folder_2); String musicFolderId = Util.getSelectedMusicFolderId(context); @@ -110,17 +115,35 @@ public class ArtistAdapter extends SectionAdapter<Artist> implements FastScrolle @Override public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { - return new UpdateView.UpdateViewHolder(new ArtistView(context)); + UpdateView updateView = null; + if(viewType == VIEW_TYPE_ARTIST) { + updateView = new ArtistView(context); + } else if(viewType == VIEW_TYPE_SONG) { + updateView = new SongView(context); + } + + return new UpdateView.UpdateViewHolder(updateView); } @Override - public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Artist item, int viewType) { - holder.getUpdateView().setObject(item); + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Serializable item, int viewType) { + UpdateView view = holder.getUpdateView(); + if(viewType == VIEW_TYPE_ARTIST) { + view.setObject(item); + } else if(viewType == VIEW_TYPE_SONG) { + SongView songView = (SongView) view; + Entry entry = (Entry) item; + songView.setObject(entry, checkable && !entry.isVideo()); + } } @Override - public int getItemViewType(Artist item) { - return VIEW_TYPE_ARTIST; + public int getItemViewType(Serializable item) { + if(item instanceof Artist) { + return VIEW_TYPE_ARTIST; + } else { + return VIEW_TYPE_SONG; + } } @Override diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java index d4613994..5b3dc289 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/DownloadFileAdapter.java @@ -16,13 +16,18 @@ package github.daneren2005.dsub.adapter; import android.content.Context; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import java.util.List; +import github.daneren2005.dsub.R; import github.daneren2005.dsub.service.DownloadFile; +import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.FastScroller; import github.daneren2005.dsub.view.SongView; import github.daneren2005.dsub.view.UpdateView; @@ -33,6 +38,7 @@ public class DownloadFileAdapter extends SectionAdapter<DownloadFile> implements public DownloadFileAdapter(Context context, List<DownloadFile> entries, OnItemClickedListener onItemClickedListener) { super(context, entries); this.onItemClickedListener = onItemClickedListener; + this.checkable = true; } @Override @@ -43,7 +49,7 @@ public class DownloadFileAdapter extends SectionAdapter<DownloadFile> implements @Override public void onBindViewHolder(UpdateView.UpdateViewHolder holder, DownloadFile item, int viewType) { SongView songView = (SongView) holder.getUpdateView(); - songView.setObject(item.getSong(), false); + songView.setObject(item.getSong(), Util.isBatchMode(context)); songView.setDownloadFile(item); } @@ -56,4 +62,21 @@ public class DownloadFileAdapter extends SectionAdapter<DownloadFile> implements public String getTextToShowInBubble(int position) { return null; } + + @Override + public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) { + if(Util.isOffline(context)) { + menuInflater.inflate(R.menu.multiselect_nowplaying_offline, menu); + } else { + menuInflater.inflate(R.menu.multiselect_nowplaying, menu); + } + + if(!selected.isEmpty()) { + MenuItem starItem = menu.findItem(R.id.menu_star); + if(starItem != null) { + boolean isStarred = selected.get(0).getSong().isStarred(); + starItem.setTitle(isStarred ? R.string.common_unstar : R.string.common_star); + } + } + } } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java index 38931482..e75a5104 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java @@ -16,13 +16,12 @@ package github.daneren2005.dsub.adapter; import android.content.Context; -import android.util.Log; import android.view.Menu; import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import java.util.ArrayList; import java.util.List; import github.daneren2005.dsub.R; @@ -45,8 +44,8 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { private ImageLoader imageLoader; private boolean largeAlbums; private boolean showArtist = false; + private boolean showAlbum = false; private boolean removeFromPlaylist = false; - private boolean removeStarred = true; private View header; public EntryGridAdapter(Context context, List<Entry> entries, ImageLoader imageLoader, boolean largeCell) { @@ -89,6 +88,7 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { albumView.setObject(entry, imageLoader); } else if(viewType == VIEW_TYPE_SONG) { SongView songView = (SongView) view; + songView.setShowAlbum(showAlbum); songView.setObject(entry, checkable && !entry.isVideo()); } } @@ -96,7 +96,7 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { public UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { return new UpdateViewHolder(header, false); } - public void onBindHeaderHolder(UpdateViewHolder holder, String header) { + public void onBindHeaderHolder(UpdateViewHolder holder, String header, int sectionIndex) { } @@ -125,6 +125,10 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { this.showArtist = showArtist; } + public void setShowAlbum(boolean showAlbum) { + this.showAlbum = showAlbum; + } + public void removeAt(int index) { sections.get(0).remove(index); if(header != null) { @@ -136,9 +140,6 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { public void setRemoveFromPlaylist(boolean removeFromPlaylist) { this.removeFromPlaylist = removeFromPlaylist; } - public void setRemoveStarred(boolean removeStarred) { - this.removeStarred = removeStarred; - } @Override public void onCreateActionModeMenu(Menu menu, MenuInflater menuInflater) { @@ -151,8 +152,13 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { if(!removeFromPlaylist) { menu.removeItem(R.id.menu_remove_playlist); } - if(removeStarred) { - menu.removeItem(R.id.menu_unstar); + + if(!selected.isEmpty()) { + MenuItem starItem = menu.findItem(R.id.menu_star); + if(starItem != null) { + boolean isStarred = selected.get(0).isStarred(); + starItem.setTitle(isStarred ? R.string.common_unstar : R.string.common_star); + } } } } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java index 2c4f75dc..6c1c14da 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/EntryInfiniteGridAdapter.java @@ -26,6 +26,7 @@ import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.ServerInfo; +import github.daneren2005.dsub.fragments.MainFragment; import github.daneren2005.dsub.service.MusicService; import github.daneren2005.dsub.service.MusicServiceFactory; import github.daneren2005.dsub.util.ImageLoader; @@ -88,6 +89,10 @@ public class EntryInfiniteGridAdapter extends EntryGridAdapter { this.type = type; this.extra = extra; this.size = size; + + if(super.getItemCount() < size) { + allLoaded = true; + } } public void loadMore() { @@ -110,7 +115,7 @@ public class EntryInfiniteGridAdapter extends EntryGridAdapter { appendCachedData(newData); loading = false; - if(newData.isEmpty()) { + if(newData.size() < size) { allLoaded = true; notifyDataSetChanged(); } @@ -126,6 +131,8 @@ public class EntryInfiniteGridAdapter extends EntryGridAdapter { result = service.getAlbumList(type, extra, size, offset, false, context, null); } else if("genres".equals(type) || "genres-songs".equals(type)) { result = service.getSongsByGenre(extra, size, offset, context, null); + }else if(type.indexOf(MainFragment.SONGS_LIST_PREFIX) != -1) { + result = service.getSongList(type, size, offset, context, null); } else { result = service.getAlbumList(type, size, offset, false, context, null); } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/ExpandableSectionAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/ExpandableSectionAdapter.java new file mode 100644 index 00000000..6ebb34e3 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/adapter/ExpandableSectionAdapter.java @@ -0,0 +1,150 @@ +/* + 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 2015 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.adapter; + +import android.content.Context; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.util.DrawableTint; +import github.daneren2005.dsub.view.BasicHeaderView; +import github.daneren2005.dsub.view.UpdateView; + +public abstract class ExpandableSectionAdapter<T> extends SectionAdapter<T> { + private static final String TAG = ExpandableSectionAdapter.class.getSimpleName(); + private static final int DEFAULT_VISIBLE = 4; + private static final int EXPAND_TOGGLE = R.attr.select_server; + private static final int COLLAPSE_TOGGLE = R.attr.select_tabs; + + protected List<Integer> sectionsDefaultVisible; + protected List<List<T>> sectionsExtras; + protected int expandToggleRes; + protected int collapseToggleRes; + + protected ExpandableSectionAdapter() {} + public ExpandableSectionAdapter(Context context, List<T> section) { + List<List<T>> sections = new ArrayList<>(); + sections.add(section); + + init(context, Arrays.asList("Section"), sections, Arrays.asList((Integer) null)); + } + public ExpandableSectionAdapter(Context context, List<String> headers, List<List<T>> sections) { + init(context, headers, sections, null); + } + public ExpandableSectionAdapter(Context context, List<String> headers, List<List<T>> sections, List<Integer> sectionsDefaultVisible) { + init(context, headers, sections, sectionsDefaultVisible); + } + protected void init(Context context, List<String> headers, List<List<T>> fullSections, List<Integer> sectionsDefaultVisible) { + this.context = context; + this.headers = headers; + this.sectionsDefaultVisible = sectionsDefaultVisible; + if(sectionsDefaultVisible == null) { + sectionsDefaultVisible = new ArrayList<>(fullSections.size()); + for(int i = 0; i < fullSections.size(); i++) { + sectionsDefaultVisible.add(DEFAULT_VISIBLE); + } + } + + this.sections = new ArrayList<>(); + this.sectionsExtras = new ArrayList<>(); + int i = 0; + for(List<T> fullSection: fullSections) { + List<T> visibleSection = new ArrayList<>(); + + Integer defaultVisible = sectionsDefaultVisible.get(i); + if(defaultVisible == null || defaultVisible >= fullSection.size()) { + visibleSection.addAll(fullSection); + this.sectionsExtras.add(null); + } else { + visibleSection.addAll(fullSection.subList(0, defaultVisible)); + this.sectionsExtras.add(fullSection.subList(defaultVisible, fullSection.size())); + } + this.sections.add(visibleSection); + + i++; + } + + expandToggleRes = DrawableTint.getDrawableRes(context, EXPAND_TOGGLE); + collapseToggleRes = DrawableTint.getDrawableRes(context, COLLAPSE_TOGGLE); + } + + @Override + public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { + return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.expandable_header)); + } + + @Override + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, final int sectionIndex) { + UpdateView view = holder.getUpdateView(); + ImageView toggleSelectionView = (ImageView) view.findViewById(R.id.item_select); + + List<T> visibleSelection = sections.get(sectionIndex); + List<T> sectionExtras = sectionsExtras.get(sectionIndex); + + if(sectionExtras != null && !sectionExtras.isEmpty()) { + toggleSelectionView.setVisibility(View.VISIBLE); + toggleSelectionView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + List<T> visibleSelection = sections.get(sectionIndex); + List<T> sectionExtras = sectionsExtras.get(sectionIndex); + + // Update icon + int selectToggleAttr; + if (!visibleSelection.contains(sectionExtras.get(0))) { + selectToggleAttr = COLLAPSE_TOGGLE; + + // Update how many are displayed + int lastIndex = getItemPosition(visibleSelection.get(visibleSelection.size() - 1)); + visibleSelection.addAll(sectionExtras); + notifyItemRangeInserted(lastIndex, sectionExtras.size()); + } else { + selectToggleAttr = EXPAND_TOGGLE; + + // Update how many are displayed + visibleSelection.removeAll(sectionExtras); + int lastIndex = getItemPosition(visibleSelection.get(visibleSelection.size() - 1)); + notifyItemRangeRemoved(lastIndex, sectionExtras.size()); + } + + ((ImageView) v).setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); + } + }); + + int selectToggleAttr; + if (!visibleSelection.contains(sectionExtras.get(0))) { + selectToggleAttr = EXPAND_TOGGLE; + } else { + selectToggleAttr = COLLAPSE_TOGGLE; + } + + toggleSelectionView.setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); + } else { + toggleSelectionView.setVisibility(View.GONE); + } + + if(view != null) { + view.setObject(header); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java index 8f1f1c38..dd70aa99 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java @@ -77,7 +77,7 @@ public class MainAdapter extends SectionAdapter<Integer> { return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.album_list_header)); } @Override - public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header) { + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header, int sectionIndex) { UpdateView view = holder.getUpdateView(); CheckBox checkBox = (CheckBox) view.findViewById(R.id.item_checkbox); @@ -100,6 +100,9 @@ public class MainAdapter extends SectionAdapter<Integer> { } else if("videos".equals(header)) { display = context.getResources().getString(R.string.main_videos); checkBox.setVisibility(View.GONE); + } else if("songs".equals(header)) { + display = context.getResources().getString(R.string.search_songs); + checkBox.setVisibility(View.GONE); } else { display = header; checkBox.setVisibility(View.GONE); diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java index a95abeda..f843a722 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java @@ -34,21 +34,17 @@ import github.daneren2005.dsub.view.SongView; import github.daneren2005.dsub.view.UpdateView; import java.io.Serializable; +import java.util.Arrays; import java.util.List; -public class PodcastChannelAdapter extends SectionAdapter<Serializable> implements FastScroller.BubbleTextGetter { +public class PodcastChannelAdapter extends ExpandableSectionAdapter<Serializable> implements FastScroller.BubbleTextGetter { public static final int VIEW_TYPE_PODCAST_LEGACY = 1; public static final int VIEW_TYPE_PODCAST_LINE = 2; public static final int VIEW_TYPE_PODCAST_CELL = 3; public static final int VIEW_TYPE_PODCAST_EPISODE = 4; - public static final String EPISODE_HEADER = "episodes"; - public static final String CHANNEL_HEADER = "channels"; - private ImageLoader imageLoader; private boolean largeCell; - private int selectToggleAttr = R.attr.select_server; - private List<Serializable> extraEpisodes; public PodcastChannelAdapter(Context context, List<Serializable> podcasts, ImageLoader imageLoader, OnItemClickedListener listener, boolean largeCell) { super(context, podcasts); @@ -56,9 +52,8 @@ public class PodcastChannelAdapter extends SectionAdapter<Serializable> implemen this.onItemClickedListener = listener; this.largeCell = largeCell; } - public PodcastChannelAdapter(Context context, List<String> headers, List<List<Serializable>> sections, List<Serializable> extraEpisodes, ImageLoader imageLoader, OnItemClickedListener listener, boolean largeCell) { - super(context, headers, sections); - this.extraEpisodes = extraEpisodes; + public PodcastChannelAdapter(Context context, List<String> headers, List<List<Serializable>> sections, ImageLoader imageLoader, OnItemClickedListener listener, boolean largeCell) { + super(context, headers, sections, Arrays.asList(3, null)); this.imageLoader = imageLoader; this.onItemClickedListener = listener; this.largeCell = largeCell; @@ -126,58 +121,6 @@ public class PodcastChannelAdapter extends SectionAdapter<Serializable> implemen } menu.removeItem(R.id.menu_remove_playlist); - menu.removeItem(R.id.menu_unstar); - } - - @Override - public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { - return new UpdateView.UpdateViewHolder(new BasicHeaderView(context, R.layout.newest_episode_header)); - } - - @Override - public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String header) { - UpdateView view = holder.getUpdateView(); - ImageView toggleSelectionView = (ImageView) view.findViewById(R.id.item_select); - - String display; - if(EPISODE_HEADER.equals(header)) { - display = context.getResources().getString(R.string.main_albums_newest); - - if(extraEpisodes != null && !extraEpisodes.isEmpty()) { - toggleSelectionView.setVisibility(View.VISIBLE); - toggleSelectionView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // Update icon - if (selectToggleAttr == R.attr.select_server) { - selectToggleAttr = R.attr.select_tabs; - - // Update how many are displayed - sections.get(0).addAll(extraEpisodes); - notifyItemRangeInserted(4, extraEpisodes.size()); - } else { - selectToggleAttr = R.attr.select_server; - - // Update how many are displayed - sections.get(0).removeAll(extraEpisodes); - notifyItemRangeRemoved(4, extraEpisodes.size()); - } - - ((ImageView) v).setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); - - } - }); - toggleSelectionView.setImageResource(DrawableTint.getDrawableRes(context, selectToggleAttr)); - } else { - toggleSelectionView.setVisibility(View.GONE); - } - } else { - display = context.getResources().getString(R.string.select_podcasts_channels); - toggleSelectionView.setVisibility(View.GONE); - } - - if(view != null) { - view.setObject(display); - } + menu.removeItem(R.id.menu_star); } } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java index 66f2db21..69e5d56d 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SearchAdapter.java @@ -19,7 +19,9 @@ import android.content.Context; import android.content.res.Resources; import android.view.Menu; import android.view.MenuInflater; +import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import java.io.Serializable; import java.util.ArrayList; @@ -28,10 +30,12 @@ import java.util.List; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.SearchResult; +import github.daneren2005.dsub.util.DrawableTint; import github.daneren2005.dsub.util.ImageLoader; import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.AlbumView; import github.daneren2005.dsub.view.ArtistView; +import github.daneren2005.dsub.view.BasicHeaderView; import github.daneren2005.dsub.view.SongView; import github.daneren2005.dsub.view.UpdateView; @@ -40,32 +44,39 @@ import static github.daneren2005.dsub.adapter.EntryGridAdapter.VIEW_TYPE_ALBUM_C import static github.daneren2005.dsub.adapter.EntryGridAdapter.VIEW_TYPE_ALBUM_LINE; import static github.daneren2005.dsub.adapter.EntryGridAdapter.VIEW_TYPE_SONG; -public class SearchAdapter extends SectionAdapter<Serializable> { - private SearchResult searchResult; +public class SearchAdapter extends ExpandableSectionAdapter<Serializable> { private ImageLoader imageLoader; private boolean largeAlbums; + private static final int MAX_ARTISTS = 10; + private static final int MAX_ALBUMS = 4; + private static final int MAX_SONGS = 10; + public SearchAdapter(Context context, SearchResult searchResult, ImageLoader imageLoader, boolean largeAlbums, OnItemClickedListener listener) { - this.context = context; - this.searchResult = searchResult; this.imageLoader = imageLoader; this.largeAlbums = largeAlbums; - this.sections = new ArrayList<>(); - this.headers = new ArrayList<>(); + List<List<Serializable>> sections = new ArrayList<>(); + List<String> headers = new ArrayList<>(); + List<Integer> defaultVisible = new ArrayList<>(); Resources res = context.getResources(); if(!searchResult.getArtists().isEmpty()) { - this.sections.add((List<Serializable>) (List<?>) searchResult.getArtists()); - this.headers.add(res.getString(R.string.search_artists)); + sections.add((List<Serializable>) (List<?>) searchResult.getArtists()); + headers.add(res.getString(R.string.search_artists)); + defaultVisible.add(MAX_ARTISTS); } if(!searchResult.getAlbums().isEmpty()) { - this.sections.add((List<Serializable>) (List<?>) searchResult.getAlbums()); - this.headers.add(res.getString(R.string.search_albums)); + sections.add((List<Serializable>) (List<?>) searchResult.getAlbums()); + headers.add(res.getString(R.string.search_albums)); + defaultVisible.add(MAX_ALBUMS); } if(!searchResult.getSongs().isEmpty()) { - this.sections.add((List<Serializable>) (List<?>) searchResult.getSongs()); - this.headers.add(res.getString(R.string.search_songs)); + sections.add((List<Serializable>) (List<?>) searchResult.getSongs()); + headers.add(res.getString(R.string.search_songs)); + defaultVisible.add(MAX_SONGS); } + init(context, headers, sections, defaultVisible); + this.onItemClickedListener = listener; checkable = true; } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java index d5f9a6ea..33bbb384 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java @@ -194,7 +194,7 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH for(List<T> section: sections) { boolean validHeader = headers.get(subHeader) != null; if(position == subPosition && validHeader) { - onBindHeaderHolder(holder, headers.get(subHeader)); + onBindHeaderHolder(holder, headers.get(subHeader), subHeader); return; } @@ -289,7 +289,7 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH public UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { return new UpdateViewHolder(new BasicHeaderView(context)); } - public void onBindHeaderHolder(UpdateViewHolder holder, String header) { + public void onBindHeaderHolder(UpdateViewHolder holder, String header, int sectionIndex) { UpdateView view = holder.getUpdateView(); if(view != null) { view.setObject(header); @@ -414,14 +414,15 @@ public abstract class SectionAdapter<T> extends RecyclerView.Adapter<UpdateViewH @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { currentActionMode = mode; - onCreateActionModeMenu(menu, mode.getMenuInflater()); - MenuUtil.hideMenuItems(context, menu, updateView); T item = holder.getItem(); selected.add(item); selectedViews.add(updateView); setChecked(updateView, true); + onCreateActionModeMenu(menu, mode.getMenuInflater()); + MenuUtil.hideMenuItems(context, menu, updateView); + mode.setTitle(context.getResources().getString(R.string.select_album_n_selected, selected.size())); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_COLOR_ACTION_BAR, true)) { TypedValue typedValue = new TypedValue(); diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java index 1cb9c34e..4e75a2f7 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java @@ -16,18 +16,20 @@ package github.daneren2005.dsub.adapter; import android.content.Context; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.TextView; +import java.util.ArrayList; import java.util.List; import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.domain.User; import github.daneren2005.dsub.util.ImageLoader; +import github.daneren2005.dsub.util.UserUtil; +import github.daneren2005.dsub.view.BasicHeaderView; import github.daneren2005.dsub.view.RecyclingImageView; import github.daneren2005.dsub.view.SettingView; import github.daneren2005.dsub.view.UpdateView; @@ -37,56 +39,81 @@ import static github.daneren2005.dsub.domain.User.Setting; public class SettingsAdapter extends SectionAdapter<Setting> { private static final String TAG = SettingsAdapter.class.getSimpleName(); public final int VIEW_TYPE_SETTING = 1; + public final int VIEW_TYPE_SETTING_HEADER = 2; private final User user; private final boolean editable; private final ImageLoader imageLoader; - public SettingsAdapter(Context context, User user, ImageLoader imageLoader, boolean editable, OnItemClickedListener<Setting> onItemClickedListener) { - super(context, user.getSettings(), imageLoader != null); + public SettingsAdapter(Context context, User user, List<String> headers, List<List<User.Setting>> settingSections, ImageLoader imageLoader, boolean editable, OnItemClickedListener<Setting> onItemClickedListener) { + super(context, headers, settingSections, imageLoader != null); this.user = user; this.imageLoader = imageLoader; this.editable = editable; this.onItemClickedListener = onItemClickedListener; - List<Setting> settings = sections.get(0); - for(Setting setting: settings) { - if(setting.getValue()) { - addSelected(setting); + for(List<Setting> settings: sections) { + for (Setting setting : settings) { + if (setting.getValue()) { + addSelected(setting); + } } } } + @Override + public int getItemViewType(int position) { + int viewType = super.getItemViewType(position); + if(viewType == SectionAdapter.VIEW_TYPE_HEADER) { + if(position == 0 && imageLoader != null) { + return VIEW_TYPE_HEADER; + } else { + return VIEW_TYPE_SETTING_HEADER; + } + } else { + return viewType; + } + } + public UpdateView.UpdateViewHolder onCreateHeaderHolder(ViewGroup parent) { View header = LayoutInflater.from(context).inflate(R.layout.user_header, parent, false); return new UpdateView.UpdateViewHolder(header, false); } - public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String description) { + public void onBindHeaderHolder(UpdateView.UpdateViewHolder holder, String description, int sectionIndex) { View header = holder.getView(); RecyclingImageView coverArtView = (RecyclingImageView) header.findViewById(R.id.user_avatar); - imageLoader.loadAvatar(context, coverArtView, user.getUsername()); - coverArtView.setOnInvalidated(new RecyclingImageView.OnInvalidated() { - @Override - public void onInvalidated(RecyclingImageView imageView) { - imageLoader.loadAvatar(context, imageView, user.getUsername()); + if(coverArtView != null) { + imageLoader.loadAvatar(context, coverArtView, user.getUsername()); + coverArtView.setOnInvalidated(new RecyclingImageView.OnInvalidated() { + @Override + public void onInvalidated(RecyclingImageView imageView) { + imageLoader.loadAvatar(context, imageView, user.getUsername()); + } + }); + + TextView usernameView = (TextView) header.findViewById(R.id.user_username); + usernameView.setText(user.getUsername()); + + final TextView emailView = (TextView) header.findViewById(R.id.user_email); + if (user.getEmail() != null) { + emailView.setText(user.getEmail()); + } else { + emailView.setVisibility(View.GONE); } - }); - - TextView usernameView = (TextView) header.findViewById(R.id.user_username); - usernameView.setText(user.getUsername()); - - final TextView emailView = (TextView) header.findViewById(R.id.user_email); - if(user.getEmail() != null) { - emailView.setText(user.getEmail()); } else { - emailView.setVisibility(View.GONE); + TextView nameView = (TextView) header.findViewById(R.id.item_name); + nameView.setText(description); } } @Override public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { - return new UpdateView.UpdateViewHolder(new SettingView(context)); + if(viewType == VIEW_TYPE_SETTING_HEADER) { + return new UpdateView.UpdateViewHolder(new BasicHeaderView(context)); + } else { + return new UpdateView.UpdateViewHolder(new SettingView(context)); + } } @Override @@ -105,4 +132,21 @@ public class SettingsAdapter extends SectionAdapter<Setting> { updateView.setChecked(checked); } } + + public static SettingsAdapter getSettingsAdapter(Context context, User user, ImageLoader imageLoader, OnItemClickedListener<Setting> onItemClickedListener) { + return getSettingsAdapter(context, user, imageLoader, UserUtil.isCurrentAdmin() && ServerInfo.checkServerVersion(context, "1.10"), onItemClickedListener); + } + public static SettingsAdapter getSettingsAdapter(Context context, User user, ImageLoader imageLoader, boolean isEditable, OnItemClickedListener<Setting> onItemClickedListener) { + List<String> headers = new ArrayList<>(); + List<List<User.Setting>> settingsSections = new ArrayList<>(); + headers.add(context.getResources().getString(R.string.admin_permissions)); + settingsSections.add(user.getSettings()); + + if(user.getMusicFolderSettings() != null) { + headers.add(context.getResources().getString(R.string.admin_musicFolders)); + settingsSections.add(user.getMusicFolderSettings()); + } + + return new SettingsAdapter(context, user, headers, settingsSections, imageLoader, isEditable, onItemClickedListener); + } } diff --git a/app/src/main/java/github/daneren2005/dsub/adapter/SimilarArtistAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/SimilarArtistAdapter.java new file mode 100644 index 00000000..2234d4cd --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SimilarArtistAdapter.java @@ -0,0 +1,53 @@ +/* + 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 2016 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.adapter; + +import android.content.Context; +import android.view.ViewGroup; + +import java.util.List; + +import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.view.ArtistView; +import github.daneren2005.dsub.view.UpdateView; + +public class SimilarArtistAdapter extends SectionAdapter<Artist> { + public static int VIEW_TYPE_ARTIST = 4; + + public SimilarArtistAdapter(Context context, List<Artist> artists, OnItemClickedListener onItemClickedListener) { + super(context, artists); + this.onItemClickedListener = onItemClickedListener; + } + public SimilarArtistAdapter(Context context, List<String> headers, List<List<Artist>> sections, OnItemClickedListener onItemClickedListener) { + super(context, headers, sections); + this.onItemClickedListener = onItemClickedListener; + } + + @Override + public UpdateView.UpdateViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + return new UpdateView.UpdateViewHolder(new ArtistView(context)); + } + + @Override + public void onBindViewHolder(UpdateView.UpdateViewHolder holder, Artist item, int viewType) { + holder.getUpdateView().setObject(item); + } + + @Override + public int getItemViewType(Artist item) { + return VIEW_TYPE_ARTIST; + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Artist.java b/app/src/main/java/github/daneren2005/dsub/domain/Artist.java index 56e8f92e..ff4d86ce 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/Artist.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/Artist.java @@ -21,15 +21,19 @@ package github.daneren2005.dsub.domain; import android.util.Log; import java.io.Serializable; +import java.text.Collator; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Locale; /** * @author Sindre Mehus */ public class Artist implements Serializable { private static final String TAG = Artist.class.getSimpleName(); + public static final String ROOT_ID = "-1"; + public static final String MISSING_ID = "-2"; private String id; private String name; @@ -38,6 +42,14 @@ public class Artist implements Serializable { private Integer rating; private int closeness; + public Artist() { + + } + public Artist(String id, String name) { + this.id = id; + this.name = name; + } + public String getId() { return id; } @@ -109,30 +121,18 @@ public class Artist implements Serializable { public static class ArtistComparator implements Comparator<Artist> { private String[] ignoredArticles; + private Collator collator; public ArtistComparator(String[] ignoredArticles) { this.ignoredArticles = ignoredArticles; + this.collator = Collator.getInstance(Locale.US); + this.collator.setStrength(Collator.PRIMARY); } public int compare(Artist lhsArtist, Artist rhsArtist) { - if("root".equals(lhsArtist.getId())) { - return 1; - } else if("root".equals(rhsArtist.getId())) { - return -1; - } - String lhs = lhsArtist.getName().toLowerCase(); String rhs = rhsArtist.getName().toLowerCase(); - char lhs1 = lhs.charAt(0); - char rhs1 = rhs.charAt(0); - - if (Character.isDigit(lhs1) && !Character.isDigit(rhs1)) { - return 1; - } else if (Character.isDigit(rhs1) && !Character.isDigit(lhs1)) { - return -1; - } - for (String article : ignoredArticles) { int index = lhs.indexOf(article.toLowerCase() + " "); if (index == 0) { @@ -144,7 +144,7 @@ public class Artist implements Serializable { } } - return lhs.compareTo(rhs); + return collator.compare(lhs, rhs); } } diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java b/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java index e15ccf9f..05e686ca 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/Indexes.java @@ -52,13 +52,6 @@ public class Indexes implements Serializable { this.shortcuts = shortcuts; this.artists = artists; this.entries = entries; - if(!entries.isEmpty()) { - Artist root = new Artist(); - root.setId("root"); - root.setName("Root"); - root.setIndex("#"); - artists.add(root); - } } public long getLastModified() { diff --git a/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java b/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java index 3c022cea..1da3d51b 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java @@ -24,6 +24,8 @@ import android.content.SharedPreferences; import android.media.MediaMetadataRetriever; import android.os.Build; import android.util.Log; + +import java.text.Collator; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -31,8 +33,11 @@ import java.io.File; import java.io.Serializable; import java.util.Collections; import java.util.Comparator; +import java.util.Locale; +import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.UpdateHelper; import github.daneren2005.dsub.util.Util; /** @@ -135,13 +140,14 @@ public class MusicDirectory implements Serializable { EntryComparator.sort(children, byYear); } - public synchronized void updateMetadata(MusicDirectory refreshedDirectory) { + public synchronized boolean updateMetadata(MusicDirectory refreshedDirectory) { + boolean metadataUpdated = false; Iterator<Entry> it = children.iterator(); while(it.hasNext()) { Entry entry = it.next(); int index = refreshedDirectory.children.indexOf(entry); if(index != -1) { - Entry refreshed = refreshedDirectory.children.get(index); + final Entry refreshed = refreshedDirectory.children.get(index); entry.setTitle(refreshed.getTitle()); entry.setAlbum(refreshed.getAlbum()); @@ -155,8 +161,36 @@ public class MusicDirectory implements Serializable { entry.setStarred(refreshed.isStarred()); entry.setRating(refreshed.getRating()); entry.setType(refreshed.getType()); + if(!Util.equals(entry.getCoverArt(), refreshed.getCoverArt())) { + metadataUpdated = true; + entry.setCoverArt(refreshed.getCoverArt()); + } + + new UpdateHelper.EntryInstanceUpdater(entry) { + @Override + public void update(Entry found) { + found.setTitle(refreshed.getTitle()); + found.setAlbum(refreshed.getAlbum()); + found.setArtist(refreshed.getArtist()); + found.setTrack(refreshed.getTrack()); + found.setYear(refreshed.getYear()); + found.setGenre(refreshed.getGenre()); + found.setTranscodedContentType(refreshed.getTranscodedContentType()); + found.setTranscodedSuffix(refreshed.getTranscodedSuffix()); + found.setDiscNumber(refreshed.getDiscNumber()); + found.setStarred(refreshed.isStarred()); + found.setRating(refreshed.getRating()); + found.setType(refreshed.getType()); + if(!Util.equals(found.getCoverArt(), refreshed.getCoverArt())) { + found.setCoverArt(refreshed.getCoverArt()); + metadataUpdate = DownloadService.METADATA_UPDATED_COVER_ART; + } + } + }.execute(); } } + + return metadataUpdated; } public synchronized boolean updateEntriesList(Context context, int instance, MusicDirectory refreshedDirectory) { boolean changed = false; @@ -275,6 +309,10 @@ public class MusicDirectory implements Serializable { public void rebaseTitleOffPath() { try { String filename = getPath(); + if(filename == null) { + return; + } + int index = filename.lastIndexOf('/'); if (index != -1) { filename = filename.substring(index + 1); @@ -586,9 +624,12 @@ public class MusicDirectory implements Serializable { public static class EntryComparator implements Comparator<Entry> { private boolean byYear; + private Collator collator; public EntryComparator(boolean byYear) { this.byYear = byYear; + this.collator = Collator.getInstance(Locale.US); + this.collator.setStrength(Collator.PRIMARY); } public int compare(Entry lhs, Entry rhs) { @@ -608,8 +649,8 @@ public class MusicDirectory implements Serializable { return 1; } } - - return lhs.getAlbumDisplay().compareToIgnoreCase(rhs.getAlbumDisplay()); + + return collator.compare(lhs.getAlbumDisplay(), rhs.getAlbumDisplay()); } Integer lhsDisc = lhs.getDiscNumber(); @@ -633,7 +674,7 @@ public class MusicDirectory implements Serializable { return 1; } - return lhs.getTitle().compareToIgnoreCase(rhs.getTitle()); + return collator.compare(lhs.getTitle(), rhs.getTitle()); } public static void sort(List<Entry> entries) { diff --git a/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java b/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java index 99e86e23..37f76249 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/MusicFolder.java @@ -18,7 +18,12 @@ */ package github.daneren2005.dsub.domain; +import android.util.Log; + import java.io.Serializable; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; /** * Represents a top level directory in which music or other media is stored. @@ -27,23 +32,49 @@ import java.io.Serializable; * @version $Id$ */ public class MusicFolder implements Serializable { - - private String id; - private String name; + private static final String TAG = MusicFolder.class.getSimpleName(); + private String id; + private String name; + private boolean enabled; public MusicFolder() { } - public MusicFolder(String id, String name) { - this.id = id; - this.name = name; - } + public MusicFolder(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public boolean getEnabled() { + return enabled; + } - public String getId() { - return id; - } + public static class MusicFolderComparator implements Comparator<MusicFolder> { + public int compare(MusicFolder lhsMusicFolder, MusicFolder rhsMusicFolder) { + if(lhsMusicFolder == rhsMusicFolder || lhsMusicFolder.getName().equals(rhsMusicFolder.getName())) { + return 0; + } else { + return lhsMusicFolder.getName().compareToIgnoreCase(rhsMusicFolder.getName()); + } + } + } - public String getName() { - return name; - } + public static void sort(List<MusicFolder> musicFolders) { + try { + Collections.sort(musicFolders, new MusicFolderComparator()); + } catch (Exception e) { + Log.w(TAG, "Failed to sort music folders", e); + } + } } diff --git a/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java b/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java index 20d46aa0..ed2400ef 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/SearchCritera.java @@ -18,6 +18,8 @@ */ package github.daneren2005.dsub.domain; +import java.util.regex.Pattern; + /** * The criteria for a music search. * @@ -25,31 +27,67 @@ package github.daneren2005.dsub.domain; */ public class SearchCritera { - private final String query; - private final int artistCount; - private final int albumCount; - private final int songCount; - - public SearchCritera(String query, int artistCount, int albumCount, int songCount) { - this.query = query; - this.artistCount = artistCount; - this.albumCount = albumCount; - this.songCount = songCount; - } - - public String getQuery() { - return query; - } - - public int getArtistCount() { - return artistCount; - } - - public int getAlbumCount() { - return albumCount; - } - - public int getSongCount() { - return songCount; - } -}
\ No newline at end of file + private final String query; + private final int artistCount; + private final int albumCount; + private final int songCount; + private Pattern pattern; + + public SearchCritera(String query, int artistCount, int albumCount, int songCount) { + this.query = query; + this.artistCount = artistCount; + this.albumCount = albumCount; + this.songCount = songCount; + } + + public String getQuery() { + return query; + } + + public int getArtistCount() { + return artistCount; + } + + public int getAlbumCount() { + return albumCount; + } + + public int getSongCount() { + return songCount; + } + + /** + * Returns and caches a pattern instance that can be used to check if a + * string matches the query. + */ + public Pattern getPattern() { + + // If the pattern wasn't already cached, create a new regular expression + // from the search string : + // * Surround the search string with ".*" (match anything) + // * Replace spaces and wildcard '*' characters with ".*" + // * All other characters are properly quoted + if (this.pattern == null) { + String regex = ".*"; + String currentPart = ""; + for (int i = 0; i < query.length(); i++) { + char c = query.charAt(i); + if (c == '*' || c == ' ') { + regex += Pattern.quote(currentPart); + regex += ".*"; + currentPart = ""; + } else { + currentPart += c; + } + } + if (currentPart.length() > 0) { + regex += Pattern.quote(currentPart); + } + + regex += ".*"; + this.pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + } + + return this.pattern; + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java b/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java index 73037c4a..7f538484 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/ServerInfo.java @@ -35,6 +35,7 @@ import github.daneren2005.dsub.util.Util; public class ServerInfo implements Serializable { public static final int TYPE_SUBSONIC = 1; public static final int TYPE_MADSONIC = 2; + public static final int TYPE_AMPACHE = 3; private static final Map<Integer, ServerInfo> SERVERS = new ConcurrentHashMap<Integer, ServerInfo>(); private boolean isLicenseValid; @@ -189,13 +190,20 @@ public class ServerInfo implements Serializable { public static boolean isMadsonic6(Context context, int instance) { return getServerType(context, instance) == TYPE_MADSONIC && checkServerVersion(context, "2.0", instance); } + + public static boolean isAmpache(Context context) { + return isAmpache(context, Util.getActiveServer(context)); + } + public static boolean isAmpache(Context context, int instance) { + return getServerType(context, instance) == TYPE_AMPACHE; + } private static String getCacheName(Context context, int instance) { return "server-" + Util.getRestUrl(context, null, instance, false).hashCode() + ".ser"; } public static boolean hasArtistInfo(Context context) { - if(isStockSubsonic(context) && ServerInfo.checkServerVersion(context, "1.11")) { + if(!isMadsonic(context) && ServerInfo.checkServerVersion(context, "1.11")) { return true; } else if(isMadsonic(context)) { return checkServerVersion(context, "2.0"); diff --git a/app/src/main/java/github/daneren2005/dsub/domain/User.java b/app/src/main/java/github/daneren2005/dsub/domain/User.java index 797a1271..5307828a 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/User.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/User.java @@ -15,6 +15,8 @@ package github.daneren2005.dsub.domain; +import android.util.Pair; + import java.io.Serializable; import java.util.ArrayList; import java.util.List; @@ -31,8 +33,9 @@ public class User implements Serializable { public static final String STREAM = "streamRole"; public static final String JUKEBOX = "jukeboxRole"; public static final String SHARE = "shareRole"; + public static final String VIDEO_CONVERSION = "videoConversionRole"; public static final String LASTFM = "lastFMRole"; - public static final List<String> ROLES = new ArrayList<String>(); + public static final List<String> ROLES = new ArrayList<>(); static { ROLES.add(ADMIN); @@ -45,6 +48,7 @@ public class User implements Serializable { ROLES.add(PODCAST); ROLES.add(JUKEBOX); ROLES.add(SHARE); + ROLES.add(VIDEO_CONVERSION); } private String username; @@ -52,6 +56,7 @@ public class User implements Serializable { private String email; private List<Setting> settings = new ArrayList<Setting>(); + private List<Setting> musicFolders; public User() { @@ -92,9 +97,27 @@ public class User implements Serializable { settings.add(new Setting(name, value)); } + public void addMusicFolder(MusicFolder musicFolder) { + if(musicFolders == null) { + musicFolders = new ArrayList<>(); + } + + musicFolders.add(new MusicFolderSetting(musicFolder.getId(), musicFolder.getName(), false)); + } + public void addMusicFolder(MusicFolderSetting musicFolderSetting, boolean defaultValue) { + if(musicFolders == null) { + musicFolders = new ArrayList<>(); + } + + musicFolders.add(new MusicFolderSetting(musicFolderSetting.getName(), musicFolderSetting.getLabel(), defaultValue)); + } + public List<Setting> getMusicFolderSettings() { + return musicFolders; + } + public static class Setting implements Serializable { - String name; - Boolean value; + private String name; + private Boolean value; public Setting() { @@ -114,4 +137,20 @@ public class User implements Serializable { this.value = value; } } + + public static class MusicFolderSetting extends Setting { + private String label; + + public MusicFolderSetting() { + + } + public MusicFolderSetting(String name, String label, Boolean value) { + super(name, value); + this.label = label; + } + + public String getLabel() { + return label; + } + } } diff --git a/app/src/main/java/github/daneren2005/dsub/domain/Version.java b/app/src/main/java/github/daneren2005/dsub/domain/Version.java index 97246ecf..9df0dbb4 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/Version.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/Version.java @@ -27,41 +27,41 @@ import java.io.Serializable; * @version $Revision: 1.3 $ $Date: 2006/01/20 21:25:16 $ */ public class Version implements Comparable<Version>, Serializable { - private int major; - private int minor; - private int beta; - private int bugfix; + private int major; + private int minor; + private int beta; + private int bugfix; public Version() { // For Kryo } - /** - * Creates a new version instance by parsing the given string. - * @param version A string of the format "1.27", "1.27.2" or "1.27.beta3". - */ - public Version(String version) { - String[] s = version.split("\\."); - major = Integer.valueOf(s[0]); - minor = Integer.valueOf(s[1]); - - if (s.length > 2) { - if (s[2].contains("beta")) { - beta = Integer.valueOf(s[2].replace("beta", "")); - } else { - bugfix = Integer.valueOf(s[2]); - } - } - } - - public int getMajor() { - return major; - } - - public int getMinor() { - return minor; - } - + /** + * Creates a new version instance by parsing the given string. + * @param version A string of the format "1.27", "1.27.2" or "1.27.beta3". + */ + public Version(String version) { + String[] s = version.split("\\."); + major = Integer.valueOf(s[0]); + minor = Integer.valueOf(s[1]); + + if (s.length > 2) { + if (s[2].contains("beta")) { + beta = Integer.valueOf(s[2].replace("beta", "")); + } else { + bugfix = Integer.valueOf(s[2]); + } + } + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + public String getVersion() { switch(major) { case 1: @@ -90,96 +90,98 @@ public class Version implements Comparable<Version>, Serializable { return "4.9"; case 11: return "5.1"; - case 12: - return "5.2"; - case 13: - return "5.3"; + case 12: + return "5.2"; + case 13: + return "5.3"; + case 14: + return "6.0"; } } return ""; } - /** - * Return whether this object is equal to another. - * @param o Object to compare to. - * @return Whether this object is equals to another. - */ - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final Version version = (Version) o; - - if (beta != version.beta) return false; - if (bugfix != version.bugfix) return false; - if (major != version.major) return false; - return minor == version.minor; - } - - /** - * Returns a hash code for this object. - * @return A hash code for this object. - */ - public int hashCode() { - int result; - result = major; - result = 29 * result + minor; - result = 29 * result + beta; - result = 29 * result + bugfix; - return result; - } - - /** - * Returns a string representation of the form "1.27", "1.27.2" or "1.27.beta3". - * @return A string representation of the form "1.27", "1.27.2" or "1.27.beta3". - */ - public String toString() { - StringBuffer buf = new StringBuffer(); - buf.append(major).append('.').append(minor); - if (beta != 0) { - buf.append(".beta").append(beta); - } else if (bugfix != 0) { - buf.append('.').append(bugfix); - } - - return buf.toString(); - } - - /** - * Compares this object with the specified object for order. - * @param version The object to compare to. - * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or - * greater than the specified object. - */ - @Override - public int compareTo(Version version) { - if (major < version.major) { - return -1; - } else if (major > version.major) { - return 1; - } - - if (minor < version.minor) { - return -1; - } else if (minor > version.minor) { - return 1; - } - - if (bugfix < version.bugfix) { - return -1; - } else if (bugfix > version.bugfix) { - return 1; - } - - int thisBeta = beta == 0 ? Integer.MAX_VALUE : beta; - int otherBeta = version.beta == 0 ? Integer.MAX_VALUE : version.beta; - - if (thisBeta < otherBeta) { - return -1; - } else if (thisBeta > otherBeta) { - return 1; - } - - return 0; - } + /** + * Return whether this object is equal to another. + * @param o Object to compare to. + * @return Whether this object is equals to another. + */ + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Version version = (Version) o; + + if (beta != version.beta) return false; + if (bugfix != version.bugfix) return false; + if (major != version.major) return false; + return minor == version.minor; + } + + /** + * Returns a hash code for this object. + * @return A hash code for this object. + */ + public int hashCode() { + int result; + result = major; + result = 29 * result + minor; + result = 29 * result + beta; + result = 29 * result + bugfix; + return result; + } + + /** + * Returns a string representation of the form "1.27", "1.27.2" or "1.27.beta3". + * @return A string representation of the form "1.27", "1.27.2" or "1.27.beta3". + */ + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(major).append('.').append(minor); + if (beta != 0) { + buf.append(".beta").append(beta); + } else if (bugfix != 0) { + buf.append('.').append(bugfix); + } + + return buf.toString(); + } + + /** + * Compares this object with the specified object for order. + * @param version The object to compare to. + * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or + * greater than the specified object. + */ + @Override + public int compareTo(Version version) { + if (major < version.major) { + return -1; + } else if (major > version.major) { + return 1; + } + + if (minor < version.minor) { + return -1; + } else if (minor > version.minor) { + return 1; + } + + if (bugfix < version.bugfix) { + return -1; + } else if (bugfix > version.bugfix) { + return 1; + } + + int thisBeta = beta == 0 ? Integer.MAX_VALUE : beta; + int otherBeta = version.beta == 0 ? Integer.MAX_VALUE : version.beta; + + if (thisBeta < otherBeta) { + return -1; + } else if (thisBeta > otherBeta) { + return 1; + } + + return 0; + } }
\ No newline at end of file diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java index 630acf2c..187f0d55 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/AdminFragment.java @@ -47,7 +47,7 @@ public class AdminFragment extends SelectRecyclerFragment<User> { switch (item.getItemId()) { case R.id.menu_add_user: - UserUtil.addNewUser(context, this); + UserUtil.addNewUser(context, this, objects.get(0)); break; } diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java index 5daf3d7a..8c2fa4bf 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java @@ -1,11 +1,12 @@ package github.daneren2005.dsub.fragments; -import android.content.res.Resources; -import android.os.Environment; import android.content.Intent; -import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.res.Resources; import android.net.Uri; import android.os.Build; +import android.os.Environment; +import android.content.SharedPreferences; import android.os.Bundle; import android.os.StatFs; import android.util.Log; @@ -18,6 +19,7 @@ import github.daneren2005.dsub.adapter.MainAdapter; import github.daneren2005.dsub.adapter.SectionAdapter; import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.EnvironmentVariables; import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.LoadingTask; import github.daneren2005.dsub.util.ProgressListener; @@ -28,13 +30,28 @@ import github.daneren2005.dsub.service.MusicServiceFactory; import github.daneren2005.dsub.view.ChangeLog; import github.daneren2005.dsub.view.UpdateView; +import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.URL; +import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import javax.net.ssl.HttpsURLConnection; + public class MainFragment extends SelectRecyclerFragment<Integer> { private static final String TAG = MainFragment.class.getSimpleName(); + public static final String SONGS_LIST_PREFIX = "songs-"; + public static final String SONGS_NEWEST = SONGS_LIST_PREFIX + "newest"; + public static final String SONGS_TOP_PLAYED = SONGS_LIST_PREFIX + "topPlayed"; + public static final String SONGS_RECENT = SONGS_LIST_PREFIX + "recent"; + public static final String SONGS_FREQUENT = SONGS_LIST_PREFIX + "frequent"; public MainFragment() { super(); @@ -47,6 +64,7 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { menuInflater.inflate(R.menu.main, menu); + onFinishSetupOptionsMenu(menu); try { if (!ServerInfo.isMadsonic(context) || !UserUtil.isCurrentAdmin()) { @@ -113,6 +131,22 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { sections.add(albums); headers.add("albums"); + if(ServerInfo.isMadsonic6(context)) { + List<Integer> songs = new ArrayList<>(); + + songs.add(R.string.main_songs_newest); + if(ServerInfo.checkServerVersion(context, "2.0.1")) { + songs.add(R.string.main_songs_top_played); + } + songs.add(R.string.main_songs_recent); + if(ServerInfo.checkServerVersion(context, "2.0.1")) { + songs.add(R.string.main_songs_frequent); + } + + sections.add(songs); + headers.add("songs"); + } + if(ServerInfo.checkServerVersion(context, "1.8")) { List<Integer> videos = Arrays.asList(R.string.main_videos); sections.add(videos); @@ -237,10 +271,10 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { private void getLogs() { try { - final String version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; - new LoadingTask<File>(context) { + final PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + new LoadingTask<String>(context) { @Override - protected File doInBackground() throws Throwable { + protected String doInBackground() throws Throwable { updateProgress("Gathering Logs"); File logcat = new File(Environment.getExternalStorageDirectory(), "dsub-logcat.txt"); Util.delete(logcat); @@ -258,30 +292,94 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { logcatProc = Runtime.getRuntime().exec(progs.toArray(new String[progs.size()])); logcatProc.waitFor(); - } catch(Exception e) { - Util.toast(context, "Failed to gather logs"); } finally { if(logcatProc != null) { logcatProc.destroy(); } } - return logcat; + URL url = new URL("https://pastebin.com/api/api_post.php"); + HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); + StringBuffer responseBuffer = new StringBuffer(); + try { + urlConnection.setReadTimeout(10000); + urlConnection.setConnectTimeout(15000); + urlConnection.setRequestMethod("POST"); + urlConnection.setDoInput(true); + urlConnection.setDoOutput(true); + + OutputStream os = urlConnection.getOutputStream(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, Constants.UTF_8)); + writer.write("api_dev_key=" + URLEncoder.encode(EnvironmentVariables.PASTEBIN_DEV_KEY, Constants.UTF_8) + "&api_option=paste&api_paste_private=1&api_paste_code="); + + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(logcat))); + String line; + while ((line = reader.readLine()) != null) { + writer.write(URLEncoder.encode(line + "\n", Constants.UTF_8)); + } + } finally { + Util.close(reader); + } + + File stacktrace = new File(Environment.getExternalStorageDirectory(), "dsub-stacktrace.txt"); + if(stacktrace.exists() && stacktrace.isFile()) { + writer.write("\n\nMost Recent Stacktrace:\n\n"); + + reader = null; + try { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(stacktrace))); + String line; + while ((line = reader.readLine()) != null) { + writer.write(URLEncoder.encode(line + "\n", Constants.UTF_8)); + } + } finally { + Util.close(reader); + } + } + + writer.flush(); + writer.close(); + os.close(); + + BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); + String inputLine; + while ((inputLine = in.readLine()) != null) { + responseBuffer.append(inputLine); + } + in.close(); + } finally { + urlConnection.disconnect(); + } + + String response = responseBuffer.toString(); + if(response.indexOf("http") == 0) { + return response.replace("http:", "https:"); + } else { + throw new Exception("Pastebin Error: " + response); + } + } + + @Override + protected void error(Throwable error) { + Log.e(TAG, "Failed to gather logs", error); + Util.toast(context, "Failed to gather logs"); } @Override - protected void done(File logcat) { + protected void done(String logcat) { String footer = "Android SDK: " + Build.VERSION.SDK; footer += "\nDevice Model: " + Build.MODEL; footer += "\nDevice Name: " + Build.MANUFACTURER + " " + Build.PRODUCT; footer += "\nROM: " + Build.DISPLAY; + footer += "\nLogs: " + logcat; + footer += "\nBuild Number: " + packageInfo.versionCode; Intent email = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", "dsub.android@gmail.com", null)); - email.putExtra(Intent.EXTRA_SUBJECT, "DSub " + version + " Error Logs"); + email.putExtra(Intent.EXTRA_SUBJECT, "DSub " + packageInfo.versionName + " Error Logs"); email.putExtra(Intent.EXTRA_TEXT, "Describe the problem here\n\n\n" + footer); - Uri attachment = Uri.fromFile(logcat); - email.putExtra(Intent.EXTRA_STREAM, attachment); startActivity(email); } }.execute(); @@ -310,6 +408,14 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { showAlbumList("alphabeticalByName"); } else if(item == R.string.main_videos) { showVideos(); + } else if (item == R.string.main_songs_newest) { + showAlbumList(SONGS_NEWEST); + } else if (item == R.string.main_songs_top_played) { + showAlbumList(SONGS_TOP_PLAYED); + } else if (item == R.string.main_songs_recent) { + showAlbumList(SONGS_RECENT); + } else if (item == R.string.main_songs_frequent) { + showAlbumList(SONGS_FREQUENT); } } diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java index c557a174..7c20dc28 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java @@ -51,6 +51,7 @@ import android.view.animation.AnimationUtils; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; +import android.widget.PopupMenu; import android.widget.SeekBar; import android.widget.TextView; import android.widget.ViewFlipper; @@ -59,7 +60,6 @@ import github.daneren2005.dsub.activity.SubsonicFragmentActivity; import github.daneren2005.dsub.adapter.SectionAdapter; import github.daneren2005.dsub.audiofx.EqualizerController; import github.daneren2005.dsub.domain.Bookmark; -import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.domain.RepeatMode; import github.daneren2005.dsub.domain.ServerInfo; @@ -73,6 +73,7 @@ import github.daneren2005.dsub.service.ServerTooOldException; import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.adapter.DownloadFileAdapter; +import github.daneren2005.dsub.view.compat.CustomMediaRouteDialogFactory; import github.daneren2005.dsub.view.FadeOutAnimation; import github.daneren2005.dsub.view.FastScroller; import github.daneren2005.dsub.view.UpdateView; @@ -88,7 +89,6 @@ import java.util.concurrent.ScheduledFuture; public class NowPlayingFragment extends SubsonicFragment implements OnGestureListener, SectionAdapter.OnItemClickedListener<DownloadFile>, OnSongChangedListener { private static final String TAG = NowPlayingFragment.class.getSimpleName(); private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 10; - private static final int INCREMENT_TIME = 5000; private static final int ACTION_PREVIOUS = 1; private static final int ACTION_NEXT = 2; @@ -106,6 +106,8 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis private SeekBar progressBar; private AutoRepeatButton previousButton; private AutoRepeatButton nextButton; + private AutoRepeatButton rewindButton; + private AutoRepeatButton fastforwardButton; private View pauseButton; private View stopButton; private View startButton; @@ -115,6 +117,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis private ImageButton bookmarkButton; private ImageButton rateBadButton; private ImageButton rateGoodButton; + private ImageButton playbackSpeedButton; private ScheduledExecutorService executorService; private DownloadFile currentPlaying; @@ -172,6 +175,8 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis progressBar = (SeekBar)rootView.findViewById(R.id.download_progress_bar); previousButton = (AutoRepeatButton)rootView.findViewById(R.id.download_previous); nextButton = (AutoRepeatButton)rootView.findViewById(R.id.download_next); + rewindButton = (AutoRepeatButton) rootView.findViewById(R.id.download_rewind); + fastforwardButton = (AutoRepeatButton) rootView.findViewById(R.id.download_fastforward); pauseButton =rootView.findViewById(R.id.download_pause); stopButton =rootView.findViewById(R.id.download_stop); startButton =rootView.findViewById(R.id.download_start); @@ -179,6 +184,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis bookmarkButton = (ImageButton) rootView.findViewById(R.id.download_bookmark); rateBadButton = (ImageButton) rootView.findViewById(R.id.download_rating_bad); rateGoodButton = (ImageButton) rootView.findViewById(R.id.download_rating_good); + playbackSpeedButton = (ImageButton) rootView.findViewById(R.id.download_playback_speed); toggleListButton =rootView.findViewById(R.id.download_toggle_list); playlistView = (RecyclerView)rootView.findViewById(R.id.download_list); @@ -194,6 +200,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis @Override public void onClick(View v) { getDownloadService().toggleStarred(); + setControlsVisible(true); } }); } else { @@ -212,6 +219,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis bookmarkButton.setOnTouchListener(touchListener); rateBadButton.setOnTouchListener(touchListener); rateGoodButton.setOnTouchListener(touchListener); + playbackSpeedButton.setOnTouchListener(touchListener); emptyTextView.setOnTouchListener(touchListener); albumArtImageView.setOnTouchListener(new View.OnTouchListener() { @Override @@ -239,7 +247,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis }); previousButton.setOnRepeatListener(new Runnable() { public void run() { - changeProgress(-INCREMENT_TIME); + changeProgress(true); } }); @@ -259,10 +267,35 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis }); nextButton.setOnRepeatListener(new Runnable() { public void run() { - changeProgress(INCREMENT_TIME); + changeProgress(false); + } + }); + + rewindButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + changeProgress(true); + } + }); + rewindButton.setOnRepeatListener(new Runnable() { + public void run() { + changeProgress(true); + } + }); + + fastforwardButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + changeProgress(false); + } + }); + fastforwardButton.setOnRepeatListener(new Runnable() { + public void run() { + changeProgress(false); } }); + pauseButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -330,6 +363,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis @Override public void onClick(View view) { createBookmark(); + setControlsVisible(true); } }); @@ -341,6 +375,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis return; } downloadService.toggleRating(1); + setControlsVisible(true); } }); rateGoodButton.setOnClickListener(new View.OnClickListener() { @@ -351,9 +386,53 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis return; } downloadService.toggleRating(5); + setControlsVisible(true); } }); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + playbackSpeedButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + PopupMenu popup = new PopupMenu(context, v); + popup.getMenuInflater().inflate(R.menu.playback_speed_options, popup.getMenu()); + + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + DownloadService downloadService = getDownloadService(); + if (downloadService == null) { + return false; + } + + float playbackSpeed = 1.0f; + switch (menuItem.getItemId()) { + case R.id.playback_speed_half: + playbackSpeed = 0.5f; + break; + case R.id.playback_speed_one_half: + playbackSpeed = 1.5f; + break; + case R.id.playback_speed_double: + playbackSpeed = 2.0f; + break; + case R.id.playback_speed_tripple: + playbackSpeed = 3.0f; + break; + } + + downloadService.setPlaybackSpeed(playbackSpeed); + updateTitle(); + return true; + } + }); + popup.show(); + } + }); + } else { + playbackSpeedButton.setVisibility(View.GONE); + } + toggleListButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -405,11 +484,6 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } }); - if(Build.MODEL.equals("Nexus 4") || Build.MODEL.equals("GT-I9100")) { - View slider = rootView.findViewById(R.id.download_slider); - slider.setPadding(0, 0, 0, 0); - } - return rootView; } @@ -438,7 +512,8 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } boolean equalizerAvailable = downloadService != null && downloadService.getEqualizerAvailable(); - if(equalizerAvailable && !downloadService.isRemoteEnabled()) { + boolean isRemoteEnabled = downloadService != null && downloadService.isRemoteEnabled(); + if(equalizerAvailable && !isRemoteEnabled) { SharedPreferences prefs = Util.getPreferences(context); boolean equalizerOn = prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false); if (equalizerOn && downloadService != null) { @@ -450,13 +525,24 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis menu.removeItem(R.id.menu_equalizer); } + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isRemoteEnabled) { + playbackSpeedButton.setVisibility(View.GONE); + } else { + playbackSpeedButton.setVisibility(View.VISIBLE); + } + if(downloadService != null) { MenuItem mediaRouteItem = menu.findItem(R.id.menu_mediaroute); if(mediaRouteItem != null) { MediaRouteButton mediaRouteButton = (MediaRouteButton) MenuItemCompat.getActionView(mediaRouteItem); + mediaRouteButton.setDialogFactory(new CustomMediaRouteDialogFactory()); mediaRouteButton.setRouteSelector(downloadService.getRemoteSelector()); } } + + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, false)) { + menu.findItem(R.id.menu_batch_mode).setChecked(true); + } } @Override @@ -474,7 +560,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis menuInflater.inflate(R.menu.nowplaying_context_offline, menu); } else { menuInflater.inflate(R.menu.nowplaying_context, menu); - menu.findItem(R.id.menu_star).setTitle(downloadFile.getSong().isStarred() ? R.string.common_unstar : R.string.common_star); + menu.findItem(R.id.song_menu_star).setTitle(downloadFile.getSong().isStarred() ? R.string.common_unstar : R.string.common_star); } if (downloadFile.getSong().getParent() == null) { @@ -620,9 +706,6 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } createNewPlaylist(entries, true); return true; - case R.id.menu_star: - UpdateHelper.toggleStarred(context, song.getSong()); - return true; case R.id.menu_rate: UpdateHelper.setRating(context, song.getSong()); return true; @@ -634,11 +717,6 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis startTimer(); } return true; - case R.id.menu_add_playlist: - songs = new ArrayList<Entry>(1); - songs.add(song.getSong()); - addToPlaylist(songs); - return true; case R.id.menu_info: displaySongInfo(song.getSong()); return true; @@ -663,7 +741,18 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis // Any failed condition will get here Util.toast(context, "Failed to start equalizer. Try restarting."); return true; - } default: + }case R.id.menu_batch_mode: + if(Util.isBatchMode(context)) { + Util.setBatchMode(context, false); + songListAdapter.notifyDataSetChanged(); + } else { + Util.setBatchMode(context, true); + songListAdapter.notifyDataSetChanged(); + } + context.supportInvalidateOptionsMenu(); + + return true; + default: return false; } } @@ -678,13 +767,13 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } } private void onResumeHandlers() { - final Handler handler = new Handler(); executorService = Executors.newSingleThreadScheduledExecutor(); setControlsVisible(true); final DownloadService downloadService = getDownloadService(); if (downloadService == null || downloadService.getCurrentPlaying() == null || startFlipped) { playlistFlipper.setDisplayedChild(1); + startFlipped = false; } if (downloadService != null && downloadService.getKeepScreenOn()) { context.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); @@ -701,12 +790,13 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis context.runWhenServiceAvailable(new Runnable() { @Override public void run() { - if(primaryFragment) { + if (primaryFragment) { DownloadService downloadService = getDownloadService(); downloadService.startRemoteScan(); downloadService.addOnSongChangedListener(NowPlayingFragment.this, true); } updateRepeatButton(); + updateTitle(); } }); } @@ -899,10 +989,14 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis private int getMinutes(int progress) { if(progress < 30) { return progress + 1; - } else if(progress < 61) { + } else if(progress < 49) { return (progress - 30) * 5 + getMinutes(29); + } else if(progress < 57) { + return (progress - 48) * 30 + getMinutes(48); + } else if(progress < 81) { + return (progress - 56) * 60 + getMinutes(56); } else { - return (progress - 61) * 15 + getMinutes(60); + return (progress - 80) * 150 + getMinutes(80); } } @@ -938,31 +1032,22 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } } - private void changeProgress(final int ms) { + private void changeProgress(final boolean rewind) { final DownloadService downloadService = getDownloadService(); if(downloadService == null) { return; } new SilentBackgroundTask<Void>(context) { - boolean isJukeboxEnabled; - int msPlayed; - Integer duration; - PlayerState playerState; int seekTo; @Override protected Void doInBackground() throws Throwable { - msPlayed = Math.max(0, downloadService.getPlayerPosition()); - duration = downloadService.getPlayerDuration(); - playerState = getDownloadService().getPlayerState(); - int msTotal = duration == null ? 0 : duration; - if(msPlayed + ms > msTotal) { - seekTo = msTotal; + if(rewind) { + seekTo = downloadService.rewind(); } else { - seekTo = msPlayed + ms; + seekTo = downloadService.fastForward(); } - downloadService.seekTo(seekTo); return null; } @@ -1158,11 +1243,38 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis @Override public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex) { this.currentPlaying = currentPlaying; + setupSubtitle(currentPlayingIndex); + + if(currentPlaying != null && !currentPlaying.isSong()) { + previousButton.setVisibility(View.GONE); + nextButton.setVisibility(View.GONE); + + rewindButton.setVisibility(View.VISIBLE); + fastforwardButton.setVisibility(View.VISIBLE); + } else { + previousButton.setVisibility(View.VISIBLE); + nextButton.setVisibility(View.VISIBLE); + + rewindButton.setVisibility(View.GONE); + fastforwardButton.setVisibility(View.GONE); + } + updateTitle(); + } + + private void setupSubtitle(int currentPlayingIndex) { if (currentPlaying != null) { Entry song = currentPlaying.getSong(); songTitleTextView.setText(song.getTitle()); getImageLoader().loadImage(albumArtImageView, song, true, true); - setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize)); + + DownloadService downloadService = getDownloadService(); + if(downloadService.isShufflePlayEnabled()) { + setSubtitle(context.getResources().getString(R.string.download_playerstate_playing_shuffle)); + } else if(downloadService.isArtistRadio()) { + setSubtitle(context.getResources().getString(R.string.download_playerstate_playing_artist_radio)); + } else { + setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize)); + } } else { songTitleTextView.setText(null); getImageLoader().loadImage(albumArtImageView, (Entry) null, true, false); @@ -1199,10 +1311,11 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis scrollWhenLoaded = false; } - setSubtitle(context.getResources().getString(R.string.download_playing_out_of, currentPlayingIndex + 1, currentPlayingSize)); if(this.currentPlaying != currentPlaying) { onSongChanged(currentPlaying, currentPlayingIndex); onMetadataUpdate(currentPlaying != null ? currentPlaying.getSong() : null, DownloadService.METADATA_UPDATED_ALL); + } else { + setupSubtitle(currentPlayingIndex); } } @@ -1334,6 +1447,10 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis } bookmarkButton.setImageResource(bookmark); } + + if(song != null && albumArtImageView != null && fieldChange == DownloadService.METADATA_UPDATED_COVER_ART) { + getImageLoader().loadImage(albumArtImageView, song, true, true); + } } public void updateRepeatButton() { @@ -1352,4 +1469,39 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis break; } } + private void updateTitle() { + DownloadService downloadService = getDownloadService(); + float playbackSpeed = downloadService.getPlaybackSpeed(); + + String title = context.getResources().getString(R.string.button_bar_now_playing); + int stringRes = -1; + if(playbackSpeed == 0.5f) { + stringRes = R.string.download_playback_speed_half; + } else if(playbackSpeed == 1.5f) { + stringRes = R.string.download_playback_speed_one_half; + } else if(playbackSpeed == 2.0f) { + stringRes = R.string.download_playback_speed_double; + } else if(playbackSpeed == 3.0f) { + stringRes = R.string.download_playback_speed_tripple; + } + + if(stringRes != -1) { + title += " (" + context.getResources().getString(stringRes) + ")"; + } + setTitle(title); + } + + @Override + protected List<Entry> getSelectedEntries() { + List<DownloadFile> selected = getCurrentAdapter().getSelected(); + List<Entry> entries = new ArrayList<>(); + + for(DownloadFile downloadFile: selected) { + if(downloadFile.getSong() != null) { + entries.add(downloadFile.getSong()); + } + } + + return entries; + } } diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java index d21b82e0..eed714af 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java @@ -7,6 +7,7 @@ import java.util.List; import android.content.Intent; import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; @@ -39,9 +40,9 @@ import github.daneren2005.dsub.view.UpdateView; public class SearchFragment extends SubsonicFragment implements SectionAdapter.OnItemClickedListener<Serializable> { private static final String TAG = SearchFragment.class.getSimpleName(); - private static final int MAX_ARTISTS = 10; - private static final int MAX_ALBUMS = 10; - private static final int MAX_SONGS = 25; + private static final int MAX_ARTISTS = 20; + private static final int MAX_ALBUMS = 20; + private static final int MAX_SONGS = 50; private static final int MIN_CLOSENESS = 1; protected RecyclerView recyclerView; @@ -108,13 +109,13 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O } @Override - public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() { + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { return new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { int viewType = adapter.getItemViewType(position); if(viewType == EntryGridAdapter.VIEW_TYPE_SONG || viewType == EntryGridAdapter.VIEW_TYPE_HEADER || viewType == ArtistAdapter.VIEW_TYPE_ARTIST) { - return getRecyclerColumnCount(); + return gridLayoutManager.getSpanCount(); } else { return 1; } @@ -125,18 +126,7 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O @Override public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { menuInflater.inflate(R.menu.search, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_search: - context.startSearch(currentQuery, false, null, false); - return true; - } - - return super.onOptionsItemSelected(item); - + onFinishSetupOptionsMenu(menu); } @Override @@ -187,6 +177,11 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O return selectedMedia; } + @Override + protected boolean isShowArtistEnabled() { + return true; + } + public void search(final String query, final boolean autoplay) { if(skipSearch) { skipSearch = false; @@ -213,6 +208,14 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O } }; task.execute(); + + if(searchItem != null) { + MenuItemCompat.collapseActionView(searchItem); + } + } + + protected String getCurrentQuery() { + return currentQuery; } private void onArtistSelected(Artist artist, boolean autoplay) { diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java index 3df0a9a9..e971bfb6 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java @@ -17,6 +17,7 @@ import github.daneren2005.dsub.adapter.SectionAdapter; import github.daneren2005.dsub.domain.Artist; import github.daneren2005.dsub.domain.Indexes; import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.MusicFolder; import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.service.MusicService; @@ -29,12 +30,11 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; -public class SelectArtistFragment extends SelectRecyclerFragment<Artist> implements ArtistAdapter.OnMusicFolderChanged { +public class SelectArtistFragment extends SelectRecyclerFragment<Serializable> implements ArtistAdapter.OnMusicFolderChanged { private static final String TAG = SelectArtistFragment.class.getSimpleName(); - private static final int MENU_GROUP_MUSIC_FOLDER = 10; private List<MusicFolder> musicFolders = null; - private List<MusicDirectory.Entry> entries; + private List<Entry> entries; private String groupId; private String groupName; @@ -63,12 +63,14 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { Bundle args = getArguments(); if(args != null) { - groupId = args.getString(Constants.INTENT_EXTRA_NAME_ID); - groupName = args.getString(Constants.INTENT_EXTRA_NAME_NAME); + if(args.getBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, false)) { + groupId = args.getString(Constants.INTENT_EXTRA_NAME_ID); + groupName = args.getString(Constants.INTENT_EXTRA_NAME_NAME); - if(groupName != null) { - setTitle(groupName); - context.invalidateOptionsMenu(); + if (groupName != null) { + setTitle(groupName); + context.invalidateOptionsMenu(); + } } } @@ -78,47 +80,56 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme } @Override - public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<Artist> updateView, Artist item) { + public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<Serializable> updateView, Serializable item) { onCreateContextMenuSupport(menu, menuInflater, updateView, item); recreateContextMenu(menu); } @Override - public boolean onContextItemSelected(MenuItem menuItem, UpdateView<Artist> updateView, Artist item) { + public boolean onContextItemSelected(MenuItem menuItem, UpdateView<Serializable> updateView, Serializable item) { return onContextItemSelected(menuItem, item); } @Override - public void onItemClicked(UpdateView<Artist> updateView, Artist artist) { + public void onItemClicked(UpdateView<Serializable> updateView, Serializable item) { SubsonicFragment fragment; - if((Util.isFirstLevelArtist(context) || Util.isOffline(context) || Util.isTagBrowsing(context)) || "root".equals(artist.getId()) || groupId != null) { - fragment = new SelectDirectoryFragment(); - Bundle args = new Bundle(); - args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); - args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); - - if ("root".equals(artist.getId())) { - args.putSerializable(Constants.FRAGMENT_LIST, (Serializable) entries); - } - if(ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) { - args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new MusicDirectory.Entry(artist)); + if(item instanceof Artist) { + Artist artist = (Artist) item; + + if ((Util.isFirstLevelArtist(context) || Util.isOffline(context) || Util.isTagBrowsing(context)) || groupId != null) { + fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); + + if (ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) { + args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new Entry(artist)); + } + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + + fragment.setArguments(args); + } else { + fragment = new SelectArtistFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + if (ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) { + args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new Entry(artist)); + } + + fragment.setArguments(args); } - args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); - fragment.setArguments(args); + replaceFragment(fragment); } else { - fragment = new SelectArtistFragment(); - Bundle args = new Bundle(); - args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); - args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); - if(ServerInfo.checkServerVersion(context, "1.13") && !Util.isOffline(context)) { - args.putSerializable(Constants.INTENT_EXTRA_NAME_DIRECTORY, new MusicDirectory.Entry(artist)); + Entry entry = (Entry) item; + if (entry.isVideo()) { + playVideo(entry); + } else { + onSongPress(entries, entry); } - - fragment.setArguments(args); } - - replaceFragment(fragment); } @Override @@ -155,15 +166,15 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme } @Override - public SectionAdapter getAdapter(List<Artist> objects) { + public SectionAdapter getAdapter(List<Serializable> objects) { return new ArtistAdapter(context, objects, musicFolders, this, this); } @Override - public List<Artist> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { - List<Artist> artists; + public List<Serializable> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + List<Serializable> items; if(groupId == null) { - if (!Util.isOffline(context) && !Util.isTagBrowsing(context)) { + if (!Util.isOffline(context) && (!Util.isTagBrowsing(context) || ServerInfo.checkServerVersion(context, "1.14"))) { musicFolders = musicService.getMusicFolders(refresh, context, listener); // Hide folders option if there is only one @@ -178,14 +189,16 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme Indexes indexes = musicService.getIndexes(musicFolderId, refresh, context, listener); indexes.sortChildren(context); - artists = new ArrayList<>(indexes.getShortcuts().size() + indexes.getArtists().size()); - artists.addAll(indexes.getShortcuts()); - artists.addAll(indexes.getArtists()); + items = new ArrayList<>(indexes.getShortcuts().size() + indexes.getArtists().size()); + items.addAll(indexes.getShortcuts()); + items.addAll(indexes.getArtists()); entries = indexes.getEntries(); + items.addAll(entries); } else { - artists = new ArrayList<>(); + List<Artist> artists = new ArrayList<>(); + items = new ArrayList<>(); MusicDirectory dir = musicService.getMusicDirectory(groupId, groupName, refresh, context, listener); - for(MusicDirectory.Entry entry: dir.getChildren(true, false)) { + for(Entry entry: dir.getChildren(true, false)) { Artist artist = new Artist(); artist.setId(entry.getId()); artist.setName(entry.getTitle()); @@ -193,21 +206,17 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme artists.add(artist); } - entries = new ArrayList<>(); - entries.addAll(dir.getChildren(false, true)); - if(!entries.isEmpty()) { - Artist root = new Artist(); - root.setId("root"); - root.setName("Root"); - root.setIndex("#"); - artists.add(root); - } - - Indexes indexes = new Indexes(0, artists, new ArrayList<Artist>()); + Indexes indexes = new Indexes(0, new ArrayList<Artist>(), artists); indexes.sortChildren(context); + items.addAll(indexes.getArtists()); + + entries = dir.getChildren(false, true); + for(Entry entry: entries) { + items.add(entry); + } } - return artists; + return items; } @Override diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java index 5f3ca38b..c320f3c1 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectBookmarkFragment.java @@ -18,6 +18,7 @@ */ package github.daneren2005.dsub.fragments; +import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -28,6 +29,7 @@ import github.daneren2005.dsub.domain.Bookmark; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.MenuUtil; import github.daneren2005.dsub.util.ProgressListener; import github.daneren2005.dsub.util.SilentBackgroundTask; @@ -89,19 +91,29 @@ public class SelectBookmarkFragment extends SelectRecyclerFragment<MusicDirector return; } - new SilentBackgroundTask<Void>(context) { - @Override - protected Void doInBackground() throws Throwable { - downloadService.clear(); - downloadService.download(Arrays.asList(bookmark), false, true, false, false, 0, bookmark.getBookmark().getPosition()); - return null; - } - - @Override - protected void done(Void result) { - context.openNowPlaying(); - } - }.execute(); + boolean allowPlayAll = ((!Util.isTagBrowsing(context) && bookmark.getParent() != null) || (Util.isTagBrowsing(context) && bookmark.getAlbumId() != null)) && !bookmark.isPodcast(); + if(allowPlayAll && "all".equals(Util.getSongPressAction(context))) { + new RecursiveLoader(context) { + @Override + protected Boolean doInBackground() throws Throwable { + getSiblingsRecursively(bookmark); + + if(songs.isEmpty() || !songs.contains(bookmark)) { + playNowInTask(Arrays.asList(bookmark), bookmark, bookmark.getBookmark().getPosition()); + } else { + playNowInTask(songs, bookmark, bookmark.getBookmark().getPosition()); + } + return null; + } + + @Override + protected void done(Boolean result) { + context.openNowPlaying(); + } + }.execute(); + } else { + onSongPress(Arrays.asList(bookmark), bookmark, bookmark.getBookmark().getPosition(), false); + } } private void displayBookmarkInfo(final MusicDirectory.Entry entry) { diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java index d2282117..0ac968b7 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java @@ -39,6 +39,7 @@ import github.daneren2005.dsub.domain.ArtistInfo; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.domain.Share; +import github.daneren2005.dsub.service.CachedMusicService; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.util.DrawableTint; import github.daneren2005.dsub.util.ImageLoader; @@ -86,6 +87,11 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section private ArtistInfo artistInfo; private String artistInfoDelayed; + private SilentBackgroundTask updateCoverArtTask; + private ImageView coverArtView; + private Entry coverArtRep; + private String coverArtId; + String id; String name; Entry directory; @@ -184,27 +190,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section recyclerView.setHasFixedSize(true); fastScroller = (FastScroller) rootView.findViewById(R.id.fragment_fast_scroller); setupScrollList(recyclerView); - - if(largeAlbums) { - GridLayoutManager gridLayoutManager = new GridLayoutManager(context, getRecyclerColumnCount()); - gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { - @Override - public int getSpanSize(int position) { - int viewType = entryGridAdapter.getItemViewType(position); - if(viewType == EntryGridAdapter.VIEW_TYPE_SONG || viewType == EntryGridAdapter.VIEW_TYPE_HEADER || viewType == EntryInfiniteGridAdapter.VIEW_TYPE_LOADING) { - return getRecyclerColumnCount(); - } else { - return 1; - } - } - }); - recyclerView.addItemDecoration(new GridSpacingDecoration()); - recyclerView.setLayoutManager(gridLayoutManager); - } else { - LinearLayoutManager layoutManager = new LinearLayoutManager(context); - layoutManager.setOrientation(LinearLayoutManager.VERTICAL); - recyclerView.setLayoutManager(layoutManager); - } + setupLayoutManager(recyclerView, largeAlbums); if(entries == null) { if(primaryFragment || secondaryFragment) { @@ -248,7 +234,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section if(!ServerInfo.hasTopSongs(context)) { menu.removeItem(R.id.menu_top_tracks); } - if(!ServerInfo.checkServerVersion(context, "1.11") || (id != null && "root".equals(id))) { + if(!ServerInfo.checkServerVersion(context, "1.11")) { menu.removeItem(R.id.menu_radio); menu.removeItem(R.id.menu_similar_artists); } else if(!ServerInfo.hasSimilarArtists(context)) { @@ -305,9 +291,6 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section case R.id.menu_show_all: setShowAll(); return true; - case R.id.menu_unstar: - unstarSelected(); - return true; case R.id.menu_top_tracks: showTopTracks(); return true; @@ -329,10 +312,6 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section if(!entry.isVideo() && !Util.isOffline(context) && (playlistId == null || !playlistOwner) && (podcastId == null || Util.isOffline(context) && podcastId != null)) { menu.removeItem(R.id.song_menu_remove_playlist); } - // Remove show artists if parent is not set and if not on a album list - if((albumListType == null || (entry.getParent() == null && entry.getArtistId() == null)) && !Util.isOffline(context)) { - menu.removeItem(R.id.album_menu_show_artist); - } recreateContextMenu(menu); } @@ -380,28 +359,20 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section return; } - playNow(Arrays.asList(entry)); + onSongPress(Arrays.asList(entry), entry, false); } else { - List<Entry> songs = new ArrayList<Entry>(); - - if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_PLAY_NOW_AFTER, true)) { - Iterator it = entries.listIterator(entries.indexOf(entry)); - while(it.hasNext()) { - songs.add((Entry) it.next()); - } - } else { - songs.add(entry); - } - - playNow(songs); + onSongPress(entries, entry, albumListType == null || "starred".equals(albumListType)); } } @Override protected void refresh(boolean refresh) { - if(!"root".equals(id)) { - load(refresh); - } + load(refresh); + } + + @Override + protected boolean isShowArtistEnabled() { + return albumListType != null; } private void load(boolean refresh) { @@ -577,6 +548,14 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section setTitle(albumListExtra); } else if("alphabeticalByName".equals(albumListType)) { setTitle(R.string.main_albums_alphabetical); + } if (MainFragment.SONGS_NEWEST.equals(albumListType)) { + setTitle(R.string.main_songs_newest); + } else if (MainFragment.SONGS_TOP_PLAYED.equals(albumListType)) { + setTitle(R.string.main_songs_top_played); + } else if (MainFragment.SONGS_RECENT.equals(albumListType)) { + setTitle(R.string.main_songs_recent); + } else if (MainFragment.SONGS_FREQUENT.equals(albumListType)) { + setTitle(R.string.main_songs_frequent); } new LoadTask(true) { @@ -593,6 +572,8 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section } } else if("genres".equals(albumListType) || "genres-songs".equals(albumListType)) { result = service.getSongsByGenre(albumListExtra, size, 0, context, this); + } else if(albumListType.indexOf(MainFragment.SONGS_LIST_PREFIX) != -1) { + result = service.getSongList(albumListType, size, 0, context, this); } else { result = service.getAlbumList(albumListType, size, 0, refresh, context, this); } @@ -657,9 +638,19 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section } @Override - public void updateCache() { - if(entryGridAdapter != null) { + public void updateCache(int changeCode) { + if(entryGridAdapter != null && changeCode == CachedMusicService.CACHE_UPDATE_LIST) { entryGridAdapter.notifyDataSetChanged(); + } else if(changeCode == CachedMusicService.CACHE_UPDATE_METADATA) { + if(coverArtView != null && coverArtRep != null && !Util.equals(coverArtRep.getCoverArt(), coverArtId)) { + synchronized (coverArtRep) { + if (updateCoverArtTask != null && updateCoverArtTask.isRunning()) { + updateCoverArtTask.cancel(); + } + updateCoverArtTask = getImageLoader().loadImage(coverArtView, coverArtRep, false, true); + coverArtId = coverArtRep.getCoverArt(); + } + } } } } @@ -669,6 +660,21 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section return entryGridAdapter; } + @Override + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { + return new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + int viewType = entryGridAdapter.getItemViewType(position); + if(viewType == EntryGridAdapter.VIEW_TYPE_SONG || viewType == EntryGridAdapter.VIEW_TYPE_HEADER || viewType == EntryInfiniteGridAdapter.VIEW_TYPE_LOADING) { + return gridLayoutManager.getSpanCount(); + } else { + return 1; + } + } + }; + } + private void finishLoading() { boolean validData = !entries.isEmpty() || !albums.isEmpty(); if(!validData) { @@ -682,7 +688,6 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section if(albumListType == null || "starred".equals(albumListType)) { entryGridAdapter = new EntryGridAdapter(context, entries, getImageLoader(), largeAlbums); entryGridAdapter.setRemoveFromPlaylist(playlistId != null); - entryGridAdapter.setRemoveStarred(albumListType == null); } else { if("alphabeticalByName".equals(albumListType)) { entryGridAdapter = new AlphabeticalAlbumAdapter(context, entries, getImageLoader(), largeAlbums); @@ -728,11 +733,14 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section if(!artist) { entryGridAdapter.setShowArtist(true); } + if(topTracks) { + entryGridAdapter.setShowAlbum(true); + } // Show header if not album list type and not root and not artist // For Subsonic 5.1+ display a header for artists with getArtistInfo data if it exists boolean addedHeader = false; - if(albumListType == null && !"root".equals(id) && (!artist || artistInfo != null || artistInfoDelayed != null) && (share == null || entries.size() != albums.size())) { + if(albumListType == null && (!artist || artistInfo != null || artistInfoDelayed != null) && (share == null || entries.size() != albums.size())) { View header = createHeader(); if(header != null) { @@ -792,7 +800,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section Bundle args = getArguments(); boolean playAll = args.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false); if (playAll && !restoredInstance) { - playAll(args.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false), false); + playAll(args.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false), false, false); } } @@ -802,20 +810,19 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section if(!songs.isEmpty()) { download(songs, append, false, !append, playNext, shuffle); entryGridAdapter.clearSelected(); - } - else { - playAll(shuffle, append); + } else { + playAll(shuffle, append, playNext); } } - private void playAll(final boolean shuffle, final boolean append) { + private void playAll(final boolean shuffle, final boolean append, final boolean playNext) { boolean hasSubFolders = albums != null && !albums.isEmpty(); if (hasSubFolders && (id != null || share != null || "starred".equals(albumListType))) { - downloadRecursively(id, false, append, !append, shuffle, false); + downloadRecursively(id, false, append, !append, shuffle, false, playNext); } else if(hasSubFolders && albumListType != null) { - downloadRecursively(albums, shuffle, append); + downloadRecursively(albums, shuffle, append, playNext); } else { - download(entries, append, false, !append, false, shuffle); + download(entries, append, false, !append, playNext, shuffle); } } @@ -953,70 +960,28 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section }.execute(); } - public void unstarSelected() { - List<Entry> selected = getSelectedEntries(); - if(selected.size() == 0) { - selected = entries; - } - if(selected.size() == 0) { - return; - } - final List<Entry> unstar = new ArrayList<Entry>(); - unstar.addAll(selected); + @Override + protected void toggleSelectedStarred() { + UpdateHelper.OnStarChange onStarChange = null; + if(albumListType != null && "starred".equals(albumListType)) { + onStarChange = new UpdateHelper.OnStarChange() { + @Override + public void starChange(boolean starred) { - new LoadingTask<Void>(context, true) { - @Override - protected Void doInBackground() throws Throwable { - MusicService musicService = MusicServiceFactory.getMusicService(context); - List<Entry> entries = new ArrayList<Entry>(); - List<Entry> artists = new ArrayList<Entry>(); - List<Entry> albums = new ArrayList<Entry>(); - for(Entry entry: unstar) { - if(entry.isDirectory() && Util.isTagBrowsing(context)) { - if(entry.isAlbum()) { - albums.add(entry); - } else { - artists.add(entry); - } - } else { - entries.add(entry); - } } - musicService.setStarred(entries, artists, albums, false, this, context); - for(Entry entry: unstar) { - new UpdateHelper.EntryInstanceUpdater(entry) { - @Override - public void update(Entry found) { - found.setStarred(false); + @Override + public void starCommited(boolean starred) { + if(!starred) { + for (Entry entry : entries) { + entryGridAdapter.removeItem(entry); } - }.execute(); - } - - return null; - } - - @Override - protected void done(Void result) { - Util.toast(context, context.getResources().getString(R.string.starring_content_unstarred, Integer.toString(unstar.size()))); - - for(Entry entry: unstar) { - entryGridAdapter.removeItem(entry); - } - } - - @Override - protected void error(Throwable error) { - String msg; - if (error instanceof OfflineException || error instanceof ServerTooOldException) { - msg = getErrorMessage(error); - } else { - msg = context.getResources().getString(R.string.starring_content_error, Integer.toString(unstar.size())) + " " + getErrorMessage(error); + } } + }; + } - Util.toast(context, msg, false); - } - }.execute(); + UpdateHelper.toggleStarred(context, getSelectedEntries(), onStarChange); } private void checkLicenseAndTrialPeriod(LoadingTask onValid) { @@ -1105,11 +1070,8 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section @Override protected Void doInBackground() throws Throwable { DownloadService downloadService = getDownloadService(); + downloadService.clear(); downloadService.setArtistRadio(artistId); - if(downloadService.size() == 0) { - Log.e(TAG, "Failed to create artist radio"); - throw new Exception("Failed to create artist radio"); - } return null; } @@ -1159,22 +1121,22 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section }); imageLoader.loadImage(coverArtView, url, false); } else if(entries.size() > 0) { - Entry coverArt = null; - for (int i = 0; (i < 3) && (coverArt == null || coverArt.getCoverArt() == null); i++) { - coverArt = entries.get(random.nextInt(entries.size())); + coverArtRep = null; + this.coverArtView = coverArtView; + for (int i = 0; (i < 3) && (coverArtRep == null || coverArtRep.getCoverArt() == null); i++) { + coverArtRep = entries.get(random.nextInt(entries.size())); } - final Entry albumRep = coverArt; coverArtView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (albumRep.getCoverArt() == null) { + if (coverArtRep == null || coverArtRep.getCoverArt() == null) { return; } AlertDialog.Builder builder = new AlertDialog.Builder(context); ImageView fullScreenView = new ImageView(context); - imageLoader.loadImage(fullScreenView, albumRep, true, true); + imageLoader.loadImage(fullScreenView, coverArtRep, true, true); builder.setCancelable(true); AlertDialog imageDialog = builder.create(); @@ -1183,7 +1145,10 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section imageDialog.show(); } }); - imageLoader.loadImage(coverArtView, albumRep, false, true); + synchronized (coverArtRep) { + coverArtId = coverArtRep.getCoverArt(); + updateCoverArtTask = imageLoader.loadImage(coverArtView, coverArtRep, false, true); + } } coverArtView.setOnInvalidated(new RecyclingImageView.OnInvalidated() { @@ -1349,6 +1314,11 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section starButton.setImageResource(DrawableTint.getDrawableRes(context, R.attr.star_outline)); } } + + @Override + public void starCommited(boolean starred) { + + } }); } }); diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java index 6e2c9da5..28cf9911 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java @@ -184,6 +184,22 @@ public class SelectPlaylistFragment extends SelectRecyclerFragment<Playlist> { replaceFragment(fragment); } + @Override + public void onFinishRefresh() { + Bundle args = getArguments(); + if(args != null) { + String playlistId = args.getString(Constants.INTENT_EXTRA_NAME_ID, null); + if (playlistId != null && objects != null) { + for (Playlist playlist : objects) { + if (playlistId.equals(playlist.getId())) { + onItemClicked(null, playlist); + break; + } + } + } + } + } + private void deletePlaylist(final Playlist playlist) { Util.confirmDialog(context, R.string.common_delete, playlist.getName(), new DialogInterface.OnClickListener() { @Override diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java index 3f8f7844..9011b4c5 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java @@ -154,25 +154,18 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> if(newestEpisodes == null || newestEpisodes.getChildrenSize() == 0) { return new PodcastChannelAdapter(context, channels, hasCoverArt ? getImageLoader() : null, this, largeAlbums); } else { - List<String> headers = Arrays.asList(PodcastChannelAdapter.EPISODE_HEADER, PodcastChannelAdapter.CHANNEL_HEADER); + Resources res = context.getResources(); + List<String> headers = Arrays.asList(res.getString(R.string.main_albums_newest), res.getString(R.string.select_podcasts_channels)); List<MusicDirectory.Entry> episodes = newestEpisodes.getChildren(false, true); List<Serializable> serializableEpisodes = new ArrayList<>(); - - // Put 3 in current list - while(serializableEpisodes.size() < 3 && !episodes.isEmpty()) { - serializableEpisodes.add(episodes.remove(0)); - } - - // Put rest in extra set - List<Serializable> extraEpisodes = new ArrayList<>(); - extraEpisodes.addAll(episodes); + serializableEpisodes.addAll(episodes); List<List<Serializable>> sections = new ArrayList<>(); sections.add(serializableEpisodes); sections.add(channels); - return new PodcastChannelAdapter(context, headers, sections, extraEpisodes, ServerInfo.checkServerVersion(context, "1.13") ? getImageLoader() : null, this, largeAlbums); + return new PodcastChannelAdapter(context, headers, sections, ServerInfo.checkServerVersion(context, "1.13") ? getImageLoader() : null, this, largeAlbums); } } @@ -182,7 +175,7 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> if(!Util.isOffline(context) && ServerInfo.hasNewestPodcastEpisodes(context)) { try { - newestEpisodes = musicService.getNewestPodcastEpisodes(10, context, listener); + newestEpisodes = musicService.getNewestPodcastEpisodes(refresh, context, listener, 10); for(MusicDirectory.Entry entry: newestEpisodes.getChildren()) { for(PodcastChannel channel: channels) { @@ -246,12 +239,12 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> return; } - playNow(Arrays.asList((MusicDirectory.Entry) episode)); + onSongPress(Arrays.asList((MusicDirectory.Entry) episode), episode, false); } } @Override - public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() { + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { return new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { @@ -259,7 +252,7 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> if(adapter != null) { int viewType = getCurrentAdapter().getItemViewType(position); if (viewType == SectionAdapter.VIEW_TYPE_HEADER || viewType == PodcastChannelAdapter.VIEW_TYPE_PODCAST_EPISODE || viewType == PodcastChannelAdapter.VIEW_TYPE_PODCAST_LEGACY) { - return getRecyclerColumnCount(); + return gridLayoutManager.getSpanCount(); } else { return 1; } @@ -270,6 +263,25 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> }; } + @Override + public void onFinishRefresh() { + Bundle args = getArguments(); + if(args != null) { + String podcastId = args.getString(Constants.INTENT_EXTRA_NAME_ID, null); + if (podcastId != null && objects != null) { + for (Serializable ser : objects) { + if (ser instanceof PodcastChannel) { + PodcastChannel podcast = (PodcastChannel) ser; + if (podcastId.equals(podcast.getId())) { + onItemClicked(null, podcast); + break; + } + } + } + } + } + } + public void refreshPodcasts() { new SilentBackgroundTask<Void>(context) { @Override diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java index 7ae7fff8..0d4506ac 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectRecyclerFragment.java @@ -15,10 +15,15 @@ package github.daneren2005.dsub.fragments; +import android.app.SearchManager; +import android.app.SearchableInfo; +import android.content.Context; import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -102,6 +107,7 @@ public abstract class SelectRecyclerFragment<T> extends SubsonicFragment impleme } menuInflater.inflate(getOptionsMenu(), menu); + onFinishSetupOptionsMenu(menu); } @Override diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java index cb0e48b9..f231fa33 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectShareFragment.java @@ -27,6 +27,7 @@ import android.widget.CompoundButton; import android.widget.DatePicker; import android.widget.EditText; +import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -104,10 +105,34 @@ public class SelectShareFragment extends SelectRecyclerFragment<Share> { } private void displayShareInfo(final Share share) { - String message = context.getResources().getString(R.string.share_info, - share.getUsername(), (share.getDescription() != null) ? share.getDescription() : "", share.getUrl(), - Util.formatDate(share.getCreated()), Util.formatDate(share.getLastVisited()), Util.formatDate(share.getExpires()), share.getVisitCount()); - Util.info(context, share.getName(), message); + List<Integer> headers = new ArrayList<>(); + List<String> details = new ArrayList<>(); + + headers.add(R.string.details_title); + details.add(share.getName()); + + headers.add(R.string.details_owner); + details.add(share.getUsername()); + + headers.add(R.string.details_description); + details.add(share.getDescription()); + + headers.add(R.string.details_url); + details.add(share.getUrl()); + + headers.add(R.string.details_created); + details.add(Util.formatDate(share.getCreated())); + + headers.add(R.string.details_last_played); + details.add(Util.formatDate(share.getLastVisited())); + + headers.add(R.string.details_expiration); + details.add(Util.formatDate(share.getExpires(), false)); + + headers.add(R.string.details_played_count); + details.add(Long.toString(share.getVisitCount())); + + Util.showDetailsDialog(context, R.string.details_title_playlist, headers, details); } private void updateShareInfo(final Share share) { diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java index 9853e046..ad064d01 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SettingsFragment.java @@ -56,6 +56,7 @@ import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.LoadingTask; import github.daneren2005.dsub.util.SyncUtil; import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.view.CacheLocationPreference; import github.daneren2005.dsub.view.ErrorDialog; public class SettingsFragment extends PreferenceCompatFragment implements SharedPreferences.OnSharedPreferenceChangeListener { @@ -69,7 +70,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared private ListPreference maxVideoBitrateWifi; private ListPreference maxVideoBitrateMobile; private ListPreference networkTimeout; - private EditTextPreference cacheLocation; + private CacheLocationPreference cacheLocation; private ListPreference preloadCountWifi; private ListPreference preloadCountMobile; private ListPreference keepPlayedCount; @@ -77,6 +78,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared private ListPreference pauseDisconnect; private Preference addServerPreference; private PreferenceCategory serversCategory; + private ListPreference songPressAction; private ListPreference videoPlayer; private ListPreference syncInterval; private CheckBoxPreference syncEnabled; @@ -137,6 +139,8 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared xml = R.xml.settings_playback; } else if("servers".equals(name)) { xml = R.xml.settings_servers; + } else if ("cast".equals(name)) { + xml = R.xml.settings_cast; } if(xml != 0) { @@ -205,7 +209,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared maxVideoBitrateWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI); maxVideoBitrateMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE); networkTimeout = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT); - cacheLocation = (EditTextPreference) this.findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION); + cacheLocation = (CacheLocationPreference) this.findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION); preloadCountWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_WIFI); preloadCountMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_MOBILE); keepPlayedCount = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_KEEP_PLAYED_CNT); @@ -214,6 +218,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared serversCategory = (PreferenceCategory) this.findPreference(Constants.PREFERENCES_KEY_SERVER_KEY); addServerPreference = this.findPreference(Constants.PREFERENCES_KEY_SERVER_ADD); videoPlayer = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER); + songPressAction = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_SONG_PRESS_ACTION); syncInterval = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_INTERVAL); syncEnabled = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_ENABLED); syncWifi = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_WIFI); @@ -350,6 +355,8 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared if(theme != null) { theme.setSummary(theme.getEntry()); + } + if(openToTab != null) { openToTab.setSummary(openToTab.getEntry()); } @@ -379,6 +386,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared keepPlayedCount.setSummary(keepPlayedCount.getEntry()); tempLoss.setSummary(tempLoss.getEntry()); pauseDisconnect.setSummary(pauseDisconnect.getEntry()); + songPressAction.setSummary(songPressAction.getEntry()); videoPlayer.setSummary(videoPlayer.getEntry()); if(replayGain.isChecked()) { @@ -633,7 +641,7 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared } catch(Exception e) { Log.w(TAG, "Failed to create " + musicNoMedia, e); } - } else if (nomediaDir.exists()) { + } else if (!hide && nomediaDir.exists()) { if (!nomediaDir.delete()) { Log.w(TAG, "Failed to delete " + nomediaDir); } @@ -664,8 +672,11 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared SharedPreferences.Editor editor = prefs.edit(); editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultPath); editor.commit(); - cacheLocation.setSummary(defaultPath); - cacheLocation.setText(defaultPath); + + if(cacheLocation != null) { + cacheLocation.setSummary(defaultPath); + cacheLocation.setText(defaultPath); + } } // Clear download queue. diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java index 93e3a93a..a41b9d6f 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SimilarArtistFragment.java @@ -15,14 +15,16 @@ package github.daneren2005.dsub.fragments; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import github.daneren2005.dsub.R; -import github.daneren2005.dsub.adapter.ArtistAdapter; import github.daneren2005.dsub.adapter.SectionAdapter; +import github.daneren2005.dsub.adapter.SimilarArtistAdapter; import github.daneren2005.dsub.domain.Artist; import github.daneren2005.dsub.domain.ArtistInfo; import github.daneren2005.dsub.domain.MusicDirectory; @@ -35,6 +37,8 @@ import github.daneren2005.dsub.util.Util; import github.daneren2005.dsub.view.UpdateView; import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -52,18 +56,6 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { } @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { - super.onCreateOptionsMenu(menu, menuInflater); - if(!primaryFragment) { - return; - } - - if(info.getMissingArtists().isEmpty()) { - menu.removeItem(R.id.menu_show_missing); - } - } - - @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_play_now: @@ -72,9 +64,6 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { case R.id.menu_shuffle: playAll(true); return true; - case R.id.menu_show_missing: - showMissingArtists(); - break; } return super.onOptionsItemSelected(item); @@ -82,8 +71,10 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { @Override public void onCreateContextMenu(Menu menu, MenuInflater menuInflater, UpdateView<Artist> updateView, Artist item) { - onCreateContextMenuSupport(menu, menuInflater, updateView, item); - recreateContextMenu(menu); + if(!Artist.MISSING_ID.equals(item.getId())) { + onCreateContextMenuSupport(menu, menuInflater, updateView, item); + recreateContextMenu(menu); + } } @Override @@ -93,14 +84,21 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { @Override public void onItemClicked(UpdateView<Artist> updateView, Artist artist) { - SubsonicFragment fragment = new SelectDirectoryFragment(); - Bundle args = new Bundle(); - args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); - args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); - args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); - fragment.setArguments(args); - - replaceFragment(fragment); + if(Artist.MISSING_ID.equals(artist.getId())) { + String url = "http://www.last.fm/music/" + URLEncoder.encode(artist.getName()); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + startActivity(intent); + } else { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + fragment.setArguments(args); + + replaceFragment(fragment); + } } @Override @@ -109,8 +107,22 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { } @Override - public SectionAdapter getAdapter(List<Artist> objects) { - return new ArtistAdapter(context, objects, this); + public SectionAdapter getAdapter(List<Artist> artists) { + if(info.getMissingArtists().isEmpty()) { + return new SimilarArtistAdapter(context, artists, this); + } else { + List<String> headers = new ArrayList<>(); + headers.add(null); + headers.add(context.getResources().getString(R.string.menu_similar_artists_missing)); + + List<Artist> missingArtists = new ArrayList<>(); + for(String artistName: info.getMissingArtists()) { + Artist artist = new Artist(Artist.MISSING_ID, artistName); + missingArtists.add(artist); + } + + return new SimilarArtistAdapter(context, headers, Arrays.asList(artists, missingArtists), this); + } } @Override @@ -124,16 +136,6 @@ public class SimilarArtistFragment extends SelectRecyclerFragment<Artist> { return R.string.menu_similar_artists; } - private void showMissingArtists() { - StringBuilder b = new StringBuilder(); - - for(String name: info.getMissingArtists()) { - b.append("<h3><a href=\"https://www.google.com/#q=" + URLEncoder.encode(name) + "\">" + name + "</a></h3> "); - } - - Util.showHTMLDialog(context, R.string.menu_similar_artists, b.toString()); - } - private void playAll(final boolean shuffle) { new RecursiveLoader(context) { @Override diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java index c503ec6c..b7478c8a 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java @@ -20,6 +20,9 @@ package github.daneren2005.dsub.fragments; import android.annotation.TargetApi; import android.app.Activity; +import android.app.SearchManager; +import android.app.SearchableInfo; +import android.support.v4.view.MenuItemCompat; import android.support.v7.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; @@ -37,6 +40,7 @@ import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; import android.util.Log; import android.view.GestureDetector; import android.view.Menu; @@ -125,6 +129,8 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR protected boolean artistOverride = false; protected SwipeRefreshLayout refreshLayout; protected boolean firstRun; + protected MenuItem searchItem; + protected SearchView searchView; public SubsonicFragment() { super(); @@ -177,15 +183,36 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR this.context = context; } + protected void onFinishSetupOptionsMenu(final Menu menu) { + searchItem = menu.findItem(R.id.menu_global_search); + if(searchItem != null) { + searchView = (SearchView) MenuItemCompat.getActionView(searchItem); + SearchManager searchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE); + SearchableInfo searchableInfo = searchManager.getSearchableInfo(context.getComponentName()); + if(searchableInfo == null) { + Log.w(TAG, "Failed to get SearchableInfo"); + } else { + searchView.setSearchableInfo(searchableInfo); + } + + String currentQuery = getCurrentQuery(); + if(currentQuery != null) { + searchView.setOnSearchClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + searchView.setQuery(getCurrentQuery(), false); + } + }); + } + } + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_global_shuffle: onShuffleRequested(); return true; - case R.id.menu_global_search: - context.onSearchRequested(); - return true; case R.id.menu_exit: exit(); return true; @@ -221,6 +248,9 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR addToPlaylist(songs); clearSelected(); return true; + case R.id.menu_star:case R.id.menu_unstar: + toggleSelectedStarred(); + return true; } return false; @@ -269,7 +299,6 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR menu.removeItem(R.id.menu_rate); } } - menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star).setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star); } else if(!entry.isVideo()) { if(Util.isOffline(context)) { menuInflater.inflate(R.menu.select_song_context_offline, menu); @@ -281,7 +310,6 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR menu.removeItem(R.id.bookmark_menu_delete); } } - menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star).setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star); } else { if(Util.isOffline(context)) { menuInflater.inflate(R.menu.select_video_context_offline, menu); @@ -290,6 +318,15 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR menuInflater.inflate(R.menu.select_video_context, menu); } } + + MenuItem starMenu = menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star); + if(starMenu != null) { + starMenu.setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star); + } + + if(!isShowArtistEnabled() || (!Util.isTagBrowsing(context) && entry.getParent() == null) || (Util.isTagBrowsing(context) && entry.getArtistId() == null)) { + menu.setGroupVisible(R.id.hide_show_artist, false); + } } else if(selected instanceof Artist) { Artist artist = (Artist) selected; if(Util.isOffline(context)) { @@ -323,6 +360,9 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR public boolean onContextItemSelected(MenuItem menuItem, Object selectedItem) { Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null; Entry entry = selectedItem instanceof Entry ? (Entry) selectedItem : null; + if(selectedItem instanceof DownloadFile) { + entry = ((DownloadFile) selectedItem).getSong(); + } List<Entry> songs = new ArrayList<Entry>(1); songs.add(entry); @@ -656,7 +696,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR final int columns = getRecyclerColumnCount(); GridLayoutManager gridLayoutManager = new GridLayoutManager(context, columns); - GridLayoutManager.SpanSizeLookup spanSizeLookup = getSpanSizeLookup(); + GridLayoutManager.SpanSizeLookup spanSizeLookup = getSpanSizeLookup(gridLayoutManager); if(spanSizeLookup != null) { gridLayoutManager.setSpanSizeLookup(spanSizeLookup); } @@ -671,15 +711,15 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR layoutManager.setOrientation(LinearLayoutManager.VERTICAL); return layoutManager; } - public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() { + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { return new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { SectionAdapter adapter = getCurrentAdapter(); if(adapter != null) { - int viewType = getCurrentAdapter().getItemViewType(position); + int viewType = adapter.getItemViewType(position); if (viewType == SectionAdapter.VIEW_TYPE_HEADER) { - return getRecyclerColumnCount(); + return gridLayoutManager.getSpanCount(); } else { return 1; } @@ -722,6 +762,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR if(downloadService == null) { return; } + downloadService.clear(); downloadService.setShufflePlayEnabled(true); context.openNowPlaying(); return; @@ -825,6 +866,8 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR if (downloadService == null) { return; } + + downloadService.clear(); downloadService.setShufflePlayEnabled(true); context.openNowPlaying(); } @@ -909,7 +952,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR }.execute(); } - protected void downloadRecursively(final List<Entry> albums, final boolean shuffle, final boolean append) { + protected void downloadRecursively(final List<Entry> albums, final boolean shuffle, final boolean append, final boolean playNext) { new RecursiveLoader(context) { @Override protected Boolean doInBackground() throws Throwable { @@ -937,7 +980,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR downloadService.clear(); } - downloadService.download(songs, false, true, false, false); + downloadService.download(songs, false, true, playNext, false); if(!append) { transition = true; } @@ -965,6 +1008,14 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } protected void addToPlaylist(final List<Entry> songs) { + Iterator<Entry> it = songs.iterator(); + while(it.hasNext()) { + Entry entry = it.next(); + if(entry.isDirectory()) { + it.remove(); + } + } + if(songs.isEmpty()) { Util.toast(context, "No songs selected"); return; @@ -1572,7 +1623,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR .setPositiveButton(R.string.bookmark_action_resume, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { - playNow(songs, song, position); + playNow(songs, song, position, playlistName, playlistId); } }) .setNegativeButton(R.string.bookmark_action_start_over, new DialogInterface.OnClickListener() { @@ -1605,13 +1656,40 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } }.execute(); - playNow(songs, 0); + playNow(songs, 0, playlistName, playlistId); } }); AlertDialog dialog = builder.create(); dialog.show(); } + protected void onSongPress(List<Entry> entries, Entry entry) { + onSongPress(entries, entry, 0, true); + } + protected void onSongPress(List<Entry> entries, Entry entry, boolean allowPlayAll) { + onSongPress(entries, entry, 0, allowPlayAll); + } + protected void onSongPress(List<Entry> entries, Entry entry, int position, boolean allowPlayAll) { + List<Entry> songs = new ArrayList<Entry>(); + + String songPressAction = Util.getSongPressAction(context); + if("all".equals(songPressAction) && allowPlayAll) { + for(Entry song: entries) { + if(!song.isDirectory() && !song.isVideo()) { + songs.add(song); + } + } + playNow(songs, entry, position); + } else if("next".equals(songPressAction)) { + getDownloadService().download(Arrays.asList(entry), false, false, true, false); + } else if("last".equals(songPressAction)) { + getDownloadService().download(Arrays.asList(entry), false, false, false, false); + } else { + songs.add(entry); + playNow(songs); + } + } + protected void playNow(List<Entry> entries) { playNow(entries, null, null); } @@ -1659,15 +1737,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR new LoadingTask<Void>(context) { @Override protected Void doInBackground() throws Throwable { - DownloadService downloadService = getDownloadService(); - if(downloadService == null) { - return null; - } - - downloadService.clear(); - downloadService.download(entries, false, true, true, false, entries.indexOf(song), position); - downloadService.setSuggestedPlaylistName(playlistName, playlistId); - + playNowInTask(entries, song, position, playlistName, playlistId); return null; } @@ -1677,6 +1747,19 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } }.execute(); } + protected void playNowInTask(final List<Entry> entries, final Entry song, final int position) { + playNowInTask(entries, song, position, null, null); + } + protected void playNowInTask(final List<Entry> entries, final Entry song, final int position, final String playlistName, final String playlistId) { + DownloadService downloadService = getDownloadService(); + if(downloadService == null) { + return; + } + + downloadService.clear(); + downloadService.download(entries, false, true, true, false, entries.indexOf(song), position); + downloadService.setSuggestedPlaylistName(playlistName, playlistId); + } protected void deleteBookmark(final MusicDirectory.Entry entry, final SectionAdapter adapter) { Util.confirmDialog(context, R.string.bookmark_delete_title, entry.getTitle(), new DialogInterface.OnClickListener() { @@ -1691,7 +1774,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR MusicService musicService = MusicServiceFactory.getMusicService(context); musicService.deleteBookmark(entry, context, null); - new UpdateHelper.EntryInstanceUpdater(entry) { + new UpdateHelper.EntryInstanceUpdater(entry, DownloadService.METADATA_UPDATED_BOOKMARK) { @Override public void update(Entry found) { found.setBookmark(null); @@ -1898,6 +1981,18 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } } + protected void toggleSelectedStarred() { + UpdateHelper.toggleStarred(context, getSelectedEntries()); + } + + protected boolean isShowArtistEnabled() { + return false; + } + + protected String getCurrentQuery() { + return null; + } + public abstract class RecursiveLoader extends LoadingTask<Boolean> { protected MusicService musicService; protected static final int MAX_SONGS = 500; @@ -1909,6 +2004,23 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR musicService = MusicServiceFactory.getMusicService(context); } + protected void getSiblingsRecursively(Entry entry) throws Exception { + MusicDirectory parent = new MusicDirectory(); + if(Util.isTagBrowsing(context) && !Util.isOffline(context)) { + parent.setId(entry.getAlbumId()); + } else { + parent.setId(entry.getParent()); + } + + if(parent.getId() == null) { + songs.add(entry); + } else { + MusicDirectory.Entry dir = new Entry(parent.getId()); + dir.setDirectory(true); + parent.addChild(dir); + getSongsRecursively(parent, songs); + } + } protected void getSongsRecursively(List<Entry> entry) throws Exception { getSongsRecursively(entry, false); } diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java index dab104bd..dde76624 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/UserFragment.java @@ -20,6 +20,8 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import github.daneren2005.dsub.R; @@ -78,7 +80,7 @@ public class UserFragment extends SelectRecyclerFragment<User.Setting>{ @Override public SectionAdapter<User.Setting> getAdapter(List<User.Setting> objs) { - return new SettingsAdapter(context, user, getImageLoader(), UserUtil.isCurrentAdmin() && ServerInfo.checkServerVersion(context, "1.10"), this); + return SettingsAdapter.getSettingsAdapter(context, user, getImageLoader(), this); } @Override diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java index c5632362..0f6975ba 100644 --- a/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java +++ b/app/src/main/java/github/daneren2005/dsub/provider/DLNARouteProvider.java @@ -166,7 +166,7 @@ public class DLNARouteProvider extends MediaRouteProvider { DLNADevice device = deviceEntry.getValue(); int volume; - if(device.volumeMax == 0) { + if(device.volumeMax <= 0) { volume = 5; } else { int increments = (int) Math.ceil(device.volumeMax / 10.0); diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java index f91c364e..ba8c80c1 100644 --- a/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java +++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubSearchProvider.java @@ -58,6 +58,10 @@ public class DSubSearchProvider extends ContentProvider { @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + if(selectionArgs[0].isEmpty()) { + return null; + } + String query = selectionArgs[0] + "*"; SearchResult searchResult = search(query); return createCursor(selectionArgs[0], searchResult); diff --git a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java index 18660fa2..5c90c250 100644 --- a/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java +++ b/app/src/main/java/github/daneren2005/dsub/provider/DSubWidgetProvider.java @@ -278,7 +278,7 @@ public class DSubWidgetProvider extends AppWidgetProvider { private void linkButtons(Context context, RemoteViews views, boolean playerActive) { Intent intent = new Intent(context, SubsonicFragmentActivity.class); intent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent); views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent); diff --git a/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java b/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java index d579ef54..ece8ee95 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java @@ -33,7 +33,11 @@ import java.util.ArrayList; import java.util.List; import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.Playlist; +import github.daneren2005.dsub.domain.PodcastChannel; +import github.daneren2005.dsub.domain.PodcastEpisode; import github.daneren2005.dsub.domain.ServerInfo; import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.SilentBackgroundTask; @@ -48,8 +52,12 @@ public class AutoMediaBrowserService extends MediaBrowserService { private static final String BROWSER_ALBUM_LISTS = "albumLists"; private static final String BROWSER_LIBRARY = "library"; private static final String BROWSER_PLAYLISTS = "playlists"; + private static final String BROWSER_PODCASTS = "podcasts"; + private static final String BROWSER_BOOKMARKS = "bookmarks"; private static final String PLAYLIST_PREFIX = "pl-"; + private static final String PODCAST_PREFIX = "po-"; private static final String ALBUM_TYPE_PREFIX = "ty-"; + private static final String MUSIC_DIRECTORY_PREFIX = "md-"; private DownloadService downloadService; private Handler handler = new Handler(); @@ -76,12 +84,23 @@ public class AutoMediaBrowserService extends MediaBrowserService { } else if(parentId.startsWith(ALBUM_TYPE_PREFIX)) { int id = Integer.valueOf(parentId.substring(ALBUM_TYPE_PREFIX.length())); getAlbumList(result, id); + } else if(parentId.startsWith(MUSIC_DIRECTORY_PREFIX)) { + String id = parentId.substring(MUSIC_DIRECTORY_PREFIX.length()); + getPlayOptions(result, id, Constants.INTENT_EXTRA_NAME_ID); } else if(BROWSER_LIBRARY.equals(parentId)) { getLibrary(result); } else if(BROWSER_PLAYLISTS.equals(parentId)) { getPlaylists(result); } else if(parentId.startsWith(PLAYLIST_PREFIX)) { - getPlayOptions(result, parentId.substring(PLAYLIST_PREFIX.length()), Constants.INTENT_EXTRA_NAME_PLAYLIST_ID); + String id = parentId.substring(PLAYLIST_PREFIX.length()); + getPlayOptions(result, id, Constants.INTENT_EXTRA_NAME_PLAYLIST_ID); + } else if(BROWSER_PODCASTS.equals(parentId)) { + getPodcasts(result); + } else if(parentId.startsWith(PODCAST_PREFIX)) { + String id = parentId.substring(PODCAST_PREFIX.length()); + getPodcastEpisodes(result, id); + } else if(BROWSER_BOOKMARKS.equals(parentId)) { + getBookmarks(result); } else { // No idea what it is, send empty result result.sendResult(new ArrayList<MediaBrowser.MediaItem>()); @@ -91,12 +110,12 @@ public class AutoMediaBrowserService extends MediaBrowserService { private void getRootFolders(Result<List<MediaBrowser.MediaItem>> result) { List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); - /*MediaDescription.Builder albumLists = new MediaDescription.Builder(); + MediaDescription.Builder albumLists = new MediaDescription.Builder(); albumLists.setTitle(downloadService.getString(R.string.main_albums_title)) .setMediaId(BROWSER_ALBUM_LISTS); mediaItems.add(new MediaBrowser.MediaItem(albumLists.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); - MediaDescription.Builder library = new MediaDescription.Builder(); + /*MediaDescription.Builder library = new MediaDescription.Builder(); library.setTitle(downloadService.getString(R.string.button_bar_browse)) .setMediaId(BROWSER_LIBRARY); mediaItems.add(new MediaBrowser.MediaItem(library.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE));*/ @@ -106,6 +125,20 @@ public class AutoMediaBrowserService extends MediaBrowserService { .setMediaId(BROWSER_PLAYLISTS); mediaItems.add(new MediaBrowser.MediaItem(playlists.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); + if(Util.getPreferences(downloadService).getBoolean(Constants.PREFERENCES_KEY_PODCASTS_ENABLED, true)) { + MediaDescription.Builder podcasts = new MediaDescription.Builder(); + podcasts.setTitle(downloadService.getString(R.string.button_bar_podcasts)) + .setMediaId(BROWSER_PODCASTS); + mediaItems.add(new MediaBrowser.MediaItem(podcasts.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + + if(Util.getPreferences(downloadService).getBoolean(Constants.PREFERENCES_KEY_BOOKMARKS_ENABLED, true)) { + MediaDescription.Builder podcasts = new MediaDescription.Builder(); + podcasts.setTitle(downloadService.getString(R.string.button_bar_bookmarks)) + .setMediaId(BROWSER_BOOKMARKS); + mediaItems.add(new MediaBrowser.MediaItem(podcasts.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + result.sendResult(mediaItems); } @@ -113,15 +146,10 @@ public class AutoMediaBrowserService extends MediaBrowserService { List<Integer> albums = new ArrayList<>(); albums.add(R.string.main_albums_newest); albums.add(R.string.main_albums_random); - if(ServerInfo.checkServerVersion(downloadService, "1.8")) { - albums.add(R.string.main_albums_alphabetical); - } if(!Util.isTagBrowsing(downloadService)) { albums.add(R.string.main_albums_highest); } - // albums.add(R.string.main_albums_starred); - // albums.add(R.string.main_albums_genres); - // albums.add(R.string.main_albums_year); + albums.add(R.string.main_albums_starred); albums.add(R.string.main_albums_recent); albums.add(R.string.main_albums_frequent); @@ -138,8 +166,56 @@ public class AutoMediaBrowserService extends MediaBrowserService { result.sendResult(mediaItems); } - private void getAlbumList(Result<List<MediaBrowser.MediaItem>> result, int id) { + private void getAlbumList(final Result<List<MediaBrowser.MediaItem>> result, final int id) { + new SilentServiceTask<MusicDirectory>(downloadService) { + @Override + protected MusicDirectory doInBackground(MusicService musicService) throws Throwable { + String albumListType; + switch(id) { + case R.string.main_albums_newest: + albumListType = "newest"; + break; + case R.string.main_albums_random: + albumListType = "random"; + break; + case R.string.main_albums_highest: + albumListType = "highest"; + break; + case R.string.main_albums_starred: + albumListType = "starred"; + break; + case R.string.main_albums_recent: + albumListType = "recent"; + break; + case R.string.main_albums_frequent: + albumListType = "frequent"; + break; + default: + albumListType = "newest"; + } + + return musicService.getAlbumList(albumListType, 20, 0, true, downloadService, null); + } + + @Override + protected void done(MusicDirectory albumSet) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(Entry album: albumSet.getChildren(true, false)) { + MediaDescription description = new MediaDescription.Builder() + .setTitle(album.getAlbumDisplay()) + .setSubtitle(album.getArtist()) + .setMediaId(MUSIC_DIRECTORY_PREFIX + album.getId()) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + + result.sendResult(mediaItems); + } + }.execute(); + result.detach(); } private void getLibrary(Result<List<MediaBrowser.MediaItem>> result) { @@ -172,6 +248,100 @@ public class AutoMediaBrowserService extends MediaBrowserService { result.detach(); } + + private void getPodcasts(final Result<List<MediaBrowser.MediaItem>> result) { + new SilentServiceTask<List<PodcastChannel>>(downloadService) { + @Override + protected List<PodcastChannel> doInBackground(MusicService musicService) throws Throwable { + return musicService.getPodcastChannels(false, downloadService, null); + } + + @Override + protected void done(List<PodcastChannel> podcasts) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(PodcastChannel podcast: podcasts) { + MediaDescription description = new MediaDescription.Builder() + .setTitle(podcast.getName()) + .setMediaId(PODCAST_PREFIX + podcast.getId()) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE)); + } + + result.sendResult(mediaItems); + } + }.execute(); + + result.detach(); + } + private void getPodcastEpisodes(final Result<List<MediaBrowser.MediaItem>> result, final String podcastId) { + new SilentServiceTask<MusicDirectory>(downloadService) { + @Override + protected MusicDirectory doInBackground(MusicService musicService) throws Throwable { + return musicService.getPodcastEpisodes(false, podcastId, downloadService, null); + } + + @Override + protected void done(MusicDirectory podcasts) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(Entry entry: podcasts.getChildren(false, true)) { + PodcastEpisode podcast = (PodcastEpisode) entry; + Bundle podcastExtras = new Bundle(); + podcastExtras.putSerializable(Constants.INTENT_EXTRA_ENTRY, podcast); + podcastExtras.putString(Constants.INTENT_EXTRA_NAME_PODCAST_ID, podcast.getId()); + + MediaDescription description = new MediaDescription.Builder() + .setTitle(podcast.getTitle()) + .setSubtitle(Util.formatDate(downloadService, podcast.getDate(), false)) + .setMediaId(PODCAST_PREFIX + podcast.getId()) + .setExtras(podcastExtras) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE)); + } + + result.sendResult(mediaItems); + } + }.execute(); + + result.detach(); + } + + private void getBookmarks(final Result<List<MediaBrowser.MediaItem>> result) { + new SilentServiceTask<MusicDirectory>(downloadService) { + @Override + protected MusicDirectory doInBackground(MusicService musicService) throws Throwable { + return musicService.getBookmarks(false, downloadService, null); + } + + @Override + protected void done(MusicDirectory bookmarkList) { + List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); + + for(Entry entry: bookmarkList.getChildren(false, true)) { + Bundle extras = new Bundle(); + extras.putSerializable(Constants.INTENT_EXTRA_ENTRY, entry); + extras.putString(Constants.INTENT_EXTRA_NAME_CHILD_ID, entry.getId()); + + MediaDescription description = new MediaDescription.Builder() + .setTitle(entry.getTitle()) + .setSubtitle(Util.formatDuration(entry.getBookmark().getPosition() / 1000)) + .setMediaId(entry.getId()) + .setExtras(extras) + .build(); + + mediaItems.add(new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE)); + } + + result.sendResult(mediaItems); + } + }.execute(); + + result.detach(); + } + private void getPlayOptions(Result<List<MediaBrowser.MediaItem>> result, String id, String idConstant) { List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); @@ -194,7 +364,7 @@ public class AutoMediaBrowserService extends MediaBrowserService { .setExtras(shuffleExtras); mediaItems.add(new MediaBrowser.MediaItem(shuffle.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE)); - /*Bundle playLastExtras = new Bundle(); + Bundle playLastExtras = new Bundle(); playLastExtras.putString(idConstant, id); playLastExtras.putBoolean(Constants.INTENT_EXTRA_PLAY_LAST, true); @@ -202,7 +372,7 @@ public class AutoMediaBrowserService extends MediaBrowserService { playLast.setTitle(downloadService.getString(R.string.menu_play_last)) .setMediaId("playLast-" + id) .setExtras(playLastExtras); - mediaItems.add(new MediaBrowser.MediaItem(playLast.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE));*/ + mediaItems.add(new MediaBrowser.MediaItem(playLast.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE)); result.sendResult(mediaItems); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java index 53433f5c..1a17dfb3 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java @@ -69,6 +69,9 @@ public class CachedMusicService implements MusicService { private static final int MUSIC_DIR_CACHE_SIZE = 20; private static final int TTL_MUSIC_DIR = 5 * 60; // Five minutes + public static final int CACHE_UPDATE_LIST = 1; + public static final int CACHE_UPDATE_METADATA = 2; + private static final int CACHED_LAST_FM = 24 * 60; private final RESTMusicService musicService; private final TimeLimitedCache<Boolean> cachedLicenseValid = new TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS); @@ -121,11 +124,13 @@ public class CachedMusicService implements MusicService { if(!refresh) { result = FileUtil.deserialize(context, getCacheName(context, "musicFolders"), ArrayList.class); } - + if(result == null) { result = musicService.getMusicFolders(refresh, context, progressListener); FileUtil.serialize(context, new ArrayList<MusicFolder>(result), getCacheName(context, "musicFolders")); } + + MusicFolder.sort(result); cachedMusicFolders.set(result); } return result; @@ -150,7 +155,7 @@ public class CachedMusicService implements MusicService { if(!refresh) { result = FileUtil.deserialize(context, name, Indexes.class); } - + if(result == null) { result = musicService.getIndexes(musicFolderId, refresh, context, progressListener); FileUtil.serialize(context, result, name); @@ -169,12 +174,13 @@ public class CachedMusicService implements MusicService { new SilentBackgroundTask<Void>(context) { MusicDirectory refreshed; + private boolean metadataUpdated; @Override protected Void doInBackground() throws Throwable { refreshed = musicService.getMusicDirectory(id, name, true, context, null); updateAllSongs(context, refreshed); - cached.updateMetadata(refreshed); + metadataUpdated = cached.updateMetadata(refreshed); deleteRemovedEntries(context, refreshed, cached); FileUtil.serialize(context, refreshed, getCacheName(context, "directory", id)); return null; @@ -185,7 +191,10 @@ public class CachedMusicService implements MusicService { public void done(Void result) { if(progressListener != null) { if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { - progressListener.updateCache(); + progressListener.updateCache(CACHE_UPDATE_LIST); + } + if(metadataUpdated) { + progressListener.updateCache(CACHE_UPDATE_METADATA); } } } @@ -201,7 +210,7 @@ public class CachedMusicService implements MusicService { dir = musicService.getMusicDirectory(id, name, refresh, context, progressListener); updateAllSongs(context, dir); FileUtil.serialize(context, dir, getCacheName(context, "directory", id)); - + // If a cached copy exists to check against, look for removes deleteRemovedEntries(context, dir, cached); } @@ -234,7 +243,7 @@ public class CachedMusicService implements MusicService { public void done(Void result) { if(progressListener != null) { if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { - progressListener.updateCache(); + progressListener.updateCache(CACHE_UPDATE_LIST); } } } @@ -267,12 +276,13 @@ public class CachedMusicService implements MusicService { new SilentBackgroundTask<Void>(context) { MusicDirectory refreshed; + private boolean metadataUpdated; @Override protected Void doInBackground() throws Throwable { refreshed = musicService.getAlbum(id, name, refresh, context, null); updateAllSongs(context, refreshed); - cached.updateMetadata(refreshed); + metadataUpdated = cached.updateMetadata(refreshed); deleteRemovedEntries(context, refreshed, cached); FileUtil.serialize(context, refreshed, getCacheName(context, "album", id)); return null; @@ -283,7 +293,10 @@ public class CachedMusicService implements MusicService { public void done(Void result) { if(progressListener != null) { if(cached.updateEntriesList(context, musicService.getInstance(context), refreshed)) { - progressListener.updateCache(); + progressListener.updateCache(CACHE_UPDATE_LIST); + } + if(metadataUpdated) { + progressListener.updateCache(CACHE_UPDATE_METADATA); } } } @@ -656,6 +669,11 @@ public class CachedMusicService implements MusicService { } @Override + public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + return musicService.getSongList(type, size, offset, context, progressListener); + } + + @Override public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { return musicService.getRandomSongs(size, artistId, context, progressListener); } @@ -905,8 +923,18 @@ public class CachedMusicService implements MusicService { } @Override - public MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception { - return musicService.getNewestPodcastEpisodes(count, context, progressListener); + public MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception { + MusicDirectory result = null; + + String cacheName = getCacheName(context, "newestPodcastEpisodes"); + try { + result = musicService.getNewestPodcastEpisodes(refresh, context, progressListener, count); + FileUtil.serialize(context, result, cacheName); + } catch(IOException e) { + result = FileUtil.deserialize(context, cacheName, MusicDirectory.class, 24); + } finally { + return result; + } } @Override @@ -1136,12 +1164,22 @@ public class CachedMusicService implements MusicService { String cacheName = getCacheName(context, "artistInfo", id); ArtistInfo info = null; if(!refresh) { - info = FileUtil.deserialize(context, cacheName, ArtistInfo.class); + info = FileUtil.deserialize(context, cacheName, ArtistInfo.class, CACHED_LAST_FM); } if(info == null && allowNetwork) { - info = musicService.getArtistInfo(id, refresh, allowNetwork, context, progressListener); - FileUtil.serialize(context, info, cacheName); + try { + info = musicService.getArtistInfo(id, refresh, allowNetwork, context, progressListener); + FileUtil.serialize(context, info, cacheName); + } catch(Exception e) { + Log.w(TAG, "Failed to refresh Artist Info"); + info = FileUtil.deserialize(context, cacheName, ArtistInfo.class); + + // Nothing ever cached, throw error further upstream + if(info == null) { + throw e; + } + } } return info; diff --git a/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java b/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java index 670ea7d2..c2007139 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java +++ b/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java @@ -62,6 +62,8 @@ public class ChromeCastController extends RemoteController { private boolean error = false; private boolean ignoreNextPaused = false; private String sessionId; + private boolean isStopping = false; + private Runnable afterUpdateComplete = null; private ServerProxy proxy; private String rootLocation; @@ -247,18 +249,38 @@ public class ChromeCastController extends RemoteController { } } - void startSong(DownloadFile currentPlaying, boolean autoStart, int position) { + void startSong(final DownloadFile currentPlaying, final boolean autoStart, final int position) { if(currentPlaying == null) { try { - if (mediaPlayer != null && !error) { - mediaPlayer.stop(apiClient); + if (mediaPlayer != null && !error && !isStopping) { + isStopping = true; + mediaPlayer.stop(apiClient).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() { + @Override + public void onResult(RemoteMediaPlayer.MediaChannelResult mediaChannelResult) { + isStopping = false; + + if(afterUpdateComplete != null) { + afterUpdateComplete.run(); + afterUpdateComplete = null; + } + } + }); } } catch(Exception e) { // Just means it didn't need to be stopped } downloadService.setPlayerState(PlayerState.IDLE); return; + } else if(isStopping) { + afterUpdateComplete = new Runnable() { + @Override + public void run() { + startSong(currentPlaying, autoStart, position); + } + }; + return; } + downloadService.setPlayerState(PlayerState.PREPARING); MusicDirectory.Entry song = currentPlaying.getSong(); @@ -503,7 +525,7 @@ public class ChromeCastController extends RemoteController { try { Cast.CastApi.setMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace(), mediaPlayer); - } catch (IOException e) { + } catch (Exception e) { Log.e(TAG, "Exception while creating channel", e); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java b/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java index 0673cdeb..24890057 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DLNAController.java @@ -163,7 +163,10 @@ public class DLNAController extends RemoteController { protected void eventReceived(GENASubscription genaSubscription) { Map<String, StateVariableValue> m = genaSubscription.getCurrentValues(); try { - LastChange lastChange = new LastChange(new AVTransportLastChangeParser(), m.get("LastChange").toString()); + String lastChangeText = m.get("LastChange").toString(); + lastChangeText = lastChangeText.replace(",X_DLNA_SeekTime","").replace(",X_DLNA_SeekByte", ""); + LastChange lastChange = new LastChange(new AVTransportLastChangeParser(), lastChangeText); + if (lastChange.getEventedValue(0, AVTransportVariable.TransportState.class) == null) { return; } diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java index 3febfaea..e4bab798 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java @@ -84,6 +84,9 @@ public class DownloadFile implements BufferFile { public MusicDirectory.Entry getSong() { return song; } + public boolean isSong() { + return song.isSong(); + } public Context getContext() { return context; @@ -111,7 +114,7 @@ public class DownloadFile implements BufferFile { } } else if(song.getSuffix() != null && (song.getTranscodedSuffix() == null || song.getSuffix().equals(song.getTranscodedSuffix()))) { // If just downsampling, don't try to upsample (ie: 128 kpbs -> 192 kpbs) - if(song.getBitRate() != null && br > song.getBitRate()) { + if(song.getBitRate() != null && (br == 0 || br > song.getBitRate())) { br = song.getBitRate(); } } @@ -379,6 +382,20 @@ public class DownloadFile implements BufferFile { return "DownloadFile (" + song + ")"; } + // Don't do this. Causes infinite loop if two instances of same song + /*@Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DownloadFile downloadFile = (DownloadFile) o; + return Util.equals(this.getSong(), downloadFile.getSong()); + }*/ + private class DownloadTask extends SilentBackgroundTask<Void> { private MusicService musicService; @@ -515,8 +532,9 @@ public class DownloadFile implements BufferFile { } // Only run these if not interrupted, ie: cancelled - if(!isCancelled()) { - new CacheCleaner(context, DownloadService.getInstance()).cleanSpace(); + DownloadService downloadService = DownloadService.getInstance(); + if(downloadService != null && !isCancelled()) { + new CacheCleaner(context, downloadService).cleanSpace(); checkDownloads(); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java index 6641d040..cec98865 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java @@ -76,6 +76,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.media.AudioManager; import android.media.MediaPlayer; +import android.media.PlaybackParams; import android.media.audiofx.AudioEffect; import android.net.wifi.WifiManager; import android.os.Build; @@ -87,6 +88,7 @@ import android.support.v7.media.MediaRouteSelector; import android.support.v7.media.MediaRouter; import android.util.Log; import android.support.v4.util.LruCache; +import android.view.KeyEvent; /** * @author Sindre Mehus @@ -105,6 +107,7 @@ public class DownloadService extends Service { public static final String START_PLAY = "github.daneren2005.dsub.START_PLAYING"; public static final int FAST_FORWARD = 30000; public static final int REWIND = 10000; + private static final long DEFAULT_DELAY_UPDATE_PROGRESS = 1000L; private static final double DELETE_CUTOFF = 0.84; private static final int REQUIRED_ALBUM_MATCHES = 4; private static final int REMOTE_PLAYLIST_TOTAL = 3; @@ -116,6 +119,7 @@ public class DownloadService extends Service { public static final int METADATA_UPDATED_STAR = 1; public static final int METADATA_UPDATED_RATING = 2; public static final int METADATA_UPDATED_BOOKMARK = 4; + public static final int METADATA_UPDATED_COVER_ART = 8; private RemoteControlClientBase mRemoteControl; @@ -160,6 +164,7 @@ public class DownloadService extends Service { private int cachedPosition = 0; private boolean downloadOngoing = false; private float volume = 1.0f; + private long delayUpdateProgress = DEFAULT_DELAY_UPDATE_PROGRESS; private AudioEffectsController effectsController; private RemoteControlState remoteState = LOCAL; @@ -219,14 +224,14 @@ public class DownloadService extends Service { } }); - try { + /*try { Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId); i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); sendBroadcast(i); } catch(Throwable e) { // Froyo or lower - } + }*/ effectsController = new AudioEffectsController(DownloadService.this, audioSessionId); if(prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false)) { @@ -573,7 +578,6 @@ public class DownloadService extends Service { public synchronized void setShufflePlayEnabled(boolean enabled) { shufflePlay = enabled; if (shufflePlay) { - clear(); checkDownloads(); } SharedPreferences.Editor editor = Util.getPreferences(this).edit(); @@ -600,6 +604,9 @@ public class DownloadService extends Service { } editor.commit(); } + public boolean isArtistRadio() { + return artistRadio; + } public synchronized void shuffle() { Collections.shuffle(downloadList); @@ -739,7 +746,7 @@ public class DownloadService extends Service { int position = getPlayerPosition(); int duration = getPlayerDuration(); boolean cutoff = isPastCutoff(position, duration, true); - if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode) { + if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode && !currentPlaying.isSaved()) { if(cutoff) { currentPlaying.delete(); } @@ -757,7 +764,7 @@ public class DownloadService extends Service { checkAddBookmark(); } if(currentPlaying != null) { - scrobbler.conditionalScrobble(this, currentPlaying, position, duration); + scrobbler.conditionalScrobble(this, currentPlaying, position, duration, cutoff); } reset(); @@ -781,6 +788,10 @@ public class DownloadService extends Service { suggestedPlaylistName = null; suggestedPlaylistId = null; + + setShufflePlayEnabled(false); + setArtistRadio(null); + checkDownloads(); } public synchronized void remove(int which) { @@ -807,6 +818,8 @@ public class DownloadService extends Service { if(downloadFile == nextPlaying) { setNextPlaying(); } + + checkDownloads(); } public synchronized void removeBackground(DownloadFile downloadFile) { if (downloadFile == currentDownloading && downloadFile != currentPlaying && downloadFile != nextPlaying) { @@ -843,6 +856,9 @@ public class DownloadService extends Service { if(this.currentPlaying != null) { this.currentPlaying.setPlaying(false); } + if(delayUpdateProgress != DEFAULT_DELAY_UPDATE_PROGRESS && !isNextPlayingSameAlbum(currentPlaying, this.currentPlaying)) { + resetPlaybackSpeed(); + } this.currentPlaying = currentPlaying; if(currentPlaying == null) { currentPlayingIndex = -1; @@ -853,7 +869,10 @@ public class DownloadService extends Service { if (currentPlaying != null && currentPlaying.getSong() != null) { Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); - mRemoteControl.updateMetadata(this, currentPlaying.getSong()); + + if(mRemoteControl != null) { + mRemoteControl.updateMetadata(this, currentPlaying.getSong()); + } } else { Util.broadcastNewTrackInfo(this, null); Notifications.hidePlayingNotification(this, this, handler); @@ -1033,6 +1052,8 @@ public class DownloadService extends Service { bufferAndPlay(position, start); checkDownloads(); setNextPlaying(); + } else { + checkDownloads(); } } } @@ -1070,6 +1091,7 @@ public class DownloadService extends Service { setCurrentPlaying(nextPlaying, true); setPlayerState(PlayerState.STARTED); setupHandlers(currentPlaying, false, start); + applyPlaybackParamsMain(); setNextPlaying(); // Proxy should not be being used here since the next player was already setup to play @@ -1120,6 +1142,27 @@ public class DownloadService extends Service { handleError(x); } } + public synchronized int rewind() { + return seekToWrapper(-REWIND); + } + public synchronized int fastForward() { + return seekToWrapper(FAST_FORWARD); + } + protected int seekToWrapper(int difference) { + int msPlayed = Math.max(0, getPlayerPosition()); + Integer duration = getPlayerDuration(); + int msTotal = duration == null ? 0 : duration; + + int seekTo; + if(msPlayed + difference > msTotal) { + seekTo = msTotal; + } else { + seekTo = msPlayed + difference; + } + seekTo(seekTo); + + return seekTo; + } public synchronized void previous() { int index = getCurrentPlayingIndex(); @@ -1128,8 +1171,8 @@ public class DownloadService extends Service { } // If only one song, just skip within song - if(size() == 1) { - seekTo(getPlayerPosition() - REWIND); + if(size() == 1 || (currentPlaying != null && !currentPlaying.isSong())) { + rewind(); return; } @@ -1154,8 +1197,8 @@ public class DownloadService extends Service { } public synchronized void next(boolean forceCutoff, boolean forceStart) { // If only one song, just skip within song - if(size() == 1) { - seekTo(getPlayerPosition() + FAST_FORWARD); + if(size() == 1 || (currentPlaying != null && !currentPlaying.isSong())) { + fastForward(); return; } else if(playerState == PREPARING || playerState == PREPARED) { return; @@ -1170,7 +1213,7 @@ public class DownloadService extends Service { } else { cutoff = isPastCutoff(position, duration); } - if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode) { + if(currentPlaying != null && currentPlaying.getSong() instanceof PodcastEpisode && !currentPlaying.isSaved()) { if(cutoff) { toDelete.add(currentPlaying); } @@ -1179,7 +1222,7 @@ public class DownloadService extends Service { clearCurrentBookmark(true); } if(currentPlaying != null) { - scrobbler.conditionalScrobble(this, currentPlaying, position, duration); + scrobbler.conditionalScrobble(this, currentPlaying, position, duration, cutoff); } int index = getCurrentPlayingIndex(); @@ -1258,6 +1301,7 @@ public class DownloadService extends Service { // Only start if done preparing if(playerState != PREPARING) { mediaPlayer.start(); + applyPlaybackParamsMain(); } else { // Otherwise, we need to set it up to start when done preparing autoPlayStart = true; @@ -1372,6 +1416,9 @@ public class DownloadService extends Service { if (playerState == PAUSED) { lifecycleSupport.serializeDownloadQueue(); + if(!isPastCutoff()) { + checkAddBookmark(true); + } } boolean show = playerState == PlayerState.STARTED; @@ -1402,9 +1449,9 @@ public class DownloadService extends Service { } if (playerState == STARTED) { - scrobbler.scrobble(this, currentPlaying, false); + scrobbler.scrobble(this, currentPlaying, false, false); } else if (playerState == COMPLETED) { - scrobbler.scrobble(this, currentPlaying, true); + scrobbler.scrobble(this, currentPlaying, true, true); } if(playerState == STARTED && positionCache == null) { @@ -1450,7 +1497,7 @@ public class DownloadService extends Service { positionCache.stop(); positionCache = null; } - scrobbler.scrobble(this, currentPlaying, true); + scrobbler.scrobble(this, currentPlaying, true, true); onStateUpdate(); } @@ -1468,7 +1515,7 @@ public class DownloadService extends Service { while(isRunning) { try { onSongProgress(); - Thread.sleep(1000L); + Thread.sleep(delayUpdateProgress); } catch(Exception e) { isRunning = false; @@ -1510,7 +1557,7 @@ public class DownloadService extends Service { } } onSongProgress(cachedPosition < 2000 ? true: false); - Thread.sleep(1000L); + Thread.sleep(delayUpdateProgress); } catch(Exception e) { Log.w(TAG, "Crashed getting current position", e); @@ -1699,11 +1746,12 @@ public class DownloadService extends Service { nextPlayingTask.cancel(); nextPlayingTask = null; } - } - if(remoteState == LOCAL) { - checkDownloads(); + if(nextPlayerState != IDLE) { + setNextPlayerState(IDLE); + } } + checkDownloads(); if(routeId != null) { final Runnable delayedReconnect = new Runnable() { @@ -1844,6 +1892,7 @@ public class DownloadService extends Service { if (start || autoPlayStart) { mediaPlayer.start(); + applyPlaybackParamsMain(); setPlayerState(STARTED); // Disable autoPlayStart after done @@ -2138,7 +2187,7 @@ public class DownloadService extends Service { int preloaded = 0; - if(n != 0 && remoteState == LOCAL) { + if(n != 0 && (remoteState == LOCAL || Util.shouldCacheDuringCasting(this))) { int start = currentPlaying == null ? 0 : getCurrentPlayingIndex(); if(start == -1) { start = 0; @@ -2342,7 +2391,7 @@ public class DownloadService extends Service { } // Make cutoff a maximum of 10 minutes - int cutoffPoint = Math.min((int) (duration * DELETE_CUTOFF), 10 * 60 * 1000); + int cutoffPoint = Math.max((int) (duration * DELETE_CUTOFF), duration - 10 * 60 * 1000); boolean isPastCutoff = duration > 0 && position > cutoffPoint; // Check to make sure song isn't within 10 seconds of where it was created @@ -2423,8 +2472,11 @@ public class DownloadService extends Service { }.execute(); } } - + private void checkAddBookmark() { + checkAddBookmark(false); + } + private void checkAddBookmark(final boolean updateMetadata) { // Don't do anything if no current playing if(currentPlaying == null || !ServerInfo.canBookmark(this)) { return; @@ -2454,6 +2506,9 @@ public class DownloadService extends Service { if(found != null) { found.setBookmark(new Bookmark(position)); } + if(updateMetadata) { + onMetadataUpdate(METADATA_UPDATED_BOOKMARK); + } return null; } @@ -2482,9 +2537,9 @@ public class DownloadService extends Service { SharedPreferences prefs = Util.getPreferences(this); try { - float[] rg = BastpUtil.getReplayGainValues(downloadFile.getFile().getCanonicalPath()); /* track, album */ float adjust = 0f; if (prefs.getBoolean(Constants.PREFERENCES_KEY_REPLAY_GAIN, false)) { + float[] rg = BastpUtil.getReplayGainValues(downloadFile.getFile().getCanonicalPath()); /* track, album */ boolean singleAlbum = false; String replayGainType = prefs.getString(Constants.PREFERENCES_KEY_REPLAY_GAIN_TYPE, "1"); @@ -2558,6 +2613,48 @@ public class DownloadService extends Service { } } + public void setPlaybackSpeed(float playbackSpeed) { + Util.getPreferences(this).edit().putFloat(Constants.PREFERENCES_KEY_PLAYBACK_SPEED, playbackSpeed).commit(); + if(mediaPlayer != null && (playerState == PREPARED || playerState == STARTED || playerState == PAUSED || playerState == PAUSED_TEMP)) { + applyPlaybackParamsMain(); + } + + delayUpdateProgress = Math.round(DEFAULT_DELAY_UPDATE_PROGRESS / playbackSpeed); + } + private void resetPlaybackSpeed() { + Util.getPreferences(this).edit().remove(Constants.PREFERENCES_KEY_PLAYBACK_SPEED).commit(); + } + + public float getPlaybackSpeed() { + return Util.getPreferences(this).getFloat(Constants.PREFERENCES_KEY_PLAYBACK_SPEED, 1.0f); + } + + private synchronized void applyPlaybackParamsMain() { + applyPlaybackParams(mediaPlayer); + } + private synchronized boolean isNextPlayingSameAlbum() { + return isNextPlayingSameAlbum(currentPlaying, nextPlaying); + } + private synchronized boolean isNextPlayingSameAlbum(DownloadFile currentPlaying, DownloadFile nextPlaying) { + if(currentPlaying == null || nextPlaying == null) { + return false; + } else { + return currentPlaying.getSong().getAlbum().equals(nextPlaying.getSong().getAlbum()); + } + } + + private synchronized void applyPlaybackParams(MediaPlayer mediaPlayer) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + float playbackSpeed = getPlaybackSpeed(); + + if(Math.abs(playbackSpeed - 1.0) > 0.01 || mediaPlayer.getPlaybackParams() != null) { + PlaybackParams playbackParams = new PlaybackParams(); + playbackParams.setSpeed(playbackSpeed); + mediaPlayer.setPlaybackParams(playbackParams); + } + } + } + public void toggleStarred() { final DownloadFile currentPlaying = this.currentPlaying; if(currentPlaying == null) { @@ -2571,6 +2668,11 @@ public class DownloadService extends Service { onMetadataUpdate(METADATA_UPDATED_STAR); } } + + @Override + public void starCommited(boolean starred) { + + } }); } public void toggleRating(int rating) { @@ -2600,7 +2702,7 @@ public class DownloadService extends Service { UpdateHelper.setRating(this, entry, rating, new UpdateHelper.OnRatingChange() { @Override public void ratingChange(int rating) { - if(currentPlaying == DownloadService.this.currentPlaying) { + if (currentPlaying == DownloadService.this.currentPlaying) { onMetadataUpdate(METADATA_UPDATED_RATING); } } @@ -2613,13 +2715,19 @@ public class DownloadService extends Service { wakeLock.acquire(ms); } + public void handleKeyEvent(KeyEvent keyEvent) { + lifecycleSupport.handleKeyEvent(keyEvent); + } + public void addOnSongChangedListener(OnSongChangedListener listener) { addOnSongChangedListener(listener, false); } - public synchronized void addOnSongChangedListener(OnSongChangedListener listener, boolean run) { - int index = onSongChangedListeners.indexOf(listener); - if(index == -1) { - onSongChangedListeners.add(listener); + public void addOnSongChangedListener(OnSongChangedListener listener, boolean run) { + synchronized(onSongChangedListeners) { + int index = onSongChangedListeners.indexOf(listener); + if (index == -1) { + onSongChangedListeners.add(listener); + } } if(run) { @@ -2630,6 +2738,7 @@ public class DownloadService extends Service { onSongsChanged(); onSongProgress(); onStateUpdate(); + onMetadataUpdate(METADATA_UPDATED_ALL); } }); } else { @@ -2637,53 +2746,59 @@ public class DownloadService extends Service { } } } - public synchronized void removeOnSongChangeListener(OnSongChangedListener listener) { - int index = onSongChangedListeners.indexOf(listener); - if(index != -1) { - onSongChangedListeners.remove(index); + public void removeOnSongChangeListener(OnSongChangedListener listener) { + synchronized(onSongChangedListeners) { + int index = onSongChangedListeners.indexOf(listener); + if (index != -1) { + onSongChangedListeners.remove(index); + } } } - private synchronized void onSongChanged() { + private void onSongChanged() { final long atRevision = revision; - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(revision == atRevision && instance != null) { - listener.onSongChanged(currentPlaying, currentPlayingIndex); + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongChanged(currentPlaying, currentPlayingIndex); - MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; - listener.onMetadataUpdate(entry, METADATA_UPDATED_ALL); + MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; + listener.onMetadataUpdate(entry, METADATA_UPDATED_ALL); + } } - } - }); - } + }); + } - if(mediaPlayerHandler != null && !onSongChangedListeners.isEmpty()) { - mediaPlayerHandler.post(new Runnable() { - @Override - public void run() { - onSongProgress(); - } - }); + if (mediaPlayerHandler != null && !onSongChangedListeners.isEmpty()) { + mediaPlayerHandler.post(new Runnable() { + @Override + public void run() { + onSongProgress(); + } + }); + } } } - private synchronized void onSongsChanged() { + private void onSongsChanged() { final long atRevision = revision; - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(revision == atRevision && instance != null) { - listener.onSongsChanged(downloadList, currentPlaying, currentPlayingIndex); + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongsChanged(downloadList, currentPlaying, currentPlayingIndex); + } } - } - }); + }); + } } } - private synchronized void onSongProgress() { + private void onSongProgress() { onSongProgress(true); } private synchronized void onSongProgress(boolean manual) { @@ -2691,22 +2806,27 @@ public class DownloadService extends Service { final Integer duration = getPlayerDuration(); final boolean isSeekable = isSeekable(); final int position = getPlayerPosition(); - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(revision == atRevision && instance != null) { - listener.onSongProgress(currentPlaying, position, duration, isSeekable); + + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onSongProgress(currentPlaying, position, duration, isSeekable); + } } - } - }); + }); + } } if(manual) { handler.post(new Runnable() { @Override public void run() { - mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState()); + if(mRemoteControl != null) { + mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState()); + } } }); } @@ -2718,39 +2838,45 @@ public class DownloadService extends Service { } } } - private synchronized void onStateUpdate() { + private void onStateUpdate() { final long atRevision = revision; - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(revision == atRevision && instance != null) { - listener.onStateUpdate(currentPlaying, playerState); + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (revision == atRevision && instance != null) { + listener.onStateUpdate(currentPlaying, playerState); + } } - } - }); + }); + } } } - private synchronized void onMetadataUpdate() { + public void onMetadataUpdate() { onMetadataUpdate(METADATA_UPDATED_ALL); } - private synchronized void onMetadataUpdate(final int updateType) { - for(final OnSongChangedListener listener: onSongChangedListeners) { - handler.post(new Runnable() { - @Override - public void run() { - if(instance != null) { - MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; - listener.onMetadataUpdate(entry, updateType); + public void onMetadataUpdate(final int updateType) { + synchronized(onSongChangedListeners) { + for (final OnSongChangedListener listener : onSongChangedListeners) { + handler.post(new Runnable() { + @Override + public void run() { + if (instance != null) { + MusicDirectory.Entry entry = currentPlaying != null ? currentPlaying.getSong() : null; + listener.onMetadataUpdate(entry, updateType); + } } - } - }); + }); + } } handler.post(new Runnable() { @Override public void run() { - mRemoteControl.metadataChanged(currentPlaying.getSong()); + if(currentPlaying != null) { + mRemoteControl.metadataChanged(currentPlaying.getSong()); + } } }); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java index 4989db40..f8272356 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java @@ -56,6 +56,7 @@ import static github.daneren2005.dsub.domain.PlayerState.PREPARING; public class DownloadServiceLifecycleSupport { private static final String TAG = DownloadServiceLifecycleSupport.class.getSimpleName(); public static final String FILENAME_DOWNLOADS_SER = "downloadstate2.ser"; + private static final int DEBOUNCE_TIME = 200; private final DownloadService downloadService; private Looper eventLooper; @@ -225,6 +226,7 @@ public class DownloadServiceLifecycleSupport { } editor.commit(); + downloadService.clear(); downloadService.setShufflePlayEnabled(true); } else { downloadService.start(); @@ -384,14 +386,25 @@ public class DownloadServiceLifecycleSupport { return lastChange; } - private void handleKeyEvent(KeyEvent event) { - if(event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + public void handleKeyEvent(KeyEvent event) { + if(event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() > 0) { + switch (event.getKeyCode()) { + case RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + downloadService.fastForward(); + break; + case RemoteControlClient.FLAG_KEY_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_NEXT: + downloadService.rewind(); + break; + } + } else if(event.getAction() == KeyEvent.ACTION_UP) { switch (event.getKeyCode()) { case RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE: - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: downloadService.togglePlayPause(); break; case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: if(lastPressTime < (System.currentTimeMillis() - 500)) { lastPressTime = System.currentTimeMillis(); downloadService.togglePlayPause(); @@ -401,11 +414,23 @@ public class DownloadServiceLifecycleSupport { break; case RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS: case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - downloadService.previous(); + if(lastPressTime < (System.currentTimeMillis() - DEBOUNCE_TIME)) { + lastPressTime = System.currentTimeMillis(); + downloadService.previous(); + } break; case RemoteControlClient.FLAG_KEY_MEDIA_NEXT: case KeyEvent.KEYCODE_MEDIA_NEXT: - downloadService.next(); + if(lastPressTime < (System.currentTimeMillis() - DEBOUNCE_TIME)) { + lastPressTime = System.currentTimeMillis(); + downloadService.next(); + } + break; + case KeyEvent.KEYCODE_MEDIA_REWIND: + downloadService.rewind(); + break; + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + downloadService.fastForward(); break; case RemoteControlClient.FLAG_KEY_MEDIA_STOP: case KeyEvent.KEYCODE_MEDIA_STOP: diff --git a/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java b/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java index e9d7cbc8..82ef45e1 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java +++ b/app/src/main/java/github/daneren2005/dsub/service/JukeboxController.java @@ -22,6 +22,7 @@ import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.RemoteStatus; import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.domain.RemoteControlState; +import github.daneren2005.dsub.domain.RepeatMode; import github.daneren2005.dsub.service.parser.SubsonicRESTException; import github.daneren2005.dsub.util.Util; @@ -200,11 +201,15 @@ public class JukeboxController extends RemoteController { // Track change? Integer index = jukeboxStatus.getCurrentPlayingIndex(); + int currentPlayingIndex = downloadService.getCurrentPlayingIndex(); if (index != null && index != -1 && index != downloadService.getCurrentPlayingIndex()) { downloadService.setPlayerState(PlayerState.COMPLETED); downloadService.setCurrentPlaying(index, true); if(jukeboxStatus.isPlaying()) { downloadService.setPlayerState(PlayerState.STARTED); + } else if(index == 0 && currentPlayingIndex == downloadService.size() - 1 && downloadService.getRepeatMode() == RepeatMode.ALL) { + // Jukebox does not support any form of auto repeat + start(); } } } diff --git a/app/src/main/java/github/daneren2005/dsub/service/MusicService.java b/app/src/main/java/github/daneren2005/dsub/service/MusicService.java index 2972bb7c..22f154c4 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/MusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/MusicService.java @@ -94,6 +94,8 @@ public interface MusicService { MusicDirectory getAlbumList(String type, String extra, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception; + MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception; + MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception; MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception; @@ -147,7 +149,7 @@ public interface MusicService { MusicDirectory getPodcastEpisodes(boolean refresh, String id, Context context, ProgressListener progressListener) throws Exception; - MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception; + MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception; void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception; diff --git a/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java index b8633624..e004101d 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java @@ -95,7 +95,7 @@ public class OfflineMusicService implements MusicService { artist.setIndex(file.getName().substring(0, 1)); artist.setName(file.getName()); artists.add(artist); - } else { + } else if(!file.getName().equals("albumart.jpg") && !file.getName().equals(".nomedia")) { entries.add(createEntry(context, file)); } } @@ -307,7 +307,15 @@ public class OfflineMusicService implements MusicService { } } }); - + + // Respect counts in search criteria + int artistCount = Math.min(artists.size(), criteria.getArtistCount()); + int albumCount = Math.min(albums.size(), criteria.getAlbumCount()); + int songCount = Math.min(songs.size(), criteria.getSongCount()); + artists = artists.subList(0, artistCount); + albums = albums.subList(0, albumCount); + songs = songs.subList(0, songCount); + return new SearchResult(artists, albums, songs); } @@ -359,20 +367,13 @@ public class OfflineMusicService implements MusicService { } } private int matchCriteria(SearchCritera criteria, String name) { - String query = criteria.getQuery().toLowerCase(); - String[] queryParts = query.split(" "); - String[] nameParts = name.toLowerCase().split(" "); - - int closeness = 0; - for(String queryPart : queryParts) { - for(String namePart : nameParts) { - if(namePart.equals(queryPart)) { - closeness++; - } - } + if (criteria.getPattern().matcher(name).matches()) { + return Util.getStringDistance( + criteria.getQuery().toLowerCase(), + name.toLowerCase()); + } else { + return 0; } - - return closeness; } @Override @@ -587,6 +588,11 @@ public class OfflineMusicService implements MusicService { } @Override + public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(ERRORMSG); } @@ -769,7 +775,7 @@ public class OfflineMusicService implements MusicService { } @Override - public MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception { + public MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception { throw new OfflineException(ERRORMSG); } diff --git a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java index 4a6e5108..1f9e5494 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java @@ -72,7 +72,8 @@ import android.util.Log; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.*; -import github.daneren2005.dsub.service.parser.AlbumListParser; +import github.daneren2005.dsub.fragments.MainFragment; +import github.daneren2005.dsub.service.parser.EntryListParser; import github.daneren2005.dsub.service.parser.ArtistInfoParser; import github.daneren2005.dsub.service.parser.BookmarkParser; import github.daneren2005.dsub.service.parser.ChatMessageParser; @@ -95,6 +96,7 @@ import github.daneren2005.dsub.service.parser.SearchResult2Parser; import github.daneren2005.dsub.service.parser.SearchResultParser; import github.daneren2005.dsub.service.parser.ShareParser; import github.daneren2005.dsub.service.parser.StarredListParser; +import github.daneren2005.dsub.service.parser.TopSongsParser; import github.daneren2005.dsub.service.parser.UserParser; import github.daneren2005.dsub.service.parser.VideosParser; import github.daneren2005.dsub.service.ssl.SSLSocketFactory; @@ -571,7 +573,7 @@ public class RESTMusicService implements MusicService { Reader reader = getReader(context, progressListener, method, null, names, values, true); try { - return new AlbumListParser(context, getInstance(context)).parse(reader, progressListener); + return new EntryListParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } @@ -606,7 +608,7 @@ public class RESTMusicService implements MusicService { int decade = Integer.parseInt(extra); // Reverse chronological order only supported in 5.3+ - if(ServerInfo.checkServerVersion(context, "1.13", instance) && ServerInfo.isStockSubsonic(context, instance)) { + if(ServerInfo.checkServerVersion(context, "1.13", instance) && !ServerInfo.isMadsonic(context, instance)) { values.add(decade + 9); values.add(decade); } else { @@ -637,13 +639,49 @@ public class RESTMusicService implements MusicService { Reader reader = getReader(context, progressListener, method, null, names, values, true); try { - return new AlbumListParser(context, instance).parse(reader, progressListener); + return new EntryListParser(context, instance).parse(reader, progressListener); } finally { Util.close(reader); } } - @Override + @Override + public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + List<String> names = new ArrayList<String>(); + List<Object> values = new ArrayList<Object>(); + + names.add("size"); + values.add(size); + names.add("offset"); + values.add(offset); + + String method; + switch(type) { + case MainFragment.SONGS_NEWEST: + method = "getNewaddedSongs"; + break; + case MainFragment.SONGS_TOP_PLAYED: + method = "getTopplayedSongs"; + break; + case MainFragment.SONGS_RECENT: + method = "getLastplayedSongs"; + break; + case MainFragment.SONGS_FREQUENT: + method = "getMostplayedSongs"; + break; + default: + method = "getNewaddedSongs"; + } + + Reader reader = getReader(context, progressListener, method, null, names, values, true); + try { + return new EntryListParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.11", "Artist radio is not supported"); @@ -658,7 +696,13 @@ public class RESTMusicService implements MusicService { int instance = getInstance(context); String method; - if(ServerInfo.isMadsonic(context, instance)) { + if(ServerInfo.isMadsonic6(context, instance)) { + if (Util.isTagBrowsing(context, instance)) { + method = "getSimilarSongsID3"; + } else { + method = "getSimilarSongs"; + } + } else if(ServerInfo.isMadsonic(context, instance)) { method = "getPandoraSongs"; } else { if (Util.isTagBrowsing(context, instance)) { @@ -909,10 +953,9 @@ public class RESTMusicService implements MusicService { StringBuilder builder = new StringBuilder(getRestUrl(context, "stream")); builder.append("&id=").append(song.getId()); - // If we are doing mp3 to mp3, just specify raw so that stuff works better - if("mp3".equals(song.getSuffix()) && (song.getTranscodedSuffix() == null || "mp3".equals(song.getTranscodedSuffix())) && ServerInfo.checkServerVersion(context, "1.9", getInstance(context))) { + // Allow user to specify to stream raw formats if available + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CAST_STREAM_ORIGINAL, true) && ("mp3".equals(song.getSuffix()) || "flac".equals(song.getSuffix()) || "wav".equals(song.getSuffix()) || "aac".equals(song.getSuffix())) && ServerInfo.checkServerVersion(context, "1.9", getInstance(context))) { builder.append("&format=raw"); - builder.append("&estimateContentLength=true"); } else { builder.append("&maxBitRate=").append(maxBitrate); } @@ -1258,7 +1301,7 @@ public class RESTMusicService implements MusicService { String method = ServerInfo.isMadsonic(context, getInstance(context)) ? "getTopTrackSongs" : "getTopSongs"; Reader reader = getReader(context, progressListener, method, null, parameterNames, parameterValues); try { - return new RandomSongsParser(context, getInstance(context)).parse(reader, progressListener); + return new TopSongsParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } @@ -1299,8 +1342,8 @@ public class RESTMusicService implements MusicService { } @Override - public MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception { - Reader reader = getReader(context, progressListener, "getNewestPodcasts", null, Arrays.asList("count"), Arrays.<Object>asList(count)); + public MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception { + Reader reader = getReader(context, progressListener, "getNewestPodcasts", null, Arrays.asList("count"), Arrays.<Object>asList(count), true); try { return new PodcastEntryParser(context, getInstance(context)).parse(null, reader, progressListener); @@ -1462,6 +1505,15 @@ public class RESTMusicService implements MusicService { values.add(setting.getValue()); } + if(user.getMusicFolderSettings() != null) { + for(User.Setting setting: user.getMusicFolderSettings()) { + if(setting.getValue()) { + names.add("musicFolderId"); + values.add(setting.getName()); + } + } + } + Reader reader = getReader(context, progressListener, "createUser", null, names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); @@ -1487,6 +1539,15 @@ public class RESTMusicService implements MusicService { } } + if(user.getMusicFolderSettings() != null) { + for(User.Setting setting: user.getMusicFolderSettings()) { + if(setting.getValue()) { + names.add("musicFolderId"); + values.add(setting.getName()); + } + } + } + Reader reader = getReader(context, progressListener, "updateUser", null, names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); @@ -1714,13 +1775,13 @@ public class RESTMusicService implements MusicService { int count = offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0); int retry = 0; for(int i = 1; i <= count; i++) { - String id = offline.getString(Constants.OFFLINE_SCROBBLE_ID + i, null); - long time = offline.getLong(Constants.OFFLINE_SCROBBLE_TIME + i, 0); - if(id != null) { - scrobble(id, true, time, context, progressListener); - } else { - String search = offline.getString(Constants.OFFLINE_SCROBBLE_SEARCH + i, ""); - try{ + try { + String id = offline.getString(Constants.OFFLINE_SCROBBLE_ID + i, null); + long time = offline.getLong(Constants.OFFLINE_SCROBBLE_TIME + i, 0); + if(id != null) { + scrobble(id, true, time, context, progressListener); + } else { + String search = offline.getString(Constants.OFFLINE_SCROBBLE_SEARCH + i, ""); SearchCritera critera = new SearchCritera(search, 0, 0, 1); SearchResult result = searchNew(critera, context, progressListener); if(result.getSongs().size() == 1){ @@ -1732,10 +1793,10 @@ public class RESTMusicService implements MusicService { throw new Exception("Song not found on server"); } } - catch(Exception e){ - Log.e(TAG, e.toString()); - retry++; - } + } + catch(Exception e){ + Log.e(TAG, e.toString()); + retry++; } } @@ -1887,7 +1948,7 @@ public class RESTMusicService implements MusicService { for (int i = 0; i < parameterNames.size(); i++) { builder.append("&").append(parameterNames.get(i)).append("="); String part = URLEncoder.encode(String.valueOf(parameterValues.get(i)), "UTF-8"); - part = part.replaceAll("\\%27", "'"); + part = part.replaceAll("\\%27", "'"); builder.append(part); } url = builder.toString(); diff --git a/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java b/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java index 99502f5e..6c70496d 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java +++ b/app/src/main/java/github/daneren2005/dsub/service/RemoteController.java @@ -25,6 +25,8 @@ import java.util.Iterator; import java.util.concurrent.LinkedBlockingQueue; import github.daneren2005.dsub.domain.RemoteStatus; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.Util; import github.daneren2005.serverproxy.WebProxy; public abstract class RemoteController { @@ -43,7 +45,11 @@ public abstract class RemoteController { // Really is abstract, just don't want to require RemoteController's support it public void changeNextTrack(DownloadFile song) {} public boolean isNextSupported() { - return this.nextSupported; + if(Util.getPreferences(downloadService).getBoolean(Constants.PREFERENCES_KEY_CAST_GAPLESS_PLAYBACK, true)) { + return this.nextSupported; + } else { + return false; + } } public abstract void setVolume(int volume); public abstract void updateVolume(boolean up); diff --git a/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java b/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java index 1d9fecef..168a7777 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java +++ b/app/src/main/java/github/daneren2005/dsub/service/Scrobbler.java @@ -3,6 +3,7 @@ package github.daneren2005.dsub.service; import android.content.Context; import android.util.Log; +import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PodcastEpisode; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.util.SongDBHandler; @@ -21,18 +22,18 @@ public class Scrobbler { private String lastSubmission; private String lastNowPlaying; - public void conditionalScrobble(Context context, DownloadFile song, int playerPosition, int duration) { + public void conditionalScrobble(Context context, DownloadFile song, int playerPosition, int duration, boolean isPastCutoff) { // More than 4 minutes if(playerPosition > FOUR_MINUTES) { - scrobble(context, song, true); + scrobble(context, song, true, isPastCutoff); } // More than 50% played else if(duration > 0 && playerPosition > (duration / 2)) { - scrobble(context, song, true); + scrobble(context, song, true, isPastCutoff); } } - public void scrobble(final Context context, final DownloadFile song, final boolean submission) { + public void scrobble(final Context context, final DownloadFile song, final boolean submission, final boolean isPastCutoff) { if(song == null) { return; } @@ -55,7 +56,9 @@ public class Scrobbler { new SilentBackgroundTask<Void>(context) { @Override protected Void doInBackground() { - SongDBHandler.getHandler(context).setSongPlayed(song, submission); + if(isPastCutoff) { + SongDBHandler.getHandler(context).setSongPlayed(song, submission); + } // Scrobbling disabled if (!Util.isScrobblingEnabled(context)) { diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java index 664adcfb..d6e1a002 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/AbstractParser.java @@ -36,6 +36,12 @@ import github.daneren2005.dsub.util.Util; */ public abstract class AbstractParser { private static final String TAG = AbstractParser.class.getSimpleName(); + private static final String SUBSONIC_RESPONSE = "subsonic-response"; + private static final String MADSONIC_RESPONSE = "madsonic-response"; + private static final String SUBSONIC = "subsonic"; + private static final String MADSONIC = "madsonic"; + private static final String AMPACHE = "ampache"; + protected final Context context; protected final int instance; private XmlPullParser parser; @@ -127,21 +133,32 @@ public abstract class AbstractParser { } protected int nextParseEvent() throws Exception { - return parser.next(); + try { + return parser.next(); + } catch(Exception e) { + if(ServerInfo.isMadsonic6(context, instance)) { + ServerInfo overrideInfo = new ServerInfo(); + overrideInfo.saveServerInfo(context, instance); + } + + throw e; + } } protected String getElementName() { String name = parser.getName(); - if ("subsonic-response".equals(name) || "madsonic-response".equals(name)) { + if (SUBSONIC_RESPONSE.equals(name) || MADSONIC_RESPONSE.equals(name)) { rootElementFound = true; String version = get("version"); if (version != null) { ServerInfo server = new ServerInfo(); server.setRestVersion(new Version(version)); - if("madsonic".equals(get("type")) || "madsonic-response".equals(name)) { + if(MADSONIC.equals(get("type")) || MADSONIC_RESPONSE.equals(name)) { server.setRestType(ServerInfo.TYPE_MADSONIC); - } else if("subsonic".equals(get("type")) && server.checkServerVersion(context, "1.13")) { + } if(AMPACHE.equals(get("type"))) { + server.setRestType(ServerInfo.TYPE_AMPACHE); + } else if(SUBSONIC.equals(get("type")) && server.checkServerVersion(context, "1.13")) { // Oh am I going to regret this server.setRestType(ServerInfo.TYPE_MADSONIC); server.setRestVersion(new Version("2.0.0")); @@ -154,6 +171,11 @@ public abstract class AbstractParser { protected void validate() throws Exception { if (!rootElementFound) { + if(ServerInfo.isMadsonic6(context, instance)) { + ServerInfo overrideInfo = new ServerInfo(); + overrideInfo.saveServerInfo(context, instance); + } + throw new Exception(context.getResources().getString(R.string.background_task_parse_error)); } } diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/AlbumListParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/EntryListParser.java index 773c241b..f91aaae1 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/parser/AlbumListParser.java +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/EntryListParser.java @@ -29,9 +29,9 @@ import java.io.Reader; /** * @author Sindre Mehus */ -public class AlbumListParser extends MusicDirectoryEntryParser { +public class EntryListParser extends MusicDirectoryEntryParser { - public AlbumListParser(Context context, int instance) { + public EntryListParser(Context context, int instance) { super(context, instance); } @@ -46,7 +46,12 @@ public class AlbumListParser extends MusicDirectoryEntryParser { String name = getElementName(); if ("album".equals(name)) { MusicDirectory.Entry entry = parseEntry(""); - entry.setDirectory(true); + if(get("isDir") == null) { + entry.setDirectory(true); + } + dir.addChild(entry); + } else if ("song".equals(name)) { + MusicDirectory.Entry entry = parseEntry(""); dir.addChild(entry); } else if ("error".equals(name)) { handleError(); diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/TopSongsParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/TopSongsParser.java new file mode 100644 index 00000000..c3719782 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/TopSongsParser.java @@ -0,0 +1,58 @@ +/* + 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 2016 (C) Scott Jackson +*/ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; + +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.ProgressListener; + +public class TopSongsParser extends MusicDirectoryEntryParser { + + public TopSongsParser(Context context, int instance) { + super(context, instance); + } + + public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + int trackNumber = 1; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("song".equals(name)) { + MusicDirectory.Entry entry = parseEntry(""); + entry.setTrack(trackNumber); + dir.addChild(entry); + + trackNumber++; + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return dir; + } +}
\ No newline at end of file diff --git a/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java b/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java index e20556c0..fc2ddd7e 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java +++ b/app/src/main/java/github/daneren2005/dsub/service/parser/UserParser.java @@ -16,6 +16,7 @@ package github.daneren2005.dsub.service.parser; import android.content.Context; +import android.util.Log; import org.xmlpull.v1.XmlPullParser; @@ -23,10 +24,16 @@ import java.io.Reader; import java.util.ArrayList; import java.util.List; +import github.daneren2005.dsub.domain.MusicFolder; import github.daneren2005.dsub.domain.User; +import github.daneren2005.dsub.domain.User.MusicFolderSetting; +import github.daneren2005.dsub.domain.User.Setting; +import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.service.MusicServiceFactory; import github.daneren2005.dsub.util.ProgressListener; public class UserParser extends AbstractParser { + private static final String TAG = UserParser.class.getSimpleName(); public UserParser(Context context, int instance) { super(context, instance); @@ -35,14 +42,17 @@ public class UserParser extends AbstractParser { public List<User> parse(Reader reader, ProgressListener progressListener) throws Exception { init(reader); List<User> result = new ArrayList<User>(); + List<MusicFolder> musicFolders = null; + User user = null; int eventType; + String tagName = null; do { eventType = nextParseEvent(); if (eventType == XmlPullParser.START_TAG) { - String name = getElementName(); - if ("user".equals(name)) { - User user = new User(); + tagName = getElementName(); + if ("user".equals(tagName)) { + user = new User(); user.setUsername(get("username")); user.setEmail(get("email")); @@ -53,9 +63,31 @@ public class UserParser extends AbstractParser { parseSetting(user, User.LASTFM); result.add(user); - } else if ("error".equals(name)) { + } else if ("error".equals(tagName)) { handleError(); } + } else if(eventType == XmlPullParser.TEXT) { + if("folder".equals(tagName)) { + String id = getText(); + if(musicFolders == null) { + musicFolders = getMusicFolders(); + } + + if(user != null) { + if(user.getMusicFolderSettings() == null) { + for (MusicFolder musicFolder : musicFolders) { + user.addMusicFolder(musicFolder); + } + } + + for(Setting musicFolder: user.getMusicFolderSettings()) { + if(musicFolder.getName().equals(id)) { + musicFolder.setValue(true); + break; + } + } + } + } } } while (eventType != XmlPullParser.END_DOCUMENT); @@ -63,6 +95,11 @@ public class UserParser extends AbstractParser { return result; } + + private List<MusicFolder> getMusicFolders() throws Exception{ + MusicService musicService = MusicServiceFactory.getMusicService(context); + return musicService.getMusicFolders(false, context, null); + } private void parseSetting(User user, String name) { String value = get(name); diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java index 8da83be1..bcb7b92f 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/MostRecentSyncAdapter.java @@ -49,7 +49,7 @@ public class MostRecentSyncAdapter extends SubsonicSyncAdapter { } @Override - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { try { ArrayList<String> syncedList = SyncUtil.getSyncedMostRecent(context, instance); MusicDirectory albumList = musicService.getAlbumList("newest", 20, 0, tagBrowsing, context, null); diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java index a0996628..cb3c3877 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/PlaylistSyncAdapter.java @@ -56,7 +56,7 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter { } @Override - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { String serverName = Util.getServerName(context, instance); List<Playlist> remainder = null; @@ -69,6 +69,7 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter { ArrayList<SyncSet> playlistList = SyncUtil.getSyncedPlaylists(context, instance); List<String> updated = new ArrayList<String>(); + String updatedId = null; boolean removed = false; for(int i = 0; i < playlistList.size(); i++) { SyncSet cachedPlaylist = playlistList.get(i); @@ -94,9 +95,13 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter { DownloadFile file = new DownloadFile(context, entry, true); String path = file.getCompleteFile().getPath(); while(!file.isSaved() && !file.isFailedMax()) { + throwIfNetworkInvalid(); file.downloadNow(musicService); if(file.isSaved() && !updated.contains(playlist.getName())) { updated.add(playlist.getName()); + if(updatedId == null) { + updatedId = playlist.getId(); + } } } @@ -147,7 +152,7 @@ public class PlaylistSyncAdapter extends SubsonicSyncAdapter { } if(updated.size() > 0) { - Notifications.showSyncNotification(context, R.string.sync_new_playlists, SyncUtil.joinNames(updated)); + Notifications.showSyncNotification(context, R.string.sync_new_playlists, SyncUtil.joinNames(updated), updatedId); } } } diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java index 985a7267..7afcad25 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/PodcastSyncAdapter.java @@ -23,7 +23,6 @@ import android.annotation.TargetApi; import android.content.Context; import android.util.Log; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; @@ -54,7 +53,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { } @Override - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { ArrayList<SyncSet> podcastList = SyncUtil.getSyncedPodcasts(context, instance); try { @@ -68,6 +67,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { } List<String> updated = new ArrayList<String>(); + String updatedId = null; for(int i = 0; i < podcastList.size(); i++) { SyncSet set = podcastList.get(i); String id = set.id; @@ -80,6 +80,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { if(entry.getId() != null && "completed".equals(((PodcastEpisode)entry).getStatus()) && !existingEpisodes.contains(entry.getId())) { DownloadFile file = new DownloadFile(context, entry, false); while(!file.isCompleteFileAvailable() && !file.isFailedMax()) { + throwIfNetworkInvalid(); file.downloadNow(musicService); } // Only add if actualy downloaded correctly @@ -87,6 +88,9 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { existingEpisodes.add(entry.getId()); if(!updated.contains(podcasts.getName())) { updated.add(podcasts.getName()); + if(updatedId == null) { + updatedId = podcasts.getId(); + } } } } @@ -104,7 +108,7 @@ public class PodcastSyncAdapter extends SubsonicSyncAdapter { // Make sure there are is at least one change before re-syncing if(updated.size() > 0) { FileUtil.serialize(context, podcastList, SyncUtil.getPodcastSyncFile(context, instance)); - Notifications.showSyncNotification(context, R.string.sync_new_podcasts, SyncUtil.joinNames(updated)); + Notifications.showSyncNotification(context, R.string.sync_new_podcasts, SyncUtil.joinNames(updated), updatedId); } } catch(Exception e) { Log.w(TAG, "Failed to get podcasts for " + Util.getServerName(context, instance)); diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java index cf985227..0af8886b 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/StarredSyncAdapter.java @@ -20,7 +20,6 @@ package github.daneren2005.dsub.service.sync; import android.annotation.TargetApi; -import android.app.Notification; import android.content.Context; import android.util.Log; @@ -50,7 +49,7 @@ public class StarredSyncAdapter extends SubsonicSyncAdapter { } @Override - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { try { ArrayList<String> syncedList = new ArrayList<String>(); MusicDirectory starredList = musicService.getStarredList(context, null); diff --git a/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java b/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java index 661f126d..4879d032 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/service/sync/SubsonicSyncAdapter.java @@ -65,39 +65,54 @@ public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter { @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { - ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = manager.getActiveNetworkInfo(); - - // Don't try to sync if no network! - if(networkInfo == null || !networkInfo.isConnected() || Util.isOffline(context)) { - Log.w(TAG, "Not running sync, not connected to network"); + String invalidMessage = isNetworkValid(); + if(invalidMessage != null) { + Log.w(TAG, "Not running sync: " + invalidMessage); return; } - + // Make sure battery > x% or is charging IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); Intent batteryStatus = context.registerReceiver(null, intentFilter); int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); - if(status != BatteryManager.BATTERY_STATUS_CHARGING && status != BatteryManager.BATTERY_STATUS_FULL) { + if (status != BatteryManager.BATTERY_STATUS_CHARGING && status != BatteryManager.BATTERY_STATUS_FULL) { int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1); - - if((level / (float)scale) < 0.15) { + + if ((level / (float) scale) < 0.15) { Log.w(TAG, "Not running sync, battery too low"); return; } } + executeSync(context); + } + + private String isNetworkValid() { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + + // Don't try to sync if no network! + if(networkInfo == null || !networkInfo.isConnected() || Util.isOffline(context)) { + return "Not connected to any network"; + } + // Check if user wants to only sync on wifi SharedPreferences prefs = Util.getPreferences(context); if(prefs.getBoolean(Constants.PREFERENCES_KEY_SYNC_WIFI, true)) { if(networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { - executeSync(context); + return null; } else { - Log.w(TAG, "Not running sync, not connected to wifi"); + return "Not connected to WIFI"; } } else { - executeSync(context); + return null; + } + } + protected void throwIfNetworkInvalid() throws NetworkNotValidException { + String invalidMessage = isNetworkValid(); + if(invalidMessage != null) { + throw new NetworkNotValidException(invalidMessage); } } @@ -106,32 +121,39 @@ public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter { Log.i(TAG, "Running sync for " + className); long start = System.currentTimeMillis(); int servers = Util.getServerCount(context); - for(int i = 1; i <= servers; i++) { - try { - if(isValidServer(context, i) && Util.isSyncEnabled(context, i)) { - tagBrowsing = Util.isTagBrowsing(context, i); - musicService.setInstance(i); - onExecuteSync(context, i); - } else { - Log.i(TAG, "Skipped sync for " + i); + try { + for (int i = 1; i <= servers; i++) { + try { + throwIfNetworkInvalid(); + + if (isValidServer(context, i) && Util.isSyncEnabled(context, i)) { + tagBrowsing = Util.isTagBrowsing(context, i); + musicService.setInstance(i); + onExecuteSync(context, i); + } else { + Log.i(TAG, "Skipped sync for " + i); + } + } catch (Exception e) { + Log.e(TAG, "Failed sync for " + className + "(" + i + ")", e); } - } catch(Exception e) { - Log.e(TAG, "Failed sync for " + className + "(" + i + ")", e); } + } catch (NetworkNotValidException e) { + Log.e(TAG, "Stopped sync due to network loss", e); } Log.i(TAG, className + " executed in " + (System.currentTimeMillis() - start) + " ms"); } - public void onExecuteSync(Context context, int instance) { + public void onExecuteSync(Context context, int instance) throws NetworkNotValidException { } - protected boolean downloadRecursively(List<String> paths, MusicDirectory parent, Context context, boolean save) throws Exception { + protected boolean downloadRecursively(List<String> paths, MusicDirectory parent, Context context, boolean save) throws Exception,NetworkNotValidException { boolean downloaded = false; for (MusicDirectory.Entry song: parent.getChildren(false, true)) { if (!song.isVideo()) { DownloadFile file = new DownloadFile(context, song, save); while(!(save && file.isSaved() || !save && file.isCompleteFileAvailable()) && !file.isFailedMax()) { + throwIfNetworkInvalid(); file.downloadNow(musicService); if(!file.isFailed()) { downloaded = true; @@ -171,4 +193,10 @@ public class SubsonicSyncAdapter extends AbstractThreadedSyncAdapter { String url = Util.getRestUrl(context, "null", instance, false); return !(url.contains("demo.subsonic.org") || url.contains("yourhost")); } + + public class NetworkNotValidException extends Throwable { + public NetworkNotValidException(String reason) { + super("Not running sync: " + reason); + } + } } diff --git a/app/src/main/java/github/daneren2005/dsub/updates/Updater.java b/app/src/main/java/github/daneren2005/dsub/updates/Updater.java index a2870941..bc053b1e 100644 --- a/app/src/main/java/github/daneren2005/dsub/updates/Updater.java +++ b/app/src/main/java/github/daneren2005/dsub/updates/Updater.java @@ -37,13 +37,17 @@ public class Updater { protected Context context; public Updater(int version) { + // 5.2 should show as 520 instead of 52 + if(version < 100) { + version *= 10; + } this.version = version; } public void checkUpdates(Context context) { this.context = context; List<Updater> updaters = new ArrayList<Updater>(); - updaters.add(new Updater403()); + updaters.add(new UpdaterSongPress()); SharedPreferences prefs = Util.getPreferences(context); int lastVersion = prefs.getInt(Constants.LAST_VERSION, 0); diff --git a/app/src/main/java/github/daneren2005/dsub/updates/Updater403.java b/app/src/main/java/github/daneren2005/dsub/updates/Updater403.java deleted file mode 100644 index 4f2cbf43..00000000 --- a/app/src/main/java/github/daneren2005/dsub/updates/Updater403.java +++ /dev/null @@ -1,58 +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 github.daneren2005.dsub.updates; - -import android.content.Context; -import android.util.Log; -import github.daneren2005.dsub.updates.Updater; -import github.daneren2005.dsub.util.Constants; -import github.daneren2005.dsub.util.FileUtil; -import java.io.File; - -/** - * - * @author Scott - */ -public class Updater403 extends Updater { - public Updater403() { - super(403); - TAG = Updater403.class.getSimpleName(); - } - - @Override - public void update(Context context) { - // Rename cover.jpeg to cover.jpg - Log.i(TAG, "Running Updater403: updating cover.jpg to albumart.jpg"); - File dir = FileUtil.getMusicDirectory(context); - if(dir != null) { - moveArt(dir); - } - } - - private void moveArt(File dir) { - for(File file: dir.listFiles()) { - if(file.isDirectory()) { - moveArt(file); - } else if("cover.jpg".equals(file.getName()) || "cover.jpeg".equals(file.getName())) { - File renamed = new File(dir, Constants.ALBUM_ART_FILE); - file.renameTo(renamed); - } - } - } -} diff --git a/app/src/main/java/github/daneren2005/dsub/updates/UpdaterSongPress.java b/app/src/main/java/github/daneren2005/dsub/updates/UpdaterSongPress.java new file mode 100644 index 00000000..7efa18e4 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/updates/UpdaterSongPress.java @@ -0,0 +1,42 @@ +/* + 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 2016 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.updates; + +import android.content.Context; +import android.content.SharedPreferences; + +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.Util; + +public class UpdaterSongPress extends Updater { + public UpdaterSongPress() { + super(521); + TAG = this.getClass().getSimpleName(); + } + + @Override + public void update(Context context) { + SharedPreferences prefs = Util.getPreferences(context); + boolean playNowAfter = prefs.getBoolean("playNowAfter", true); + + // Migrate the old preference so behavior stays the same + if(playNowAfter == false) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_SONG_PRESS_ACTION, "single"); + editor.commit(); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java b/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java index 2af468f6..bdd961b4 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java +++ b/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java @@ -66,7 +66,6 @@ public class ArtistRadioBuffer { buffer.clear(); } - context.clear(); this.artistId = artistId; awaitingResults = true; refill(); @@ -108,7 +107,7 @@ public class ArtistRadioBuffer { } private void refill() { - if (buffer != null && (buffer.size() > refillThreshold || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0)) { + if (buffer != null && executorService != null && (buffer.size() > refillThreshold || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0)) { executorService.shutdown(); return; } diff --git a/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java index 18f245d5..31e83200 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java +++ b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java @@ -176,7 +176,7 @@ public abstract class BackgroundTask<T> implements ProgressListener { } @Override - public void updateCache() { + public void updateCache(int changeCode) { } @@ -208,8 +208,18 @@ public abstract class BackgroundTask<T> implements ProgressListener { handler.post(new Runnable() { @Override public void run() { - if(!isCancelled()) { - onDone(result); + if (!isCancelled()) { + try { + onDone(result); + } catch (Throwable t) { + if(!isCancelled()) { + try { + onError(t); + } catch(Exception e) { + // Don't care + } + } + } } taskStart.set(false); diff --git a/app/src/main/java/github/daneren2005/dsub/util/Constants.java b/app/src/main/java/github/daneren2005/dsub/util/Constants.java index 89e7de3b..4109ea67 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Constants.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Constants.java @@ -67,6 +67,7 @@ public final class Constants { public static final String INTENT_EXTRA_TOP_TRACKS = "topTracks"; public static final String INTENT_EXTRA_SHOW_ALL = "showAll"; public static final String INTENT_EXTRA_PLAY_LAST = "playLast"; + public static final String INTENT_EXTRA_ENTRY = "passedEntry"; // Preferences keys. public static final String PREFERENCES_KEY_SERVER_KEY = "server"; @@ -147,7 +148,8 @@ public final class Constants { public static final String PREFERENCES_KEY_BROWSE_TAGS = "browseTags"; public static final String PREFERENCES_KEY_OPEN_TO_TAB = "openToTab"; public static final String PREFERENCES_KEY_OVERRIDE_SYSTEM_LANGUAGE = "overrideSystemLanguage"; - public static final String PREFERENCES_KEY_PLAY_NOW_AFTER = "playNowAfter"; + // public static final String PREFERENCES_KEY_PLAY_NOW_AFTER = "playNowAfter"; + public static final String PREFERENCES_KEY_SONG_PRESS_ACTION = "songPressAction"; public static final String PREFERENCES_KEY_LARGE_ALBUM_ART = "largeAlbumArt"; public static final String PREFERENCES_KEY_ADMIN_ENABLED = "adminEnabled"; public static final String PREFERENCES_KEY_PLAYLIST_NAME = "suggestedPlaylistName"; @@ -168,6 +170,12 @@ public final class Constants { public static final String PREFERENCES_KEY_COLOR_ACTION_BAR = "colorActionBar"; public static final String PREFERENCES_KEY_SHUFFLE_BY_ALBUM = "shuffleByAlbum"; public static final String PREFERENCES_KEY_RESUME_PLAY_QUEUE_NEVER = "neverResumePlayQueue"; + public static final String PREFERENCES_KEY_BATCH_MODE = "batchMode"; + public static final String PREFERENCES_KEY_CAST_GAPLESS_PLAYBACK = "castingGaplessPlayback"; + public static final String PREFERENCES_KEY_CAST_STREAM_ORIGINAL = "castStreamOriginal"; + public static final String PREFERENCES_KEY_HEADS_UP_NOTIFICATION = "headsUpNotification"; + public static final String PREFERENCES_KEY_CAST_CACHE = "castCache"; + public static final String PREFERENCES_KEY_PLAYBACK_SPEED = "playbackSpeed"; public static final String OFFLINE_SCROBBLE_COUNT = "scrobbleCount"; public static final String OFFLINE_SCROBBLE_ID = "scrobbleID"; @@ -184,6 +192,7 @@ public final class Constants { public static final String MAIN_BACK_STACK = "backStackIds"; public static final String MAIN_BACK_STACK_SIZE = "backStackIdsSize"; public static final String MAIN_NOW_PLAYING = "nowPlayingId"; + public static final String MAIN_NOW_PLAYING_SECONDARY = "nowPlayingSecondaryId"; public static final String MAIN_SLIDE_PANEL_STATE = "slidePanelState"; public static final String FRAGMENT_LIST = "fragmentList"; public static final String FRAGMENT_LIST2 = "fragmentList2"; diff --git a/app/src/main/java/github/daneren2005/dsub/util/DrawableTint.java b/app/src/main/java/github/daneren2005/dsub/util/DrawableTint.java index 2da72579..cc8e241d 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/DrawableTint.java +++ b/app/src/main/java/github/daneren2005/dsub/util/DrawableTint.java @@ -21,6 +21,7 @@ import android.content.res.TypedArray; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.support.annotation.AttrRes; +import android.support.annotation.ColorRes; import android.support.annotation.DrawableRes; import android.util.TypedValue; @@ -48,6 +49,17 @@ public class DrawableTint { tintedDrawables.put(drawableRes, background); return background; } + public static Drawable getTintedDrawableFromColor(Context context, @DrawableRes int drawableRes, @ColorRes int colorRes) { + if(tintedDrawables.containsKey(drawableRes)) { + return tintedDrawables.get(drawableRes); + } + + int color = context.getResources().getColor(colorRes); + Drawable background = context.getResources().getDrawable(drawableRes); + background.setColorFilter(color, PorterDuff.Mode.SRC_IN); + tintedDrawables.put(drawableRes, background); + return background; + } public static int getColorRes(Context context, @AttrRes int colorAttr) { int color; if(attrMap.containsKey(colorAttr)) { diff --git a/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java b/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java new file mode 100644 index 00000000..d8046d1b --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java @@ -0,0 +1,20 @@ +/* + 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 2016 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.util; + +public final class EnvironmentVariables { + public static final String PASTEBIN_DEV_KEY = ""; +} diff --git a/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java b/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java index 85844360..a85141df 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java +++ b/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java @@ -524,44 +524,49 @@ public class ImageLoader { @Override protected Void doInBackground() throws Throwable { - MusicService musicService = MusicServiceFactory.getMusicService(mContext); - ArtistInfo artistInfo = musicService.getArtistInfo(mEntry.getId(), false, true, mContext, null); - String url = artistInfo.getImageUrl(); - - // Figure out whether we are going to get a artist image or the standard image - if(url != null && !"".equals(url.trim())) { - // If getting the artist image fails for any reason, retry for the standard version - subTask = new ViewUrlTask(mContext, mView, url, mSize) { - @Override - protected void failedToDownload() { - // Call loadImage so we can take advantage of all of it's logic checks - loadImage(mView, mEntry, mSize == imageSizeLarge, mCrossfade); - - // Delete subTask so it doesn't get called in done - subTask = null; + try { + MusicService musicService = MusicServiceFactory.getMusicService(mContext); + ArtistInfo artistInfo = musicService.getArtistInfo(mEntry.getId(), false, true, mContext, null); + String url = artistInfo.getImageUrl(); + + // Figure out whether we are going to get a artist image or the standard image + if (url != null && !"".equals(url.trim())) { + // If getting the artist image fails for any reason, retry for the standard version + subTask = new ViewUrlTask(mContext, mView, url, mSize) { + @Override + protected void failedToDownload() { + // Call loadImage so we can take advantage of all of it's logic checks + loadImage(mView, mEntry, mSize == imageSizeLarge, mCrossfade); + + // Delete subTask so it doesn't get called in done + subTask = null; + } + }; + } else { + if (mEntry != null && mEntry.getCoverArt() == null && mEntry.isDirectory() && !Util.isOffline(context)) { + // Try to lookup child cover art + MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, mEntry, true); + if (firstChild != null) { + mEntry.setCoverArt(firstChild.getCoverArt()); + } } - }; - } else { - if (mEntry != null && mEntry.getCoverArt() == null && mEntry.isDirectory() && !Util.isOffline(context)) { - // Try to lookup child cover art - MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, mEntry, true); - if (firstChild != null) { - mEntry.setCoverArt(firstChild.getCoverArt()); + + if (mEntry != null && mEntry.getCoverArt() != null) { + subTask = new ViewImageTask(mContext, mEntry, mSize, mSaveSize, mIsNowPlaying, mView, mCrossfade); + } else { + // If entry is null as well, we need to just set as a blank image + Bitmap bitmap = getUnknownImage(mEntry, mSize); + mDrawable = Util.createDrawableFromBitmap(mContext, bitmap); + return null; } } - if (mEntry != null && mEntry.getCoverArt() != null) { - subTask = new ViewImageTask(mContext, mEntry, mSize, mSaveSize, mIsNowPlaying, mView, mCrossfade); - } else { - // If entry is null as well, we need to just set as a blank image - Bitmap bitmap = getUnknownImage(mEntry, mSize); - mDrawable = Util.createDrawableFromBitmap(mContext, bitmap); - return null; - } + // Execute whichever way we decided to go + subTask.doInBackground(); + } catch (Throwable x) { + Log.e(TAG, "Failed to get artist info", x); + cancelled.set(true); } - - // Execute whichever way we decided to go - subTask.doInBackground(); return null; } diff --git a/app/src/main/java/github/daneren2005/dsub/util/Notifications.java b/app/src/main/java/github/daneren2005/dsub/util/Notifications.java index 375c9966..2948844b 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Notifications.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Notifications.java @@ -41,6 +41,7 @@ import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.provider.DSubWidgetProvider; import github.daneren2005.dsub.service.DownloadFile; import github.daneren2005.dsub.service.DownloadService; +import github.daneren2005.dsub.view.UpdateView; public final class Notifications { private static final String TAG = Notifications.class.getSimpleName(); @@ -74,6 +75,10 @@ public final class Notifications { } if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { notification.visibility = Notification.VISIBILITY_PUBLIC; + + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_HEADS_UP_NOTIFICATION, false) && !UpdateView.hasActiveActivity()) { + notification.vibrate = new long[0]; + } } RemoteViews smallContentView = new RemoteViews(context.getPackageName(), R.layout.notification); @@ -82,7 +87,7 @@ public final class Notifications { Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class); notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true); - notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); notification.contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0); playShowing = true; @@ -100,7 +105,7 @@ public final class Notifications { handler.post(new Runnable() { @Override public void run() { - if(playing) { + if (playing) { downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification); } else { playShowing = false; @@ -117,7 +122,8 @@ public final class Notifications { DSubWidgetProvider.notifyInstances(context, downloadService, playing); } - private static void setupViews(RemoteViews rv, Context context, MusicDirectory.Entry song, boolean expanded, boolean playing, boolean remote){ + private static void setupViews(RemoteViews rv, Context context, MusicDirectory.Entry song, boolean expanded, boolean playing, boolean remote) { + boolean isLongFile = song.isAudioBook() || song.isPodcast(); // Use the same text for the ticker and the expanded notification String title = song.getTitle(); @@ -152,20 +158,47 @@ public final class Notifications { if(persistent) { if(expanded) { rv.setImageViewResource(R.id.control_pause, playing ? R.drawable.notification_pause : R.drawable.notification_start); + + if(isLongFile && playing) { + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_rewind); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_fastforward); + } else { + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_backward); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_forward); + } } else { rv.setImageViewResource(R.id.control_previous, playing ? R.drawable.notification_pause : R.drawable.notification_start); - rv.setImageViewResource(R.id.control_pause, R.drawable.notification_forward); + if(isLongFile && playing) { + rv.setImageViewResource(R.id.control_pause, R.drawable.notification_fastforward); + } else { + rv.setImageViewResource(R.id.control_pause, R.drawable.notification_forward); + } rv.setImageViewResource(R.id.control_next, R.drawable.notification_close); } + } else if(isLongFile) { + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_rewind); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_fastforward); + } else { + // Necessary for switching back since it appears to re-use the same layout + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_backward); + rv.setImageViewResource(R.id.control_next, R.drawable.notification_forward); } // Create actions for media buttons PendingIntent pendingIntent; - int previous = 0, pause = 0, next = 0, close = 0; + int previous = 0, pause = 0, next = 0, close = 0, rewind = 0, fastForward = 0; if(persistent && !expanded) { pause = R.id.control_previous; - next = R.id.control_pause; + if(isLongFile && playing) { + fastForward = R.id.control_pause; + } else { + next = R.id.control_pause; + } close = R.id.control_next; + } else if(isLongFile && (!persistent || (expanded && playing))) { + rewind = R.id.control_previous; + pause = R.id.control_pause; + fastForward = R.id.control_next; } else { previous = R.id.control_previous; pause = R.id.control_pause; @@ -180,21 +213,28 @@ public final class Notifications { if(previous > 0) { Intent prevIntent = new Intent("KEYCODE_MEDIA_PREVIOUS"); prevIntent.setComponent(new ComponentName(context, DownloadService.class)); - prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); + prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); rv.setOnClickPendingIntent(previous, pendingIntent); } + if(rewind > 0) { + Intent rewindIntent = new Intent("KEYCODE_MEDIA_REWIND"); + rewindIntent.setComponent(new ComponentName(context, DownloadService.class)); + rewindIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_REWIND)); + pendingIntent = PendingIntent.getService(context, 0, rewindIntent, 0); + rv.setOnClickPendingIntent(rewind, pendingIntent); + } if(pause > 0) { if(playing) { Intent pauseIntent = new Intent("KEYCODE_MEDIA_PLAY_PAUSE"); pauseIntent.setComponent(new ComponentName(context, DownloadService.class)); - pauseIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); + pauseIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); pendingIntent = PendingIntent.getService(context, 0, pauseIntent, 0); rv.setOnClickPendingIntent(pause, pendingIntent); } else { Intent prevIntent = new Intent("KEYCODE_MEDIA_START"); prevIntent.setComponent(new ComponentName(context, DownloadService.class)); - prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY)); + prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY)); pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); rv.setOnClickPendingIntent(pause, pendingIntent); } @@ -202,14 +242,21 @@ public final class Notifications { if(next > 0) { Intent nextIntent = new Intent("KEYCODE_MEDIA_NEXT"); nextIntent.setComponent(new ComponentName(context, DownloadService.class)); - nextIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); + nextIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT)); pendingIntent = PendingIntent.getService(context, 0, nextIntent, 0); rv.setOnClickPendingIntent(next, pendingIntent); } + if(fastForward > 0) { + Intent fastForwardIntent = new Intent("KEYCODE_MEDIA_FAST_FORWARD"); + fastForwardIntent.setComponent(new ComponentName(context, DownloadService.class)); + fastForwardIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD)); + pendingIntent = PendingIntent.getService(context, 0, fastForwardIntent, 0); + rv.setOnClickPendingIntent(fastForward, pendingIntent); + } if(close > 0) { Intent prevIntent = new Intent("KEYCODE_MEDIA_STOP"); prevIntent.setComponent(new ComponentName(context, DownloadService.class)); - prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP)); + prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_STOP)); pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); rv.setOnClickPendingIntent(close, pendingIntent); } @@ -270,7 +317,7 @@ public final class Notifications { Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class); notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD_VIEW, true); - notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); builder.setContentIntent(PendingIntent.getActivity(context, 2, notificationIntent, 0)); final Notification notification = builder.build(); @@ -306,6 +353,9 @@ public final class Notifications { } public static void showSyncNotification(final Context context, int stringId, String extra) { + showSyncNotification(context, stringId, extra, null); + } + public static void showSyncNotification(final Context context, int stringId, String extra, String extraId) { if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_SYNC_NOTIFICATION, true)) { if(extra == null) { extra = ""; @@ -323,7 +373,7 @@ public final class Notifications { .setAutoCancel(true); Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class); - notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); String tab = null, type = null; switch(stringId) { @@ -346,6 +396,9 @@ public final class Notifications { if(type != null) { notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type); } + if(extraId != null) { + notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_ID, extraId); + } builder.setContentIntent(PendingIntent.getActivity(context, stringId, notificationIntent, 0)); diff --git a/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java b/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java index 22f35efc..603b1ccb 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java +++ b/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java @@ -24,5 +24,5 @@ package github.daneren2005.dsub.util; public interface ProgressListener { void updateProgress(String message); void updateProgress(int messageId); - void updateCache(); + void updateCache(int changeCode); } diff --git a/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java b/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java index 8d91a251..90c885f6 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java +++ b/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java @@ -120,6 +120,7 @@ public class SongDBHandler extends SQLiteOpenHelper { values.put(SONGS_SERVER_KEY, serverKey); values.put(SONGS_SERVER_ID, entry.getFirst()); values.put(SONGS_COMPLETE_PATH, entry.getSecond()); + // Util.sleepQuietly(10000); db.insertWithOnConflict(TABLE_SONGS, null, values, SQLiteDatabase.CONFLICT_IGNORE); } @@ -150,11 +151,23 @@ public class SongDBHandler extends SQLiteOpenHelper { db.close(); } + public boolean hasBeenPlayed(MusicDirectory.Entry entry) { + Long[] lastPlayed = getLastPlayed(entry); + return lastPlayed != null && lastPlayed[0] != null && lastPlayed[0] > 0; + } + public boolean hasBeenCompleted(MusicDirectory.Entry entry) { + Long[] lastPlayed = getLastPlayed(entry); + return lastPlayed != null && lastPlayed[1] != null && lastPlayed[1] > 0; + } public synchronized Long[] getLastPlayed(MusicDirectory.Entry entry) { return getLastPlayed(getOnlineSongId(entry)); } protected synchronized Long[] getLastPlayed(Pair<Integer, String> pair) { - return getLastPlayed(pair.getFirst(), pair.getSecond()); + if(pair == null) { + return null; + } else { + return getLastPlayed(pair.getFirst(), pair.getSecond()); + } } public synchronized Long[] getLastPlayed(int serverKey, String id) { SQLiteDatabase db = this.getReadableDatabase(); @@ -169,9 +182,12 @@ public class SongDBHandler extends SQLiteOpenHelper { dates[0] = cursor.getLong(0); dates[1] = cursor.getLong(1); return dates; - } catch(Exception e) {} - - return null; + } catch(Exception e) { + return null; + } + finally { + db.close(); + } } public synchronized Pair<Integer, String> getOnlineSongId(MusicDirectory.Entry entry) { @@ -210,9 +226,12 @@ public class SongDBHandler extends SQLiteOpenHelper { try { cursor.moveToFirst(); return new Pair(cursor.getInt(0), cursor.getString(1)); - } catch(Exception e) {} - - return null; + } catch(Exception e) { + return null; + } + finally { + db.close(); + } } public synchronized Pair<Integer, String> getIdFromPath(int serverKey, String path) { SQLiteDatabase db = this.getReadableDatabase(); @@ -223,9 +242,12 @@ public class SongDBHandler extends SQLiteOpenHelper { try { cursor.moveToFirst(); return new Pair(cursor.getInt(0), cursor.getString(1)); - } catch(Exception e) {} - - return null; + } catch(Exception e) { + return null; + } + finally { + db.close(); + } } public static SongDBHandler getHandler(Context context) { diff --git a/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java b/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java index c7e0a04b..4cf25b30 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java +++ b/app/src/main/java/github/daneren2005/dsub/util/UpdateHelper.java @@ -27,6 +27,7 @@ import android.util.Log; import android.view.View; import android.widget.RatingBar; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -51,9 +52,24 @@ public final class UpdateHelper { } public static void toggleStarred(final Context context, final Entry entry, final OnStarChange onStarChange) { - final boolean starred = !entry.isStarred(); - entry.setStarred(starred); + toggleStarred(context, Arrays.asList(entry), onStarChange); + } + + public static void toggleStarred(Context context, List<Entry> entries) { + toggleStarred(context, entries, null); + } + public static void toggleStarred(final Context context, final List<Entry> entries, final OnStarChange onStarChange) { + if(entries.isEmpty()) { + return; + } + + final Entry firstEntry = entries.get(0); + final boolean starred = !firstEntry.isStarred(); + for(Entry entry: entries) { + entry.setStarred(starred); + } if(onStarChange != null) { + onStarChange.entries = entries; onStarChange.starChange(starred); } @@ -61,22 +77,30 @@ public final class UpdateHelper { @Override protected Void doInBackground() throws Throwable { MusicService musicService = MusicServiceFactory.getMusicService(context); - if(entry.isDirectory() && Util.isTagBrowsing(context) && !Util.isOffline(context)) { - if(entry.isAlbum()) { - musicService.setStarred(null, null, Arrays.asList(entry), starred, null, context); + List<Entry> songs = new ArrayList<Entry>(); + List<Entry> artists = new ArrayList<Entry>(); + List<Entry> albums = new ArrayList<Entry>(); + for(Entry entry: entries) { + if(entry.isDirectory() && Util.isTagBrowsing(context)) { + if(entry.isAlbum()) { + albums.add(entry); + } else { + artists.add(entry); + } } else { - musicService.setStarred(null, Arrays.asList(entry), null, starred, null, context); + songs.add(entry); } - } else { - musicService.setStarred(Arrays.asList(entry), null, null, starred, null, context); } - - new EntryInstanceUpdater(entry) { - @Override - public void update(Entry found) { - found.setStarred(starred); - } - }.execute(); + musicService.setStarred(songs, artists, albums, starred, this, context); + + for(Entry entry: entries) { + new UpdateHelper.EntryInstanceUpdater(entry) { + @Override + public void update(Entry found) { + found.setStarred(starred); + } + }.execute(); + } return null; } @@ -84,13 +108,21 @@ public final class UpdateHelper { @Override protected void done(Void result) { // UpdateView - Util.toast(context, context.getResources().getString(starred ? R.string.starring_content_starred : R.string.starring_content_unstarred, entry.getTitle())); + int starMsgId = starred ? R.string.starring_content_starred : R.string.starring_content_unstarred; + String starMsgBody = (entries.size() > 1) ? Integer.toString(entries.size()) : firstEntry.getTitle(); + Util.toast(context, context.getResources().getString(starMsgId, starMsgBody)); + + if(onStarChange != null) { + onStarChange.starCommited(starred); + } } @Override protected void error(Throwable error) { Log.w(TAG, "Failed to star", error); - entry.setStarred(!starred); + for(Entry entry: entries) { + entry.setStarred(!starred); + } if(onStarChange != null) { onStarChange.starChange(!starred); } @@ -99,7 +131,8 @@ public final class UpdateHelper { if (error instanceof OfflineException || error instanceof ServerTooOldException) { msg = getErrorMessage(error); } else { - msg = context.getResources().getString(R.string.starring_content_error, entry.getTitle()) + " " + getErrorMessage(error); + String errorBody = (entries.size() > 1) ? Integer.toString(entries.size()) : firstEntry.getTitle(); + msg = context.getResources().getString(R.string.starring_content_error, errorBody) + " " + getErrorMessage(error); } Util.toast(context, msg, false); @@ -215,6 +248,7 @@ public final class UpdateHelper { msg = context.getResources().getString(rating > 0 ? R.string.rating_set_rating_failed : R.string.rating_remove_rating_failed, entry.getTitle()) + " " + getErrorMessage(error); } + Log.e(TAG, "Failed to setRating", error); Util.toast(context, msg, false); } }.execute(); @@ -222,10 +256,15 @@ public final class UpdateHelper { public static abstract class EntryInstanceUpdater { private Entry entry; + protected int metadataUpdate = DownloadService.METADATA_UPDATED_ALL; public EntryInstanceUpdater(Entry entry) { this.entry = entry; } + public EntryInstanceUpdater(Entry entry, int metadataUpdate) { + this.entry = entry; + this.metadataUpdate = metadataUpdate; + } public abstract void update(Entry found); @@ -234,11 +273,17 @@ public final class UpdateHelper { if(downloadService != null && !entry.isDirectory()) { boolean serializeChanges = false; List<DownloadFile> downloadFiles = downloadService.getDownloads(); + DownloadFile currentPlaying = downloadService.getCurrentPlaying(); + for(DownloadFile file: downloadFiles) { Entry check = file.getSong(); if(entry.getId().equals(check.getId())) { - update(entry); + update(check); serializeChanges = true; + + if(currentPlaying != null && currentPlaying.getSong() != null && currentPlaying.getSong().getId().equals(entry.getId())) { + downloadService.onMetadataUpdate(metadataUpdate); + } } } @@ -255,7 +300,10 @@ public final class UpdateHelper { } public static abstract class OnStarChange { + protected List<Entry> entries; + public abstract void starChange(boolean starred); + public abstract void starCommited(boolean starred); } public static abstract class OnRatingChange { public abstract void ratingChange(int rating); diff --git a/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java b/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java index 6417dc81..24d3906b 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java +++ b/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java @@ -29,8 +29,6 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.WindowManager; -import android.widget.ArrayAdapter; -import android.widget.ListView; import android.widget.TextView; import github.daneren2005.dsub.R; @@ -159,8 +157,11 @@ public final class UserUtil { return defaultValue; } - - public static void confirmCredentials(final Activity context, final Runnable onSuccess) { + + public static void confirmCredentials(Activity context, Runnable onSuccess) { + confirmCredentials(context, onSuccess, null); + } + public static void confirmCredentials(final Activity context, final Runnable onSuccess, final Runnable onCancel) { final long currentTime = System.currentTimeMillis(); // If already ran this check within last x time, just go ahead and auth if((currentTime - lastVerifiedTime) < MIN_VERIFY_DURATION) { @@ -175,12 +176,7 @@ public final class UserUtil { .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { - String password = passwordView.getText().toString(); - - SharedPreferences prefs = Util.getPreferences(context); - String correctPassword = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + Util.getActiveServer(context), null); - - if(password != null && password.equals(correctPassword)) { + if(isPasswordCorrect(context, passwordView)) { lastVerifiedTime = currentTime; onSuccess.run(); } else { @@ -188,7 +184,14 @@ public final class UserUtil { } } }) - .setNegativeButton(R.string.common_cancel, null) + .setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if(onCancel != null) { + onCancel.run(); + } + } + }) .setCancelable(true); AlertDialog dialog = builder.create(); @@ -199,8 +202,14 @@ public final class UserUtil { public static void changePassword(final Activity context, final User user) { View layout = context.getLayoutInflater().inflate(R.layout.change_password, null); + View currentPasswordLayout = layout.findViewById(R.id.current_password_layout); + final TextView currentPasswordView = (TextView) layout.findViewById(R.id.current_password); final TextView passwordView = (TextView) layout.findViewById(R.id.new_password); + if(isCurrentAdmin()) { + currentPasswordLayout.setVisibility(View.GONE); + } + AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.admin_change_password) .setView(layout) @@ -215,8 +224,12 @@ public final class UserUtil { @Override public void onClick(View v) { final String password = passwordView.getText().toString(); + if(!isCurrentAdmin() && !isPasswordCorrect(context, currentPasswordView)) { + Util.toast(context, R.string.admin_confirm_password_bad); + return; + } // Don't allow blank passwords - if ("".equals(password)) { + else if ("".equals(password)) { Util.toast(context, R.string.admin_change_password_invalid); return; } @@ -252,6 +265,16 @@ public final class UserUtil { }); } + private static boolean isPasswordCorrect(Context context, TextView passwordView) { + return isPasswordCorrect(context, passwordView.getText().toString()); + } + private static boolean isPasswordCorrect(Context context, String password) { + SharedPreferences prefs = Util.getPreferences(context); + String correctPassword = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + Util.getActiveServer(context), null); + + return password != null && password.equals(correctPassword); + } + public static void updateSettings(final Context context, final User user) { new SilentBackgroundTask<Void>(context) { @Override @@ -373,7 +396,7 @@ public final class UserUtil { }); } - public static void addNewUser(final Activity context, final SubsonicFragment fragment) { + public static void addNewUser(final Activity context, final SubsonicFragment fragment, User sampleUser) { final User user = new User(); for(String role: User.ROLES) { if(role.equals(User.SETTINGS) || role.equals(User.STREAM)) { @@ -383,6 +406,13 @@ public final class UserUtil { } } + if(sampleUser.getMusicFolderSettings() != null) { + for(User.Setting setting: sampleUser.getMusicFolderSettings()) { + User.MusicFolderSetting musicFolderSetting = (User.MusicFolderSetting) setting; + user.addMusicFolder(musicFolderSetting, true); + } + } + View layout = context.getLayoutInflater().inflate(R.layout.create_user, null); final TextView usernameView = (TextView) layout.findViewById(R.id.username); final TextView emailView = (TextView) layout.findViewById(R.id.email); @@ -391,7 +421,7 @@ public final class UserUtil { LinearLayoutManager layoutManager = new LinearLayoutManager(context); layoutManager.setOrientation(LinearLayoutManager.VERTICAL); recyclerView.setLayoutManager(layoutManager); - recyclerView.setAdapter(new SettingsAdapter(context, user, null, true, new SectionAdapter.OnItemClickedListener<User.Setting>() { + recyclerView.setAdapter(SettingsAdapter.getSettingsAdapter(context, user, null, true, new SectionAdapter.OnItemClickedListener<User.Setting>() { @Override public void onItemClicked(UpdateView<User.Setting> updateView, User.Setting item) { if(updateView.isCheckable()) { diff --git a/app/src/main/java/github/daneren2005/dsub/util/Util.java b/app/src/main/java/github/daneren2005/dsub/util/Util.java index bf4af20d..fc7292f6 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Util.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Util.java @@ -22,6 +22,8 @@ import android.app.Activity; import android.graphics.Color; import android.support.annotation.StringRes; import android.support.v7.app.AlertDialog; +import android.content.ClipboardManager; +import android.content.ClipData; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -46,9 +48,11 @@ import android.text.method.LinkMovementMethod; import android.text.util.Linkify; import android.util.Log; import android.util.SparseArray; +import android.view.View; import android.view.Gravity; import android.view.Window; import android.view.WindowManager; +import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; @@ -107,6 +111,7 @@ public final class Util { private static DecimalFormat BYTE_LOCALIZED_FORMAT = null; private static SimpleDateFormat DATE_FORMAT_SHORT = new SimpleDateFormat("MMM d h:mm a"); private static SimpleDateFormat DATE_FORMAT_LONG = new SimpleDateFormat("MMM d, yyyy h:mm a"); + private static SimpleDateFormat DATE_FORMAT_NO_TIME = new SimpleDateFormat("MMM d, yyyy"); private static int CURRENT_YEAR = new Date().getYear(); public static final String EVENT_META_CHANGED = "github.daneren2005.dsub.EVENT_META_CHANGED"; @@ -376,6 +381,12 @@ public final class Util { int cacheSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CACHE_SIZE, "-1")); return cacheSize == -1 ? Integer.MAX_VALUE : cacheSize; } + public static boolean isBatchMode(Context context) { + return Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, false); + } + public static void setBatchMode(Context context, boolean batchMode) { + Util.getPreferences(context).edit().putBoolean(Constants.PREFERENCES_KEY_BATCH_MODE, batchMode).commit(); + } public static String getRestUrl(Context context, String method) { return getRestUrl(context, method, true); @@ -401,13 +412,15 @@ public final class Util { String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); if(allowAltAddress && Util.isWifiConnected(context)) { String SSID = prefs.getString(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance, ""); - String currentSSID = Util.getSSID(context); - - String[] ssidParts = SSID.split(","); - if("".equals(SSID) || SSID.equals(currentSSID) || Arrays.asList(ssidParts).contains(currentSSID)) { - String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null); - if(internalUrl != null && !"".equals(internalUrl) && !"http://".equals(internalUrl)) { - serverUrl = internalUrl; + if(!SSID.isEmpty()) { + String currentSSID = Util.getSSID(context); + + String[] ssidParts = SSID.split(","); + if ("".equals(SSID) || SSID.equals(currentSSID) || Arrays.asList(ssidParts).contains(currentSSID)) { + String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null); + if (internalUrl != null && !"".equals(internalUrl) && !"http://".equals(internalUrl)) { + serverUrl = internalUrl; + } } } } @@ -683,11 +696,19 @@ public final class Util { editor.commit(); } + public static boolean shouldCacheDuringCasting(Context context) { + return Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CAST_CACHE, false); + } + public static boolean shouldStartOnHeadphones(Context context) { SharedPreferences prefs = getPreferences(context); return prefs.getBoolean(Constants.PREFERENCES_KEY_START_ON_HEADPHONES, false); } + public static String getSongPressAction(Context context) { + return getPreferences(context).getString(Constants.PREFERENCES_KEY_SONG_PRESS_ACTION, "all"); + } + /** * Get the contents of an <code>InputStream</code> as a <code>byte[]</code>. * <p/> @@ -901,26 +922,42 @@ public final class Util { } public static String formatDate(Context context, String dateString) { + return formatDate(context, dateString, true); + } + public static String formatDate(Context context, String dateString, boolean includeTime) { + if(dateString == null) { + return ""; + } + try { + dateString = dateString.replace(' ', 'T'); boolean isDateNormalized = ServerInfo.checkServerVersion(context, "1.11"); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH); if (isDateNormalized) { dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); } - return formatDate(dateFormat.parse(dateString)); + return formatDate(dateFormat.parse(dateString), includeTime); } catch(ParseException e) { + Log.e(TAG, "Failed to parse date string", e); return dateString; } } public static String formatDate(Date date) { + return formatDate(date, true); + } + public static String formatDate(Date date, boolean includeTime) { if(date == null) { return "Never"; } else { - if(date.getYear() != CURRENT_YEAR) { - return DATE_FORMAT_LONG.format(date); + if(includeTime) { + if (date.getYear() != CURRENT_YEAR) { + return DATE_FORMAT_LONG.format(date); + } else { + return DATE_FORMAT_SHORT.format(date); + } } else { - return DATE_FORMAT_SHORT.format(date); + return DATE_FORMAT_NO_TIME.format(date); } } } @@ -1222,12 +1259,36 @@ public final class Util { } showDetailsDialog(context, context.getResources().getString(title), headerStrings, details); } - public static void showDetailsDialog(Context context, String title, List<String> headers, List<String> details) { + public static void showDetailsDialog(Context context, String title, List<String> headers, final List<String> details) { ListView listView = new ListView(context); listView.setAdapter(new DetailsAdapter(context, R.layout.details_item, headers, details)); listView.setDivider(null); listView.setScrollbarFadingEnabled(false); + // Let the user long-click on a row to copy its value to the clipboard + final Context contextRef = context; + listView.setOnItemLongClickListener(new ListView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView<?> parent, View view, int pos, long id) { + TextView nameView = (TextView) view.findViewById(R.id.detail_name); + TextView detailsView = (TextView) view.findViewById(R.id.detail_value); + if(nameView == null || detailsView == null) { + return false; + } + + CharSequence name = nameView.getText(); + CharSequence value = detailsView.getText(); + + ClipboardManager clipboard = (ClipboardManager) contextRef.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(name, value); + clipboard.setPrimaryClip(clip); + + toast(contextRef, "Copied " + name + " to clipboard"); + + return true; + } + }); + new AlertDialog.Builder(context) // .setIcon(android.R.drawable.ic_dialog_info) .setTitle(title) @@ -1367,72 +1428,79 @@ public final class Util { * <p>Broadcasts the given song info as the new song being played.</p> */ public static void broadcastNewTrackInfo(Context context, MusicDirectory.Entry song) { - DownloadService downloadService = (DownloadService)context; - Intent intent = new Intent(EVENT_META_CHANGED); - Intent avrcpIntent = new Intent(AVRCP_METADATA_CHANGED); - - if (song != null) { - intent.putExtra("title", song.getTitle()); - intent.putExtra("artist", song.getArtist()); - intent.putExtra("album", song.getAlbum()); - - File albumArtFile = FileUtil.getAlbumArtFile(context, song); - intent.putExtra("coverart", albumArtFile.getAbsolutePath()); - avrcpIntent.putExtra("playing", true); - } else { - intent.putExtra("title", ""); - intent.putExtra("artist", ""); - intent.putExtra("album", ""); - intent.putExtra("coverart", ""); - avrcpIntent.putExtra("playing", false); - } - addTrackInfo(context, song, avrcpIntent); + try { + Intent intent = new Intent(EVENT_META_CHANGED); + Intent avrcpIntent = new Intent(AVRCP_METADATA_CHANGED); - context.sendBroadcast(intent); - context.sendBroadcast(avrcpIntent); + if (song != null) { + intent.putExtra("title", song.getTitle()); + intent.putExtra("artist", song.getArtist()); + intent.putExtra("album", song.getAlbum()); + + File albumArtFile = FileUtil.getAlbumArtFile(context, song); + intent.putExtra("coverart", albumArtFile.getAbsolutePath()); + avrcpIntent.putExtra("playing", true); + } else { + intent.putExtra("title", ""); + intent.putExtra("artist", ""); + intent.putExtra("album", ""); + intent.putExtra("coverart", ""); + avrcpIntent.putExtra("playing", false); + } + addTrackInfo(context, song, avrcpIntent); + + context.sendBroadcast(intent); + context.sendBroadcast(avrcpIntent); + } catch(Exception e) { + Log.e(TAG, "Failed to broadcastNewTrackInfo", e); + } } /** * <p>Broadcasts the given player state as the one being set.</p> */ public static void broadcastPlaybackStatusChange(Context context, MusicDirectory.Entry song, PlayerState state) { - Intent intent = new Intent(EVENT_PLAYSTATE_CHANGED); - Intent avrcpIntent = new Intent(AVRCP_PLAYSTATE_CHANGED); - - switch (state) { - case STARTED: - intent.putExtra("state", "play"); - avrcpIntent.putExtra("playing", true); - break; - case STOPPED: - intent.putExtra("state", "stop"); - avrcpIntent.putExtra("playing", false); - break; - case PAUSED: - intent.putExtra("state", "pause"); - avrcpIntent.putExtra("playing", false); - break; - case PREPARED: - // Only send quick pause event for samsung devices, causes issues for others - if(Build.MANUFACTURER.toLowerCase().indexOf("samsung") != -1) { + try { + Intent intent = new Intent(EVENT_PLAYSTATE_CHANGED); + Intent avrcpIntent = new Intent(AVRCP_PLAYSTATE_CHANGED); + + switch (state) { + case STARTED: + intent.putExtra("state", "play"); + avrcpIntent.putExtra("playing", true); + break; + case STOPPED: + intent.putExtra("state", "stop"); avrcpIntent.putExtra("playing", false); - } else { - return; // Don't broadcast anything - } - break; - case COMPLETED: - intent.putExtra("state", "complete"); - avrcpIntent.putExtra("playing", false); - break; - default: - return; // No need to broadcast. - } - addTrackInfo(context, song, avrcpIntent); + break; + case PAUSED: + intent.putExtra("state", "pause"); + avrcpIntent.putExtra("playing", false); + break; + case PREPARED: + // Only send quick pause event for samsung devices, causes issues for others + if (Build.MANUFACTURER.toLowerCase().indexOf("samsung") != -1) { + avrcpIntent.putExtra("playing", false); + } else { + return; // Don't broadcast anything + } + break; + case COMPLETED: + intent.putExtra("state", "complete"); + avrcpIntent.putExtra("playing", false); + break; + default: + return; // No need to broadcast. + } + addTrackInfo(context, song, avrcpIntent); - if(state != PlayerState.PREPARED) { - context.sendBroadcast(intent); + if (state != PlayerState.PREPARED) { + context.sendBroadcast(intent); + } + context.sendBroadcast(avrcpIntent); + } catch(Exception e) { + Log.e(TAG, "Failed to broadcastPlaybackStatusChange", e); } - context.sendBroadcast(avrcpIntent); } private static void addTrackInfo(Context context, MusicDirectory.Entry song, Intent intent) { @@ -1448,6 +1516,7 @@ public final class Util { intent.putExtra("duration", (long) downloadService.getPlayerDuration()); intent.putExtra("position", (long) downloadService.getPlayerPosition()); intent.putExtra("coverart", albumArtFile.getAbsolutePath()); + intent.putExtra("package","github.daneren2005.dsub"); } else { intent.putExtra("track", ""); intent.putExtra("artist", ""); @@ -1457,6 +1526,7 @@ public final class Util { intent.putExtra("duration", (long) 0); intent.putExtra("position", (long) 0); intent.putExtra("coverart", ""); + intent.putExtra("package","github.daneren2005.dsub"); } } diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java index 456446f3..df468155 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java +++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientLP.java @@ -34,16 +34,23 @@ import android.media.session.PlaybackState; import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; +import android.support.annotation.NonNull; import android.support.v7.media.MediaRouter; +import android.util.Log; +import android.view.KeyEvent; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; import github.daneren2005.dsub.R; import github.daneren2005.dsub.activity.SubsonicActivity; import github.daneren2005.dsub.activity.SubsonicFragmentActivity; +import github.daneren2005.dsub.domain.Bookmark; import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicDirectory.Entry; import github.daneren2005.dsub.domain.Playlist; +import github.daneren2005.dsub.domain.PodcastEpisode; import github.daneren2005.dsub.domain.SearchCritera; import github.daneren2005.dsub.domain.SearchResult; import github.daneren2005.dsub.service.DownloadFile; @@ -87,7 +94,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { Intent activityIntent = new Intent(context, SubsonicFragmentActivity.class); activityIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true); - activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + activityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0, activityIntent, 0); mediaSession.setSessionActivity(activityPendingIntent); @@ -141,11 +148,17 @@ public class RemoteControlClientLP extends RemoteControlClientBase { position = downloadService.getPlayerPosition(); } builder.setState(newState, position, 1.0f); - builder.setActions(getPlaybackActions()); - DownloadFile downloadFile = downloadService.getCurrentPlaying(); + Entry entry = null; + boolean isSong = true; if(downloadFile != null) { - MusicDirectory.Entry entry = downloadFile.getSong(); + entry = downloadFile.getSong(); + isSong = entry.isSong(); + } + + builder.setActions(getPlaybackActions(isSong)); + + if(entry != null) { addCustomActions(entry, builder); builder.setActiveQueueItemId(entry.getId().hashCode()); } @@ -156,7 +169,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } @Override - public void updateMetadata(Context context, MusicDirectory.Entry currentSong) { + public void updateMetadata(Context context, Entry currentSong) { setMetadata(currentSong, null); if(currentSong != null && imageLoader != null) { @@ -165,11 +178,11 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } @Override - public void metadataChanged(MusicDirectory.Entry currentSong) { + public void metadataChanged(Entry currentSong) { setPlaybackState(previousState); } - public void setMetadata(MusicDirectory.Entry currentSong, Bitmap bitmap) { + public void setMetadata(Entry currentSong, Bitmap bitmap) { MediaMetadata.Builder builder = new MediaMetadata.Builder(); builder.putString(MediaMetadata.METADATA_KEY_ARTIST, (currentSong == null) ? null : currentSong.getArtist()) .putString(MediaMetadata.METADATA_KEY_ALBUM, (currentSong == null) ? null : currentSong.getAlbum()) @@ -189,7 +202,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } @Override - public void updateAlbumArt(MusicDirectory.Entry currentSong, Bitmap bitmap) { + public void updateAlbumArt(Entry currentSong, Bitmap bitmap) { setMetadata(currentSong, bitmap); } @@ -208,7 +221,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { List<MediaSession.QueueItem> queue = new ArrayList<>(); for(DownloadFile file: playlist) { - MusicDirectory.Entry entry = file.getSong(); + Entry entry = file.getSong(); MediaDescription description = new MediaDescription.Builder() .setMediaId(entry.getId()) @@ -227,7 +240,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { return mediaSession; } - protected long getPlaybackActions() { + protected long getPlaybackActions(boolean isSong) { long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_SEEK_TO | @@ -235,16 +248,21 @@ public class RemoteControlClientLP extends RemoteControlClientBase { int currentIndex = downloadService.getCurrentPlayingIndex(); int size = downloadService.size(); - if(currentIndex > 0) { + if(isSong) { + if (currentIndex > 0) { + actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS; + } + if (currentIndex < size - 1) { + actions |= PlaybackState.ACTION_SKIP_TO_NEXT; + } + } else { actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS; - } - if(currentIndex < size - 1) { actions |= PlaybackState.ACTION_SKIP_TO_NEXT; } return actions; } - protected void addCustomActions(MusicDirectory.Entry currentSong, PlaybackState.Builder builder) { + protected void addCustomActions(Entry currentSong, PlaybackState.Builder builder) { Bundle showOnWearExtras = new Bundle(); showOnWearExtras.putBoolean(SHOW_ON_WEAR, true); @@ -296,7 +314,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { SearchResult results = musicService.search(searchCritera, downloadService, null); if(results.hasArtists()) { - playFromParent(new MusicDirectory.Entry(results.getArtists().get(0))); + playFromParent(new Entry(results.getArtists().get(0))); } else if(results.hasAlbums()) { playFromParent(results.getAlbums().get(0)); } else if(results.hasSongs()) { @@ -307,13 +325,13 @@ public class RemoteControlClientLP extends RemoteControlClientBase { return null; } - - private void playFromParent(MusicDirectory.Entry parent) throws Exception { - List<MusicDirectory.Entry> songs = new ArrayList<>(); + + private void playFromParent(Entry parent) throws Exception { + List<Entry> songs = new ArrayList<>(); getSongsRecursively(parent, songs); playSongs(songs); } - private void getSongsRecursively(MusicDirectory.Entry parent, List<MusicDirectory.Entry> songs) throws Exception { + private void getSongsRecursively(Entry parent, List<Entry> songs) throws Exception { MusicDirectory musicDirectory; if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) { musicDirectory = musicService.getAlbum(parent.getId(), parent.getTitle(), false, downloadService, this); @@ -321,7 +339,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { musicDirectory = musicService.getMusicDirectory(parent.getId(), parent.getTitle(), false, downloadService, this); } - for (MusicDirectory.Entry dir : musicDirectory.getChildren(true, false)) { + for (Entry dir : musicDirectory.getChildren(true, false)) { if (dir.getRating() == 1) { continue; } @@ -329,7 +347,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { getSongsRecursively(dir, songs); } - for (MusicDirectory.Entry song : musicDirectory.getChildren(false, true)) { + for (Entry song : musicDirectory.getChildren(false, true)) { if (!song.isVideo() && song.getRating() != 1) { songs.add(song); } @@ -349,20 +367,69 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } }.execute(); } + private void playMusicDirectory(Entry dir, boolean shuffle, boolean append, boolean playFromBookmark) { + playMusicDirectory(dir.getId(), shuffle, append, playFromBookmark); + } + private void playMusicDirectory(final String dirId, final boolean shuffle, final boolean append, final boolean playFromBookmark) { + new SilentServiceTask<Void>(downloadService) { + @Override + protected Void doInBackground(MusicService musicService) throws Throwable { + MusicDirectory musicDirectory; + if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) { + musicDirectory = musicService.getAlbum(dirId, "dir", false, downloadService, null); + } else { + musicDirectory = musicService.getMusicDirectory(dirId, "dir", false, downloadService, null); + } + + List<Entry> playEntries = new ArrayList<>(); + List<Entry> allEntries = musicDirectory.getChildren(false, true); + for(Entry song: allEntries) { + if (!song.isVideo() && song.getRating() != 1) { + playEntries.add(song); + } + } + playSongs(playEntries, shuffle, append, playFromBookmark); + + return null; + } + }.execute(); + } - private void playSong(MusicDirectory.Entry entry) { - List<MusicDirectory.Entry> entries = new ArrayList<>(); + private void playSong(Entry entry) { + + } + private void playSong(Entry entry, boolean resumeFromBookmark) { + List<Entry> entries = new ArrayList<>(); entries.add(entry); - playSongs(entries); + playSongs(entries, false, false, resumeFromBookmark); } - private void playSongs(List<MusicDirectory.Entry> entries) { + private void playSongs(List<Entry> entries) { playSongs(entries, false, false); } - private void playSongs(List<MusicDirectory.Entry> entries, boolean shuffle, boolean append) { + private void playSongs(List<Entry> entries, boolean shuffle, boolean append) { + playSongs(entries, shuffle, append, false); + } + private void playSongs(List<Entry> entries, boolean shuffle, boolean append, boolean resumeFromBookmark) { if(!append) { downloadService.clear(); } - downloadService.download(entries, false, true, false, shuffle); + + int startIndex = 0; + int startPosition = 0; + if(resumeFromBookmark) { + int bookmarkIndex = 0; + for(Entry entry: entries) { + if(entry.getBookmark() != null) { + Bookmark bookmark = entry.getBookmark(); + startIndex = bookmarkIndex; + startPosition = bookmark.getPosition(); + break; + } + bookmarkIndex++; + } + } + + downloadService.download(entries, false, !append, false, shuffle, startIndex, startPosition); } private void noResults() { @@ -415,6 +482,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { public void onPlayFromSearch (String query, Bundle extras) { // User just asked to playing something if("".equals(query)) { + downloadService.clear(); downloadService.setShufflePlayEnabled(true); } else { String mediaFocus = extras.getString(MediaStore.EXTRA_MEDIA_FOCUS); @@ -435,6 +503,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, genre); editor.commit(); + downloadService.clear(); downloadService.setShufflePlayEnabled(true); } else { @@ -467,6 +536,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } } + @Override public void onPlayFromMediaId (String mediaId, Bundle extras) { if(extras == null) { return; @@ -474,11 +544,33 @@ public class RemoteControlClientLP extends RemoteControlClientBase { boolean shuffle = extras.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false); boolean playLast = extras.getBoolean(Constants.INTENT_EXTRA_PLAY_LAST, false); + Entry entry = (Entry) extras.getSerializable(Constants.INTENT_EXTRA_ENTRY); + String playlistId = extras.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, null); if(playlistId != null) { Playlist playlist = new Playlist(playlistId, null); playPlaylist(playlist, shuffle, playLast); } + String musicDirectoryId = extras.getString(Constants.INTENT_EXTRA_NAME_ID); + if(musicDirectoryId != null) { + Entry dir = new Entry(musicDirectoryId); + playMusicDirectory(dir, shuffle, playLast, true); + } + + String podcastId = extras.getString(Constants.INTENT_EXTRA_NAME_PODCAST_ID, null); + if(podcastId != null) { + playSong(entry, true); + } + + // Currently only happens when playing bookmarks so we should be looking up parent + String childId = extras.getString(Constants.INTENT_EXTRA_NAME_CHILD_ID, null); + if(childId != null) { + if(Util.isTagBrowsing(downloadService) && !Util.isOffline(downloadService)) { + playMusicDirectory(entry.getAlbumId(), shuffle, playLast, true); + } else { + playMusicDirectory(entry.getParent(), shuffle, playLast, true); + } + } } @Override @@ -491,5 +583,18 @@ public class RemoteControlClientLP extends RemoteControlClientBase { downloadService.toggleStarred(); } } + + @Override + public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) { + if (getMediaSession() != null && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) { + KeyEvent keyEvent = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); + if (keyEvent != null) { + downloadService.handleKeyEvent(keyEvent); + return true; + } + } + + return super.onMediaButtonEvent(mediaButtonIntent); + } } } diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java b/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java index ea61f36c..69668475 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java +++ b/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java @@ -134,7 +134,11 @@ public class ID3v2File extends Common { if(endValue != -1) { parts.add(txData[1].substring(endName + 1, endValue)); nextStartIndex = endValue + 1; + } else { + break; } + } else { + break; } startName = txData[1].toLowerCase(Locale.US).indexOf("replaygain_", nextStartIndex); diff --git a/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java b/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java index 048d5a75..3084e962 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java +++ b/app/src/main/java/github/daneren2005/dsub/view/AlbumView.java @@ -30,6 +30,7 @@ import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.ImageLoader; +import github.daneren2005.dsub.util.Util; import java.io.File; @@ -40,6 +41,7 @@ public class AlbumView extends UpdateView2<MusicDirectory.Entry, ImageLoader> { private TextView titleView; private TextView artistView; private boolean showArtist = true; + private String coverArtId; public AlbumView(Context context, boolean cell) { super(context); @@ -82,12 +84,13 @@ public class AlbumView extends UpdateView2<MusicDirectory.Entry, ImageLoader> { artist += album.getYear(); } artistView.setText(album.getArtist() == null ? "" : artist); - imageTask = imageLoader.loadImage(coverArtView, album, false, true); + onUpdateImageView(); file = null; } public void onUpdateImageView() { imageTask = item2.loadImage(coverArtView, item, false, true); + coverArtId = item.getCoverArt(); } @Override @@ -101,6 +104,15 @@ public class AlbumView extends UpdateView2<MusicDirectory.Entry, ImageLoader> { isRated = item.getRating(); } + @Override + public void update() { + super.update(); + + if(!Util.equals(item.getCoverArt(), coverArtId)) { + onUpdateImageView(); + } + } + public MusicDirectory.Entry getEntry() { return item; } diff --git a/app/src/main/java/github/daneren2005/dsub/view/CacheLocationPreference.java b/app/src/main/java/github/daneren2005/dsub/view/CacheLocationPreference.java new file mode 100644 index 00000000..35ce71bc --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/CacheLocationPreference.java @@ -0,0 +1,146 @@ +/* + 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 2015 (C) Scott Jackson +*/ +package github.daneren2005.dsub.view; + +import android.content.Context; +import android.os.Build; +import android.os.Environment; +import android.preference.DialogPreference; +import android.preference.EditTextPreference; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.io.File; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.util.FileUtil; + +public class CacheLocationPreference extends EditTextPreference { + private static final String TAG = CacheLocationPreference.class.getSimpleName(); + private Context context; + + public CacheLocationPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + this.context = context; + } + public CacheLocationPreference(Context context, AttributeSet attrs) { + super(context, attrs); + this.context = context; + } + public CacheLocationPreference(Context context) { + super(context); + this.context = context; + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + view.setLayoutParams(new ViewGroup.LayoutParams(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, android.view.ViewGroup.LayoutParams.WRAP_CONTENT)); + + final EditText editText = (EditText) view.findViewById(android.R.id.edit); + ViewGroup vg = (ViewGroup) editText.getParent(); + + LinearLayout cacheButtonsWrapper = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.cache_location_buttons, vg, true); + Button internalLocation = (Button) cacheButtonsWrapper.findViewById(R.id.location_internal); + Button externalLocation = (Button) cacheButtonsWrapper.findViewById(R.id.location_external); + + File[] dirs; + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + dirs = context.getExternalMediaDirs(); + } else { + dirs = ContextCompat.getExternalFilesDirs(context, null); + } + + // Past 5.0 we can query directly for SD Card + File internalDir = null, externalDir = null; + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + for(int i = 0; i < dirs.length; i++) { + try { + if (dirs[i] != null) { + if(Environment.isExternalStorageRemovable(dirs[i])) { + if(externalDir != null) { + externalDir = dirs[i]; + } + } else { + internalDir = dirs[i]; + } + + if(internalDir != null && externalDir != null) { + break; + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to check if is external", e); + } + } + } + + // Before 5.0, we have to guess. Most of the time the SD card is last + if(externalDir == null) { + for (int i = dirs.length - 1; i >= 0; i--) { + if (dirs[i] != null) { + externalDir = dirs[i]; + break; + } + } + } + if(internalDir == null) { + for (int i = 0; i < dirs.length; i++) { + if (dirs[i] != null) { + internalDir = dirs[i]; + break; + } + } + } + final File finalInternalDir = new File(internalDir, "music"); + final File finalExternalDir = new File(externalDir, "music"); + + final EditText editTextBox = (EditText)view.findViewById(android.R.id.edit); + if(finalInternalDir != null && (finalInternalDir.exists() || finalInternalDir.mkdirs())) { + internalLocation.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String path = finalInternalDir.getPath(); + editTextBox.setText(path); + } + }); + } else { + internalLocation.setEnabled(false); + } + + if(finalExternalDir != null && !finalInternalDir.equals(finalExternalDir) && (finalExternalDir.exists() || finalExternalDir.mkdirs())) { + externalLocation.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String path = finalExternalDir.getPath(); + editTextBox.setText(path); + } + }); + } else { + externalLocation.setEnabled(false); + } + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/CardView.java b/app/src/main/java/github/daneren2005/dsub/view/CardView.java new file mode 100644 index 00000000..d6bca330 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/CardView.java @@ -0,0 +1,67 @@ +package github.daneren2005.dsub.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.RectF; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.util.DrawableTint; + +public class CardView extends FrameLayout{ + private static final String TAG = CardView.class.getSimpleName(); + + public CardView(Context context) { + super(context); + init(context); + } + + public CardView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public CardView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public CardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context); + } + + @Override + public void onDraw(Canvas canvas) { + try { + Path clipPath = new Path(); + float roundedDp = getResources().getDimension(R.dimen.Card_Radius); + clipPath.addRoundRect(new RectF(canvas.getClipBounds()), roundedDp, roundedDp, Path.Direction.CW); + canvas.clipPath(clipPath); + } catch(Exception e) { + Log.e(TAG, "Failed to clip path on canvas", e); + } + super.onDraw(canvas); + } + + private void init(Context context) { + setClipChildren(true); + setBackgroundResource(DrawableTint.getDrawableRes(context, R.attr.cardBackgroundDrawable)); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setElevation(getResources().getInteger(R.integer.Card_Elevation)); + } + + // clipPath is not supported with Hardware Acceleration before API 18 + // http://stackoverflow.com/questions/8895677/work-around-canvas-clippath-that-is-not-supported-in-android-any-more/8895894#8895894 + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 && isHardwareAccelerated()) { + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java b/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java index 7cb29835..d3eacefc 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java +++ b/app/src/main/java/github/daneren2005/dsub/view/FastScroller.java @@ -95,7 +95,7 @@ public class FastScroller extends LinearLayout { switch(action) { case MotionEvent.ACTION_DOWN: - if(event.getX() < (handle.getX() - 20)) { + if(event.getX() < (handle.getX() - 30)) { return false; } diff --git a/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java b/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java index b59e7157..45b34b9f 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java +++ b/app/src/main/java/github/daneren2005/dsub/view/GridSpacingDecoration.java @@ -18,10 +18,17 @@ package github.daneren2005.dsub.view; import android.graphics.Rect; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.util.TypedValue; import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import static android.widget.LinearLayout.*; public class GridSpacingDecoration extends RecyclerView.ItemDecoration { + private static final String TAG = GridSpacingDecoration.class.getSimpleName(); public static final int SPACING = 10; @Override @@ -39,30 +46,52 @@ public class GridSpacingDecoration extends RecyclerView.ItemDecoration { } int spanCount = getTotalSpan(view, parent); int spanIndex = childIndex % spanCount; + + // If we can, use the SpanSizeLookup since headers screw up the index calculation + RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); + if(layoutManager instanceof GridLayoutManager) { + GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; + GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup(); + if(spanSizeLookup != null) { + spanIndex = spanSizeLookup.getSpanIndex(childIndex, spanCount); + } + } int spanSize = getSpanSize(parent, childIndex); /* INVALID SPAN */ if (spanCount < 1 || spanSize > 1) return; - outRect.top = halfSpacing; - outRect.bottom = halfSpacing; - outRect.left = halfSpacing; - outRect.right = halfSpacing; + int margins = 0; + if(view instanceof UpdateView) { + View firstChild = ((ViewGroup) view).getChildAt(0); + ViewGroup.LayoutParams layoutParams = firstChild.getLayoutParams(); + if (layoutParams instanceof LinearLayout.LayoutParams) { + margins = ((LinearLayout.LayoutParams) layoutParams).bottomMargin; + } else if (layoutParams instanceof FrameLayout.LayoutParams) { + margins = ((FrameLayout.LayoutParams) layoutParams).bottomMargin; + } + } + int doubleMargins = margins * 2; + + outRect.top = halfSpacing - margins; + outRect.bottom = halfSpacing - margins; + outRect.left = halfSpacing - margins; + outRect.right = halfSpacing - margins; - if (isTopEdge(childIndex, spanCount)) { - outRect.top = spacing; + if (isTopEdge(childIndex, spanIndex, spanCount)) { + outRect.top = spacing - doubleMargins; } if (isLeftEdge(spanIndex, spanCount)) { - outRect.left = spacing; + outRect.left = spacing - doubleMargins; } if (isRightEdge(spanIndex, spanCount)) { - outRect.right = spacing; + outRect.right = spacing - doubleMargins; } if (isBottomEdge(childIndex, childCount, spanCount)) { - outRect.bottom = spacing; + outRect.bottom = spacing - doubleMargins; } } @@ -94,8 +123,8 @@ public class GridSpacingDecoration extends RecyclerView.ItemDecoration { return spanIndex == spanCount - 1; } - protected boolean isTopEdge(int childIndex, int spanCount) { - return childIndex < spanCount; + protected boolean isTopEdge(int childIndex, int spanIndex, int spanCount) { + return childIndex < spanCount && childIndex == spanIndex; } protected boolean isBottomEdge(int childIndex, int childCount, int spanCount) { diff --git a/app/src/main/java/github/daneren2005/dsub/view/MyViewFlipper.java b/app/src/main/java/github/daneren2005/dsub/view/MyViewFlipper.java deleted file mode 100644 index 26a3de08..00000000 --- a/app/src/main/java/github/daneren2005/dsub/view/MyViewFlipper.java +++ /dev/null @@ -1,53 +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 github.daneren2005.dsub.view; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.ViewFlipper; - -/** - * Work-around for Android Issue 6191 (http://code.google.com/p/android/issues/detail?id=6191) - * - * @author Sindre Mehus - * @version $Id$ - */ -public class MyViewFlipper extends ViewFlipper { - - public MyViewFlipper(Context context) { - super(context); - } - - public MyViewFlipper(Context context, AttributeSet attrs) { - super(context, attrs); - } - - - @Override - protected void onDetachedFromWindow() { - try { - super.onDetachedFromWindow(); - } - catch (IllegalArgumentException e) { - // Call stopFlipping() in order to kick off updateRunning() - stopFlipping(); - } - } -} - diff --git a/app/src/main/java/github/daneren2005/dsub/view/SettingView.java b/app/src/main/java/github/daneren2005/dsub/view/SettingView.java index d46dc5d2..6dc116f8 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/SettingView.java +++ b/app/src/main/java/github/daneren2005/dsub/view/SettingView.java @@ -23,6 +23,7 @@ import android.widget.TextView; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.User; +import github.daneren2005.dsub.domain.User.MusicFolderSetting; import static github.daneren2005.dsub.domain.User.Setting; @@ -51,12 +52,14 @@ public class SettingView extends UpdateView2<Setting, Boolean> { protected void setObjectImpl(Setting setting, Boolean isEditable) { // Can't edit non-role parts String name = setting.getName(); - if(name.indexOf("Role") == -1) { + if(name.indexOf("Role") == -1 && !(setting instanceof MusicFolderSetting)) { item2 = false; } int res = -1; - if(User.SCROBBLING.equals(name)) { + if(setting instanceof MusicFolderSetting) { + titleView.setText(((MusicFolderSetting) setting).getLabel()); + } else if(User.SCROBBLING.equals(name)) { res = R.string.admin_scrobblingEnabled; } else if(User.ADMIN.equals(name)) { res = R.string.admin_role_admin; @@ -78,6 +81,8 @@ public class SettingView extends UpdateView2<Setting, Boolean> { res = R.string.admin_role_jukebox; } else if(User.SHARE.equals(name)) { res = R.string.admin_role_share; + } else if(User.VIDEO_CONVERSION.equals(name)) { + res = R.string.admin_role_video_conversion; } else if(User.LASTFM.equals(name)) { res = R.string.admin_role_lastfm; } else { diff --git a/app/src/main/java/github/daneren2005/dsub/view/SongView.java b/app/src/main/java/github/daneren2005/dsub/view/SongView.java index 625303b7..8cb0c21c 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/SongView.java +++ b/app/src/main/java/github/daneren2005/dsub/view/SongView.java @@ -29,6 +29,7 @@ import github.daneren2005.dsub.domain.PodcastEpisode; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.DownloadFile; import github.daneren2005.dsub.util.DrawableTint; +import github.daneren2005.dsub.util.SongDBHandler; import github.daneren2005.dsub.util.Util; import java.io.File; @@ -41,12 +42,15 @@ import java.io.File; public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { private static final String TAG = SongView.class.getSimpleName(); + private TextView trackTextView; private TextView titleTextView; + private TextView playingTextView; private TextView artistTextView; private TextView durationTextView; private TextView statusTextView; private ImageView statusImageView; private ImageView bookmarkButton; + private ImageView playedButton; private View bottomRowView; private DownloadService downloadService; @@ -63,13 +67,17 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { private boolean partialFileExists = false; private boolean loaded = false; private boolean isBookmarked = false; - private boolean bookmarked = false; + private boolean isBookmarkedShown = false; private boolean showPodcast = false; + private boolean isPlayed = false; + private boolean isPlayedShown = false; + private boolean showAlbum = false; public SongView(Context context) { super(context); LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true); + trackTextView = (TextView) findViewById(R.id.song_track); titleTextView = (TextView) findViewById(R.id.song_title); artistTextView = (TextView) findViewById(R.id.song_artist); durationTextView = (TextView) findViewById(R.id.song_duration); @@ -80,6 +88,7 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { starButton.setFocusable(false); bookmarkButton = (ImageButton) findViewById(R.id.song_bookmark); bookmarkButton.setFocusable(false); + playedButton = (ImageButton) findViewById(R.id.song_played); moreButton = (ImageView) findViewById(R.id.item_more); bottomRowView = findViewById(R.id.song_bottom); } @@ -102,12 +111,15 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { if(artist.length() != 0) { artist.append(" - "); } - int index = date.indexOf(" "); - artist.append(date.substring(0, index != -1 ? index : date.length())); + artist.append(Util.formatDate(context, date, false)); } } else if(song.getArtist() != null) { - artist.append(song.getArtist()); + if(showAlbum) { + artist.append(song.getAlbum()); + } else { + artist.append(song.getArtist()); + } } if(isPodcast) { @@ -138,8 +150,23 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { String title = song.getTitle(); Integer track = song.getTrack(); + TextView newPlayingTextView; if(track != null && Util.getDisplayTrack(context)) { - title = String.format("%02d", track) + " " + title; + trackTextView.setText(String.format("%02d", track)); + trackTextView.setVisibility(View.VISIBLE); + newPlayingTextView = trackTextView; + } else { + trackTextView.setVisibility(View.GONE); + newPlayingTextView = titleTextView; + } + + if(newPlayingTextView != playingTextView || playingTextView == null) { + if(playing) { + playingTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + playing = false; + } + + playingTextView = newPlayingTextView; } titleTextView.setText(title); @@ -191,6 +218,10 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { item.loadMetadata(downloadFile.getCompleteFile()); loaded = true; } + + if(item instanceof PodcastEpisode || item.isAudioBook() || item.isPodcast()) { + isPlayed = SongDBHandler.getHandler(context).hasBeenCompleted(item); + } } @Override @@ -242,32 +273,48 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { rightImage = false; } - boolean playing = downloadService.getCurrentPlaying() == downloadFile; + boolean playing = Util.equals(downloadService.getCurrentPlaying(), downloadFile); if (playing) { if(!this.playing) { this.playing = playing; - titleTextView.setCompoundDrawablesWithIntrinsicBounds(DrawableTint.getDrawableRes(context, R.attr.playing), 0, 0, 0); + playingTextView.setCompoundDrawablesWithIntrinsicBounds(DrawableTint.getDrawableRes(context, R.attr.playing), 0, 0, 0); } } else { if(this.playing) { this.playing = playing; - titleTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + playingTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); } } if(isBookmarked) { - if(!bookmarked) { + if(!isBookmarkedShown) { if(bookmarkButton.getDrawable() == null) { bookmarkButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_menu_bookmark_selected)); } bookmarkButton.setVisibility(View.VISIBLE); - bookmarked = true; + isBookmarkedShown = true; } } else { - if(bookmarked) { + if(isBookmarkedShown) { bookmarkButton.setVisibility(View.GONE); - bookmarked = false; + isBookmarkedShown = false; + } + } + + if(isPlayed) { + if(!isPlayedShown) { + if(playedButton.getDrawable() == null) { + playedButton.setImageDrawable(DrawableTint.getTintedDrawable(context, R.drawable.ic_toggle_played)); + } + + playedButton.setVisibility(View.VISIBLE); + isPlayedShown = true; + } + } else { + if(isPlayedShown) { + playedButton.setVisibility(View.GONE); + isPlayedShown = false; } } @@ -288,7 +335,15 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { // Still highlight red if a 1-star if(isRated == 1) { this.setBackgroundColor(Color.RED); - this.getBackground().setAlpha(20); + + String theme = Util.getTheme(context); + if("black".equals(theme)) { + this.getBackground().setAlpha(80); + } else if("dark".equals(theme) || "holo".equals(theme)) { + this.getBackground().setAlpha(60); + } else { + this.getBackground().setAlpha(20); + } } else if(rating == 1) { this.setBackgroundColor(0x00000000); } @@ -304,4 +359,8 @@ public class SongView extends UpdateView2<MusicDirectory.Entry, Boolean> { public void setShowPodcast(boolean showPodcast) { this.showPodcast = showPodcast; } + + public void setShowAlbum(boolean showAlbum) { + this.showAlbum = showAlbum; + } } diff --git a/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java b/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java index 8f3b5271..0041eac5 100644 --- a/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java +++ b/app/src/main/java/github/daneren2005/dsub/view/UpdateView.java @@ -201,6 +201,10 @@ public abstract class UpdateView<T> extends LinearLayout { }); } + public static boolean hasActiveActivity() { + return activeActivities > 0; + } + public static void addActiveActivity() { activeActivities++; diff --git a/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteChooserDialogFragment.java b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteChooserDialogFragment.java new file mode 100644 index 00000000..da9b135f --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteChooserDialogFragment.java @@ -0,0 +1,15 @@ +package github.daneren2005.dsub.view.compat; + +import android.content.Context; +import android.os.Bundle; +import android.support.v7.app.MediaRouteChooserDialog; +import android.support.v7.app.MediaRouteChooserDialogFragment; + +import github.daneren2005.dsub.util.Util; + +public class CustomMediaRouteChooserDialogFragment extends MediaRouteChooserDialogFragment { + @Override + public MediaRouteChooserDialog onCreateChooserDialog(Context context, Bundle savedInstanceState) { + return new MediaRouteChooserDialog(context, Util.getThemeRes(context)); + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteControllerDialogFragment.java b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteControllerDialogFragment.java new file mode 100644 index 00000000..7fd54142 --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteControllerDialogFragment.java @@ -0,0 +1,15 @@ +package github.daneren2005.dsub.view.compat; + +import android.content.Context; +import android.os.Bundle; +import android.support.v7.app.MediaRouteControllerDialog; +import android.support.v7.app.MediaRouteControllerDialogFragment; + +import github.daneren2005.dsub.util.Util; + +public class CustomMediaRouteControllerDialogFragment extends MediaRouteControllerDialogFragment { + @Override + public MediaRouteControllerDialog onCreateControllerDialog(Context context, Bundle savedInstanceState) { + return new MediaRouteControllerDialog(context, Util.getThemeRes(context)); + } +} diff --git a/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteDialogFactory.java b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteDialogFactory.java new file mode 100644 index 00000000..8bc890cb --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/compat/CustomMediaRouteDialogFactory.java @@ -0,0 +1,17 @@ +package github.daneren2005.dsub.view.compat; + +import android.support.v7.app.MediaRouteChooserDialogFragment; +import android.support.v7.app.MediaRouteControllerDialogFragment; +import android.support.v7.app.MediaRouteDialogFactory; + +public class CustomMediaRouteDialogFactory extends MediaRouteDialogFactory { + @Override + public MediaRouteChooserDialogFragment onCreateChooserDialogFragment() { + return new CustomMediaRouteChooserDialogFragment(); + } + + @Override + public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() { + return new CustomMediaRouteControllerDialogFragment(); + } +} diff --git a/app/src/main/res/drawable-hdpi/actionbar_button_normal.9.png b/app/src/main/res/drawable-hdpi/actionbar_button_normal.9.png Binary files differdeleted file mode 100644 index 385f751c..00000000 --- a/app/src/main/res/drawable-hdpi/actionbar_button_normal.9.png +++ /dev/null diff --git a/app/src/main/res/drawable-hdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-hdpi/ic_action_playback_speed_dark.png Binary files differnew file mode 100644 index 00000000..5f4d890a --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_action_playback_speed_dark.png diff --git a/app/src/main/res/drawable-hdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-hdpi/ic_action_playback_speed_light.png Binary files differnew file mode 100644 index 00000000..4b481822 --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_action_playback_speed_light.png diff --git a/app/src/main/res/drawable-hdpi/ic_toggle_played.png b/app/src/main/res/drawable-hdpi/ic_toggle_played.png Binary files differnew file mode 100644 index 00000000..944ff8be --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_toggle_played.png diff --git a/app/src/main/res/drawable-hdpi/media_fastforward_dark.png b/app/src/main/res/drawable-hdpi/media_fastforward_dark.png Binary files differnew file mode 100644 index 00000000..eab0cdfd --- /dev/null +++ b/app/src/main/res/drawable-hdpi/media_fastforward_dark.png diff --git a/app/src/main/res/drawable-hdpi/media_fastforward_light.png b/app/src/main/res/drawable-hdpi/media_fastforward_light.png Binary files differnew file mode 100644 index 00000000..905faa9c --- /dev/null +++ b/app/src/main/res/drawable-hdpi/media_fastforward_light.png diff --git a/app/src/main/res/drawable-hdpi/media_rewind_dark.png b/app/src/main/res/drawable-hdpi/media_rewind_dark.png Binary files differnew file mode 100644 index 00000000..5d2d62a7 --- /dev/null +++ b/app/src/main/res/drawable-hdpi/media_rewind_dark.png diff --git a/app/src/main/res/drawable-hdpi/media_rewind_light.png b/app/src/main/res/drawable-hdpi/media_rewind_light.png Binary files differnew file mode 100644 index 00000000..15bc91a8 --- /dev/null +++ b/app/src/main/res/drawable-hdpi/media_rewind_light.png diff --git a/app/src/main/res/drawable-hdpi/stat_notify_download.png b/app/src/main/res/drawable-hdpi/stat_notify_download.png Binary files differdeleted file mode 100644 index 48ca6924..00000000 --- a/app/src/main/res/drawable-hdpi/stat_notify_download.png +++ /dev/null diff --git a/app/src/main/res/drawable-hdpi/toast_frame.9.png b/app/src/main/res/drawable-hdpi/toast_frame.9.png Binary files differdeleted file mode 100644 index 8f5d8119..00000000 --- a/app/src/main/res/drawable-hdpi/toast_frame.9.png +++ /dev/null diff --git a/app/src/main/res/drawable-mdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-mdpi/ic_action_playback_speed_dark.png Binary files differnew file mode 100644 index 00000000..b05ecefd --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_action_playback_speed_dark.png diff --git a/app/src/main/res/drawable-mdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-mdpi/ic_action_playback_speed_light.png Binary files differnew file mode 100644 index 00000000..392d2b4d --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_action_playback_speed_light.png diff --git a/app/src/main/res/drawable-mdpi/ic_toggle_played.png b/app/src/main/res/drawable-mdpi/ic_toggle_played.png Binary files differnew file mode 100644 index 00000000..02524f4c --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_toggle_played.png diff --git a/app/src/main/res/drawable-mdpi/media_fastforward_dark.png b/app/src/main/res/drawable-mdpi/media_fastforward_dark.png Binary files differnew file mode 100644 index 00000000..f999e0b8 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/media_fastforward_dark.png diff --git a/app/src/main/res/drawable-mdpi/media_fastforward_light.png b/app/src/main/res/drawable-mdpi/media_fastforward_light.png Binary files differnew file mode 100644 index 00000000..23107742 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/media_fastforward_light.png diff --git a/app/src/main/res/drawable-mdpi/media_rewind_dark.png b/app/src/main/res/drawable-mdpi/media_rewind_dark.png Binary files differnew file mode 100644 index 00000000..2ecda48a --- /dev/null +++ b/app/src/main/res/drawable-mdpi/media_rewind_dark.png diff --git a/app/src/main/res/drawable-mdpi/media_rewind_light.png b/app/src/main/res/drawable-mdpi/media_rewind_light.png Binary files differnew file mode 100644 index 00000000..f7c9b303 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/media_rewind_light.png diff --git a/app/src/main/res/drawable-mdpi/stat_notify_download.png b/app/src/main/res/drawable-mdpi/stat_notify_download.png Binary files differdeleted file mode 100644 index 4164e0fa..00000000 --- a/app/src/main/res/drawable-mdpi/stat_notify_download.png +++ /dev/null diff --git a/app/src/main/res/drawable-v21/notification_fastforward.xml b/app/src/main/res/drawable-v21/notification_fastforward.xml new file mode 100644 index 00000000..d0ab76a2 --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_fastforward.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<bitmap + xmlns:android="http://schemas.android.com/apk/res/android" + android:src="@drawable/media_fastforward_light"/>
\ No newline at end of file diff --git a/app/src/main/res/drawable-v21/notification_rewind.xml b/app/src/main/res/drawable-v21/notification_rewind.xml new file mode 100644 index 00000000..25a16a02 --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_rewind.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<bitmap + xmlns:android="http://schemas.android.com/apk/res/android" + android:src="@drawable/media_rewind_light"/>
\ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_dark.png Binary files differnew file mode 100644 index 00000000..2aeadf7e --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_dark.png diff --git a/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_light.png Binary files differnew file mode 100644 index 00000000..ce3f561c --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_action_playback_speed_light.png diff --git a/app/src/main/res/drawable-xhdpi/ic_toggle_played.png b/app/src/main/res/drawable-xhdpi/ic_toggle_played.png Binary files differnew file mode 100644 index 00000000..c681150c --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_toggle_played.png diff --git a/app/src/main/res/drawable-xhdpi/media_fastforward_dark.png b/app/src/main/res/drawable-xhdpi/media_fastforward_dark.png Binary files differnew file mode 100644 index 00000000..3c653286 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/media_fastforward_dark.png diff --git a/app/src/main/res/drawable-xhdpi/media_fastforward_light.png b/app/src/main/res/drawable-xhdpi/media_fastforward_light.png Binary files differnew file mode 100644 index 00000000..f105b458 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/media_fastforward_light.png diff --git a/app/src/main/res/drawable-xhdpi/media_rewind_dark.png b/app/src/main/res/drawable-xhdpi/media_rewind_dark.png Binary files differnew file mode 100644 index 00000000..08fdff06 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/media_rewind_dark.png diff --git a/app/src/main/res/drawable-xhdpi/media_rewind_light.png b/app/src/main/res/drawable-xhdpi/media_rewind_light.png Binary files differnew file mode 100644 index 00000000..3998f715 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/media_rewind_light.png diff --git a/app/src/main/res/drawable-xhdpi/stat_notify_download.png b/app/src/main/res/drawable-xhdpi/stat_notify_download.png Binary files differdeleted file mode 100644 index 96ceb383..00000000 --- a/app/src/main/res/drawable-xhdpi/stat_notify_download.png +++ /dev/null diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_dark.png Binary files differnew file mode 100644 index 00000000..a9acc5e6 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_dark.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_light.png Binary files differnew file mode 100644 index 00000000..dcd6fea8 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_action_playback_speed_light.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_toggle_played.png b/app/src/main/res/drawable-xxhdpi/ic_toggle_played.png Binary files differnew file mode 100644 index 00000000..33f9d819 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_toggle_played.png diff --git a/app/src/main/res/drawable-xxhdpi/media_fastforward_dark.png b/app/src/main/res/drawable-xxhdpi/media_fastforward_dark.png Binary files differnew file mode 100644 index 00000000..90f045ea --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/media_fastforward_dark.png diff --git a/app/src/main/res/drawable-xxhdpi/media_fastforward_light.png b/app/src/main/res/drawable-xxhdpi/media_fastforward_light.png Binary files differnew file mode 100644 index 00000000..73f0fba4 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/media_fastforward_light.png diff --git a/app/src/main/res/drawable-xxhdpi/media_rewind_dark.png b/app/src/main/res/drawable-xxhdpi/media_rewind_dark.png Binary files differnew file mode 100644 index 00000000..ff5bda9a --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/media_rewind_dark.png diff --git a/app/src/main/res/drawable-xxhdpi/media_rewind_light.png b/app/src/main/res/drawable-xxhdpi/media_rewind_light.png Binary files differnew file mode 100644 index 00000000..c50c0825 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/media_rewind_light.png diff --git a/app/src/main/res/drawable-xxhdpi/stat_notify_download.png b/app/src/main/res/drawable-xxhdpi/stat_notify_download.png Binary files differdeleted file mode 100644 index b2dc5651..00000000 --- a/app/src/main/res/drawable-xxhdpi/stat_notify_download.png +++ /dev/null diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_dark.png Binary files differnew file mode 100644 index 00000000..c1e92342 --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_dark.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_light.png b/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_light.png Binary files differnew file mode 100644 index 00000000..3dc5f32a --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_action_playback_speed_light.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_toggle_played.png b/app/src/main/res/drawable-xxxhdpi/ic_toggle_played.png Binary files differnew file mode 100644 index 00000000..0907fd2c --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_toggle_played.png diff --git a/app/src/main/res/drawable-xxxhdpi/media_fastforward_dark.png b/app/src/main/res/drawable-xxxhdpi/media_fastforward_dark.png Binary files differnew file mode 100644 index 00000000..63ac58e0 --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/media_fastforward_dark.png diff --git a/app/src/main/res/drawable-xxxhdpi/media_fastforward_light.png b/app/src/main/res/drawable-xxxhdpi/media_fastforward_light.png Binary files differnew file mode 100644 index 00000000..d2f7506b --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/media_fastforward_light.png diff --git a/app/src/main/res/drawable-xxxhdpi/media_rewind_dark.png b/app/src/main/res/drawable-xxxhdpi/media_rewind_dark.png Binary files differnew file mode 100644 index 00000000..e911b342 --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/media_rewind_dark.png diff --git a/app/src/main/res/drawable-xxxhdpi/media_rewind_light.png b/app/src/main/res/drawable-xxxhdpi/media_rewind_light.png Binary files differnew file mode 100644 index 00000000..461118f1 --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/media_rewind_light.png diff --git a/app/src/main/res/drawable/card_rounded_corners_black.xml b/app/src/main/res/drawable/card_rounded_corners_black.xml new file mode 100644 index 00000000..7592de64 --- /dev/null +++ b/app/src/main/res/drawable/card_rounded_corners_black.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/background_material_dark"/> + <corners android:radius="@dimen/Card.Radius"/> + <padding android:left="0dip" android:top="0dip" android:right="0dip" android:bottom="0dip" /> +</shape>
\ No newline at end of file diff --git a/app/src/main/res/drawable/card_rounded_corners_dark.xml b/app/src/main/res/drawable/card_rounded_corners_dark.xml new file mode 100644 index 00000000..4db7d4b0 --- /dev/null +++ b/app/src/main/res/drawable/card_rounded_corners_dark.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@android:color/black"/> + <corners android:radius="@dimen/Card.Radius"/> + <padding android:left="0dip" android:top="0dip" android:right="0dip" android:bottom="0dip" /> +</shape>
\ No newline at end of file diff --git a/app/src/main/res/drawable/card_rounded_corners_light.xml b/app/src/main/res/drawable/card_rounded_corners_light.xml new file mode 100644 index 00000000..5475c3d6 --- /dev/null +++ b/app/src/main/res/drawable/card_rounded_corners_light.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@android:color/white"/> + <corners android:radius="@dimen/Card.Radius"/> + <padding android:left="0dip" android:top="0dip" android:right="0dip" android:bottom="0dip" /> +</shape>
\ No newline at end of file diff --git a/app/src/main/res/drawable/drawer_header.jpg b/app/src/main/res/drawable/drawer_header.jpg Binary files differdeleted file mode 100644 index f54a30e2..00000000 --- a/app/src/main/res/drawable/drawer_header.jpg +++ /dev/null diff --git a/app/src/main/res/drawable/drawer_header_dark.png b/app/src/main/res/drawable/drawer_header_dark.png Binary files differnew file mode 100644 index 00000000..a1c8d61f --- /dev/null +++ b/app/src/main/res/drawable/drawer_header_dark.png diff --git a/app/src/main/res/drawable/drawer_header_holo.png b/app/src/main/res/drawable/drawer_header_holo.png Binary files differnew file mode 100644 index 00000000..d84d096d --- /dev/null +++ b/app/src/main/res/drawable/drawer_header_holo.png diff --git a/app/src/main/res/drawable/drawer_header_light.png b/app/src/main/res/drawable/drawer_header_light.png Binary files differnew file mode 100644 index 00000000..1bcf4ec3 --- /dev/null +++ b/app/src/main/res/drawable/drawer_header_light.png diff --git a/app/src/main/res/drawable/notification_fastforward.xml b/app/src/main/res/drawable/notification_fastforward.xml new file mode 100644 index 00000000..355c6a5b --- /dev/null +++ b/app/src/main/res/drawable/notification_fastforward.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<bitmap + xmlns:android="http://schemas.android.com/apk/res/android" + android:src="@drawable/media_fastforward_dark"/>
\ No newline at end of file diff --git a/app/src/main/res/drawable/notification_rewind.xml b/app/src/main/res/drawable/notification_rewind.xml new file mode 100644 index 00000000..ab7827a9 --- /dev/null +++ b/app/src/main/res/drawable/notification_rewind.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<bitmap + xmlns:android="http://schemas.android.com/apk/res/android" + android:src="@drawable/media_rewind_dark"/>
\ No newline at end of file diff --git a/app/src/main/res/layout-land/download.xml b/app/src/main/res/layout-land/download.xml index f3e39a5f..894ae62e 100644 --- a/app/src/main/res/layout-land/download.xml +++ b/app/src/main/res/layout-land/download.xml @@ -1,129 +1,122 @@ <?xml version="1.0" encoding="utf-8"?> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/download_layout_container" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/download_layout" - android:orientation="vertical" - android:layout_width="fill_parent" - android:layout_height="fill_parent"> - - <LinearLayout android:orientation="horizontal" +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/download_layout" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ViewFlipper + android:id="@+id/download_playlist_flipper" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_weight="1"> + + <github.daneren2005.dsub.view.RecyclingImageView + android:id="@+id/download_album_art_image" + android:src="@drawable/unknown_album_large" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_weight="1" + android:scaleType="fitCenter"/> + + <include layout="@layout/download_playlist"/> + + </ViewFlipper> + + <RelativeLayout + android:id="@+id/download_control_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:background="@android:color/transparent"> + + <LinearLayout + android:id="@+id/download_other_controls_wrapper" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_above="@+id/download_song_title"> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/download_other_controls_layout" + android:orientation="horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal"> + + <ImageButton + android:id="@+id/download_rating_bad" + style="@style/DownloadActionImageButton" + android:src="?attr/rating_bad"/> + + <ImageButton + android:id="@+id/download_star" + style="@style/DownloadActionImageButton" + android:src="@android:drawable/star_big_off"/> + + <ImageButton + android:id="@+id/download_playback_speed" + style="@style/DownloadActionImageButton" + android:src="?attr/playback_speed"/> + + <ImageButton + android:id="@+id/download_bookmark" + style="@style/DownloadActionImageButton" + android:src="?attr/bookmark"/> + + <ImageButton + android:id="@+id/download_rating_good" + style="@style/DownloadActionImageButton" + android:src="?attr/rating_good"/> + </LinearLayout> + </LinearLayout> + + <TextView + android:id="@+id/download_song_title" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:singleLine="true" + android:ellipsize="end" + android:gravity="center_horizontal" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?android:textColorPrimary" + android:layout_above="@+id/download_status"/> + + <TextView + android:id="@+id/download_status" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:layout_marginBottom="8dip" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:singleLine="true" + android:ellipsize="end" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:textColorSecondary" + android:layout_above="@+id/download_media_buttons_wrapper"/> + + <LinearLayout + android:id="@+id/download_media_buttons_wrapper" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_above="@+id/download_slider_wrapper"> + + <include layout="@layout/download_media_buttons"/> + </LinearLayout> + + <LinearLayout + android:id="@+id/download_slider_wrapper" android:layout_width="fill_parent" - android:layout_height="0dip" - android:layout_weight="1"> - - <github.daneren2005.dsub.view.MyViewFlipper - android:id="@+id/download_playlist_flipper" - android:layout_width="0dp" - android:layout_height="fill_parent" - android:layout_weight="1"> - - <github.daneren2005.dsub.view.RecyclingImageView - android:id="@+id/download_album_art_image" - android:src="@drawable/unknown_album_large" - android:layout_width="wrap_content" - android:layout_height="fill_parent" - android:layout_weight="1" - android:scaleType="fitStart"/> - - <include layout="@layout/download_playlist"/> - - </github.daneren2005.dsub.view.MyViewFlipper> - - <RelativeLayout android:orientation="vertical" - android:id="@+id/download_control_layout" - android:layout_width="0dp" - android:layout_height="fill_parent" - android:layout_weight="1" - android:background="@android:color/transparent"> - - <LinearLayout - android:id="@+id/download_other_controls_wrapper" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_centerHorizontal="true" - android:layout_above="@+id/download_song_title"> - - <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/download_other_controls_layout" - android:orientation="horizontal" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_horizontal"> - - <ImageButton - android:id="@+id/download_rating_bad" - style="@style/DownloadActionImageButton" - android:src="?attr/rating_bad"/> - - <ImageButton - android:id="@+id/download_star" - style="@style/DownloadActionImageButton" - android:src="@android:drawable/star_big_off"/> - - <ImageButton - android:id="@+id/download_bookmark" - style="@style/DownloadActionImageButton" - android:src="?attr/bookmark"/> - - <ImageButton - android:id="@+id/download_rating_good" - style="@style/DownloadActionImageButton" - android:src="?attr/rating_good"/> - </LinearLayout> - </LinearLayout> - - <TextView - android:id="@+id/download_song_title" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_centerHorizontal="true" - android:layout_marginLeft="12dip" - android:layout_marginRight="12dip" - android:singleLine="true" - android:ellipsize="end" - android:gravity="center_horizontal" - android:textAppearance="?android:attr/textAppearanceMedium" - android:textColor="?android:textColorPrimary" - android:layout_above="@+id/download_status"/> - - <TextView - android:id="@+id/download_status" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:gravity="center_horizontal" - android:layout_marginBottom="8dip" - android:layout_marginLeft="12dip" - android:layout_marginRight="12dip" - android:singleLine="true" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceSmall" - android:textColor="?android:textColorSecondary" - android:layout_above="@+id/download_media_buttons_wrapper"/> - - <LinearLayout - android:id="@+id/download_media_buttons_wrapper" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_above="@+id/download_slider_wrapper"> - - <include layout="@layout/download_media_buttons"/> - </LinearLayout> - - <LinearLayout - android:id="@+id/download_slider_wrapper" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_alignParentBottom="true"> - - <include layout="@layout/download_slider"/> - </LinearLayout> - - </RelativeLayout> + android:layout_height="wrap_content" + android:layout_alignParentBottom="true"> + <include layout="@layout/download_slider"/> </LinearLayout> - </LinearLayout> -</FrameLayout> + + </RelativeLayout> + +</LinearLayout> diff --git a/app/src/main/res/layout-large-land/download.xml b/app/src/main/res/layout-large-land/download.xml index 8b252190..cf5ef571 100644 --- a/app/src/main/res/layout-large-land/download.xml +++ b/app/src/main/res/layout-large-land/download.xml @@ -1,130 +1,121 @@ <?xml version="1.0" encoding="utf-8"?> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/download_layout_container" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/download_layout" - android:orientation="vertical" - android:layout_width="fill_parent" - android:layout_height="fill_parent"> - - <LinearLayout android:orientation="horizontal" - android:layout_width="fill_parent" - android:layout_height="0dip" - android:layout_weight="1"> - - <github.daneren2005.dsub.view.RecyclingImageView - android:id="@+id/download_album_art_image" - android:src="@drawable/unknown_album_large" - android:layout_width="0dp" - android:layout_height="fill_parent" - android:layout_weight="1" - android:scaleType="fitStart"/> - - <RelativeLayout android:orientation="vertical" - android:id="@+id/download_control_layout" - android:layout_width="0dp" - android:layout_height="fill_parent" - android:layout_weight="1" - android:background="@android:color/transparent"> - - <github.daneren2005.dsub.view.MyViewFlipper - android:id="@+id/download_playlist_flipper" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:layout_above="@+id/download_song_title"> - - <RelativeLayout - android:id="@+id/download_other_controls_wrapper" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:orientation="vertical"> - - <LinearLayout - android:id="@+id/download_other_controls_layout" - android:orientation="horizontal" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_centerHorizontal="true" - android:layout_alignParentBottom="true"> - - <ImageButton - android:id="@+id/download_rating_bad" - style="@style/DownloadActionImageButton" - android:src="?attr/rating_bad"/> - - <ImageButton - android:id="@+id/download_star" - style="@style/DownloadActionImageButton" - android:src="@android:drawable/star_big_off"/> - - <ImageButton - android:id="@+id/download_bookmark" - style="@style/DownloadActionImageButton" - android:src="?attr/bookmark"/> - - <ImageButton - android:id="@+id/download_rating_good" - style="@style/DownloadActionImageButton" - android:src="?attr/rating_good"/> - </LinearLayout> - </RelativeLayout> - - <include layout="@layout/download_playlist"/> - - </github.daneren2005.dsub.view.MyViewFlipper> - - <TextView - android:id="@+id/download_song_title" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_centerHorizontal="true" - android:layout_marginLeft="12dip" - android:layout_marginRight="12dip" - android:singleLine="true" - android:ellipsize="end" - android:gravity="center_horizontal" - android:textAppearance="?android:attr/textAppearanceMedium" - android:textColor="?android:textColorPrimary" - android:layout_above="@+id/download_status"/> - - <TextView - android:id="@+id/download_status" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:gravity="center_horizontal" - android:layout_marginBottom="8dip" - android:layout_marginLeft="12dip" - android:layout_marginRight="12dip" - android:singleLine="true" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceSmall" - android:textColor="?android:textColorSecondary" - android:layout_above="@+id/download_media_buttons_wrapper"/> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/download_layout" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <github.daneren2005.dsub.view.RecyclingImageView + android:id="@+id/download_album_art_image" + android:src="@drawable/unknown_album_large" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:scaleType="fitCenter"/> + + <RelativeLayout + android:id="@+id/download_control_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:background="@android:color/transparent"> + + <ViewFlipper + android:id="@+id/download_playlist_flipper" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:layout_above="@+id/download_song_title"> + + <RelativeLayout + android:id="@+id/download_other_controls_wrapper" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:orientation="vertical"> <LinearLayout - android:id="@+id/download_media_buttons_wrapper" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_above="@+id/download_slider_wrapper"> - - <include layout="@layout/download_media_buttons"/> - </LinearLayout> - - <LinearLayout - android:id="@+id/download_slider_wrapper" - android:layout_width="fill_parent" + android:id="@+id/download_other_controls_layout" + android:orientation="horizontal" + android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_centerHorizontal="true" android:layout_alignParentBottom="true"> - <include layout="@layout/download_slider"/> + <ImageButton + android:id="@+id/download_rating_bad" + style="@style/DownloadActionImageButton" + android:src="?attr/rating_bad"/> + + <ImageButton + android:id="@+id/download_star" + style="@style/DownloadActionImageButton" + android:src="@android:drawable/star_big_off"/> + + <ImageButton + android:id="@+id/download_playback_speed" + style="@style/DownloadActionImageButton" + android:src="?attr/playback_speed"/> + + <ImageButton + android:id="@+id/download_bookmark" + style="@style/DownloadActionImageButton" + android:src="?attr/bookmark"/> + + <ImageButton + android:id="@+id/download_rating_good" + style="@style/DownloadActionImageButton" + android:src="?attr/rating_good"/> </LinearLayout> - </RelativeLayout> + <include layout="@layout/download_playlist"/> + + </ViewFlipper> + + <TextView + android:id="@+id/download_song_title" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:singleLine="true" + android:ellipsize="end" + android:gravity="center_horizontal" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?android:textColorPrimary" + android:layout_above="@+id/download_status"/> + + <TextView + android:id="@+id/download_status" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:layout_marginBottom="8dip" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:singleLine="true" + android:ellipsize="end" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:textColorSecondary" + android:layout_above="@+id/download_media_buttons_wrapper"/> + + <LinearLayout + android:id="@+id/download_media_buttons_wrapper" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_above="@+id/download_slider_wrapper"> + + <include layout="@layout/download_media_buttons"/> + </LinearLayout> + + <LinearLayout + android:id="@+id/download_slider_wrapper" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true"> + + <include layout="@layout/download_slider"/> </LinearLayout> - </LinearLayout> -</FrameLayout> + </RelativeLayout> +</LinearLayout> diff --git a/app/src/main/res/layout-port/download.xml b/app/src/main/res/layout-port/download.xml index 899b46b7..39b5e5e9 100644 --- a/app/src/main/res/layout-port/download.xml +++ b/app/src/main/res/layout-port/download.xml @@ -4,57 +4,50 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/download_layout" - android:orientation="vertical" + <LinearLayout + android:id="@+id/download_layout" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + + <ViewFlipper + android:id="@+id/download_playlist_flipper" android:layout_width="fill_parent" - android:layout_height="fill_parent"> - - <github.daneren2005.dsub.view.MyViewFlipper - android:id="@+id/download_playlist_flipper" - android:layout_width="fill_parent" - android:layout_height="0dip" - android:layout_weight="1"> - - <RelativeLayout - android:id="@+id/download_album_art_layout" - android:orientation="vertical" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:layout_weight="1" - android:background="@android:color/transparent"> - - <FrameLayout android:orientation="vertical" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentTop="true"> - - <github.daneren2005.dsub.view.RecyclingImageView - android:id="@+id/download_album_art_image" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:scaleType="fitCenter" - android:layout_gravity="center_horizontal|top"/> - - <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/download_overlay_buttons" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:background="@color/overlayColor" - android:layout_gravity="center_horizontal|bottom" - android:visibility="invisible"> - - <LinearLayout + android:layout_height="0dip" + android:layout_weight="1"> + + <LinearLayout + android:id="@+id/download_album_art_layout" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:background="@android:color/transparent"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"> + + <FrameLayout android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <github.daneren2005.dsub.view.RecyclingImageView + android:id="@+id/download_album_art_image" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_centerHorizontal="true"> + android:scaleType="fitCenter" + android:layout_gravity="center_horizontal|top"/> - <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/download_other_controls_layout" - android:orientation="horizontal" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_horizontal"> + <LinearLayout + android:id="@+id/download_overlay_buttons" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal|bottom" + android:background="@color/overlayColor" + android:visibility="invisible" + android:gravity="center" + android:orientation="horizontal"> <ImageButton android:id="@+id/download_rating_bad" @@ -67,6 +60,11 @@ android:src="@drawable/ic_toggle_star_outline_dark"/> <ImageButton + android:id="@+id/download_playback_speed" + style="@style/DownloadActionImageButton" + android:src="@drawable/ic_action_playback_speed_dark"/> + + <ImageButton android:id="@+id/download_bookmark" style="@style/DownloadActionImageButton" android:src="@drawable/ic_menu_bookmark_dark"/> @@ -75,44 +73,39 @@ android:id="@+id/download_rating_good" style="@style/DownloadActionImageButton" android:src="@drawable/ic_action_rating_good_dark"/> - </LinearLayout> </LinearLayout> - </RelativeLayout> + </FrameLayout> </FrameLayout> <TextView - android:id="@+id/download_status" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentBottom="true" - android:layout_centerHorizontal="true" - android:layout_marginLeft="16dip" - android:layout_marginRight="16dip" - android:singleLine="true" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceSmall" - android:textColor="?android:textColorSecondary"/> + android:id="@+id/download_status" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:layout_marginLeft="16dip" + android:layout_marginRight="16dip" + android:layout_marginTop="6dp" + android:singleLine="true" + android:ellipsize="end" + style="?attr/actionbarSubtitleStyle" + android:textColor="?android:textColorSecondary"/> <TextView - android:id="@+id/download_song_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:layout_above="@+id/download_status" - android:layout_centerHorizontal="true" - android:layout_marginLeft="16dip" - android:layout_marginRight="16dip" - android:singleLine="true" - android:textColor="?android:textColorPrimary" - android:textStyle="bold" - android:textSize="18sp" - android:ellipsize="end"/> - - </RelativeLayout> + android:id="@+id/download_song_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:layout_marginLeft="16dip" + android:layout_marginRight="16dip" + android:singleLine="true" + android:textStyle="bold" + style="?attr/actionbarTitleStyle" + android:textColor="?android:textColorPrimary"/> + </LinearLayout> <include layout="@layout/download_playlist"/> - </github.daneren2005.dsub.view.MyViewFlipper> + </ViewFlipper> <include layout="@layout/download_media_buttons"/> diff --git a/app/src/main/res/layout/abstract_fragment_activity.xml b/app/src/main/res/layout/abstract_fragment_activity.xml index d41b0115..462ee7ef 100644 --- a/app/src/main/res/layout/abstract_fragment_activity.xml +++ b/app/src/main/res/layout/abstract_fragment_activity.xml @@ -44,7 +44,7 @@ android:layout_width="match_parent" android:elevation="4dp" android:visibility="gone" - app:theme="?attr/actionbarThemeStyle" + android:theme="?attr/actionbarThemeStyle" app:popupTheme="?attr/actionbarPopupStyle"/> <LinearLayout @@ -60,7 +60,7 @@ android:layout_width="?attr/actionBarSize" android:layout_height="?attr/actionBarSize" android:layout_gravity="left|center" - android:scaleType="fitStart"/> + android:scaleType="fitCenter"/> <LinearLayout android:layout_width="0dp" @@ -105,6 +105,13 @@ <ImageButton style="@style/PlaybackControl.BottomBar" + android:id="@+id/download_rewind" + android:src="?attr/actionbar_rewind" + android:padding="2dp" + android:visibility="gone"/> + + <ImageButton + style="@style/PlaybackControl.BottomBar" android:id="@+id/download_previous" android:src="?attr/actionbar_backward" android:padding="2dp"/> @@ -119,6 +126,13 @@ android:id="@+id/download_next" android:src="?attr/actionbar_forward" android:padding="2dp"/> + + <ImageButton + style="@style/PlaybackControl.BottomBar" + android:id="@+id/download_fastforward" + android:src="?attr/actionbar_fastforward" + android:padding="2dp" + android:visibility="gone"/> </LinearLayout> </LinearLayout> </FrameLayout> diff --git a/app/src/main/res/layout/abstract_recycler_fragment.xml b/app/src/main/res/layout/abstract_recycler_fragment.xml index 0e0c87f4..0a443ed6 100644 --- a/app/src/main/res/layout/abstract_recycler_fragment.xml +++ b/app/src/main/res/layout/abstract_recycler_fragment.xml @@ -19,7 +19,10 @@ android:id="@+id/fragment_recycler" android:layout_width="fill_parent" android:layout_height="fill_parent" - android:scrollbars="vertical"/> + android:scrollbars="vertical" + android:paddingRight="@dimen/FastScroller.LeftAlignedMargin" + android:layout_marginRight="@dimen/FastScroller.NormalBarMargin" + android:scrollbarStyle="outsideOverlay"/> <github.daneren2005.dsub.view.FastScroller android:id="@+id/fragment_fast_scroller" diff --git a/app/src/main/res/layout/actionbar_spinner.xml b/app/src/main/res/layout/actionbar_spinner.xml index 22fa7f43..f719a67c 100644 --- a/app/src/main/res/layout/actionbar_spinner.xml +++ b/app/src/main/res/layout/actionbar_spinner.xml @@ -1,14 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" - android:layout_width="fill_parent" +<Spinner xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/spinner" + android:layout_width="wrap_content" android:layout_height="fill_parent" - android:gravity="fill_horizontal" > - - <Spinner - android:id="@+id/spinner" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:prompt="@string/common.appname" - style="?attr/android:spinnerItemStyle"/> -</RelativeLayout> + android:prompt="@string/common.appname" + style="?attr/android:spinnerItemStyle" + android:overlapAnchor="false"/>
\ No newline at end of file diff --git a/app/src/main/res/layout/album_cell_item.xml b/app/src/main/res/layout/album_cell_item.xml index f6693a7f..4ad32409 100644 --- a/app/src/main/res/layout/album_cell_item.xml +++ b/app/src/main/res/layout/album_cell_item.xml @@ -1,90 +1,98 @@ <?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" +<github.daneren2005.dsub.view.CardView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/selectableItemBackground"> + android:layout_margin="2dp"> - <RelativeLayout + <LinearLayout + android:orientation="vertical" android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="1"> + android:layout_height="match_parent" + android:background="?attr/selectableItemBackground"> - <github.daneren2005.dsub.view.SquareImageView - android:id="@+id/album_coverart" + <RelativeLayout android:layout_width="match_parent" - android:layout_height="match_parent"/> + android:layout_height="0dp" + android:layout_weight="1"> - <RatingBar - android:id="@+id/album_rating" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:isIndicator="true" - android:layout_centerHorizontal="true" - android:numStars="5" - style="@android:style/Widget.Holo.RatingBar.Small" - android:layout_alignParentBottom="true" - android:visibility="gone"/> - </RelativeLayout> + <github.daneren2005.dsub.view.SquareImageView + android:id="@+id/album_coverart" + android:layout_width="match_parent" + android:layout_height="match_parent"/> - <LinearLayout - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:paddingTop="4dp" - android:paddingLeft="2dp"> + <RatingBar + android:id="@+id/album_rating" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:isIndicator="true" + android:layout_centerHorizontal="true" + android:numStars="5" + style="@android:style/Widget.Holo.RatingBar.Small" + android:layout_alignParentBottom="true" + android:visibility="gone"/> + </RelativeLayout> <LinearLayout - android:layout_width="0dp" + android:layout_width="fill_parent" android:layout_height="wrap_content" - android:layout_weight="1" - android:gravity="center_vertical" - android:orientation="vertical"> - - <TextView - android:id="@+id/album_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="?android:attr/textAppearanceSmall" - android:singleLine="true" - android:ellipsize="marquee" - android:text="@string/search.albums" - android:textColor="?android:textColorPrimary"/> + android:orientation="horizontal" + android:paddingTop="4dp" + android:paddingBottom="4dp" + android:paddingLeft="2dp"> <LinearLayout - android:layout_width="fill_parent" - android:layout_height="wrap_content"> + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:gravity="center_vertical" + android:orientation="vertical"> <TextView - android:id="@+id/album_artist" - android:layout_width="0dp" - android:layout_weight="1" + android:id="@+id/album_title" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textSize="12sp" - android:textColor="?android:textColorSecondary" + android:textAppearance="?android:attr/textAppearanceSmall" android:singleLine="true" - android:text="@string/search.artists"/> + android:ellipsize="marquee" + android:text="@string/search.albums" + android:textColor="?android:textColorPrimary" + android:paddingLeft="@dimen/Card.TextLeftPadding"/> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content"> - <ImageButton - android:id="@+id/album_star" - android:layout_width="@dimen/Star.Small" - android:layout_height="@dimen/Star.Small" - android:scaleType="fitCenter" - android:layout_gravity="right|center_vertical" - android:background="@android:color/transparent" - android:focusable="false" - android:visibility="gone"/> + <TextView + android:id="@+id/album_artist" + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="wrap_content" + android:textSize="12sp" + android:textColor="?android:textColorSecondary" + android:singleLine="true" + android:text="@string/search.artists" + android:paddingLeft="@dimen/Card.TextLeftPadding"/> + + <ImageButton + android:id="@+id/album_star" + android:layout_width="@dimen/Star.Small" + android:layout_height="@dimen/Star.Small" + android:scaleType="fitCenter" + android:layout_gravity="right|center_vertical" + android:background="@android:color/transparent" + android:focusable="false" + android:visibility="gone"/> + </LinearLayout> </LinearLayout> + + <ImageView + android:id="@+id/item_more" + android:src="?attr/download_none" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right|center_vertical" + style="@style/MoreButton"/> </LinearLayout> - <ImageView - android:id="@+id/item_more" - android:src="?attr/download_none" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="right|center_vertical" - android:paddingRight="2dp" - style="@style/BasicButton"/> </LinearLayout> - -</LinearLayout>
\ No newline at end of file +</github.daneren2005.dsub.view.CardView>
\ No newline at end of file diff --git a/app/src/main/res/layout/basic_cell_item.xml b/app/src/main/res/layout/basic_cell_item.xml index f522b196..a10fc4be 100644 --- a/app/src/main/res/layout/basic_cell_item.xml +++ b/app/src/main/res/layout/basic_cell_item.xml @@ -1,39 +1,45 @@ <?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" +<github.daneren2005.dsub.view.CardView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/selectableItemBackground"> - - <github.daneren2005.dsub.view.SquareImageView - android:id="@+id/item_art" - android:layout_width="match_parent" - android:layout_height="match_parent"/> + android:layout_margin="2dp"> <LinearLayout + android:orientation="vertical" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:paddingTop="4dp" - android:paddingLeft="2dp"> + android:layout_height="match_parent" + android:background="?attr/selectableItemBackground"> - <TextView - android:id="@+id/item_name" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - android:textAppearance="?android:attr/textAppearanceMedium" - android:singleLine="true" - android:ellipsize="marquee" - android:textColor="?android:textColorPrimary"/> + <github.daneren2005.dsub.view.SquareImageView + android:id="@+id/item_art" + android:layout_width="match_parent" + android:layout_height="match_parent"/> - <ImageView - android:id="@+id/item_more" - android:src="?attr/download_none" - android:layout_width="wrap_content" + <LinearLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_gravity="right|center_vertical" - android:paddingRight="2dp" - style="@style/BasicButton"/> + android:orientation="horizontal" + android:paddingTop="4dp" + android:paddingLeft="2dp"> + + <TextView + android:id="@+id/item_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textAppearance="?android:attr/textAppearanceMedium" + android:singleLine="true" + android:ellipsize="marquee" + android:textColor="?android:textColorPrimary" + android:paddingLeft="@dimen/Card.TextLeftPadding"/> + + <ImageView + android:id="@+id/item_more" + android:src="?attr/download_none" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right|center_vertical" + style="@style/MoreButton"/> + </LinearLayout> </LinearLayout> -</LinearLayout>
\ No newline at end of file +</github.daneren2005.dsub.view.CardView>
\ No newline at end of file diff --git a/app/src/main/res/layout/cache_location_buttons.xml b/app/src/main/res/layout/cache_location_buttons.xml new file mode 100644 index 00000000..31e12642 --- /dev/null +++ b/app/src/main/res/layout/cache_location_buttons.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center"> + + <Button + android:id="@+id/location_internal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/settings.cache_location_internal"/> + + <Button + android:id="@+id/location_external" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/settings.cache_location_external"/> +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/change_password.xml b/app/src/main/res/layout/change_password.xml index d8043c05..68861b0b 100644 --- a/app/src/main/res/layout/change_password.xml +++ b/app/src/main/res/layout/change_password.xml @@ -4,7 +4,30 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + <LinearLayout + android:id="@+id/current_password_layout" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/current_password_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:textSize="20dp" + android:text="@string/admin.change_password_current_label" + android:textColor="?android:textColorPrimary"/> + <EditText + android:id="@+id/current_password" + android:inputType="textPassword" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginLeft="4dp" /> + </LinearLayout> + + <LinearLayout android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content"> diff --git a/app/src/main/res/layout/download_media_buttons.xml b/app/src/main/res/layout/download_media_buttons.xml index 58fda5c0..0610c5f9 100644 --- a/app/src/main/res/layout/download_media_buttons.xml +++ b/app/src/main/res/layout/download_media_buttons.xml @@ -21,6 +21,14 @@ android:layout_centerVertical="true" /> + <github.daneren2005.dsub.view.AutoRepeatButton + style="@style/PlaybackControl.Large" + android:id="@+id/download_rewind" + android:src="?attr/media_button_rewind" + android:layout_toLeftOf="@+id/download_pause" + android:layout_centerVertical="true" + android:visibility="invisible"/> + <ImageButton style="@style/PlaybackControl.Large" android:id="@+id/download_pause" @@ -44,6 +52,14 @@ android:layout_centerInParent="true" /> + <github.daneren2005.dsub.view.AutoRepeatButton + style="@style/PlaybackControl.Large" + android:id="@+id/download_fastforward" + android:src="?attr/media_button_fastforward" + android:layout_toRightOf="@+id/download_start" + android:layout_centerVertical="true" + android:visibility="invisible"/> + <github.daneren2005.dsub.view.AutoRepeatButton style="@style/PlaybackControl.Small" android:id="@+id/download_next" diff --git a/app/src/main/res/layout/download_playlist.xml b/app/src/main/res/layout/download_playlist.xml index db74f8ca..161056db 100644 --- a/app/src/main/res/layout/download_playlist.xml +++ b/app/src/main/res/layout/download_playlist.xml @@ -28,7 +28,9 @@ android:id="@+id/download_list" android:layout_width="fill_parent" android:layout_height="fill_parent" - android:scrollbars="vertical"/> + android:scrollbars="vertical" + android:paddingRight="@dimen/FastScroller.LeftAlignedMargin" + android:layout_marginRight="@dimen/FastScroller.NormalBarMargin"/> <github.daneren2005.dsub.view.FastScroller android:id="@+id/download_fast_scroller" diff --git a/app/src/main/res/layout/drawer_header.xml b/app/src/main/res/layout/drawer_header.xml index 86222f43..3a9a2d49 100644 --- a/app/src/main/res/layout/drawer_header.xml +++ b/app/src/main/res/layout/drawer_header.xml @@ -4,7 +4,7 @@ android:layout_height="178dp" android:orientation="vertical" android:weightSum="1" - android:background="@drawable/drawer_header"> + android:background="?attr/drawerHeaderBackground"> <LinearLayout android:layout_width="match_parent" diff --git a/app/src/main/res/layout/newest_episode_header.xml b/app/src/main/res/layout/expandable_header.xml index bd78275e..bd78275e 100644 --- a/app/src/main/res/layout/newest_episode_header.xml +++ b/app/src/main/res/layout/expandable_header.xml diff --git a/app/src/main/res/layout/fast_scroller.xml b/app/src/main/res/layout/fast_scroller.xml index b2e244e3..4d37ca63 100644 --- a/app/src/main/res/layout/fast_scroller.xml +++ b/app/src/main/res/layout/fast_scroller.xml @@ -18,8 +18,8 @@ <ImageView android:id="@+id/fastscroller_handle" android:layout_width="wrap_content" - android:layout_marginRight="8dp" - android:layout_marginLeft="8dp" + android:layout_marginRight="@dimen/FastScroller.RightMargin" + android:layout_marginLeft="12dp" android:layout_height="wrap_content" android:src="@drawable/fast_scroller_handle"/> </merge>
\ No newline at end of file diff --git a/app/src/main/res/layout/home.xml b/app/src/main/res/layout/home.xml deleted file mode 100644 index 043886a2..00000000 --- a/app/src/main/res/layout/home.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/home_layout" - android:orientation="vertical" - android:layout_width="fill_parent" - android:layout_height="fill_parent"> - - <View - android:layout_width="fill_parent" - android:layout_height="1px" - android:background="?attr/colorPrimary"/> - - <ListView - android:id="@+id/main_list" - android:layout_width="fill_parent" - android:layout_height="0px" - android:layout_weight="1"/> - - <View android:id="@+id/main_dummy" - android:layout_width="0px" - android:layout_height="0px"/> -</LinearLayout> - diff --git a/app/src/main/res/layout/progress.xml b/app/src/main/res/layout/progress.xml deleted file mode 100644 index 8a299d63..00000000 --- a/app/src/main/res/layout/progress.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="horizontal" - android:layout_weight="1" - android:layout_width="0dip" - android:layout_height="fill_parent" - android:padding="10dp"> - - <ProgressBar - android:layout_width="wrap_content" - android:layout_height="fill_parent" - android:layout_marginRight="10dp"/> - - <TextView - android:id="@+id/progress_message" - android:text="@string/progress.wait" - android:layout_width="wrap_content" - android:layout_height="fill_parent" - android:textColor="?android:textColorPrimary"/> -</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml index 3baa30d4..bdcc4a23 100644 --- a/app/src/main/res/layout/settings_activity.xml +++ b/app/src/main/res/layout/settings_activity.xml @@ -10,7 +10,7 @@ android:layout_width="match_parent" android:background="?attr/actionbarBackgroundColor" android:elevation="4dp" - app:theme="?attr/actionbarThemeStyle" + android:theme="?attr/actionbarThemeStyle" app:popupTheme="?attr/actionbarPopupStyle"/> <FrameLayout diff --git a/app/src/main/res/layout/song_list_item.xml b/app/src/main/res/layout/song_list_item.xml index 6bf025b8..d7c8d312 100644 --- a/app/src/main/res/layout/song_list_item.xml +++ b/app/src/main/res/layout/song_list_item.xml @@ -17,25 +17,36 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical"> + <TextView + android:id="@+id/song_track" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="left|top" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?android:textColorPrimary" + android:paddingLeft="6dip" + android:paddingRight="6dip" + android:drawablePadding="6dip"/> + <TextView android:id="@+id/song_title" android:layout_width="0dip" android:layout_height="wrap_content" android:layout_weight="1" - android:layout_gravity="left|center_vertical" + android:layout_gravity="left|top" android:textAppearance="?android:attr/textAppearanceMedium" android:singleLine="true" android:ellipsize="marquee" - android:drawablePadding="6dip" android:paddingLeft="6dip" android:paddingRight="6dip" + android:drawablePadding="6dip" android:textColor="?android:textColorPrimary"/> <ImageButton android:id="@+id/song_bookmark" - android:layout_width="24dp" - android:layout_height="24dp" - android:layout_gravity="right|center_vertical" + android:layout_width="@dimen/SongStatusIcon" + android:layout_height="@dimen/SongStatusIcon" + android:layout_gravity="right|top" android:background="@null" android:focusable="false" android:scaleType="fitCenter" @@ -46,7 +57,17 @@ android:layout_width="@dimen/Star.Small" android:layout_height="@dimen/Star.Small" android:scaleType="fitCenter" - android:layout_gravity="right|center_vertical" + android:layout_gravity="right|top" + android:background="@null" + android:focusable="false" + android:visibility="gone"/> + + <ImageButton + android:id="@+id/song_played" + android:layout_width="@dimen/SongStatusIcon" + android:layout_height="@dimen/SongStatusIcon" + android:scaleType="fitCenter" + android:layout_gravity="right|top" android:background="@null" android:focusable="false" android:visibility="gone"/> @@ -55,15 +76,15 @@ android:id="@+id/song_status" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right|center_vertical" + android:layout_gravity="right|top" android:drawablePadding="1dip" android:paddingRight="2dip"/> <ImageView android:id="@+id/song_status_icon" - android:layout_width="24dip" - android:layout_height="24dip" - android:layout_gravity="center_vertical" + android:layout_width="@dimen/SongStatusIcon" + android:layout_height="@dimen/SongStatusIcon" + android:layout_gravity="top" android:src="?attr/downloading" android:visibility="gone"/> </LinearLayout> diff --git a/app/src/main/res/layout/start_timer.xml b/app/src/main/res/layout/start_timer.xml index 59bd60e3..61a72233 100644 --- a/app/src/main/res/layout/start_timer.xml +++ b/app/src/main/res/layout/start_timer.xml @@ -18,5 +18,5 @@ android:id="@+id/timer_length_bar" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:max="97"/> + android:max="92"/> </LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/menu/abstract_top_menu.xml b/app/src/main/res/menu/abstract_top_menu.xml index 7c8d414d..b768879d 100644 --- a/app/src/main/res/menu/abstract_top_menu.xml +++ b/app/src/main/res/menu/abstract_top_menu.xml @@ -5,7 +5,8 @@ android:id="@+id/menu_global_search" android:icon="?attr/search" android:title="@string/menu.search" - compat:showAsAction="always|withText"/> + compat:actionViewClass="android.support.v7.widget.SearchView" + compat:showAsAction="always|collapseActionView"/> <group android:id="@+id/not_touchscreen"> <item diff --git a/app/src/main/res/menu/drawer_menu.xml b/app/src/main/res/menu/drawer_menu.xml deleted file mode 100644 index b3e70cfa..00000000 --- a/app/src/main/res/menu/drawer_menu.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:compat="http://schemas.android.com/apk/res-auto"> - - <item - android:id="@+id/menu_search" - android:icon="?attr/search" - android:title="@string/menu.search" - compat:showAsAction="always|withText"/> - - <item - android:id="@+id/menu_exit" - android:title="@string/menu.exit"/> -</menu> diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml index 0970c8ce..4b542668 100644 --- a/app/src/main/res/menu/main.xml +++ b/app/src/main/res/menu/main.xml @@ -5,13 +5,14 @@ android:id="@+id/menu_global_search" android:icon="?attr/search" android:title="@string/menu.search" - compat:showAsAction="always|withText"/> + compat:actionViewClass="android.support.v7.widget.SearchView" + compat:showAsAction="ifRoom|collapseActionView"/> <item android:id="@+id/menu_global_shuffle" android:icon="?attr/shuffle" android:title="@string/menu.shuffle" - compat:showAsAction="always|withText"/> + compat:showAsAction="ifRoom|withText"/> <group android:id="@+id/madsonic"> <item diff --git a/app/src/main/res/menu/multiselect_media.xml b/app/src/main/res/menu/multiselect_media.xml index 6adb4567..85bb5217 100644 --- a/app/src/main/res/menu/multiselect_media.xml +++ b/app/src/main/res/menu/multiselect_media.xml @@ -34,8 +34,8 @@ android:title="@string/menu.remove_playlist"/> <item - android:id="@+id/menu_unstar" - android:title="@string/common.unstar"/> + android:id="@+id/menu_star" + android:title="@string/common.star"/> <group android:id="@+id/hide_play_next"> <item diff --git a/app/src/main/res/menu/multiselect_nowplaying.xml b/app/src/main/res/menu/multiselect_nowplaying.xml new file mode 100644 index 00000000..9d361bf0 --- /dev/null +++ b/app/src/main/res/menu/multiselect_nowplaying.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:compat="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/menu_download" + android:title="@string/common.download" + android:icon="?attr/download" + compat:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_delete" + android:title="@string/menu.delete_cache" + android:icon="?attr/remove" + compat:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_cache" + android:title="@string/common.pin"/> + + <item + android:id="@+id/menu_add_playlist" + android:title="@string/menu.add_playlist"/> + + <item + android:id="@+id/menu_star" + android:title="@string/common.star"/> +</menu> diff --git a/app/src/main/res/menu/multiselect_nowplaying_offline.xml b/app/src/main/res/menu/multiselect_nowplaying_offline.xml new file mode 100644 index 00000000..044836c6 --- /dev/null +++ b/app/src/main/res/menu/multiselect_nowplaying_offline.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:compat="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/menu_delete" + android:title="@string/menu.delete_cache" + android:icon="?attr/remove" + compat:showAsAction="ifRoom|withText"/> +</menu> diff --git a/app/src/main/res/menu/nowplaying.xml b/app/src/main/res/menu/nowplaying.xml index 60255692..9c198e9a 100644 --- a/app/src/main/res/menu/nowplaying.xml +++ b/app/src/main/res/menu/nowplaying.xml @@ -33,6 +33,11 @@ android:title="@string/equalizer.label" android:checkable="true"/> + <item + android:id="@+id/menu_batch_mode" + android:title="@string/download.batch_mode" + android:checkable="true"/> + <item android:id="@+id/menu_screen_on_off" android:title="@string/download.menu_screen_on" diff --git a/app/src/main/res/menu/nowplaying_context.xml b/app/src/main/res/menu/nowplaying_context.xml index 60d6288e..06f4afcb 100644 --- a/app/src/main/res/menu/nowplaying_context.xml +++ b/app/src/main/res/menu/nowplaying_context.xml @@ -26,7 +26,7 @@ <group android:id="@+id/hide_star"> <item - android:id="@+id/menu_star" + android:id="@+id/song_menu_star" android:title="@string/common.star"/> </group> @@ -44,7 +44,7 @@ <group android:id="@+id/server_1.8"> <item - android:id="@+id/menu_add_playlist" + android:id="@+id/song_menu_add_playlist" android:title="@string/menu.add_playlist"/> </group> </menu> diff --git a/app/src/main/res/menu/nowplaying_context_offline.xml b/app/src/main/res/menu/nowplaying_context_offline.xml index 5f8009ff..14c95ab6 100644 --- a/app/src/main/res/menu/nowplaying_context_offline.xml +++ b/app/src/main/res/menu/nowplaying_context_offline.xml @@ -22,7 +22,7 @@ <group android:id="@+id/hide_star"> <item - android:id="@+id/menu_star" + android:id="@+id/song_menu_star" android:title="@string/common.star"/> </group> </menu> diff --git a/app/src/main/res/menu/nowplaying_offline.xml b/app/src/main/res/menu/nowplaying_offline.xml index bba5ba00..d1f6f706 100644 --- a/app/src/main/res/menu/nowplaying_offline.xml +++ b/app/src/main/res/menu/nowplaying_offline.xml @@ -25,6 +25,11 @@ android:id="@+id/menu_equalizer" android:title="@string/equalizer.label" android:checkable="true"/> + + <item + android:id="@+id/menu_batch_mode" + android:title="@string/download.batch_mode" + android:checkable="true"/> <item android:id="@+id/menu_screen_on_off" diff --git a/app/src/main/res/menu/playback_speed_options.xml b/app/src/main/res/menu/playback_speed_options.xml new file mode 100644 index 00000000..e64ecd04 --- /dev/null +++ b/app/src/main/res/menu/playback_speed_options.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/playback_speed_half" + android:title="@string/download.playback_speed_half"/> + + <item + android:id="@+id/playback_speed_normal" + android:title="@string/download.playback_speed_normal"/> + + <item + android:id="@+id/playback_speed_one_half" + android:title="@string/download.playback_speed_one_half"/> + + <item + android:id="@+id/playback_speed_double" + android:title="@string/download.playback_speed_double"/> + + <item + android:id="@+id/playback_speed_tripple" + android:title="@string/download.playback_speed_tripple"/> +</menu>
\ No newline at end of file diff --git a/app/src/main/res/menu/search.xml b/app/src/main/res/menu/search.xml index e9377d68..b957a1e4 100644 --- a/app/src/main/res/menu/search.xml +++ b/app/src/main/res/menu/search.xml @@ -5,7 +5,8 @@ android:id="@+id/menu_global_search" android:icon="?attr/search" android:title="@string/menu.search" - compat:showAsAction="ifRoom|withText"/> + compat:actionViewClass="android.support.v7.widget.SearchView" + compat:showAsAction="always|collapseActionView"/> <item android:id="@+id/menu_exit" diff --git a/app/src/main/res/menu/select_album_context.xml b/app/src/main/res/menu/select_album_context.xml index e4a901ac..0415da3c 100644 --- a/app/src/main/res/menu/select_album_context.xml +++ b/app/src/main/res/menu/select_album_context.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:id="@+id/album_menu_info" android:title="@string/common.info"/> @@ -47,9 +46,11 @@ android:title="@string/menu.delete_cache"/> </group> - <item - android:id="@+id/album_menu_show_artist" - android:title="@string/menu.show_artist"/> + <group android:id="@+id/hide_show_artist"> + <item + android:id="@+id/album_menu_show_artist" + android:title="@string/menu.show_artist"/> + </group> <group android:id="@+id/hide_star"> <item diff --git a/app/src/main/res/menu/select_album_context_offline.xml b/app/src/main/res/menu/select_album_context_offline.xml index c10f2c62..37e2ae66 100644 --- a/app/src/main/res/menu/select_album_context_offline.xml +++ b/app/src/main/res/menu/select_album_context_offline.xml @@ -35,6 +35,12 @@ android:title="@string/menu.delete_cache"/> </group> + <group android:id="@+id/hide_show_artist"> + <item + android:id="@+id/album_menu_show_artist" + android:title="@string/menu.show_artist"/> + </group> + <group android:id="@+id/hide_star"> <item android:id="@+id/album_menu_star" diff --git a/app/src/main/res/menu/select_artist.xml b/app/src/main/res/menu/select_artist.xml index 66ba37ba..e974c28e 100644 --- a/app/src/main/res/menu/select_artist.xml +++ b/app/src/main/res/menu/select_artist.xml @@ -1,16 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:compat="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/menu_global_search" + android:icon="?attr/search" + android:title="@string/menu.search" + compat:actionViewClass="android.support.v7.widget.SearchView" + compat:showAsAction="ifRoom|collapseActionView"/> + <item android:id="@+id/menu_global_shuffle" android:icon="?attr/shuffle" android:title="@string/menu.shuffle" - compat:showAsAction="always|withText"/> - - <item - android:id="@+id/menu_global_search" - android:icon="?attr/search" - android:title="@string/menu.search" compat:showAsAction="ifRoom|withText"/> <group android:id="@+id/not_touchscreen"> diff --git a/app/src/main/res/menu/select_podcasts.xml b/app/src/main/res/menu/select_podcasts.xml index 41ad62fa..25bb6188 100644 --- a/app/src/main/res/menu/select_podcasts.xml +++ b/app/src/main/res/menu/select_podcasts.xml @@ -5,13 +5,14 @@ android:id="@+id/menu_global_search" android:icon="?attr/search" android:title="@string/menu.search" - compat:showAsAction="always|withText"/> + compat:actionViewClass="android.support.v7.widget.SearchView" + compat:showAsAction="ifRoom|collapseActionView"/> <item android:id="@+id/menu_add_podcast" android:icon="?attr/add" android:title="@string/menu.add_podcast" - compat:showAsAction="always|withText"/> + compat:showAsAction="ifRoom|withText"/> <group android:id="@+id/not_touchscreen"> <item diff --git a/app/src/main/res/menu/select_song_context.xml b/app/src/main/res/menu/select_song_context.xml index 34ea27a6..b68dcad9 100644 --- a/app/src/main/res/menu/select_song_context.xml +++ b/app/src/main/res/menu/select_song_context.xml @@ -1,11 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:compat="http://schemas.android.com/apk/res-auto"> - +<menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/song_menu_info" - android:title="@string/common.info" - /> + android:title="@string/common.info"/> + + + <group android:id="@+id/hide_show_artist"> + <item + android:id="@+id/song_menu_show_album" + android:title="@string/download.menu_show_album"/> + + <item + android:id="@+id/song_menu_show_artist" + android:title="@string/menu.show_artist"/> + </group> <group android:id="@+id/hide_play_next"> <item diff --git a/app/src/main/res/menu/select_song_context_offline.xml b/app/src/main/res/menu/select_song_context_offline.xml index 1c52e792..d7182068 100644 --- a/app/src/main/res/menu/select_song_context_offline.xml +++ b/app/src/main/res/menu/select_song_context_offline.xml @@ -6,6 +6,16 @@ android:id="@+id/song_menu_info" android:title="@string/common.info"/> + <group android:id="@+id/hide_show_artist"> + <item + android:id="@+id/song_menu_show_album" + android:title="@string/download.menu_show_album"/> + + <item + android:id="@+id/song_menu_show_artist" + android:title="@string/menu.show_artist"/> + </group> + <group android:id="@+id/hide_play_next"> <item android:id="@+id/song_menu_play_next" diff --git a/app/src/main/res/menu/select_video_context.xml b/app/src/main/res/menu/select_video_context.xml index 95576efc..51301722 100644 --- a/app/src/main/res/menu/select_video_context.xml +++ b/app/src/main/res/menu/select_video_context.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:compat="http://schemas.android.com/apk/res-auto"> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/song_menu_info" android:title="@string/common.info"/> @@ -30,4 +29,10 @@ android:id="@+id/song_menu_delete" android:title="@string/menu.delete_cache"/> </group> + + <group android:id="@+id/hide_star"> + <item + android:id="@+id/song_menu_star" + android:title="@string/common.star"/> + </group> </menu> diff --git a/app/src/main/res/menu/similar_artists.xml b/app/src/main/res/menu/similar_artists.xml index f6c30fb2..2557381e 100644 --- a/app/src/main/res/menu/similar_artists.xml +++ b/app/src/main/res/menu/similar_artists.xml @@ -13,8 +13,4 @@ android:icon="?attr/shuffle" android:title="@string/menu.shuffle" compat:showAsAction="ifRoom|withText"/> - - <item - android:id="@+id/menu_show_missing" - android:title="@string/menu.show_missing"/> </menu>
\ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 534a0f75..c43f3aea 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -28,8 +28,7 @@ <string name="button_bar.home">Übersicht</string> <string name="button_bar.browse">Bibliothek</string> - <string name="button_bar.search">Suchen</string> - <string name="button_bar.playlists">Wiedergabeliste</string> + <string name="button_bar.playlists">Wiedergabeliste</string> <string name="button_bar.now_playing">Aktuelle Wiedergabe</string> <string name="button_bar.podcasts">Podcasts</string> <string name="button_bar.bookmarks">Lesezeichen</string> @@ -53,8 +52,7 @@ <br/>Im Optionsmenü deselektiere "Erste Ebene sind Künstler". Dann wird die erste Verzeichnisebene wie Künstlergruppen anstelle von Künstler behandelt. ]]> </string> - <string name="main.select_server">Wähle Server</string> - <string name="main.shuffle">Zufallswiedergabe</string> + <string name="main.shuffle">Zufallswiedergabe</string> <string name="main.offline">Gehe Offline</string> <string name="main.online">Gehe Online</string> <string name="main.settings">Einstellungen</string> @@ -106,7 +104,6 @@ <string name="menu.rate">Setze Bewertung</string> <string name="menu.top_tracks">Last.FM Top Medien</string> <string name="menu.similar_artists">Ähnliche Künstler</string> - <string name="menu.show_missing">Zeige fehlende</string> <string name="menu.start_radio">Starte Radio</string> <string name="menu.first_level_artist">Erste Ebene sind Künstler</string> @@ -126,20 +123,13 @@ <string name="search.artists">Künstler</string> <string name="search.albums">Alben</string> <string name="search.songs">Lieder</string> - <string name="search.more">Zeige mehr</string> - <string name="progress.wait">Bitte warten...</string> + <string name="progress.wait">Bitte warten...</string> <string name="progress.artist_info">Lade Informationen zum Künstler</string> - <string name="music_library.label">Medienbibliothek</string> - <string name="music_library.label_offline">Offline Medien</string> - - <string name="select_album.select">Alle auswählen</string> - <string name="select_album.n_selected">%d Lieder ausgewählt.</string> - <string name="select_album.more">Mehr</string> - <string name="select_album.offline">Offline</string> - <string name="select_album.searching">Suche...</string> - <string name="select_album.no_sdcard">Fehler: Keine SD-Karte verfügbar.</string> + <string name="select_album.n_selected">%d Lieder ausgewählt.</string> + <string name="select_album.offline">Offline</string> + <string name="select_album.no_sdcard">Fehler: Keine SD-Karte verfügbar.</string> <string name="select_album.no_network">Warnung: Kein Netzwerk verfügbar.</string> <string name="select_album.no_room">Warnung: Es sind nur %s verfügbar</string> <string name="select_album.not_licensed">Server ist nicht lizensiert. Testzeitraum läuft ab in %d Tagen.</string> @@ -198,13 +188,9 @@ <string name="download.repeat_off">Keine Wiederholung</string> <string name="download.repeat_all">Wiederhole alle</string> <string name="download.repeat_single">Aktuelles Lied wiederholen</string> - <string name="download.jukebox_on">Fernbedienung aktiviert. Musik wird auf dem Computer abgespielt.</string> - <string name="download.jukebox_off">Fernbedienung deaktiviert. Musik wird auf dem Telefon abgespielt.</string> - <string name="download.jukebox_volume">Lautstärke</string> - <string name="download.jukebox_server_too_old">Fernbedienung wird nicht unterstützt. Aktualisierung des Subsonic-Servers notwendig.</string> + <string name="download.jukebox_server_too_old">Fernbedienung wird nicht unterstützt. Aktualisierung des Subsonic-Servers notwendig.</string> <string name="download.jukebox_offline">Fernbedienung im Offline-Modus nicht verfügbar.</string> <string name="download.jukebox_not_authorized">Fernbedienung ist nicht erlaubt. Bitte aktivieren Sie den Jukebox-Modus unter <b>Nutzer > Einstellungen</b> auf Ihrem Subsonic-Server.</string> - <string name="download.timer_length">Timer:</string> <string name="download.start_timer">Starte Timer</string> <string name="download.need_download">Video muss zuerst heruntergeladen werden</string> <string name="download.no_streaming_player">Stream kann nicht wiedergegeben werden.</string> @@ -411,8 +397,6 @@ <string name="settings.override_system_language">In Englisch anzeigen</string> <string name="settings.override_system_language_summary">Verwende Englisch anstatt Deutsch für DSub. Benötigt einen Neustart der App.</string> <string name="settings.drawer_items_title">Seitenmenü</string> - <string name="settings.play_now_after">Jetzt wiedergeben bis zum Listenende</string> - <string name="settings.play_now_after_summary">\"Jetzt wiedergeben\" im Kontextmenü spielt das ausgewählte Lied und alle in der Liste nachfolgenden Lieder ab (wie in der Web-Schnittstelle des Subsonic-Server)</string> <string name="settings.large_album_art">Große Cover anzeigen</string> <string name="settings.large_album_art_summary">Verwende große Cover zur Anzeige der Alben anstatt einer Liste</string> <string name="settings.admin_enabled">Administration aktiviert</string> @@ -439,15 +423,6 @@ <string name="shuffle.genre">Genre:</string> <string name="shuffle.pick_genre">Wähle ein Genre</string> - <string name="share.info">Eigentümer: %1$s - \nBeschreibung: %2$s - \nURL: %3$s - \nErzeugt: %4$s - \nZuletzt besucht: %5$s - \nAblauf: %6$s - \nBesuchszähler: %7$s - - </string> <string name="share.expires">Ablauf: %s</string> <string name="share.expires_never">nie</string> <string name="share.deleted">Lösche Freigabe %s</string> @@ -499,9 +474,7 @@ <string name="music_service.retry">Ein Netzwerkfehler ist aufgetreten. Versuch %1$d von %2$d.</string> - <string name="background_task.wait">Bitte warten...</string> - <string name="background_task.loading">Lade.</string> - <string name="background_task.no_network">Diese Programm benötigt Netzwerkzugriff. Bitte schalten Sie Wi-Fi oder Mobiles Netzwerk ein.</string> + <string name="background_task.no_network">Diese Programm benötigt Netzwerkzugriff. Bitte schalten Sie Wi-Fi oder Mobiles Netzwerk ein.</string> <string name="background_task.network_error">Ein Netzwerkfehler ist aufgetreten. Bitte prüfen Sie die Serveradresse oder versuchen Sie es später nochmal.</string> <string name="background_task.not_found">Quelle wurde nicht gefunden. Bitte prüfen Sie die Serveradresse.</string> <string name="background_task.parse_error">Ein Fehler ist bei der Kommunikation mit dem Server aufgetreten. Bitte prüfen Sie die Serveradresse und stellen Sie sicher, das Sie der Server mit einem Webbrowser erreichen.</string> @@ -516,8 +489,7 @@ <string name="parser.server_error">Serverfehler: %s</string> <string name="parser.scan_count">%d Einträge gefunden</string> - <string name="select_artist.refresh">Aktualisieren</string> - <string name="select_artist.folder">Wähle Ordner</string> + <string name="select_artist.folder">Wähle Ordner</string> <string name="select_artist.all_folders">Alle Ordner</string> <string name="equalizer.label">Equalizer</string> @@ -537,9 +509,7 @@ <string name="changelog_ok_button">OK</string> <string name="changelog_show_full">Mehr…</string> - <string name="chat.send_a_message">Nachricht senden</string> - - <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string> + <string name="chat.send_a_message">Nachricht senden</string> <string name="tasker.start_playing">Starte Wiedergabe</string> <string name="tasker.start_playing_shuffled">Starte Zufallswiedergabe</string> @@ -642,6 +612,7 @@ <string name="settings.keep_played_count_two">2 abgespielte Lieder behalten</string> <string name="settings.keep_played_count_title">Abgespielte Lieder behalten</string> <string name="details.updated">Aktualisiert</string> + <string name="details.last_played">Zuletzt gespielt</string> <string name="details.position">Position</string> <string name="details.song">Lied</string> <string name="button_bar.offline">Offline</string> @@ -656,5 +627,25 @@ <string name="settings.shuffle_by_album.false">Alle Titel mischen</string> <string name="settings.shuffle_by_album.true">Albenreihenfolge mischen</string> <string name="settings.shuffle_by_album">Alben mischen</string> + <string name="common.never">Nie</string> + <string name="details.starred">Favorit</string> + <string name="download.thumbs_up">Gefällt mir</string> + <string name="download.thumbs_down">Gefällt mir nicht</string> + <string name="select_podcasts.channels">Podcastkanäle</string> + <string name="settings.cache_location_external">Externer Speicher</string> + <string name="settings.cache_location_internal">Interner Speicher</string> + <string name="admin.change_password_current_label">Aktuelles Passwort:</string> + <string name="admin.musicFolders">Musikordner</string> + <string name="admin.permissions">Berechtigungen</string> + <string name="details.played_count">Abspielzähler</string> + <string name="details.expiration">Ablauf</string> + <string name="download.playerstate_playing_artist_radio">Künstlerradio</string> + <string name="main.songs_top_played">Am meisten gespielt</string> + <string name="main.songs_frequent">@string/main.albums_frequent</string> + <string name="main.songs_newest">@string/main.albums_newest</string> + <string name="main.songs_recent">@string/main.albums_recent</string> + <string name="menu.similar_artists.missing">Fehlende Künstler</string> + <string name="settings.casting_stream_original">Original streamen</string> + <string name="settings.casting_stream_original_summary">Zum streamen möglichst das Originalformat verwenden.</string> </resources> diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index f56e939e..a5273a50 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -28,8 +28,7 @@ <string name="button_bar.home">Inicio</string> <string name="button_bar.browse">Biblioteca</string> - <string name="button_bar.search">Buscar</string> - <string name="button_bar.playlists">Listas de reproducción</string> + <string name="button_bar.playlists">Listas de reproducción</string> <string name="button_bar.now_playing">Ahora suena</string> <string name="button_bar.podcasts">Podcasts</string> <string name="button_bar.bookmarks">Marcadores</string> @@ -41,8 +40,7 @@ <string name="main.welcome_title">Bienvenido!</string> <string name="main.welcome_text">Bienvenido a DSub! Ahora la aplicación está configurada para usar el servidor de demostración de Subsonic. Cuando configures tu servidor personal (disponible en <b>subsonic.org</b>), accede a <b>Preferencias</b> y cambia la configuración para conectarte.</string> <string name="main.about_title">Acerca de DSub</string> - <string name="main.select_server">Seleccionar servidor</string> - <string name="main.shuffle">Reproducción aleatoria</string> + <string name="main.shuffle">Reproducción aleatoria</string> <string name="main.offline">Modo Offline</string> <string name="main.online">Modo Online</string> <string name="main.settings">Preferencias</string> @@ -96,7 +94,6 @@ <string name="menu.rate">Establecer valoración</string> <string name="menu.top_tracks">Tp Tracks de Last.FM</string> <string name="menu.similar_artists">Artistas similares</string> - <string name="menu.show_missing">Mostrar los que faltan</string> <string name="menu.start_radio">Iniciar radio</string> <string name="playlist.label">Listas de reproducción</string> @@ -115,19 +112,12 @@ <string name="search.artists">Artista</string> <string name="search.albums">Disco</string> <string name="search.songs">Canción</string> - <string name="search.more">Mostrar más</string> - <string name="progress.wait">Espere por favor...</string> + <string name="progress.wait">Espere por favor...</string> - <string name="music_library.label">Biblioteca de medios</string> - <string name="music_library.label_offline">Archivos Offline</string> - - <string name="select_album.select">Seleccionar todo</string> - <string name="select_album.n_selected">Seleccionadas %d canciones</string> - <string name="select_album.more">Más</string> - <string name="select_album.offline">Offline</string> - <string name="select_album.searching">Buscando...</string> - <string name="select_album.no_sdcard">Error: No hay tarjeta SD disponible</string> + <string name="select_album.n_selected">Seleccionadas %d canciones</string> + <string name="select_album.offline">Offline</string> + <string name="select_album.no_sdcard">Error: No hay tarjeta SD disponible</string> <string name="select_album.no_network">Aviso: No hay red disponible</string> <string name="select_album.not_licensed">Servidor sin licencia. Quedan %d días de prueba</string> <string name="select_album.donate_dialog_message">Consigue descargas ilimitadas haciendo una donación a Subsonic</string> @@ -183,13 +173,9 @@ <string name="download.repeat_off">Repetir off</string> <string name="download.repeat_all">Repetir todo</string> <string name="download.repeat_single">Repetir canción</string> - <string name="download.jukebox_on">Control remoto encendido. La música se está reproduciendo en el ordenador.</string> - <string name="download.jukebox_off">Control remoto apagado. La música se está reproduciendo en el dispositivo móvil.</string> - <string name="download.jukebox_volume">Volumen remoto</string> - <string name="download.jukebox_server_too_old">Control remoto no soportado. Por favor, actualice su servidor Subsonic.</string> + <string name="download.jukebox_server_too_old">Control remoto no soportado. Por favor, actualice su servidor Subsonic.</string> <string name="download.jukebox_offline">Control remoto no disponible en modo offline.</string> <string name="download.jukebox_not_authorized">Control remoto no permitido. Por favor, active el modo jukebox en <b>Users > Settings</b> en su servidor Subsonic.</string> - <string name="download.timer_length">Temporizador</string> <string name="download.start_timer">Iniciar temporizador</string> <string name="download.need_download">El vídeo ha de ser descargado antes</string> <string name="download.no_streaming_player">Ningún reproductor puede reproducir este stream</string> @@ -400,8 +386,6 @@ <string name="settings.override_system_language">Sobreescribir idioma del sistema</string> <string name="settings.override_system_language_summary">Mostrar la aplicación en inglés aún teniendo disponible DSub en el idioma del sistema. Probablemente necesite borrar la aplicación de la memoria para efectuar los cambios.</string> <string name="settings.drawer_items_title">Pestañas</string> - <string name="settings.play_now_after">Reproducir ahora - Después</string> - <string name="settings.play_now_after_summary">Pulsar "Reproducir ahora" desde el menú contextual actúa como la interfaz web de Subsonic, reproduciendo todos los items a partir del seleccionado</string> <string name="settings.large_album_art">Carátulas grandes</string> <string name="settings.large_album_art_summary">Mostrar los discos con carátulas grandes en vez de en lista</string> <string name="settings.admin_enabled">Admin Habilitado</string> @@ -420,14 +404,6 @@ <string name="settings.open_to_tab">Abrir en pestaña</string> <string name="settings.open_to_tab_summary">Abrir directamente a esta pestaña</string> - <string name="share.info">Dueño: %1$s - \nDescripción: %2$s - \nURL: %3$s - \nCreado: %4$s - \nÚltima visita: %5$s - \nExpira: %6$s - \nNúmero de visitas: %7$s - </string> <string name="share.expires">Expira: %s</string> <string name="share.expires_never">Nunca expira</string> <string name="share.deleted">Compartición eliminada %s</string> @@ -490,9 +466,7 @@ <string name="music_service.retry">Error de red. Reintentando %1$d de %2$d.</string> - <string name="background_task.wait">Por favor, espere...</string> - <string name="background_task.loading">Cargando.</string> - <string name="background_task.no_network">Este programa requiere de acceso a la red. Encienda el Wi-Fi o la conexión de datos móviles.</string> + <string name="background_task.no_network">Este programa requiere de acceso a la red. Encienda el Wi-Fi o la conexión de datos móviles.</string> <string name="background_task.network_error">Error de red. Por favor, compruebe la dirección del servidor o inténtelo más tarde.</string> <string name="background_task.not_found">Recurso no encontrado. Por favor, compruebe la dirección del servidor.</string> <string name="background_task.parse_error">Respuesta desconocida. Por favor, compruebe la dirección del servidor.</string> @@ -506,8 +480,7 @@ <string name="parser.artist_count">Recibidos %d artistas.</string> <string name="parser.scan_count">Escaneados %d entradas</string> - <string name="select_artist.refresh">Actualizar</string> - <string name="select_artist.folder">Seleccionar carpeta</string> + <string name="select_artist.folder">Seleccionar carpeta</string> <string name="select_artist.all_folders">Todas las carpetas</string> <string name="equalizer.bass_booster">Potenciar bajos</string> @@ -537,9 +510,7 @@ <string name="changelog_ok_button">OK</string> <string name="changelog_show_full">Más…</string> - <string name="chat.send_a_message">Enviar un mensaje</string> - - <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string> + <string name="chat.send_a_message">Enviar un mensaje</string> <string name="tasker.start_playing">Comenzar reproduciendo</string> <string name="tasker.start_playing_title">Tasker -> Encender DSub</string> diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4674ca32..a20c0fd8 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -10,7 +10,7 @@ <string name="common.play_next">Suivant</string> <string name="common.play_last">Précédent</string> <string name="common.download">Mettre en cache</string> - <string name="common.pin">Mettre en cache Permanent</string> + <string name="common.pin">Mettre en cache permanent</string> <string name="common.delete">Supprimer</string> <string name="common.star">Favori</string> <string name="common.unstar">Supp. favori</string> @@ -19,17 +19,16 @@ <string name="common.comment">Commentaire</string> <string name="common.public">Publique</string> <string name="common.play_external">Jouer Video</string> - <string name="common.stream_external">Stream Video</string> + <string name="common.stream_external">Stream Vidéo</string> <string name="common.confirm">Confirmer</string> <string name="common.confirm_message">Voulez-vous %1$s %2$s ?</string> <string name="common.confirm_message_cache">cache</string> - <string name="common.empty">Aucune donnée</string> + <string name="common.empty">Aucune données</string> <string name="common.warning">Avertissement</string> <string name="button_bar.home">Accueil</string> <string name="button_bar.browse">Bibliothèque</string> - <string name="button_bar.search">Recherche</string> - <string name="button_bar.playlists">Playlists</string> + <string name="button_bar.playlists">Playlists</string> <string name="button_bar.now_playing">Lecture en cours</string> <string name="button_bar.podcasts">Podcasts</string> <string name="button_bar.bookmarks">Favoris</string> @@ -40,7 +39,7 @@ <string name="main.welcome_title">Bienvenue !</string> <string name="main.welcome_text">Bienvenue dans DSub ! L\'application est actuellement configurée pour se connecter au serveur de démo Subsonic (<b>demo.subsonic.org</b>). Vous pouvez configurer votre propre serveur dans les paramètres. Choisir <b>Paramètres</b> et mettre à jour la configuration pour vous y connecter.</string> - <string name="main.about_title">A propos de DSub</string> + <string name="main.about_title">À propos de DSub</string> <string name="main.faq_title">FAQ</string> <string name="main.faq_text"> <![CDATA[ @@ -50,14 +49,13 @@ <br/>Assurez-vous de ne pas utiliser un certificat auto-signé, Chromecast les rejette systématiquement. ]]> </string> - <string name="main.select_server">Choisir un serveur</string> - <string name="main.shuffle">Jouer au hasard</string> + <string name="main.shuffle">Jouer au hasard</string> <string name="main.offline">Déconnecter</string> <string name="main.online">Connecter</string> <string name="main.settings">Paramètres</string> <string name="main.albums_title">Albums</string> <string name="main.albums_newest">Ajoutés récemments</string> - <string name="main.albums_recent">Joués récemment</string> + <string name="main.albums_recent">Joués récemments</string> <string name="main.albums_frequent">Les plus joués</string> <string name="main.albums_highest">Les mieux notés</string> <string name="main.albums_starred">Favoris</string> @@ -66,7 +64,7 @@ <string name="main.albums_year">Par décennies</string> <string name="main.songs_genres">@string/main.albums_genres</string> <string name="main.back_confirm">Presser retour à nouveau pour quitter</string> - <string name="main.scan_complete">Completed scan of Server</string> + <string name="main.scan_complete">Analyse du server terminée</string> <string name="menu.search">Recherche</string> <string name="menu.shuffle">Hasard</string> @@ -77,31 +75,31 @@ <string name="menu.settings">Paramètres</string> <string name="menu.help">Aide</string> - <string name="menu.about">A propos</string> + <string name="menu.about">À propos</string> <string name="menu.add_playlist">Ajouter à la playlist</string> <string name="menu.remove_playlist">Supprimer de la playlist</string> <string name="menu.deleted_playlist">Supprimer la playlist %s</string> - <string name="menu.deleted_playlist_error">Echec de la suppression de la playlist %s</string> + <string name="menu.deleted_playlist_error">Échec de la suppression de la playlist %s</string> <string name="menu.log">Envoyer le journal</string> <string name="menu.set_timer">Ajuster le minuteur</string> <string name="menu.check_podcasts">Vérifier les nouveaux podcasts</string> <string name="menu.add_podcast">Ajouter une chaîne</string> <string name="menu.keep_synced">Synchronisation automatique</string> <string name="menu.stop_sync">Arrêter la synchro.</string> - <string name="menu.show_all">Afficher tous les media</string> + <string name="menu.show_all">Afficher tous les médias</string> <string name="menu.show_artist">Afficher l\'artiste</string> <string name="menu.share">Partager</string> <string name="menu.delete_cache">Supprimer du cache</string> <string name="menu.cast">Diffuser vers appareil</string> <string name="menu.faq">FAQ</string> - <string name="menu.add_user">Ajouter utilisateur</string> - <string name="menu.rescan">Relire le server</string> + <string name="menu.add_user">Ajouter un utilisateur</string> + <string name="menu.rescan">Relire le serveur</string> <string name="menu.rate">Noter</string> <string name="playlist.label">Playlists</string> - <string name="playlist.update_info">Mise à jour informations</string> + <string name="playlist.update_info">Mise à jour des informations</string> <string name="playlist.updated_info">Informations de la playlist %s mises à jour</string> - <string name="playlist.updated_info_error">Echec de la mise à jour des informations de la playlist %s</string> + <string name="playlist.updated_info_error">Échec de la mise à jour des informations de la playlist %s</string> <string name="playlist.overwrite">Remplacer la playlist existante</string> <string name="playlist.add_to">Ajouter à la playlist</string> <string name="playlist.create_new">Créer une nouvelle</string> @@ -114,21 +112,14 @@ <string name="search.artists">Artistes</string> <string name="search.albums">Albums</string> <string name="search.songs">Chansons</string> - <string name="search.more">Afficher plus</string> - <string name="progress.wait">Patientez…</string> + <string name="progress.wait">Patientez…</string> - <string name="music_library.label">Bibliothèque</string> - <string name="music_library.label_offline">Média mode déconnecté</string> - - <string name="select_album.select">Tout sélectionner</string> - <string name="select_album.n_selected">%d pistes sélectionnées.</string> - <string name="select_album.more">Plus</string> - <string name="select_album.offline">Déconnecté</string> - <string name="select_album.searching">Recherche en cours...</string> - <string name="select_album.no_sdcard">Erreur : Aucune carte SD card disponible.</string> + <string name="select_album.n_selected">%d pistes sélectionnées.</string> + <string name="select_album.offline">Déconnecté</string> + <string name="select_album.no_sdcard">Erreur : Aucune carte SD card disponible.</string> <string name="select_album.no_network">Problème : Aucun réseau disponible.</string> - <string name="select_album.not_licensed">Serveur sans licence valide. %d jours restant.</string> + <string name="select_album.not_licensed">Serveur sans licence valide. %d jours restants.</string> <string name="select_album.donate_dialog_message">Téléchargement illimité en supportant Subsonic.</string> <string name="select_album.donate_dialog_now">Maintenant</string> <string name="select_album.donate_dialog_later">Plus tard</string> @@ -147,19 +138,19 @@ <string name="select_genre.songs">%d chansons</string> <string name="select_genre.albums">%d albums</string> - <string name="select_podcasts.error">Une erreur est survenue avec ce podcast pendant le chargement. Le serveur doit d\'abord le télécharger.</string> - <string name="select_podcasts.skipped">Ce podcast n\'a pas été chargé sur le serveur. Le serveur doit d\'abord le télécharger.</string> - <string name="select_podcasts.initializing">Le chargement du podcast a commencer sur le serveur. Recharger SVP dans quelques instants.</string> + <string name="select_podcasts.error">Une erreur est survenue avec ce podcast pendant le chargement. Le serveur doit d\'abord le télécharger.</string> + <string name="select_podcasts.skipped">Ce podcast n\'a pas été chargé sur le serveur. Le serveur doit d\'abord le télécharger.</string> + <string name="select_podcasts.initializing">Le chargement du podcast a commencé sur le serveur. Recharger SVP dans quelques instants.</string> <string name="select_podcasts.server_download">Télécharger sur le serveur</string> <string name="select_podcasts.server_delete">Supprimer du serveur</string> <string name="select_podcasts.downloading">Téléchargement %s sur le serveur</string> - <string name="select_podcasts.refreshing">Le serveur recherche les mises à jour de podcasts</string> + <string name="select_podcasts.refreshing">Le serveur recherche les mises à jour des podcasts</string> <string name="select_podcasts.deleted">Podcast supprimé %s</string> <string name="select_podcasts.deleted_error">Erreur lors de la suppression du podcast %s</string> <string name="select_podcasts.add_url">URL :</string> <string name="select_podcasts.created_error">Erreur lors de l\'ajout du podcast</string> <string name="select_podcasts.invalid_podcast_channel">Podcast invalide : %s</string> - <string name="select_podcasts.delete">Supprimer podcast</string> + <string name="select_podcasts.delete">Supprimer le podcast</string> <string name="download.empty">La playlist est vide</string> <string name="download.shuffle_loading">Chargement en cours liste au hasard...</string> @@ -169,7 +160,7 @@ <string name="download.menu_show_album">Afficher l\'album</string> <string name="download.menu_lyrics">Paroles</string> <string name="download.menu_remove_all">Enlever tout</string> - <string name="download.menu_screen_on">Ecran actif</string> + <string name="download.menu_screen_on">Écran actif</string> <string name="download.menu_shuffle">Hasard</string> <string name="download.menu_toggle">Basculer</string> <string name="download.menu_save">Enregistrer la playlist</string> @@ -183,13 +174,9 @@ <string name="download.repeat_off">Répéter inactif</string> <string name="download.repeat_all">Répéter tout</string> <string name="download.repeat_single">Répéter titre</string> - <string name="download.jukebox_on">Télécommande activée. La musique est diffusée sur l\'ordinateur.</string> - <string name="download.jukebox_off">Télécommande désactivée. La musique est diffusée sur le mobile.</string> - <string name="download.jukebox_volume">Volume distant</string> - <string name="download.jukebox_server_too_old">Télécommande non supportée. Mettre à jour le serveur Subsonic.</string> + <string name="download.jukebox_server_too_old">Télécommande non supportée. Mettre à jour le serveur Subsonic.</string> <string name="download.jukebox_offline">La télécommande n\'est pas disponible en mode déconnecté.</string> <string name="download.jukebox_not_authorized">Mode télécommande non autorisée. Activer le mode jukebox.<b>Users > Settings</b> on your Subsonic server.</string> - <string name="download.timer_length">Minuteur :</string> <string name="download.start_timer">Démarrer le minuteur</string> <string name="download.need_download">La vidéo doit d\'abord être téléchargée</string> <string name="download.no_streaming_player">Aucun lecteur ne peut afficher ce flux</string> @@ -200,8 +187,8 @@ <string name="download.downloading_summary">En cours : %1$s</string> <string name="download.downloading_summary_expanded">En cours : %1$s \nTaille estimée : %2$s</string> - <string name="download.failed_to_load">Echec du chargement</string> - <string name="download.save_bookmark_failed">Echec de la création du favori</string> + <string name="download.failed_to_load">Échec du chargement</string> + <string name="download.save_bookmark_failed">Échec de la création du favori</string> <string name="sync.new_podcasts">Nouveaux podcasts disponibles</string> <string name="sync.new_playlists">Nouveaux titres dans les playlists</string> @@ -210,17 +197,17 @@ <string name="starring_content_starred">Noté \"%s\"</string> <string name="starring_content_unstarred">Dévalués \"%s\"</string> - <string name="starring_content_error">Echec de la mise à jour \"%s\", Réessayer plus tard.</string> + <string name="starring_content_error">Échec de la mise à jour \"%s\", réessayer plus tard.</string> - <string name="playlist_error">Echec de la récupération des playlists</string> + <string name="playlist_error">Échec de la récupération des playlists</string> <string name="updated_playlist">Titre %1$s ajouté à \"%2$s\"</string> - <string name="updated_playlist_error">Echec de la mise à jour \"%s\", réessayer plus tard.</string> + <string name="updated_playlist_error">Échec de la mise à jour \"%s\", réessayer plus tard.</string> <string name="removed_playlist">Titre %1$s retiré de \"%2$s\"</string> <string name="bookmark.delete">Supprimer le favori</string> - <string name="bookmark.delete_title">Dupprimer le favori pour</string> + <string name="bookmark.delete_title">Supprimer le favori pour</string> <string name="bookmark.deleted">Favori pour \"%s\" supprimé</string> - <string name="bookmark.deleted_error">Echec de la suppression du favori pour \"%s\"</string> + <string name="bookmark.deleted_error">Échec de la suppression du favori pour \"%s\"</string> <string name="bookmark.details_title">Détails du favori</string> <string name="bookmark.details">Titre : %1$s \nPosition : %2$s @@ -230,13 +217,13 @@ <string name="bookmark.resume_title">Reprendre la lecture ?</string> <string name="bookmark.resume">Reprendre la lecture de \'%1$s\' depuis %2$s</string> <string name="bookmark.action_resume">Reprendre</string> - <string name="bookmark.action_start_over">Start Over</string> + <string name="bookmark.action_start_over">Relire depuis le début</string> <string name="rating.title">Noter \"%s\"</string> <string name="rating.set_rating">Note attribuée à \"%s\"</string> <string name="rating.set_rating_failed">Echec de l\'attribution de la note à \"%s\"</string> <string name="rating.remove_rating">Note supprimée pour \"%s\"</string> - <string name="rating.remove_rating_failed">Echec de la suppression de la note pour \"%s\"</string> + <string name="rating.remove_rating_failed">Échec de la suppression de la note pour \"%s\"</string> <string name="song_details.error">Erreur</string> <string name="song_details.skipped">Ignoré</string> @@ -277,9 +264,9 @@ <string name="settings.invalid_username">Saisir un nom d\'utilisateur valide (espaces interdits).</string> <string name="settings.appearance_title">Apparence</string> <string name="settings.theme_title">Thème</string> - <string name="settings.theme_light">Light</string> - <string name="settings.theme_dark">Dark</string> - <string name="settings.theme_black">Black</string> + <string name="settings.theme_light">Clair</string> + <string name="settings.theme_dark">Sombre</string> + <string name="settings.theme_black">Noir</string> <string name="settings.theme_holo">Holo</string> <string name="settings.theme_fullscreen">Plein écran</string> <string name="settings.theme_fullscreen_summary">Cacher autant d\'élément graphique que possible</string> @@ -313,8 +300,8 @@ <string name="settings.max_video_bitrate_3000">3000 Kbps</string> <string name="settings.max_video_bitrate_5000">5000 Kbps</string> <string name="settings.max_bitrate_unlimited">Illimité</string> - <string name="settings.wifi_required_title">Streaming en Wifi uniquement</string> - <string name="settings.wifi_required_summary">Ne lire les média qu\'avec une connexion Wifi</string> + <string name="settings.wifi_required_title">Streaming en wifi uniquement</string> + <string name="settings.wifi_required_summary">Ne lire les média qu\'avec une connexion wifi</string> <string name="settings.network_timeout_title">Délai d\'attente réseau (timeout)</string> <string name="settings.network_timeout_10000">10 secondes</string> <string name="settings.network_timeout_15000">15 secondes</string> @@ -344,7 +331,7 @@ <string name="settings.playlist_random_size_title">Taille de la liste de lecture aléatoire</string> <string name="settings.sleep_timer_title">Temporisateur</string> <string name="settings.sleep_timer_duration_title">Durée temporisation</string> - <string name="settings.sleep_timer_off">Eteindre</string> + <string name="settings.sleep_timer_off">Éteindre</string> <string name="settings.sleep_timer_on">Allumer</string> <string name="settings.sleep_timer_always_on">Toujours en fonctionnement</string> <string name="settings.temp_loss_title">Perte temporaire de focus</string> @@ -411,36 +398,25 @@ <string name="settings.override_system_language">Ne pas utiliser la langue du système</string> <string name="settings.override_system_language_summary">Afficher DSub en Anglais même si une traduction existe pour la langue système. Peut nécessiter un vidage du cache de l\'application.</string> <string name="settings.drawer_items_title">Entrées de menu</string> - <string name="settings.play_now_after">Jouer maintenant - Plus tard</string> - <string name="settings.play_now_after_summary">Play Now context menu for a song plays everything after selected item (like the Subsonic web GUI)</string> <string name="settings.large_album_art">Pochettes larges</string> <string name="settings.large_album_art_summary">Afficher les pochettes en grand plutôt qu\'en liste.</string> <string name="settings.admin_enabled">Administration</string> <string name="settings.admin_enabled_summary">Afficher ou non l\'accès aux outils d\'administration</string> - <string name="shuffle.title">Shuffle By</string> + <string name="shuffle.title">Mélanger par</string> <string name="shuffle.startYear">Année début :</string> <string name="shuffle.endYear">Année fin :</string> <string name="shuffle.genre">Genre :</string> <string name="shuffle.pick_genre">Choisir un genre</string> - <string name="share.info">Propriétaire : %1$s - \nDescription: %2$s - \nURL: %3$s - \nCréation : %4$s - \nDernière visite : %5$s - \nExpiration : %6$s - \nNombre de visites : %7$s - - </string> <string name="share.expires">Expiration : %s</string> <string name="share.expires_never">N\'expire jamais</string> <string name="share.deleted">Supprimer le partage %s</string> - <string name="share.deleted_error">Echec de la suppression du partage %s</string> + <string name="share.deleted_error">Échec de la suppression du partage %s</string> <string name="share.no_expiration">Pas d\'expiration</string> <string name="share.expiration">Expiration :</string> <string name="share.updated_info">Informations de partage mises à jour pour %s</string> - <string name="share.updated_info_error">Echec de la mise à jour des informations de partage pour %s</string> + <string name="share.updated_info_error">Échec de la mise à jour des informations de partage pour %s</string> <string name="share.via">Partager via</string> <string name="share.delete">Supprimer le partage</string> @@ -452,20 +428,20 @@ <string name="admin.change_username_invalid">Saisir un nom d\'utilisateur valide</string> <string name="admin.update_permissions">Mettre à jour les autorisations</string> <string name="admin.update_permissions_success">Autorisation mises à jour pour %1$s</string> - <string name="admin.update_permissions_error">Echec lors de lamise à jour des autorisations de %1$s</string> - <string name="admin.change_email">Modifier Email</string> + <string name="admin.update_permissions_error">Échec lors de la mise à jour des autorisations de %1$s</string> + <string name="admin.change_email">Modifier l\'email</string> <string name="admin.change_email_success">Email remplacé pour %1$s</string> - <string name="admin.change_email_error">Echec lors du remplacement de l\'Email de %1$s</string> - <string name="admin.change_email_label">Nouvel Email :</string> - <string name="admin.change_email_invalid">Saisir un Email valide</string> + <string name="admin.change_email_error">Échec lors du remplacement de l\'email de %1$s</string> + <string name="admin.change_email_label">Nouvel email :</string> + <string name="admin.change_email_invalid">Saisir un email valide</string> <string name="admin.change_password">Modifier le mot de passe</string> <string name="admin.change_password_success">Mot de passe modifié pour %1$s</string> - <string name="admin.change_password_error">Echec du remplacement du mot de passe pour %1$s</string> + <string name="admin.change_password_error">Échec du remplacement du mot de passe pour %1$s</string> <string name="admin.change_password_label">Nouveau mot de passe :</string> <string name="admin.change_password_invalid">Saisir un mot de passe valide</string> <string name="admin.delete_user">Supprimer l\'utilisateur</string> <string name="admin.delete_user_success">Suppression effectuée %1$s</string> - <string name="admin.delete_user_error">Echec de la suppression %1$s</string> + <string name="admin.delete_user_error">Échec de la suppression %1$s</string> <string name="admin.confirm_password">Confirmer le mot de passe</string> <string name="admin.confirm_password_bad">Mot de passe saisi erroné</string> @@ -477,37 +453,34 @@ <string name="admin.role.coverArt">Modifier les pochettes</string> <string name="admin.role.comment">Ajouter des commentaires</string> <string name="admin.role.podcast">Gérer les podcasts</string> - <string name="admin.role.stream">Ecouter de la musique</string> + <string name="admin.role.stream">Écouter de la musique</string> <string name="admin.role.jukebox">Télécommander la lecture (jukebox)</string> <string name="admin.role.share">Gérer les partages</string> <string name="admin.role.lastfm">Utiliser Last.FM</string> <string name="music_service.retry">Erreur réseau. Nouvelle tentative %1$d de %2$d.</string> - <string name="background_task.wait">Patienter...</string> - <string name="background_task.loading">Chargement.</string> - <string name="background_task.no_network">Cette application nécessite un accès réseau. Activer les connexion Wifi ou mobile.</string> + <string name="background_task.no_network">Cette application nécessite un accès réseau. Activer les connexion Wifi ou mobile.</string> <string name="background_task.network_error">Une erreur réseau est survenue. Merci de vérifier l\'adresse du serveur ou réessayer plus tard.</string> <string name="background_task.not_found">Ressource non trouvée. Vérifier l\'adresse du serveur.</string> <string name="background_task.parse_error">Erreur de communication avec le serveur.Vérifier l\'adresse du serveur et que la connexion via un navigateur fonctionne.</string> <string name="service.connecting">Interrogation du serveur, veuillez patienter.</string> - <string name="parser.upgrade_client"> Versions incompatible. Mettre à jour DSub.</string> - <string name="parser.upgrade_server">Versions incompatibles. Mettre à jour le serveur Subsonic.</string> + <string name="parser.upgrade_client">Version incompatible. Mettre à jour DSub.</string> + <string name="parser.upgrade_server">Version incompatible. Mettre à jour le serveur Subsonic.</string> <string name="parser.not_authenticated">Mauvais nom d\'utilisateur ou mot de passe.</string> <string name="parser.not_authorized">Non autorisé. Vérifier les droit de l\'utilisateur sur le serveur Subsonic.</string> <string name="parser.artist_count">%d artistes récupérés.</string> <string name="parser.server_error">Erreur serveur : %s</string> <string name="parser.scan_count">%d entrées trouvées</string> - <string name="select_artist.refresh">Recharger</string> - <string name="select_artist.folder">Sélectionner un dossier</string> - <string name="select_artist.all_folders">Tous les dossier</string> + <string name="select_artist.folder">Sélectionner un dossier</string> + <string name="select_artist.all_folders">Tous les dossiers</string> - <string name="equalizer.label">Equaliseur</string> + <string name="equalizer.label">Équaliseur</string> <string name="equalizer.enabled">Activé</string> - <string name="equalizer.preset">Selectioner un préréglage</string> + <string name="equalizer.preset">Sélectionner un préréglage</string> <string name="equalizer.bass_booster">Bass Booster</string> <string name="equalizer.voice_booster">Voice Booster</string> <string name="equalizer.db_size">%d dB</string> @@ -526,14 +499,12 @@ <string name="util.bytes_format.kilobyte">0 KB</string> <string name="util.bytes_format.byte">0 B</string> - <string name="changelog_full_title">Change Log</string> - <string name="changelog_title">What\'s New</string> + <string name="changelog_full_title">Liste des changements</string> + <string name="changelog_title">Nouveautés</string> <string name="changelog_ok_button">OK</string> <string name="changelog_show_full">Plus…</string> - <string name="chat.send_a_message">Envoyer un message</string> - - <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string> + <string name="chat.send_a_message">Envoyer un message</string> <string name="tasker.start_playing">Commencer la lecture</string> <string name="tasker.start_playing_title">Tasker -> Démarrer DSub</string> diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 60b127be..8ac830a7 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -32,8 +32,7 @@ <string name="button_bar.home">Főoldal</string> <string name="button_bar.browse">Médiatár</string> - <string name="button_bar.search">Keresés</string> - <string name="button_bar.playlists">Lejátszási listák</string> + <string name="button_bar.playlists">Lejátszási listák</string> <string name="button_bar.now_playing">Várólista</string> <string name="button_bar.podcasts">Podcastok</string> <string name="button_bar.bookmarks">Könyvjelzők</string> @@ -54,12 +53,11 @@ <br/>Míg a normál módon gyorsítótárazott dalok törlődhetnek amikor újak kerülnek letöltésre, addig a \"Letöltés tárolásra (megőrzés)\" menüpont segítségével letöltött dalok soha nem törlődnek automatikusan. <p/><font color="red">Ha a ChromeCast sikertelen</font>: <br/>Próbálja meg bejelölni: Beállítások -> Lejátszás -> Eszköz használata proxyként. Ez egy kerülő megoldás arra, ha a ChromeCast elutasítja a saját aláírású tanúsítványt. - <p/><font color="red">A Médiatár első szintje tulajdonképpen az előadók csoportja</font>: - <br/>A Beállítások menüben törölje az "Előadók első szintje" jelölést. Ez teszi lehetővé, hogy a mappák teljes első szintjének megjelenítése előadói csoportonként és ne előadónként legyen kezelve. + <p/><font color="red">A Médiatár első szintje ugyanolyan, mint az előadók listája</font>: + <br/>Ha a Beállítások menüben kikapcsolja az "Részletes megjelenítés" opciót, akkor a mappák teljes első szintje ugyanúgy lesz megjelenítve, mint az előadók listája. ]]> </string> - <string name="main.select_server">Kiszolgáló kiválasztása</string> - <string name="main.shuffle">Lejátszás kevert sorrendben</string> + <string name="main.shuffle">Lejátszás kevert sorrendben</string> <string name="main.offline">Offline mód</string> <string name="main.online">Online mód</string> <string name="main.settings">Beállítások</string> @@ -76,6 +74,10 @@ <string name="main.albums_alphabetical">Betűrendben</string> <string name="main.videos">Videók</string> <string name="main.songs_genres">@string/main.albums_genres</string> + <string name="main.songs_newest">@string/main.albums_newest</string> + <string name="main.songs_top_played">Top Played</string> + <string name="main.songs_recent">@string/main.albums_recent</string> + <string name="main.songs_frequent">@string/main.albums_frequent</string> <string name="main.back_confirm">Nyomja meg még egyszer a kilépéshez!</string> <string name="main.scan_complete">A médiatár frissítése befejeződött a kiszolgálón!</string> <string name="main.artist">Előadó</string> @@ -113,7 +115,7 @@ <string name="menu.similar_artists">Hasonló előadók</string> <string name="menu.show_missing">Hiányzó megjelenítése</string> <string name="menu.start_radio">Rádió indítása</string> - <string name="menu.first_level_artist">Előadók első szintje</string> + <string name="menu.first_level_artist">Részletes megjelenítés</string> <string name="playlist.label">Lejátszási listák</string> <string name="playlist.update_info">Szerkesztés</string> @@ -131,20 +133,13 @@ <string name="search.artists">Előadók</string> <string name="search.albums">Albumok</string> <string name="search.songs">Dalok</string> - <string name="search.more">Továbbiak</string> - <string name="progress.wait">Kérem várjon...</string> + <string name="progress.wait">Kérem várjon...</string> <string name="progress.artist_info">Az előadó életrajzának betöltése...</string> - <string name="music_library.label">Médiatár</string> - <string name="music_library.label_offline">Kapcsolat nélküli médiák</string> - - <string name="select_album.select">Összes jelölése be/ki</string> - <string name="select_album.n_selected">%d kijelölve.</string> - <string name="select_album.more">Továbbiak</string> - <string name="select_album.offline">Offline</string> - <string name="select_album.searching">Keresés...</string> - <string name="select_album.no_sdcard">Hiba: SD kártya nem áll rendelkezésre!</string> + <string name="select_album.n_selected">%d kijelölve.</string> + <string name="select_album.offline">Offline</string> + <string name="select_album.no_sdcard">Hiba: SD kártya nem áll rendelkezésre!</string> <string name="select_album.no_network">Figyelem: Hálózat nem áll rendelkezésre!</string> <string name="select_album.no_room">Figyelem: Már csak %s hely áll rendelkezésre!</string> <string name="select_album.not_licensed">A kiszolgálónak nincs licence! %d próbanap van hátra!</string> @@ -187,6 +182,7 @@ <string name="download.playerstate_mobile_disabled">Letöltés, várakozás a Wi-Fi hálózatra...</string> <string name="download.playerstate_buffering">Pufferelés</string> <string name="download.playerstate_playing_shuffle">Dalsorrend keverése</string> + <string name="download.playerstate_playing_artist_radio">Előadó rádió</string> <string name="download.menu_show_album">Ugrás az albumhoz</string> <string name="download.menu_lyrics">Dalszöveg</string> <string name="download.menu_remove_all">Összes eltávolítása</string> @@ -204,13 +200,9 @@ <string name="download.repeat_off">Ismétlés ki</string> <string name="download.repeat_all">Összes ismétlése</string> <string name="download.repeat_single">Dal ismétlése</string> - <string name="download.jukebox_on">Távvezérlés bekapcsolása. A zenelejátszás a számítógépen történik.</string> - <string name="download.jukebox_off">Távvezérlés kikapcsolása. A zenelejátszás az eszközön történik.</string> - <string name="download.jukebox_volume">Hangerő távvezérlése</string> - <string name="download.jukebox_server_too_old">A távvezérlés nem támogatott. Kérjük, frissítse a Subsonic kiszolgálót!</string> + <string name="download.jukebox_server_too_old">A távvezérlés nem támogatott. Kérjük, frissítse a Subsonic kiszolgálót!</string> <string name="download.jukebox_offline">A távvezérlés nem lehetséges offline módban!</string> <string name="download.jukebox_not_authorized">A távvezérlés nem lehetséges! Engedélyezze a Jukebox módot a <b>Users > Settings</b> menüben a Subsonic kiszolgálón!</string> - <string name="download.timer_length">Időhossz:</string> <string name="download.start_timer">Időzítő indítása</string> <string name="download.stop_timer">Időzítő megállítása</string> <string name="download.need_download">A videót először le kell tölteni!</string> @@ -227,6 +219,7 @@ <string name="download.restore_play_queue">Folytatás onnan, ahol egy másik eszközön abbahagyta.</string> <string name="download.thumbs_up">Jó</string> <string name="download.thumbs_down">Nem jó</string> + <string name="download.batch_mode">Kötegelt mód</string> <string name="sync.new_podcasts">Új podcastok: \"%s\"</string> <string name="sync.new_playlists">Új lejátszási listák: \"%s\"</string> @@ -291,6 +284,8 @@ <string name="settings.cache_location">Gyorsítótár helye</string> <string name="settings.cache_location_error">Hibás gyorsítótár hely! Az alapértelmezett használata.</string> <string name="settings.cache_location_reset">A beállított gyorsítótár-hely már nem írható! Ha a közelmúltban frissítette telefonja Android rendszerét 4.4.x KitKat verzióra, abban az SD kártya kezelése megváltozott, és az alkalmazások csak egy speciális helyre tudnak írni. A Dsub már automatikusan átállt a megfelelő helyre. Ahhoz, hogy a régi adatokat törölni tudja, csatlakoztassa az SD kártyát a számítógépéhez, és törölje a régi mappát!</string> + <string name="settings.cache_location_internal">Belső</string> + <string name="settings.cache_location_external">Külső</string> <string name="settings.cache_clear">Gyorsítótár törlése</string> <string name="settings.cache_clear_complete">Gyorsítótár törlése kész.</string> <string name="settings.testing_connection">Kapcsolat tesztelése...</string> @@ -447,8 +442,6 @@ <string name="settings.override_system_language">A rendszer nyelvének felülbírálása</string> <string name="settings.override_system_language_summary">A Dsub megjelenítése angol nyelven abban az esetben is, ha rendelkezik fordítással. Az alkalmazást törölni kell a memóriából, mert a beállítás csak újraindítás után lép érvénybe!</string> <string name="settings.drawer_items_title">Oldalsáv elemei</string> - <string name="settings.play_now_after">Lejátszás utána</string> - <string name="settings.play_now_after_summary">Egy helyi menü, amivel lehetővé válik minden dal lejátszása a kijelölt elem után (mint a Subsonic webes felületén)</string> <string name="settings.large_album_art">Nagyméretű albumborítók</string> <string name="settings.large_album_art_summary">Albumok megjelenítése rácsnézetben és nagyméretű albumborítóval a listanézet helyett.</string> <string name="settings.admin_enabled">Admin engedélyezése</string> @@ -473,6 +466,12 @@ <string name="settings.shuffle_by_album">Keverés albumok szerint</string> <string name="settings.shuffle_by_album.true">Dalsorrend keverése albumonként szétválasztva.</string> <string name="settings.shuffle_by_album.false">Dalsorrend keverése az összes dalt együtt kezelve.</string> + <string name="settings.casting_stream_original">Eredeti stream</string> + <string name="settings.casting_stream_original_summary">Az eredeti fájl streamelése, ha a Cast-kompatibilis eszköz támogatja a fájltípust.</string> + <string name="settings.heads_up_notification">Felugró értesítések (5.0+)</string> + <string name="settings.heads_up_notification_summary">Lejátszási értesítések megjelenítése felugró értesítésekként (Android Lollipop+).</string> + <string name="settings.casting_cache">Casting közbeni gyorsítótárazás</string> + <string name="settings.casting_cache_summary">Az éppen lejátszott dal gyorsítótárazása a tartalomátküldés (Casting) alatt.</string> <string name="shuffle.title">Dalsorrend keverése</string> <string name="shuffle.startYear">Kezdő év:</string> @@ -480,15 +479,6 @@ <string name="shuffle.genre">Műfaj:</string> <string name="shuffle.pick_genre">Műfaj kiválasztása</string> - <string name="share.info">Tulajdonos: %1$s - \nLeírás: %2$s - \nURL: %3$s - \nLétrehozva: %4$s - \nUtolsó látogatás: %5$s - \nLejárati idő: %6$s - \nLátogatások száma: %7$s - - </string> <string name="share.expires">Lejárati idő: %s</string> <string name="share.expires_never">Nincs lejárati idő</string> <string name="share.deleted">\"%s\" megosztás törölve</string> @@ -517,6 +507,7 @@ <string name="admin.change_password">Jelszó csere</string> <string name="admin.change_password_success">\"%1$s\" jelszavának módosítása kész.</string> <string name="admin.change_password_error">\"%1$s\" jelszavának módosítása sikertelen!</string> + <string name="admin.change_password_current_label">Jelenlegi jelszó:</string> <string name="admin.change_password_label">Új jelszó:</string> <string name="admin.change_password_invalid">Adjon meg egy érvényes jelszót!</string> <string name="admin.delete_user">Felhasználó törlése</string> @@ -524,6 +515,8 @@ <string name="admin.delete_user_error">\"%1$s\" felhasználó törlése sikertelen!</string> <string name="admin.confirm_password">Jelszó megerősítése</string> <string name="admin.confirm_password_bad">A beírt jelszó nem egyezik!</string> + <string name="admin.permissions">Engedélyek</string> + <string name="admin.musicFolders">Zenekönyvtárak</string> <string name="admin.scrobblingEnabled">Scrobbling használata</string> <string name="admin.role.admin">Adminisztrátor</string> @@ -536,13 +529,12 @@ <string name="admin.role.stream">Zene streamelése</string> <string name="admin.role.jukebox">Jukebox vezérlése</string> <string name="admin.role.share">Megosztások kezelése</string> + <string name="admin.role.video_conversion">Videók konvertálása</string> <string name="admin.role.lastfm">Last.fm funkció használata</string> <string name="music_service.retry">Hálózati hiba történt! Újrapróbálkozás %1$d/%2$d.</string> - <string name="background_task.wait">Kérem várjon...</string> - <string name="background_task.loading">Betöltés...</string> - <string name="background_task.no_network">Az alkalmazás hálózati hozzáférést igényel. Kérjük, kapcsolja be a Wi-Fi-t vagy a mobilhálózatot!</string> + <string name="background_task.no_network">Az alkalmazás hálózati hozzáférést igényel. Kérjük, kapcsolja be a Wi-Fi-t vagy a mobilhálózatot!</string> <string name="background_task.network_error">Hálózati hiba történt! Kérjük, ellenőrizze a kiszolgáló címét, vagy próbálja később!</string> <string name="background_task.not_found">Az erőforrás nem található! Kérjük, ellenőrizze a kiszolgáló címét!</string> <string name="background_task.parse_error">Hiba történt a kiszolgálóval történő kommunikációban. Kérjük, ellenőrizze a kiszolgáló címét, és próbáljon meg web böngészővel kapcsolódni a kiszolgálóhoz!</string> @@ -557,8 +549,7 @@ <string name="parser.server_error">Kiszolgáló hiba: %s</string> <string name="parser.scan_count">%d tétel átvizsgálva.</string> - <string name="select_artist.refresh">Frissítés</string> - <string name="select_artist.folder">Mappa kiválasztása</string> + <string name="select_artist.folder">Mappa kiválasztása</string> <string name="select_artist.all_folders">Összes mappa</string> <string name="equalizer.label">Equalizer</string> @@ -587,9 +578,7 @@ <string name="changelog_ok_button">OK</string> <string name="changelog_show_full">Továbbiak…</string> - <string name="chat.send_a_message">Üzenet küldése</string> - - <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string> + <string name="chat.send_a_message">Üzenet küldése</string> <string name="tasker.start_playing">Lejátszás indítása</string> <string name="tasker.start_playing_shuffled">Lejátszás indítása kevert sorrendben</string> @@ -642,6 +631,9 @@ <string name="details.position">Pozíció</string> <string name="details.updated">Frissítve</string> <string name="details.starred">Csillagozott</string> + <string name="details.last_played">Utoljára lejátszott</string> + <string name="details.expiration">Lejárati idő</string> + <string name="details.played_count">Lejátszások száma</string> <plurals name="select_album_n_songs"> <item quantity="zero">Nincsenek dalok</item> diff --git a/app/src/main/res/values-large/dimens.xml b/app/src/main/res/values-large/dimens.xml index dfe7ff84..faf88b3e 100644 --- a/app/src/main/res/values-large/dimens.xml +++ b/app/src/main/res/values-large/dimens.xml @@ -5,4 +5,7 @@ <dimen name="Button.Small">54dip</dimen> <dimen name="AlbumArt.Small">96dip</dimen> <dimen name="AlbumArt.Header">210dip</dimen> + <dimen name="FastScroller.LeftAlignedMargin">10dp</dimen> + <dimen name="FastScroller.NormalBarMargin">8dp</dimen> + <dimen name="FastScroller.RightMargin">8dp</dimen> </resources>
\ No newline at end of file diff --git a/app/src/main/res/values-large/integers.xml b/app/src/main/res/values-large/integers.xml index 914ec84a..243d3fc1 100644 --- a/app/src/main/res/values-large/integers.xml +++ b/app/src/main/res/values-large/integers.xml @@ -2,4 +2,5 @@ <resources> <integer name="Grid.Columns">3</integer> <integer name="TextDescriptionLength">10</integer> + <integer name="Card.Elevation">4</integer> </resources>
\ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 00000000..93ff1f1d --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,636 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="common.appname">DSub</string> + <string name="common.ok">OK</string> + <string name="common.save">Opslaan</string> + <string name="common.cancel">Annuleer</string> + <string name="common.play_now">Afspelen</string> + <string name="common.play_shuffled">Willekeurig afspelen</string> + <string name="common.play_next">Volgend afspelen</string> + <string name="common.play_last">Laatst afspelen</string> + <string name="common.download">Cache</string> + <string name="common.pin">Permanente Cache</string> + <string name="common.delete">Verwijder</string> + <string name="common.star">Ster</string> + <string name="common.unstar">Verwijder ster</string> + <string name="common.info">Details</string> + <string name="common.name">Naam</string> + <string name="common.comment">Opmerking</string> + <string name="common.public">Publiek</string> + <string name="common.play_external">Speel video</string> + <string name="common.stream_external">Stream video</string> + <string name="common.confirm">Bevestig</string> + <string name="common.confirm_message">Wil je %1$s %2$s?</string> + <string name="common.confirm_message_cache">cache</string> + <string name="common.empty">Geen data</string> + <string name="common.warning">Waarschuwing</string> + <string name="common.close">Sluiten</string> + <string name="common.false">Nee</string> + <string name="common.true">Ja</string> + <string name="common.never">Nooit</string> + + <string name="button_bar.home">Home</string> + <string name="button_bar.browse">Bibliotheek</string> + <string name="button_bar.playlists">Playlists</string> + <string name="button_bar.now_playing">Speelt nu</string> + <string name="button_bar.podcasts">Podcasts</string> + <string name="button_bar.bookmarks">Bladwijzers</string> + <string name="button_bar.shares">Shares</string> + <string name="button_bar.chat">Chatten</string> + <string name="button_bar.admin">Admin</string> + <string name="button_bar.downloading">Downloaden</string> + <string name="button_bar.offline">Offline</string> + + <string name="main.welcome_title">Welkom!</string> + <string name="main.welcome_text">Welkom bij DSub! De app is momenteel alleen geconfigureerd om de demo server te gebruiken. Nadat je eigen server geïnstalleerd hebt (beschikbaar bij <b>subsonic.org</b>), ga dan naar <b>Instellingen</b> en verander de configuratie zodat je ermee kunt verbinden.</string> + <string name="main.about_title">Over DSub</string> + <string name="main.faq_title">FAQ</string> + <string name="main.faq_text"> + <![CDATA[ + <font color="red">Cache versus Permanente Cache</font>: + <br/>Wanneer nummers zijn gedownload door DSub (cache), kunnen ze verwijderd worden om ruimte te maken voor nieuwe downloads. Permanente cache, daarentegen, wordt nooit automatisch verwijderd. + <p/><font color="red">ChromeCast faalt</font>: + <br/>Probeer de optie Instellingen -> Afspelen -> Gebruik apparaat proxy. Hiermee wordt het probleem van niet geaccepteerde \'self signed\' certificaten omzeild. + <p/><font color="red">Het eerste niveau in de bibliotheek zijn eigenlijk groepen artiesten.</font>: + <br/>In het optiemenu deselecteer "Eerste niveau artiesten". Dit zorgt ervoor dat het gehele eerste niveau directories wordt behandeld als een groep artisten ipv individuele artiesten. + ]]> + </string> + <string name="main.shuffle">Willekeurig afspelen</string> + <string name="main.offline">Ga offline</string> + <string name="main.online">Ga online</string> + <string name="main.settings">Instellingen</string> + <string name="main.albums_title">Album lijsten</string> + <string name="main.albums_per_folder">Per folder</string> + <string name="main.albums_newest">Recent toegevoegd</string> + <string name="main.albums_recent">Recent afgespeeld</string> + <string name="main.albums_frequent">Meest afgespeeld</string> + <string name="main.albums_highest">Hoogst beoordeeld</string> + <string name="main.albums_starred">Met ster</string> + <string name="main.albums_random">Willekeurig</string> + <string name="main.albums_genres">Genres</string> + <string name="main.albums_year">Decennia</string> + <string name="main.albums_alphabetical">Alfabetisch</string> + <string name="main.videos">Video\'s</string> + <string name="main.songs_genres">@string/main.albums_genres</string> + <string name="main.back_confirm">Druk nogmaals terug om te stoppen</string> + <string name="main.scan_complete">Server scan afgerond</string> + <string name="main.artist">Artiest</string> + <string name="main.title">Titel</string> + + <string name="menu.search">Zoek</string> + <string name="menu.shuffle">Willekeurig</string> + <string name="menu.refresh">Ververs</string> + <string name="menu.play">Speel af</string> + <string name="menu.play_last">Speel laatst</string> + <string name="menu.exit">Beëindigen</string> + <string name="menu.settings">Instellingen</string> + <string name="menu.help">Help</string> + <string name="menu.about">Over</string> + <string name="menu.add_playlist">Voeg toe aan playlist</string> + <string name="menu.remove_playlist">Verwijder van playlist</string> + <string name="menu.deleted_playlist">Verwijderde playlist %s</string> + <string name="menu.deleted_playlist_error">Playlist %s kon niet verwijderd worden</string> + <string name="menu.log">Stuur log</string> + <string name="menu.set_timer">Stel timer in</string> + <string name="menu.check_podcasts">Controleer op nieuwe afleveringen</string> + <string name="menu.add_podcast">Voeg kanaal toe</string> + <string name="menu.keep_synced">Gesynchroniseerd houden</string> + <string name="menu.stop_sync">Stop synchroniseren</string> + <string name="menu.show_all">Toon alle media</string> + <string name="menu.show_artist">Toon artist</string> + <string name="menu.share">Deel</string> + <string name="menu.delete_cache">Verwijder cache</string> + <string name="menu.cast">Cast naar apparaat</string> + <string name="menu.faq">FAQ</string> + <string name="menu.add_user">Gebruiker toevoegen</string> + <string name="menu.rescan">Server scannen</string> + <string name="menu.rate">Bepaal rating</string> + <string name="menu.top_tracks">Laatste .FM Top Nummers</string> + <string name="menu.similar_artists">Gelijke artiesten</string> + <string name="menu.start_radio">Start radio</string> + <string name="menu.first_level_artist">Eerste niveau artiesten</string> + + <string name="playlist.label">Speellijsten</string> + <string name="playlist.update_info">Update informatie</string> + <string name="playlist.updated_info">Update speellijst informatie voor %s</string> + <string name="playlist.updated_info_error">Niet gelust speellijst information for %s te updaten</string> + <string name="playlist.overwrite">Overschrijf bestaande playlist</string> + <string name="playlist.add_to">Voeg toe aan playlist</string> + <string name="playlist.create_new">Creëer nieuw</string> + <string name="playlist.delete">Verwijder playlist</string> + + <string name="search.label">Zoek</string> + <string name="search.title">Zoek</string> + <string name="search.search">Klik om te zoeken</string> + <string name="search.no_match">Geen overeenkomst, zoek nogmaals</string> + <string name="search.artists">Artiesten</string> + <string name="search.albums">Albums</string> + <string name="search.songs">Nummers</string> + + <string name="progress.wait">Moment aub...</string> + <string name="progress.artist_info">Laadt artiesten bio</string> + + <string name="select_album.n_selected">%d geselecteerd.</string> + <string name="select_album.offline">Offline</string> + <string name="select_album.no_sdcard">Fout: Geen SD kaart beschikbaar.</string> + <string name="select_album.no_network">Waarschuwing: Netwerk onbeschikbaar.</string> + <string name="select_album.no_room">Waarschuwing: je hebt nog maar %s over</string> + <string name="select_album.not_licensed">Server niet gelicenseerd. %d proefdagen over.</string> + <string name="select_album.donate_dialog_message">Verkrijg ongelimeteerde downloads door te doneren aan Subsonic.</string> + <string name="select_album.donate_dialog_now">Nu</string> + <string name="select_album.donate_dialog_later">Later</string> + <string name="select_album.donate_dialog_0_trial_days_left">Proeftijd is voorbij</string> + + <string name="offline.sync_dialog_title">Offline nummers om gesynchroniseerd te worden</string> + <string name="offline.sync_dialog_message">Verwerk %1$d offline scrobbles? + \nVerwerk %2$d offline sterren? + </string> + <string name="offline.sync_dialog_default">Gebruik actie als standaard</string> + <string name="offline.sync_success">Succesvol %1$d nummers gesynchroniseerd</string> + <string name="offline.sync_partial">Succesvol %1$d van %2$d nummers gesynchroniseerd</string> + <string name="offline.sync_error">Mislukt om nummers te synchroniseren</string> + + <string name="select_genre.blank">Leeg</string> + <string name="select_genre.songs">%d nummers</string> + <string name="select_genre.albums">%d albums</string> + + <string name="select_podcasts.error">Deze podcast had een foutmelding tijdens het downloaden naar de server. De server moet het eerst downloaden.</string> + <string name="select_podcasts.skipped">Deze podcast is niet op de server gedownload. De server moet het eerst downloaden.</string> + <string name="select_podcasts.initializing">Dit podcast kanaal wordt ge\ïnitieerd door de server. Over een enkel moment herladen.</string> + <string name="select_podcasts.server_download">Download op de server</string> + <string name="select_podcasts.server_delete">Verwijder van server</string> + <string name="select_podcasts.downloading">Wordt nu gedownload %s op de server</string> + <string name="select_podcasts.refreshing">De server controleert nu op nieuwe podcasts</string> + <string name="select_podcasts.deleted">Verwijderde podcast %s</string> + <string name="select_podcasts.deleted_error">Kon podcast %s niet verwijderen</string> + <string name="select_podcasts.add_url">URL:</string> + <string name="select_podcasts.created_error">Niet gelukt podcast toe te voegen</string> + <string name="select_podcasts.invalid_podcast_channel">Ongeldig podcast kanaal: %s</string> + <string name="select_podcasts.delete">Verwijder podcast</string> + <string name="select_podcasts.channels">Podcast kanalen</string> + + <string name="download.empty">Playlist is leeg</string> + <string name="download.shuffle_loading">Shuffle lijst wordt geladen...</string> + <string name="download.playerstate_downloading">Downloaden - %s</string> + <string name="download.playerstate_mobile_disabled">Wachten op WiFi network om te kunnen downloaden</string> + <string name="download.playerstate_buffering">Bufferen</string> + <string name="download.playerstate_playing_shuffle">Afspelen shuffle</string> + <string name="download.menu_show_album">Toon album</string> + <string name="download.menu_lyrics">Songteksten</string> + <string name="download.menu_remove_all">Verwijder alles</string> + <string name="download.menu_screen_on">Scherm aan</string> + <string name="download.menu_shuffle">Shuffle</string> + <string name="download.menu_toggle">Aan\/Uit</string> + <string name="download.menu_save">Bewaar speellijst</string> + <string name="download.menu_shuffle_notification">Speellijst was geshufffeld</string> + <string name="download.menu_remove_played_songs">Verwijder afgespeelde nummers</string> + <string name="download.playlist_title">Bewaar playlist</string> + <string name="download.playlist_name">Voer de naam in van de playlist:</string> + <string name="download.playlist_saving">Playlist \"%s\ wordt opgeslagen"...</string> + <string name="download.playlist_done">Playlist was succesvol opgeslagen.</string> + <string name="download.playlist_error">Niet gelukt playlist op te slaan, probeer het later nog eens.</string> + <string name="download.repeat_off">Herhalen uit</string> + <string name="download.repeat_all">Herhaal alles</string> + <string name="download.repeat_single">Herhaal nummer</string> + <string name="download.jukebox_server_too_old">Afstandbediending wordt niet ondersteund. Upgrade de Subsonic server.</string> + <string name="download.jukebox_offline">Afstandbediending is in offline mode niet beschikbaar.</string> + <string name="download.jukebox_not_authorized">Afstandbediening is niet toegestaan. Zet de jukebox mode aan in <b>Users > Instellingen</b> op je Subsonic server.</string> + <string name="download.start_timer">Start Timer</string> + <string name="download.stop_time_remaining">Stop in %1$s</string> + <string name="download.need_download">Video moet eerst gedownload worden</string> + <string name="download.no_streaming_player">Deze stream kan niet afgespeeld worden</string> + <string name="download.playing_out_of">Speelt: %1$d/%2$d</string> + <string name="download.save_bookmark_title">Cre\ëer bladwijzer</string> + <string name="download.save_bookmark">Bladwijzer aangemaakt</string> + <string name="download.save_bookmark_failed">Niet gelukt om bladwijzer te maken</string> + <string name="download.downloading_title">Downloaden %1$d nummers</string> + <string name="download.downloading_summary">Huidig: %1$s</string> + <string name="download.downloading_summary_expanded">Huidig: %1$s + \nGeschatte grootte: %2$s</string> + <string name="download.failed_to_load">Niet gelukt te laden</string> + <string name="download.restore_play_queue">Ga verder waar je op een ander apparaat gestopt bent bij</string> + <string name="download.thumbs_up">Duim omhoog</string> + <string name="download.thumbs_down">Duim omlaag</string> + + <string name="sync.new_podcasts">Nieuwe podcasts beschikbaar</string> + <string name="sync.new_playlists">Nieuwe nummers in playlists</string> + <string name="sync.new_albums">Nieuwe albums beschikbaar</string> + <string name="sync.new_starred">Nieuwe nummers met sterren beschikbaar</string> + + <string name="starring_content_starred">Met sterren \"%s\"</string> + <string name="starring_content_unstarred">Zoner sterren \"%s\"</string> + <string name="starring_content_error">Kon \"%s\" niet updaten, probeer het later nog eens.</string> + + <string name="playlist.mine">Mijn playlists</string> + <string name="playlist.shared">Gedeelde playlists</string> + <string name="playlist_error">Niet gelukt de lijst playlists op te halen</string> + <string name="updated_playlist">Toegevoegd %1$s nummer aan \"%2$s\"</string> + <string name="updated_playlist_error">Niet gelukt te updaten \"%s\", probeer later nog eens.</string> + <string name="removed_playlist">Verwijderd %1$s nummers van \"%2$s\"</string> + + <string name="bookmark.delete">Verwijder bladwijzer</string> + <string name="bookmark.delete_title">Verwijder de bladwijzer voor</string> + <string name="bookmark.deleted">Bladwijzer verwijderd voor \"%s\"</string> + <string name="bookmark.deleted_error">Niet gelukt om bladwijzer te verwijderen voor \"%s\"</string> + <string name="bookmark.details_title">Bladwijzer details</string> + <string name="bookmark.resume_title">Hervat afspelen?</string> + <string name="bookmark.resume">Hervat afspelen \'%1$s\' van %2$s</string> + <string name="bookmark.action_resume">Hervat</string> + <string name="bookmark.action_start_over">Start opnieuw</string> + + <string name="rating.title">Beoordeling \"%s\"</string> + <string name="rating.set_rating">Beoordeling ingesteld voor \"%s\"</string> + <string name="rating.set_rating_failed">Niet gelukt beoordeling in te stellen voor \"%s\"</string> + <string name="rating.remove_rating">Beoordeling verwijderd voor \"%s\"</string> + <string name="rating.remove_rating_failed">Niet gelukt beoordeling te verwijderen voor \"%s\"</string> + + <string name="song_details.error">Fout</string> + <string name="song_details.skipped">Overgeslagen</string> + <string name="song_details.downloading">Downloaden</string> + + <string name="lyrics.nomatch">Geen songteksten gevonden</string> + + <string name="error.label">Fout</string> + + <string name="settings.title">Instellingen</string> + <string name="settings.test_connection_title">Test verbinding</string> + <string name="settings.servers_add">Voeg server toe</string> + <string name="settings.servers_remove">Verwijder server</string> + <string name="settings.servers_title">Servers</string> + <string name="settings.server_unused">Ongebruikt</string> + <string name="settings.server_name">Naam</string> + <string name="settings.server_address">Server addres</string> + <string name="settings.server_local_network_ssid" >Lokaal netwerk SSID</string> + <string name="settings.server_local_network_ssid_hint">Huidig SSID: %s</string> + <string name="settings.server_internal_address">Lokaal netwerk adres</string> + <string name="settings.server_username">Gebruikersnaam</string> + <string name="settings.server_password">Wachtwoord</string> + <string name="settings.server_open_browser">Open in browser</string> + <string name="settings.server_sync_summary">Of synchronisatie ingesteld is voor deze server</string> + <string name="settings.server_sync">Synchronisatie ingesteld</string> + <string name="settings.cache_title">Muziek cache</string> + <string name="settings.preload_wifi">Nummers vooraf laden (Wifi)</string> + <string name="settings.preload_mobile">Nummers vooraf laden (Mobiel)</string> + <string name="settings.cache_size">Cache grootte</string> + <string name="settings.cache_location">Cache locatie</string> + <string name="settings.cache_location_error">Onjuiste cache locatie. Standaard wordt gebruikt.</string> + <string name="settings.cache_location_reset">De locatie van de cache die u hebt ingesteld is niet meer beschrijfbaar. Als onlangs uw Android telefoon geüpgraded is naar KitKat 4.4 en hoger, dan is de manier waarop apps naar de SD-kaart schrijven veranderd zodat er alleen naar een specifieke locatie geschreven kan worden. De locatie die DSub gebruikt is al automatisch veranderd naar de juiste locatie. Om de oude app-data te verwijderen, moet de SD-kaart gekoppeld worden aan een computer om de oude folder handmatig te verwijderen.</string> + <string name="settings.cache_clear">Cache legen</string> + <string name="settings.cache_clear_complete">Klaar met cache legen</string> + <string name="settings.testing_connection">Verbinding testen...</string> + <string name="settings.testing_ok">De verbinding is OK</string> + <string name="settings.testing_unlicensed">Verbinding is OK. Server niet gelicenseerd.</string> + <string name="settings.connection_failure">Verbinding mislukt.</string> + <string name="settings.invalid_url">Geef een valide URL op.</string> + <string name="settings.invalid_username">Geef een valide gebruikersnaam op (geen volgspaties).</string> + <string name="settings.appearance_title">Uiterlijk</string> + <string name="settings.theme_title">Thema</string> + <string name="settings.theme_light">Licht</string> + <string name="settings.theme_dark">Donker</string> + <string name="settings.theme_black">Zwart</string> + <string name="settings.theme_holo">Holo</string> + <string name="settings.theme_fullscreen">Schermvullend</string> + <string name="settings.theme_fullscreen_summary">Verberg zoveel mogelijk schermelementen als Android toestaat</string> + <string name="settings.track_title">Toon track #</string> + <string name="settings.track_summary">Toon track # voor een nummer indien er een bestaat</string> + <string name="settings.custom_sort">Sorteer op jaartal</string> + <string name="settings.custom_sort_summary">Sorteer albums op jaartal, of alfabetisch</string> + <string name="settings.open_to_tab">Open naar tabblad</string> + <string name="settings.open_to_tab_summary">Open direct naar dit tabblad</string> + <string name="settings.network_title">Netwerk</string> + <string name="settings.max_bitrate_wifi">Max audio bitrate - wifi</string> + <string name="settings.max_bitrate_mobile">Max audio bitrate - mobiel</string> + <string name="settings.max_bitrate_32">32 Kbps</string> + <string name="settings.max_bitrate_64">64 Kbps</string> + <string name="settings.max_bitrate_80">80 Kbps</string> + <string name="settings.max_bitrate_96">96 Kbps</string> + <string name="settings.max_bitrate_112">112 Kbps</string> + <string name="settings.max_bitrate_128">128 Kbps</string> + <string name="settings.max_bitrate_160">160 Kbps</string> + <string name="settings.max_bitrate_192">192 Kbps</string> + <string name="settings.max_bitrate_256">256 Kbps</string> + <string name="settings.max_bitrate_320">320 Kbps</string> + <string name="settings.max_video_bitrate_wifi">Max video bitrate - wifi</string> + <string name="settings.max_video_bitrate_mobile">Max video bitrate - mobiel</string> + <string name="settings.max_video_bitrate_200">200 Kbps</string> + <string name="settings.max_video_bitrate_300">300 Kbps</string> + <string name="settings.max_video_bitrate_400">400 Kbps</string> + <string name="settings.max_video_bitrate_500">500 Kbps</string> + <string name="settings.max_video_bitrate_700">700 Kbps</string> + <string name="settings.max_video_bitrate_1000">1000 Kbps</string> + <string name="settings.max_video_bitrate_1500">1500 Kbps</string> + <string name="settings.max_video_bitrate_2000">2000 Kbps</string> + <string name="settings.max_video_bitrate_3000">3000 Kbps</string> + <string name="settings.max_video_bitrate_5000">5000 Kbps</string> + <string name="settings.max_bitrate_unlimited">Unlimited</string> + <string name="settings.wifi_required_title">Alleen wifi streaming</string> + <string name="settings.wifi_required_summary">Alleen media streamen wanneer verbonden met wifi.</string> + <string name="settings.network_timeout_title">Netwerk timeout</string> + <string name="settings.network_timeout_10000">10 seconds</string> + <string name="settings.network_timeout_15000">15 seconds</string> + <string name="settings.network_timeout_30000">30 seconds</string> + <string name="settings.network_timeout_45000">45 seconds</string> + <string name="settings.network_timeout_60000">60 seconds</string> + <string name="settings.preload_0">0 nummer</string> + <string name="settings.preload_1">1 nummer</string> + <string name="settings.preload_2">2 nummers</string> + <string name="settings.preload_3">3 nummers</string> + <string name="settings.preload_5">5 nummers</string> + <string name="settings.preload_10">10 nummers</string> + <string name="settings.preload_unlimited">Onbeperkt</string> + <string name="settings.clear_search_history">Verwijder zoekgeschiedenis</string> + <string name="settings.search_history_cleared">Zoekgeschiedenis verwijderd</string> + <string name="settings.other_title">Andere instellingen</string> + <string name="settings.scrobble_title">Scrobble naar Last.fm</string> + <string name="settings.scrobble_summary">Vergeet niet om je last.fm account in te stellen op de Subsonic server.</string> + <string name="settings.hide_media_title">Verberg voor andere</string> + <string name="settings.hide_media_summary">Verberg muziek voor andere apps.</string> + <string name="settings.hide_media_toast">Is actief wanneer Android de volgende keer je telefoon scant voor nieuwe muziek.</string> + <string name="settings.media_button_title">Mediaknoppen</string> + <string name="settings.media_button_summary">Reageer op telefoon, koptelefoon en bluetooth mediaknoppen</string> + <string name="settings.screen_lit_title">Houd scherm aan</string> + <string name="settings.screen_lit_summary">Houdt scherm aan tijdens het downloaden verbetert de download snelheid.</string> + <string name="settings.playlist_title">Afspelen</string> + <string name="settings.playlist_random_size_title">Shuffle playlist grootte</string> + <string name="settings.sleep_timer_title">Sleeptimer</string> + <string name="settings.sleep_timer_duration_title">Sleeptimer duur</string> + <string name="settings.sleep_timer_off">Uit</string> + <string name="settings.sleep_timer_on">Aan</string> + <string name="settings.sleep_timer_always_on">Altijd aan</string> + <string name="settings.temp_loss_title">Tijdelijk focus verlies</string> + <string name="settings.temp_loss_pause">Altijd pauzeren</string> + <string name="settings.temp_loss_pause_lower">Pauze, lagere volume desgevraagd</string> + <string name="settings.temp_loss_lower">Altijd lagere volume</string> + <string name="settings.temp_loss_nothing">Doe niets</string> + <string name="settings.keep_played_count_title">Behoud afgespeelde nummers</string> + <string name="settings.keep_played_count_none">Verwijder alle afspeelde nummers</string> + <string name="settings.keep_played_count_one">Behoud laatste afgespeelde nummers</string> + <string name="settings.keep_played_count_two">Behoud laatste 2 afgespeelde nummers</string> + <string name="settings.keep_played_count_three">Behoud laatste 3 afgespeelde nummers</string> + <string name="settings.disconnect_pause_title">Pauzeren bij verbroken verbinding</string> + <string name="settings.disconnect_pause_both">Pauzeren</string> + <string name="settings.disconnect_pause_neither">Doe niets</string> + <string name="settings.persistent_title">Permanente vermelding</string> + <string name="settings.persistent_summary">Toon de vermelding zelfs na pauzeren. Druk op de stopknop om de vermelding te verwijderen.</string> + <string name="settings.gapless_playback">Ononderbroken afspelen</string> + <string name="settings.gapless_playback_summary">Indien je vreemde storingen ondervindt tijdens het afspelen, kan het uitzetten van deze functie misschien helpen.</string> + <string name="settings.chat_refresh">Chat vernieuwingsfrequentie (sec.)</string> + <string name="settings.chat_enabled">Chat aanzetten</string> + <string name="settings.chat_enabled_summary">Al dan niet weergeven van de chatlijst in de lade</string> + <string name="settings.video_title">Video</string> + <string name="settings.video_player">Videospeler</string> + <string name="settings.video_raw">Raw (Vereist Subsonic 4.8+)</string> + <string name="settings.video_hls">HTTP Live Stream (HLS) (Vereist Subsonic 4.8+)</string> + <string name="settings.video_transcode">Direct Transcode (Vereist video -> mp4 or similar setup on Server)</string> + <string name="settings.video_flash">Flash (Vereist plugin)</string> + <string name="settings.cache_screen_title">Cache/Netwerk</string> + <string name="settings.playback_title">Playback</string> + <string name="settings.hide_widget_title">Widget verbergen</string> + <string name="settings.hide_widget_summary">Verberg widget na verlaten app</string> + <string name="settings.podcasts_enabled">Podcasts ingeschakeld</string> + <string name="settings.podcasts_enabled_summary">Al dan niet weergeven van de podcastlijst in de lade</string> + <string name="settings.bookmarks_enabled">Bladwijzers ingeschakelPlayback</string> + <string name="settings.bookmarks_enabled_summary">Al dan niet weergeven van de bladwijzerlijst in de lade</string> + <string name="settings.shares_enabled">Shares ingeschakeld</string> + <string name="settings.shares_enabled_summary">Al dan niet weergeven van de shareslijst in de lade</string> + <string name="settings.sync_title">Sync</string> + <string name="settings.sync_enabled">Sync ingeschakeld</string> + <string name="settings.sync_enabled_summary">Al dan of niet playlists of padcasts periodiek gecontroleerd worden op wijzigingen</string> + <string name="settings.sync_interval">Sync interval</string> + <string name="settings.sync_interval_15">15 minuten</string> + <string name="settings.sync_interval_30">30 minute</string> + <string name="settings.sync_interval_60">1 uur</string> + <string name="settings.sync_interval_120">2 uren</string> + <string name="settings.sync_interval_240">4 uren</string> + <string name="settings.sync_interval_360">6 uren</string> + <string name="settings.sync_interval_720">12 uren</string> + <string name="settings.sync_interval_1440">Dagelijks</string> + <string name="settings.sync_wifi">Sync alleen met wifi</string> + <string name="settings.sync_wifi_summary">Alleen synchroniseren met een wifi-verbinding</string> + <string name="settings.sync_most_recent">Sync recentelijk toegevoegd</string> + <string name="settings.sync_most_recent_summary">Automatisch cache nieuw toegevoegde albums</string> + <string name="settings.sync_starred">Sync met sterren</string> + <string name="settings.sync_starred_summary">Cache automatisch nummers, album en artiesten met sterren</string> + <string name="settings.sync_notification">Toon sync notificatie</string> + <string name="settings.sync_notification_summary">Toon een notificatie nadat er nieuwe media is gesynct</string> + <string name="settings.menu_options.title">Optionele menu opties</string> + <string name="settings.menu_options.play_now_summary">Toon nu in menus</string> + <string name="settings.menu_options.play_shuffled_summary">Toon Shuffled in menu\'s</string> + <string name="settings.menu_options.play_next_summary">Toon Speel Volgend in menu\'s</string> + <string name="settings.menu_options.play_last_summary">Toon Speel Laatst in menu\'s</string> + <string name="settings.menu_options.download_summary">Toon Download in menu\'s</string> + <string name="settings.menu_options.pin_summary">Toon Permanente Cache in menu\'s</string> + <string name="settings.menu_options.delete_summary">Toon Verwijder in menu\'s</string> + <string name="settings.menu_options.star_summary">Toon Ster in menu\'s</string> + <string name="settings.menu_options.shared_summary">Toon Share in menu\'s</string> + <string name="settings.menu_options.rate_summary">Toon Waarderingen in menu\'s</string> + <string name="settings.browse_by_tags">Bladeren door tags</string> + <string name="settings.browse_by_tags_summary">Bladeren door tags in plaats van folder structuur. Vereist Subsonic 4.7+</string> + <string name="settings.disable_exit_prompt">Exit Prompt uitschakelen</string> + <string name="settings.disable_exit_prompt_summary">Verlaat meteen de app bij het op back drukken vanuit het Home scherm</string> + <string name="settings.override_system_language">Systeemtaal overschrijven</string> + <string name="settings.override_system_language_summary">Toon de app in Engels, ook als de systeemtaal voor Dsub beschikbaar is. Het is misschien nodig om de app uit cache geheugen te wissen voordat het effectief is.</string> + <string name="settings.drawer_items_title">Server tabs</string> + <string name="settings.large_album_art">Grote albumhoezen</string> + <string name="settings.large_album_art_summary">Toon albums met grote hoes in plaats van een lijst</string> + <string name="settings.admin_enabled">Beheer ingeschakeld</string> + <string name="settings.admin_enabled_summary">Al dan niet weergeven van de adminlijst in de lade</string> + <string name="settings.replay_gain">Normaliseren</string> + <string name="settings.replay_gain_summary">Aldan of niet de afspeelvolume per nummer of album normaliseren adhv (replay gain) tags</string> + <string name="settings.replay_gain_type">Lees van tags</string> + <string name="settings.replay_gain_type.smart">Slimme detectie</string> + <string name="settings.replay_gain_type.album">Album tags</string> + <string name="settings.replay_gain_type.track">Nummer tags</string> + <string name="settings.replay_gain_bump">Normaliseren Pre-amp</string> + <string name="settings.replay_gain_untagged">Nummers zonder normalisatie</string> + <string name="settings.casting">Casten</string> + <string name="settings.casting_proxy">Gebruik apparaat proxy</string> + <string name="settings.casting_proxy_summary">Stream alles via het apparaat als een proxy. Dit omzeilt problemen zoals zelf toegekende certificaten.</string> + <string name="settings.rename_duplicates">Hernoem dubbele nummers</string> + <string name="settings.rename_duplicates_summary">Hernoem dubbele nummers naar de originele bestandsnaam om ze te kunnen onderscheiden.</string> + <string name="settings.start_on_headphones">Start met koptelefoon</string> + <string name="settings.start_on_headphones_summary">Start als een koptelefoon gekoppeld wordt. Dit vereist het gebruik van een service bij het opstarten van de telefoon om de de koptelefoonplugin te monitoren, zelfs als Dsub niet gebruikt wordt.</string> + <string name="settings.color_action_bar">Kleur actiebalk</string> + <string name="settings.color_action_bar.summary">Kleur de actiebalk en statusbalk, of laat ze ongemoeid.</string> + <string name="settings.shuffle_by_album">Shuffle op basis van albums</string> + <string name="settings.shuffle_by_album.true">Shuffle de volgorde van albums</string> + <string name="settings.shuffle_by_album.false">Shuffle alle nummers</string> + + <string name="shuffle.title">Shuffle op basis van</string> + <string name="shuffle.startYear">Start jaar:</string> + <string name="shuffle.endYear">Einde jaar:</string> + <string name="shuffle.genre">Genre:</string> + <string name="shuffle.pick_genre">Kies een genre</string> + + <string name="share.expires">Vervaldatum: %s</string> + <string name="share.expires_never">Verloopt nooit</string> + <string name="share.deleted">Verwijderde share %s</string> + <string name="share.deleted_error">Mislukt om de share %s te verwijderen</string> + <string name="share.no_expiration">Verloopt nooit</string> + <string name="share.expiration">Verloopt:</string> + <string name="share.updated_info">Share informatie geüpdatet voor %s</string> + <string name="share.updated_info_error">Mislukt om share informatie te updaten voor %s</string> + <string name="share.via">Share via</string> + <string name="share.delete">Verwijder share</string> + + <string name="admin.add_user_username">Gebruikersnaam:</string> + <string name="admin.add_user_email">E-mail:</string> + <string name="admin.add_user_password">Wachtwoord:</string> + <string name="admin.create_user_success">Succesvol niet gebruiker aangemaakt</string> + <string name="admin.create_user_error">Mislukt nieuwe gebruiker aan te maken</string> + <string name="admin.change_username_invalid">Voer valide gebruikersnaam in</string> + <string name="admin.update_permissions">Update machtigingen</string> + <string name="admin.update_permissions_success">Succesvol machtigingen geüpdatet voor %1$s</string> + <string name="admin.update_permissions_error">Mislukt om machtigingen te updaten voor %1$s</string> + <string name="admin.change_email">Wijzig e-mail</string> + <string name="admin.change_email_success">Succesvol e-mail gewijzigd voor %1$s</string> + <string name="admin.change_email_error">Mislukt om e-mail te wijzigen voor %1$s</string> + <string name="admin.change_email_label">Nieuwe e-mail:</string> + <string name="admin.change_email_invalid">Voor een valide e-mail in</string> + <string name="admin.change_password">Wijzig wachtwoord</string> + <string name="admin.change_password_success">Succesvol wachtwoord gewijzigd voor %1$s</string> + <string name="admin.change_password_error">Mislukt om wachtwoord te wijzigen voor %1$s</string> + <string name="admin.change_password_label">Nieuw wachtwoord:</string> + <string name="admin.change_password_invalid">Voerin een valide wachtwoord</string> + <string name="admin.delete_user">Verwijder gebruiker</string> + <string name="admin.delete_user_success">Succesvol %1$s verwijderd</string> + <string name="admin.delete_user_error">Mislukt om %1$s te verwijderen</string> + <string name="admin.confirm_password">Bevestig wachtwoord</string> + <string name="admin.confirm_password_bad">Ingevoerde wachtwoord is onjuist</string> + + <string name="admin.scrobblingEnabled">Scrobbelen toegestaan</string> + <string name="admin.role.admin">Beheerder</string> + <string name="admin.role.settings">Verander instellingen</string> + <string name="admin.role.download">Download originele bestanden</string> + <string name="admin.role.upload">Upload naar server</string> + <string name="admin.role.coverArt">Wijzig hoes</string> + <string name="admin.role.comment">Voeg commentaar toe</string> + <string name="admin.role.podcast">Beheer podcasts</string> + <string name="admin.role.stream">Muziek streamen</string> + <string name="admin.role.jukebox">Beheer jukebox</string> + <string name="admin.role.share">Beheer shares</string> + <string name="admin.role.lastfm">Gebruik Last.FM functie</string> + + <string name="music_service.retry">Er is een netwerkfout opgetreden. Probeer opnieuw %1$d van %2$d.</string> + + <string name="background_task.no_network">Dit programma het netwerktoegang nodig. Zet wifi of mobiel netwerk aan.</string> + <string name="background_task.network_error">Er is een netwerkfout opgetreden. Controleer het serveradres of probeer het later nog eens.</string> + <string name="background_task.not_found">De bron is niet gevonden. Controleer het serveradres.</string> + <string name="background_task.parse_error">Er is een communicatieprobleem ontstaan met de server. Controleer het serveradres en verifieer dat je met een webbrowser vanaf je apparaat een verbinding kan maken met het serveradres.</string> + + <string name="service.connecting">Serververbinding wordt opgezet, moment geduld a.u.b.</string> + + <string name="parser.upgrade_client">Onverenigbare versies. DSub upgraden a.u.b.</string> + <string name="parser.upgrade_server">Onverenigbare versies. Subsonic server upgraden a.u.b.</string> + <string name="parser.not_authenticated">Verkeerde gebruikersnaam of wachtwoord.</string> + <string name="parser.not_authorized">Niet geautoriseerd. Controleer de gebruikerspermissies op Subsonic server.</string> + <string name="parser.artist_count">Heb %d artiesten.</string> + <string name="parser.server_error">Server fout: %s</string> + <string name="parser.scan_count">%d Items gescand</string> + + <string name="select_artist.folder">Selecteer map</string> + <string name="select_artist.all_folders">Alle mappen</string> + + <string name="equalizer.label">Equalizer</string> + <string name="equalizer.enabled">Ingeschakeld</string> + <string name="equalizer.preset">Selecteer voorinstelling</string> + <string name="equalizer.bass_booster">Bass booster</string> + <string name="equalizer.voice_booster">Stem booster</string> + <string name="equalizer.db_size">%d dB</string> + <string name="equalizer.bass_size">%d mille</string> + + <string name="widget.4x1">DSub (4x1)</string> + <string name="widget.4x2">DSub (4x2)</string> + <string name="widget.4x3">DSub (4x3)</string> + <string name="widget.4x4">DSub (4x4)</string> + <string name="widget.initial_text">Aanraken om muziek te selecteren</string> + <string name="widget.sdcard_busy">SD-kaart onbeschikbaar</string> + <string name="widget.sdcard_missing">Geen SD-kaart</string> + + <string name="util.bytes_format.gigabyte">0.00 GB</string> + <string name="util.bytes_format.megabyte">0.00 MB</string> + <string name="util.bytes_format.kilobyte">0 KB</string> + <string name="util.bytes_format.byte">0 B</string> + + <string name="changelog_full_title">Verander Log</string> + <string name="changelog_title">Wat is er nieuw</string> + <string name="changelog_ok_button">OK</string> + <string name="changelog_show_full">Meer…</string> + + <string name="chat.send_a_message">Stuur een bericht</string> + + <string name="changelog_version_format">Versie %s</string> + + <string name="tasker.start_playing">Begin met spelen</string> + <string name="tasker.start_playing_shuffled">Begin met spelen in shuffle-modus</string> + <string name="tasker.start_playing_title">Tasker -> Start DSub</string> + <string name="tasker.edit_shuffle_mode">Start in shuffle-modus: </string> + <string name="tasker.edit_shuffle_start_year">Shuffle start jaar:</string> + <string name="tasker.edit_shuffle_end_year">Shuffle einde jaar:</string> + <string name="tasker.edit_shuffle_genre">Shuffle genre:</string> + <string name="tasker.edit_server_offline">Schakel offline: </string> + <string name="tasker.edit_do_nothing">Doe niets</string> + + <string name="details.title.song">Nummer details</string> + <string name="details.title.album">Album details</string> + <string name="details.title.podcast">Podcast details</string> + <string name="details.title.playlist">Playlist details</string> + <string name="details.title.artist">Artiest details</string> + <string name="details.podcast">Podcast</string> + <string name="details.status">Status</string> + <string name="details.artist">Artiest</string> + <string name="details.album">Album</string> + <string name="details.track">Nummer</string> + <string name="details.genre">Genre</string> + <string name="details.year">Jaar</string> + <string name="details.server_format">Server format</string> + <string name="details.server_bitrate">Server bitrate</string> + <string name="details.cached_format">Cached format</string> + <string name="details.cached_bitrate">Gecacht bitrate</string> + <string name="details.size">Grootte</string> + <string name="details.length">Lengte</string> + <string name="details.bookmark_position">Bookmark positie</string> + <string name="details.rating">Beoordeling</string> + <string name="details.description">Beschrijving</string> + <string name="details.owner">Eigenaar</string> + <string name="details.comments">Opmerkingen</string> + <string name="details.song_count">Nummer telling</string> + <string name="details.public">Publiek</string> + <string name="details.created">Gecreëerd</string> + <string name="details.title">Titel</string> + <string name="details.url">URL</string> + <string name="details.error">Foutmelding</string> + <string name="details.author">Auteur</string> + <string name="details.email">E-mail</string> + <string name="details.version">Versie</string> + <string name="details.files_cached">Bestanden gecached</string> + <string name="details.files_permanent">Permanent gecached</string> + <string name="details.used_space">Gebruikte ruimte</string> + <string name="details.available_space">Beschikbare ruimte</string> + <string name="details.of">%1$s van %2$s</string> + <string name="details.song">Nummer</string> + <string name="details.position">Positie</string> + <string name="details.updated">Geüpdatet</string> + <string name="details.starred">Met ster</string> + <string name="details.last_played">Laatst afgespeeld</string> + + <plurals name="select_album_n_songs"> + <item quantity="zero">Geen nummer</item> + <item quantity="one">Een nummer</item> + <item quantity="other">%d nummers</item> + </plurals> + <plurals name="select_album_n_songs_downloading"> + <item quantity="one">Een nummer geplanned om te downloaden</item> + <item quantity="other">%d nummers geplanned om te downloaden.</item> + </plurals> + <plurals name="select_album_n_songs_added"> + <item quantity="one">Een nummer toegevoegd aan de speellijst</item> + <item quantity="other">%d nummers toegevoegd aan de speellijst.</item> + </plurals> + <plurals name="select_album_donate_dialog_n_trial_days_left"> + <item quantity="one">Eén dag over van proefperiode</item> + <item quantity="other">%d dagen over van proefperiode</item> + </plurals> + +</resources> + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ffbb0485..97ba1880 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -24,7 +24,6 @@ <string name="button_bar.home">Домой</string> <string name="button_bar.browse">Медиатека</string> - <string name="button_bar.search">Поиск</string> <string name="button_bar.playlists">Списки</string> <string name="button_bar.now_playing">Плеер</string> @@ -32,7 +31,6 @@ <string name="main.welcome_text">Добро пожаловать в DSub! Это приложение настроено на работу с демо сервером Subsonic. После настройки Вашего персонального сервера (доступен на <b>subsonic.org</b>), пожалуйста, перейдите в <b>Настройки</b> и измените параметры для подключения.</string> <string name="main.about_title">О программе DSub</string> - <string name="main.select_server">Выбрать сервер</string> <string name="main.shuffle">Случайное воспроизведение</string> <string name="main.offline">Отключиться</string> <string name="main.online">Подключиться</string> @@ -74,18 +72,11 @@ <string name="search.artists">Исполнители</string> <string name="search.albums">Альбомы</string> <string name="search.songs">Композиции</string> - <string name="search.more">Показать еще</string> <string name="progress.wait">Пожалуйста, подождите...</string> - <string name="music_library.label">Медиатека</string> - <string name="music_library.label_offline">Оффлайн медиа</string> - - <string name="select_album.select">Выбрать все</string> <string name="select_album.n_selected">%d композиций выбрано.</string> - <string name="select_album.more">Еще</string> <string name="select_album.offline">Оффлайн</string> - <string name="select_album.searching">Выполняется поиск...</string> <string name="select_album.no_sdcard">Ошибка: SD карта недоступна</string> <string name="select_album.no_network">Внимание: сеть недоступна.</string> <string name="select_album.not_licensed">Сервер не лицензирован. %d дней до окончания пробного периода.</string> @@ -115,14 +106,10 @@ <string name="download.repeat_off">Повторение отключено</string> <string name="download.repeat_all">Повторять все</string> <string name="download.repeat_single">Повторять композицию</string> - <string name="download.jukebox_on">Удаленное управление включено. Музыка воспроизводится на компьютере.</string> - <string name="download.jukebox_off">Удаленное управление отключено. Музыка воспроизводится на устройстве.</string> - <string name="download.jukebox_volume">Удаленное управление громкостью</string> <string name="download.jukebox_server_too_old">Удаленное управление не поддерживается. Пожалуйста, обновите Ваш сервер Subsonic.</string> <string name="download.jukebox_offline">Удаленное управление не поддерживается в оффлайн режиме.</string> <string name="download.jukebox_not_authorized">Удаленное управление запрещено. Пожалуйста, активируйте режим jukebox в разделе <b>Настройки > Проигрыватели</b> на вашем сервере Subsonic.</string> - <string name="download.timer_length">Длительность</string> - <string name="download.start_timer">Запустить таймер</string> + <string name="download.start_timer">Запустить таймер</string> <string name="download.need_download">Необходимо сначала скачать видео</string> <string name="download.no_streaming_player">Нет плеера для воспроизведения потока</string> @@ -233,8 +220,6 @@ <string name="music_service.retry">Ошибка подключения. Попытка %1$d из %2$d.</string> - <string name="background_task.wait">Пожалуйста, подождите...</string> - <string name="background_task.loading">Загрузка</string> <string name="background_task.no_network">Эта программа требует доступ к сети. Пожалуйста, включите Wi-Fi или мобильный интернет</string> <string name="background_task.network_error">Ошибка сети. Пожалуйста, проверьте адрес сервера и попробуйте снова</string> <string name="background_task.not_found">Ресурс не найден. Пожалуйста, проверьте адрес сервера</string> @@ -248,7 +233,6 @@ <string name="parser.not_authorized">Не авторизирован. Проверьте права пользователя на сервере Subsonic.</string> <string name="parser.artist_count">Получено %d исполнителей.</string> - <string name="select_artist.refresh">Обновить</string> <string name="select_artist.folder">Выбрать папку</string> <string name="select_artist.all_folders">Все папки</string> diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 38add733..ac219530 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -32,8 +32,7 @@ <string name="button_bar.home">Hem</string> <string name="button_bar.browse">Bibliotek</string> - <string name="button_bar.search">Sök</string> - <string name="button_bar.playlists">Spellista</string> + <string name="button_bar.playlists">Spellista</string> <string name="button_bar.now_playing">Spelar nu</string> <string name="button_bar.podcasts">Podcasts</string> <string name="button_bar.bookmarks">Bokmärken</string> @@ -58,8 +57,7 @@ <br/>Avmarkera "Första steget artister" i inställningsmenyn. Det kommer göra så att hela första steget kommer att hanteras som grupper istället för som enstaka artister. ]]> </string> - <string name="main.select_server">Välj server</string> - <string name="main.shuffle">Slumpad uppspelning</string> + <string name="main.shuffle">Slumpad uppspelning</string> <string name="main.offline">Gå Offline</string> <string name="main.online">Gå Online</string> <string name="main.settings">Inställningar</string> @@ -111,7 +109,6 @@ <string name="menu.rate">Sätt betyg </string> <string name="menu.top_tracks">Last.FM topp spår</string> <string name="menu.similar_artists">Liknande artister</string> - <string name="menu.show_missing">Visa saknade</string> <string name="menu.start_radio">Starta radio</string> <string name="menu.first_level_artist">Första steget artister</string> @@ -131,20 +128,13 @@ <string name="search.artists">Artister</string> <string name="search.albums">Album</string> <string name="search.songs">Spår</string> - <string name="search.more">Visa mer</string> - <string name="progress.wait">Vänta...</string> + <string name="progress.wait">Vänta...</string> <string name="progress.artist_info">Laddar information om artist</string> - <string name="music_library.label">Media bibliotek</string> - <string name="music_library.label_offline">Offline media</string> - - <string name="select_album.select">Markera allt</string> - <string name="select_album.n_selected">%d vald.</string> - <string name="select_album.more">Mer</string> - <string name="select_album.offline">Offline</string> - <string name="select_album.searching">Söker...</string> - <string name="select_album.no_sdcard">Error: Inget SD-kort tillgängligt.</string> + <string name="select_album.n_selected">%d vald.</string> + <string name="select_album.offline">Offline</string> + <string name="select_album.no_sdcard">Error: Inget SD-kort tillgängligt.</string> <string name="select_album.no_network">Varning: Inget nätverk tillgängligt.</string> <string name="select_album.no_room">Varning: Du har bara %s kvar</string> <string name="select_album.not_licensed">Servern är inte licenserad. %d testdagar kvar.</string> @@ -203,13 +193,9 @@ <string name="download.repeat_off">Upprepa inte</string> <string name="download.repeat_all">Upprepa alla</string> <string name="download.repeat_single">Upprepa spåret</string> - <string name="download.jukebox_on">Startade fjärrkontroll. Musiken spelas upp på datorn.</string> - <string name="download.jukebox_off">Stängde av fjärrkontroll. Musiken spelas upp på telefonen.</string> - <string name="download.jukebox_volume">Fjärrkontroll volym</string> - <string name="download.jukebox_server_too_old">Fjärrkontroll stöds inte. Uppdatera din Subsonic server.</string> + <string name="download.jukebox_server_too_old">Fjärrkontroll stöds inte. Uppdatera din Subsonic server.</string> <string name="download.jukebox_offline">Fjärrkontroll fungerar inte i offline läge.</string> <string name="download.jukebox_not_authorized">Fjärrkontroll acceptaras inte. Acceptera Jukebox läge i <b>Användare > Inställningar</b> på din Subsonic server.</string> - <string name="download.timer_length">Timer:</string> <string name="download.start_timer">Starta timer</string> <string name="download.stop_time_remaining">Stoppa om %1$s</string> <string name="download.need_download">Videon måste laddas ner först</string> @@ -444,8 +430,6 @@ <string name="settings.override_system_language">Ignorera systemspråket</string> <string name="settings.override_system_language_summary">Visa appen på engelska även om systemet är på ett språk Subsonic har stöd för. Du kan eventuellt behöva rensa appen från minnet för att det ska ta effekt.</string> <string name="settings.drawer_items_title">Sidoflikar</string> - <string name="settings.play_now_after">Spela nu - Efter</string> - <string name="settings.play_now_after_summary">Spela nu i context menyn spelar allt efter spåret (som i Subsonic webb GUI)</string> <string name="settings.large_album_art">Stora album bilder</string> <string name="settings.large_album_art_summary">Visa album med stora bilder istället för en lista</string> <string name="settings.admin_enabled">Admin aktiverad</string> @@ -477,15 +461,6 @@ <string name="shuffle.genre">Genre:</string> <string name="shuffle.pick_genre">Välj en genre</string> - <string name="share.info">Ägare: %1$s - \nBeskrivning: %2$s - \nURL: %3$s - \nSkapad: %4$s - \nSenast besökt: %5$s - \nGår ut: %6$s - \nAntal besök: %7$s - - </string> <string name="share.expires">Går ut: %s</string> <string name="share.expires_never">Går aldrig ut</string> <string name="share.deleted">Tog bort delning %s</string> @@ -537,9 +512,7 @@ <string name="music_service.retry">Ett nätverksproblem har uppstått. Försök %1$d av %2$d.</string> - <string name="background_task.wait">Vänta...</string> - <string name="background_task.loading">Laddar.</string> - <string name="background_task.no_network">Appen kräver nätverk. Slå på Wi-Fi eller mkobil data för att kunna använda den.</string> + <string name="background_task.no_network">Appen kräver nätverk. Slå på Wi-Fi eller mkobil data för att kunna använda den.</string> <string name="background_task.network_error">Ett nätverksproblem uppstod. Kontrollera server adressen och försök igen.</string> <string name="background_task.not_found">Resurs kunde inte hittas. Kontrollera server adressen och försök igen.</string> <string name="background_task.parse_error">Problem med kommunikationen med servern. Kontrollera server adressen och att du kan ansluta till servern via webbläsaren.</string> @@ -554,8 +527,7 @@ <string name="parser.server_error">Server fel: %s</string> <string name="parser.scan_count">Scannade %d rader</string> - <string name="select_artist.refresh">Ladda om</string> - <string name="select_artist.folder">Välj mapp</string> + <string name="select_artist.folder">Välj mapp</string> <string name="select_artist.all_folders">Alla mappar</string> <string name="equalizer.label">Equalizer</string> @@ -586,8 +558,6 @@ <string name="chat.send_a_message">Skicka ett meddelande</string> - <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string> - <string name="tasker.start_playing">Börja spela</string> <string name="tasker.start_playing_shuffled">Börja spela i slumpat läge</string> <string name="tasker.start_playing_title">Tasker -> Starta DSub</string> diff --git a/app/src/main/res/values-v21/themes.xml b/app/src/main/res/values-v21/themes.xml index 5c54b75a..6461130a 100644 --- a/app/src/main/res/values-v21/themes.xml +++ b/app/src/main/res/values-v21/themes.xml @@ -7,6 +7,12 @@ <style name="Theme.DSub.Dark" parent="Theme.DSub.Dark.Base"> <item name="android:windowTranslucentStatus">true</item> </style> + <style name="Theme.DSub.Black" parent="Theme.DSub.Black.Base"> + <item name="android:windowTranslucentStatus">true</item> + </style> + <style name="Theme.DSub.Holo" parent="Theme.DSub.Holo.Base"> + <item name="android:windowTranslucentStatus">true</item> + </style> <style name="Theme.DSub.Light.No_Color" parent="Theme.DSub.Light.No_Color.Base"> <item name="android:windowTranslucentStatus">false</item> diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index a7dd932d..01296e9e 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1,18 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string-array name="drawerItemsDescriptions"> - <item>Home</item> - <item>Artist</item> - <item>Playlist</item> - <item>Podcast</item> - <item>Bookmark</item> - <item>Share</item> - <item>Chat</item> - <item>Admin</item> - <item>Download</item> - <item>Settings</item> - </string-array> - <string-array name="defaultDrawerItems"> <item>@string/button_bar.home</item> <item>@string/button_bar.browse</item> @@ -235,4 +222,17 @@ <item>@string/main.online</item> <item>@string/main.offline</item> </string-array> + + <string-array name="songPressActionValues"> + <item>single</item> + <item>all</item> + <item>next</item> + <item>last</item> + </string-array> + <string-array name="songPressActionNames"> + <item>@string/settings.song_press_play_single</item> + <item>@string/settings.song_press_play_all</item> + <item>@string/settings.song_press_play_next</item> + <item>@string/settings.song_press_play_last</item> + </string-array> </resources> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index c4f80478..7cd447f0 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -3,6 +3,8 @@ <attr name="offline_icon" format="reference"/> <attr name="media_button_backward" format="reference"/> <attr name="media_button_forward" format="reference"/> + <attr name="media_button_rewind" format="reference"/> + <attr name="media_button_fastforward" format="reference"/> <attr name="media_button_pause" format="reference"/> <attr name="media_button_repeat_off" format="reference"/> <attr name="media_button_repeat_single" format="reference"/> @@ -14,6 +16,8 @@ <attr name="actionbar_pause" format="reference"/> <attr name="actionbar_start" format="reference"/> <attr name="actionbar_stop" format="reference"/> + <attr name="actionbar_rewind" format="reference"/> + <attr name="actionbar_fastforward" format="reference"/> <attr name="chat_send" format="reference"/> <attr name="add" format="reference"/> <attr name="download_none" format="reference"/> @@ -37,6 +41,8 @@ <attr name="rating_good" format="reference"/> <attr name="radio" format="reference"/> <attr name="star_outline" format="reference"/> + <attr name="download" format="reference"/> + <attr name="playback_speed" format="reference"/> <attr name="drawerItemsIcons" format="reference"/> <attr name="drawerHome" format="reference"/> <attr name="drawerLibrary" format="reference"/> @@ -55,6 +61,8 @@ <attr name="actionbarBackgroundColor" format="reference"/> <attr name="drawerTitleStyle" format="reference"/> <attr name="drawerSubtitleStyle" format="reference"/> + <attr name="cardBackgroundDrawable" format="reference"/> + <attr name="drawerHeaderBackground" format="reference"/> <declare-styleable name="SeekBarPreference"> <attr name="min" format="integer"/> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 23a3f4a3..9c53f472 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -6,4 +6,11 @@ <dimen name="AlbumArt.Small">78dip</dimen> <dimen name="AlbumArt.Header">120dip</dimen> <dimen name="Star.Small">20dp</dimen> + <dimen name="SongStatusIcon">24dp</dimen> + <dimen name="Card.Radius">4dp</dimen> + <dimen name="Card.TextLeftPadding">8dp</dimen> + <dimen name="Card.MarginsForShadow">2dp</dimen> + <dimen name="FastScroller.LeftAlignedMargin">6dp</dimen> + <dimen name="FastScroller.NormalBarMargin">4dp</dimen> + <dimen name="FastScroller.RightMargin">4dp</dimen> </resources>
\ No newline at end of file diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml index 05bcdb28..db45c591 100644 --- a/app/src/main/res/values/integers.xml +++ b/app/src/main/res/values/integers.xml @@ -3,4 +3,5 @@ <integer name="Grid.Columns">2</integer> <integer name="Grid.FullScreen.Columns">@integer/Grid.Columns</integer> <integer name="TextDescriptionLength">5</integer> + <integer name="Card.Elevation">10</integer> </resources>
\ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3acbff3e..d372940f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,8 +32,7 @@ <string name="button_bar.home">Home</string> <string name="button_bar.browse">Library</string> - <string name="button_bar.search">Search</string> - <string name="button_bar.playlists">Playlists</string> + <string name="button_bar.playlists">Playlists</string> <string name="button_bar.now_playing">Now Playing</string> <string name="button_bar.podcasts">Podcasts</string> <string name="button_bar.bookmarks">Bookmarks</string> @@ -58,8 +57,7 @@ <br/>In the option menu, deselect "First level artists". This will make it so that the entire first level of directories shown will be treated like groups of artists instead of the artists themselves. ]]> </string> - <string name="main.select_server">Select server</string> - <string name="main.shuffle">Shuffle play</string> + <string name="main.shuffle">Shuffle play</string> <string name="main.offline">Go Offline</string> <string name="main.online">Go Online</string> <string name="main.settings">Settings</string> @@ -76,6 +74,10 @@ <string name="main.albums_alphabetical">Alphabetically</string> <string name="main.videos">Videos</string> <string name="main.songs_genres">@string/main.albums_genres</string> + <string name="main.songs_newest">@string/main.albums_newest</string> + <string name="main.songs_top_played">Top Played</string> + <string name="main.songs_recent">@string/main.albums_recent</string> + <string name="main.songs_frequent">@string/main.albums_frequent</string> <string name="main.back_confirm">Press back again to exit</string> <string name="main.scan_complete">Completed scan of Server</string> <string name="main.artist">Artist</string> @@ -111,7 +113,7 @@ <string name="menu.rate">Set Rating</string> <string name="menu.top_tracks">Last.FM Top Tracks</string> <string name="menu.similar_artists">Similar Artists</string> - <string name="menu.show_missing">Show missing</string> + <string name="menu.similar_artists.missing">Missing Artists</string> <string name="menu.start_radio">Start Radio</string> <string name="menu.first_level_artist">First level artists</string> @@ -131,20 +133,13 @@ <string name="search.artists">Artists</string> <string name="search.albums">Albums</string> <string name="search.songs">Songs</string> - <string name="search.more">Show more</string> - <string name="progress.wait">Please wait...</string> + <string name="progress.wait">Please wait...</string> <string name="progress.artist_info">Loading Artist Bio</string> - <string name="music_library.label">Media library</string> - <string name="music_library.label_offline">Offline media</string> - - <string name="select_album.select">Select all</string> - <string name="select_album.n_selected">%d selected.</string> - <string name="select_album.more">More</string> - <string name="select_album.offline">Offline</string> - <string name="select_album.searching">Searching...</string> - <string name="select_album.no_sdcard">Error: No SD card available.</string> + <string name="select_album.n_selected">%d selected.</string> + <string name="select_album.offline">Offline</string> + <string name="select_album.no_sdcard">Error: No SD card available.</string> <string name="select_album.no_network">Warning: No network available.</string> <string name="select_album.no_room">Warning: you only have %s left</string> <string name="select_album.not_licensed">Server not licensed. %d trial days left.</string> @@ -186,7 +181,8 @@ <string name="download.playerstate_downloading">Downloading - %s</string> <string name="download.playerstate_mobile_disabled">Waiting for WiFi network to download</string> <string name="download.playerstate_buffering">Buffering</string> - <string name="download.playerstate_playing_shuffle">Playing shuffle</string> + <string name="download.playerstate_playing_shuffle">Shuffle mode</string> + <string name="download.playerstate_playing_artist_radio">Artist radio</string> <string name="download.menu_show_album">Show Album</string> <string name="download.menu_lyrics">Lyrics</string> <string name="download.menu_remove_all">Remove all</string> @@ -204,13 +200,9 @@ <string name="download.repeat_off">Repeat off</string> <string name="download.repeat_all">Repeat all</string> <string name="download.repeat_single">Repeat song</string> - <string name="download.jukebox_on">Turned on remote control. Music is played on the computer.</string> - <string name="download.jukebox_off">Turned off remote control. Music is played on the phone.</string> - <string name="download.jukebox_volume">Remote volume</string> - <string name="download.jukebox_server_too_old">Remote control is not supported. Please upgrade your Subsonic server.</string> + <string name="download.jukebox_server_too_old">Remote control is not supported. Please upgrade your Subsonic server.</string> <string name="download.jukebox_offline">Remote control is not available in offline mode.</string> <string name="download.jukebox_not_authorized">Remote control is not allowed. Please enable jukebox mode in <b>Users > Settings</b> on your Subsonic server.</string> - <string name="download.timer_length">Timer:</string> <string name="download.start_timer">Start Timer</string> <string name="download.stop_time_remaining">Stop in %1$s</string> <string name="download.need_download">Video needs to be downloaded first</string> @@ -227,6 +219,12 @@ <string name="download.restore_play_queue">continue from where you left off on another device at</string> <string name="download.thumbs_up">Thumbs Up</string> <string name="download.thumbs_down">Thumbs Down</string> + <string name="download.batch_mode">Batch Mode</string> + <string name="download.playback_speed_half">0.5x</string> + <string name="download.playback_speed_normal">1x</string> + <string name="download.playback_speed_one_half">1.5x</string> + <string name="download.playback_speed_double">2x</string> + <string name="download.playback_speed_tripple">3x</string> <string name="sync.new_podcasts">New podcasts available</string> <string name="sync.new_playlists">New songs in playlists</string> @@ -291,6 +289,8 @@ <string name="settings.cache_location">Cache location</string> <string name="settings.cache_location_error">Invalid cache location. Using default.</string> <string name="settings.cache_location_reset">The cache location you have set is no longer writable. If you recently upgraded your phone OS to KitKat 4.4, then the way apps write to the SD Card has changed so that they can only write to a specific location. The location that DSub uses has already been automatically changed to the correct location. In order to delete all of the old app data, you will need to mount the SD Card on your computer and delete the old folder manually</string> + <string name="settings.cache_location_internal">Internal</string> + <string name="settings.cache_location_external">External</string> <string name="settings.cache_clear">Clear Cache</string> <string name="settings.cache_clear_complete">Finished clearing cache</string> <string name="settings.testing_connection">Testing connection...</string> @@ -447,8 +447,11 @@ <string name="settings.override_system_language">Override System Language</string> <string name="settings.override_system_language_summary">Display app in english even if the system language is something DSub has a translation for. May need to clear the app from memory for changes to take affect.</string> <string name="settings.drawer_items_title">Drawer Tabs</string> - <string name="settings.play_now_after">Play Now - After</string> - <string name="settings.play_now_after_summary">Play Now context menu for a song plays everything after selected item (like the Subsonic web GUI)</string> + <string name="settings.song_press_action">Song Press Action</string> + <string name="settings.song_press_play_single">Play only that song</string> + <string name="settings.song_press_play_all">Adds everything in the album to the Now Playing Queue</string> + <string name="settings.song_press_play_next">Adds song as next song</string> + <string name="settings.song_press_play_last">Adds song to end of Now Playing Queue</string> <string name="settings.large_album_art">Large Album Art</string> <string name="settings.large_album_art_summary">Display albums with large album art instead of in a list</string> <string name="settings.admin_enabled">Admin Enabled</string> @@ -473,6 +476,12 @@ <string name="settings.shuffle_by_album">Shuffle By Album</string> <string name="settings.shuffle_by_album.true">Shuffle order of albums</string> <string name="settings.shuffle_by_album.false">Shuffle all songs together</string> + <string name="settings.casting_stream_original">Stream original</string> + <string name="settings.casting_stream_original_summary">Stream original files where supported by cast device</string> + <string name="settings.heads_up_notification">Heads Up Notifications (5.0+)</string> + <string name="settings.heads_up_notification_summary">Show playing notifications as Heads Up notifications (Android Lollipop+ only)</string> + <string name="settings.casting_cache">Cache While Casting</string> + <string name="settings.casting_cache_summary">Cache currently playing songs while casting</string> <string name="shuffle.title">Shuffle By</string> <string name="shuffle.startYear">Start Year:</string> @@ -480,15 +489,6 @@ <string name="shuffle.genre">Genre:</string> <string name="shuffle.pick_genre">Pick a genre</string> - <string name="share.info">Owner: %1$s - \nDescription: %2$s - \nURL: %3$s - \nCreation: %4$s - \nLast Visited: %5$s - \nExpiration: %6$s - \nVisit Count: %7$s - - </string> <string name="share.expires">Expires: %s</string> <string name="share.expires_never">Never Expires</string> <string name="share.deleted">Deleted share %s</string> @@ -517,6 +517,7 @@ <string name="admin.change_password">Change Password</string> <string name="admin.change_password_success">Successfully changed password for %1$s</string> <string name="admin.change_password_error">Failed to change password for %1$s</string> + <string name="admin.change_password_current_label">Current Password:</string> <string name="admin.change_password_label">New Password:</string> <string name="admin.change_password_invalid">Enter a valid password</string> <string name="admin.delete_user">Delete User</string> @@ -524,6 +525,8 @@ <string name="admin.delete_user_error">Failed to delete %1$s</string> <string name="admin.confirm_password">Confirm Password</string> <string name="admin.confirm_password_bad">Entered password is wrong</string> + <string name="admin.permissions">Permissions</string> + <string name="admin.musicFolders">Music Folders</string> <string name="admin.scrobblingEnabled">Scrobbling allowed</string> <string name="admin.role.admin">Administrator</string> @@ -536,13 +539,12 @@ <string name="admin.role.stream">Stream music</string> <string name="admin.role.jukebox">Control jukebox</string> <string name="admin.role.share">Manage shares</string> + <string name="admin.role.video_conversion">Convert videos</string> <string name="admin.role.lastfm">Use Last.FM feature</string> <string name="music_service.retry">A network error occurred. Retrying %1$d of %2$d.</string> - <string name="background_task.wait">Please wait...</string> - <string name="background_task.loading">Loading.</string> - <string name="background_task.no_network">This program requires network access. Please turn on Wi-Fi or mobile network.</string> + <string name="background_task.no_network">This program requires network access. Please turn on Wi-Fi or mobile network.</string> <string name="background_task.network_error">A network error occurred. Please check the server address or try again later.</string> <string name="background_task.not_found">Resource not found. Please check the server address.</string> <string name="background_task.parse_error">A problem occurred communicating with the server. Please check the server address and verify that you can connect using a regular browser on your device.</string> @@ -557,8 +559,7 @@ <string name="parser.server_error">Server error: %s</string> <string name="parser.scan_count">Scanned %d entries</string> - <string name="select_artist.refresh">Refresh</string> - <string name="select_artist.folder">Select folder</string> + <string name="select_artist.folder">Select folder</string> <string name="select_artist.all_folders">All folders</string> <string name="equalizer.label">Equalizer</string> @@ -589,7 +590,7 @@ <string name="chat.send_a_message">Send a message</string> - <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string> + <string name="changelog_version_format">Version %s</string> <string name="tasker.start_playing">Start playing</string> <string name="tasker.start_playing_shuffled">Start playing in Shuffle Mode</string> @@ -643,6 +644,8 @@ <string name="details.updated">Updated</string> <string name="details.starred">Starred</string> <string name="details.last_played">Last Played</string> + <string name="details.expiration">Expiration</string> + <string name="details.played_count">Played Count</string> <plurals name="select_album_n_songs"> <item quantity="zero">No songs</item> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 0b45b69e..04d92fa5 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -37,8 +37,8 @@ <item name="android:background">?android:dividerHorizontal</item> </style> - <style name="MoreButton" parent="BasicButton"> - <item name="android:paddingRight">14dip</item> + <style name="MoreButton" parent="@style/BasicButton"> + <item name="android:paddingRight">2dp</item> </style> <style name="PlaybackControl" parent="@style/BasicButton"> diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 6321852f..9e95fe9d 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -4,6 +4,8 @@ <item name="offline_icon">@drawable/main_offline_light</item> <item name="media_button_backward">@drawable/media_backward_light</item> <item name="media_button_forward">@drawable/media_forward_light</item> + <item name="media_button_rewind">@drawable/media_rewind_light</item> + <item name="media_button_fastforward">@drawable/media_fastforward_light</item> <item name="media_button_pause">@drawable/media_pause_light</item> <item name="media_button_repeat_off">@drawable/media_repeat_off_light</item> <item name="media_button_repeat_single">@drawable/media_repeat_single_light</item> @@ -15,6 +17,8 @@ <item name="actionbar_pause">@drawable/media_pause_dark</item> <item name="actionbar_start">@drawable/media_start_dark</item> <item name="actionbar_stop">@drawable/media_stop_dark</item> + <item name="actionbar_rewind">@drawable/media_rewind_dark</item> + <item name="actionbar_fastforward">@drawable/media_fastforward_dark</item> <item name="chat_send">@drawable/ic_menu_chat_send_light</item> <item name="add">@drawable/ic_action_add_dark</item> <item name="download_none">@drawable/download_none_light</item> @@ -38,6 +42,8 @@ <item name="rating_good">@drawable/ic_action_rating_good_light</item> <item name="radio">@drawable/ic_menu_radio_dark</item> <item name="star_outline">@drawable/ic_toggle_star_outline_light</item> + <item name="download">@drawable/ic_menu_download_dark</item> + <item name="playback_speed">@drawable/ic_action_playback_speed_light</item> <item name="drawerHome">@drawable/main_offline_light</item> <item name="drawerLibrary">@drawable/ic_menu_library_light</item> <item name="drawerPlaylists">@drawable/ic_menu_playlist_light</item> @@ -62,11 +68,15 @@ <item name="drawerSubtitleStyle">@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse</item> <item name="windowActionModeOverlay">true</item> <item name="actionModeBackground">?attr/colorPrimary</item> + <item name="cardBackgroundDrawable">@drawable/card_rounded_corners_light</item> + <item name="drawerHeaderBackground">@drawable/drawer_header_light</item> </style> <style name="Theme.DSub.Dark.Base" parent="@style/Theme.AppCompat"> <item name="offline_icon">@drawable/main_offline_dark</item> <item name="media_button_backward">@drawable/media_backward_dark</item> <item name="media_button_forward">@drawable/media_forward_dark</item> + <item name="media_button_rewind">@drawable/media_rewind_dark</item> + <item name="media_button_fastforward">@drawable/media_fastforward_dark</item> <item name="media_button_pause">@drawable/media_pause_dark</item> <item name="media_button_repeat_off">@drawable/media_repeat_off_dark</item> <item name="media_button_repeat_single">@drawable/media_repeat_single_dark</item> @@ -78,6 +88,8 @@ <item name="actionbar_pause">@drawable/media_pause_dark</item> <item name="actionbar_start">@drawable/media_start_dark</item> <item name="actionbar_stop">@drawable/media_stop_dark</item> + <item name="actionbar_rewind">@drawable/media_rewind_dark</item> + <item name="actionbar_fastforward">@drawable/media_fastforward_dark</item> <item name="chat_send">@drawable/ic_menu_chat_send_dark</item> <item name="add">@drawable/ic_action_add_dark</item> <item name="download_none">@drawable/download_none_dark</item> @@ -101,6 +113,8 @@ <item name="rating_good">@drawable/ic_action_rating_good_dark</item> <item name="radio">@drawable/ic_menu_radio_dark</item> <item name="star_outline">@drawable/ic_toggle_star_outline_dark</item> + <item name="download">@drawable/ic_menu_download_dark</item> + <item name="playback_speed">@drawable/ic_action_playback_speed_dark</item> <item name="drawerHome">@drawable/main_offline_dark</item> <item name="drawerLibrary">@drawable/ic_menu_library_dark</item> <item name="drawerPlaylists">@drawable/ic_menu_playlist_dark</item> @@ -124,20 +138,29 @@ <item name="drawerSubtitleStyle">@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle</item> <item name="windowActionModeOverlay">true</item> <item name="actionModeBackground">?attr/colorPrimary</item> + <item name="cardBackgroundDrawable">@drawable/card_rounded_corners_dark</item> + <item name="drawerHeaderBackground">@drawable/drawer_header_dark</item> </style> <style name="Theme.DSub.Light" parent="Theme.DSub.Light.Base"> </style> <style name="Theme.DSub.Dark" parent="Theme.DSub.Dark.Base"> </style> - <style name="Theme.DSub.Black" parent="Theme.DSub.Dark"> + <style name="Theme.DSub.Black.Base" parent="Theme.DSub.Dark.Base"> + <item name="cardBackgroundDrawable">@drawable/card_rounded_corners_black</item> <item name="android:windowBackground">@android:color/black</item> </style> - <style name="Theme.DSub.Holo" parent="Theme.DSub.Dark"> + <style name="Theme.DSub.Black" parent="Theme.DSub.Black.Base"> + </style> + <style name="Theme.DSub.Holo.Base" parent="Theme.DSub.Dark.Base"> <item name="android:windowBackground">@drawable/background</item> <item name="colorPrimary">@color/holoPrimary</item> <item name="colorPrimaryDark">@color/holoPrimaryDark</item> <item name="colorAccent">@color/holoAccent</item> <item name="actionbarBackgroundColor">@color/holoPrimary</item> + <item name="drawerHeaderBackground">@drawable/drawer_header_holo</item> + <item name="cardBackgroundDrawable">@drawable/card_rounded_corners_black</item> + </style> + <style name="Theme.DSub.Holo" parent="Theme.DSub.Holo.Base"> </style> <style name="Theme.DSub.Light.No_Actionbar" parent="Theme.DSub.Light"> @@ -170,6 +193,8 @@ <item name="actionbar_pause">@drawable/media_pause_light</item> <item name="actionbar_start">@drawable/media_start_light</item> <item name="actionbar_stop">@drawable/media_stop_light</item> + <item name="actionbar_rewind">@drawable/media_rewind_light</item> + <item name="actionbar_fastforward">@drawable/media_fastforward_light</item> <item name="add">@drawable/ic_action_add_light</item> <item name="shuffle">@drawable/ic_menu_shuffle_light</item> <item name="refresh">@drawable/ic_menu_refresh_light</item> @@ -179,6 +204,7 @@ <item name="add_person">@drawable/ic_menu_add_person_light</item> <item name="password">@drawable/ic_menu_password_light</item> <item name="radio">@drawable/ic_menu_radio_light</item> + <item name="download">@drawable/ic_menu_download_light</item> <item name="actionModeBackground">@color/background_material_light</item> <item name="actionModeStyle">@style/LightActionMode</item> <item name="actionModeCloseButtonStyle">@style/DarkCloseButton</item> diff --git a/app/src/main/res/xml/changelog.xml b/app/src/main/res/xml/changelog.xml index 39fc7496..490c427a 100644 --- a/app/src/main/res/xml/changelog.xml +++ b/app/src/main/res/xml/changelog.xml @@ -1,5 +1,110 @@ <?xml version="1.0" encoding="utf-8"?> <changelog> + <release version="5.2.2" versioncode="184" releasedate="8/30/2016"> + <change>Fix lagging in landscape view on the Now Playing screen</change> + </release> + <release version="5.2.1" versioncode="183" releasedate="8/24/2016"> + <change>Add option for different actions when pressing a song</change> + <change>Fix some N release issues</change> + <change>Fix grid alignment for some albums</change> + </release> + <release version="5.2" versioncode="182" releasedate="7/27/2016"> + <change>Variable Playback Speed (Android 6.0+)</change> + <change>Show album instead of artist on Top Tracks</change> + <change>Fix search/comments with ' not working</change> + <change>Fix double press to skip from some headsets</change> + <change>Fix newer versions of Madsonic's Artist Radio</change> + <change>Fix no SSID using local network address on all WiFi connections</change> + <change>Fix rotation/sleep while EQ was visible</change> + <change>Fix being able to add albums to playlists</change> + <change>Fix cast dialog in landscape</change> + </release> + <release version="5.1.9" versioncode="181" releasedate="6/13/2016"> + <change>Fix First level artists option</change> + <change>Fix some artist menu items missing</change> + </release> + <release version="5.1.8" versioncode="179" releasedate="6/9/2016"> + <change>Improved Search Bar</change> + <change>Display songs in root folder</change> + <change>Heads Up Setting: Only when out of app</change> + <change>Speed up resuming app from notifications/widgets</change> + <change>Bluetooth: Double click play/pause button to skip to next some</change> + <change>Auto: Previous/Next rewind/fast forward on Podcasts/Audio Books</change> + <change>Fix playlist not overwriting when resumed from bookmark</change> + <change>Fix some bluetooth devices sending next/previous multiple times</change> + <change>Fix reverting to stock Subsonic after using Madsonic 6+ servers</change> + <change>Fix Podcast list cache not working with no internet</change> + </release> + <release version="5.1.7" versioncode="177" releasedate="4/22/2016"> + <change>Audio Books/Podcasts: Replace back/forward with rewind/fast forward buttons</change> + <change>Expandable Search Results</change> + <change>Add option to cache while casting</change> + <change>Add optional Heads Up Notifications (Lollipop+)</change> + <change>Clicking bookmarked song plays entire album from bookmarked position</change> + <change>Auto: Remove podcast/album limit now that Google removed limit</change> + <change>Fix tall album art running into controls</change> + <change>Fix Show Artist not being an option when browsing By Tags</change> + <change>Fix 1-star song background on darker themes</change> + <change>Fix Bluetooth multiple skips</change> + </release> + <release version="5.1.6" versioncode="175" releasedate="3/16/2016"> + <change>Podcasts/Audio Books: Show listened indicator</change> + <change>Podcasts/Audio Books: Improve what is considered fully played</change> + <change>Improved Artist sorting to closer match Web GUI</change> + <change>Show Artist/Album from Search and Album Lists</change> + <change>Videos: ability to star</change> + <change>Admin: Show/update Video Conversion role (Subsonic 6.0+)</change> + <change>Stop background sync if lose Wifi</change> + <change>Fix upsampling mp3s when cache rate is set to unlimited</change> + <change>Fix "Failed to create artist radio" error</change> + <change>Fix rare issue causing arrow to get stuck</change> + <change>Fix rare issue with background download getting stuck</change> + </release> + <release version="5.1.5" versioncode="173" releasedate="2/15/2016"> + <change>Toggle for Batch Mode on Now Playing</change> + <change>Admin tab: view/update music folders (Subsonic 5.2+)</change> + <change>Tag Browsing: can specify Music Folder (Subsonic 6.0+)</change> + <change>Show shuffle/radio mode in title bar</change> + <change>Madsonic only: Songs lists on Home tab</change> + <change>Clicking songs adds songs before it as well</change> + <change>Similar Artists: show missing artists below main list</change> + <change>Improve offline search (thanks fxthomas)</change> + <change>Improve podcast date formatting</change> + <change>Increase max sleep timer to 1 hour</change> + <change>Cast: added setting to disable DLNA gapless playback</change> + <change>Cast: added setting to stream original source files</change> + <change>Long press details dialogs to copy information</change> + <change>Android Auto improvements</change> + <change>Top Tracks: display #</change> + <change>Require current password when changing password</change> + <change>Improved Share Details dialog</change> + <change>Sort Music Folders alphabetically</change> + <change>Fix repeating same song if multiples copies in play queue</change> + <change>Fix freeze on Play x from Google Now</change> + <change>Fix Show Artist/Album back behavior</change> + <change>Minor Chromecast fixes</change> + </release> + <release version="5.1.4" versioncode="172" releasedate="1/17/2016"> + <change>Minor theme improvements</change> + </release> + <release version="5.1.3" versioncode="169" releasedate="1/11/2016"> + <change>Display albums in cards</change> + <change>Improve ActionBar dropdown</change> + <change>Add Portuguese and Dutch translations</change> + <change>Add batch Star/Unstar</change> + <change>Add quick Internal/External buttons when changing Cache Location</change> + <change>Auto add bookmarks on pause for Podcasts/Audio Books</change> + <change>Android Auto: Add Podcasts, Album Lists, and Bookmarks tabs</change> + <change>Add more spacing between Track # and Title</change> + <change>Go directly to synced Playlist/Podcast on Sync notification</change> + <change>Themed drawer header images</change> + <change>Update Cover Art in background refresh</change> + <change>Fix Repeat All in Jukebox mode</change> + <change>Fix rotating re-opening Now Playing from widget</change> + <change>Fix some DLNA issues</change> + <change>Fix Black/Holo themes not having drawer behind notification panel</change> + <change>Fix songs in Album Lists for Madsonic</change> + </release> <release version="5.1.2" versioncode="167" releasedate="12/17/2015"> <change>Keep track of played songs locally</change> <change>Improved DLNA/Chromecast casting</change> diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index ac247c8f..a067130a 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -31,4 +31,9 @@ android:title="@string/settings.playback_title" android:key="playback"> </PreferenceScreen> + + <PreferenceScreen + android:title="@string/settings.casting" + android:key="cast"> + </PreferenceScreen> </PreferenceScreen> diff --git a/app/src/main/res/xml/settings_appearance.xml b/app/src/main/res/xml/settings_appearance.xml index 5e355526..530d92a1 100644 --- a/app/src/main/res/xml/settings_appearance.xml +++ b/app/src/main/res/xml/settings_appearance.xml @@ -53,14 +53,6 @@ android:key="renameDuplicates" android:defaultValue="true"/> - <ListPreference - android:title="@string/settings.open_to_tab" - android:summary="@string/settings.open_to_tab_summary" - android:key="openToTab" - android:entryValues="@array/defaultDrawerItemsDescriptions" - android:entries="@array/defaultDrawerItems" - android:defaultValue="Home"/> - <CheckBoxPreference android:title="@string/settings.disable_exit_prompt" android:summary="@string/settings.disable_exit_prompt_summary" diff --git a/app/src/main/res/xml/settings_cache.xml b/app/src/main/res/xml/settings_cache.xml index bb5710b2..248572ca 100644 --- a/app/src/main/res/xml/settings_cache.xml +++ b/app/src/main/res/xml/settings_cache.xml @@ -57,7 +57,7 @@ android:defaultValue="2000" android:digits="0123456789"/> - <EditTextPreference + <github.daneren2005.dsub.view.CacheLocationPreference android:title="@string/settings.cache_location" android:key="cacheLocation"/> diff --git a/app/src/main/res/xml/settings_cast.xml b/app/src/main/res/xml/settings_cast.xml new file mode 100644 index 00000000..c23ae04d --- /dev/null +++ b/app/src/main/res/xml/settings_cast.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" + android:title="@string/settings.casting"> + <PreferenceCategory + android:title="@string/settings.casting"> + + <CheckBoxPreference + android:title="@string/settings.casting_proxy" + android:summary="@string/settings.casting_proxy_summary" + android:key="castProxy" + android:defaultValue="false"/> + + <CheckBoxPreference + android:title="@string/settings.gapless_playback" + android:summary="@string/settings.gapless_playback_summary" + android:key="castingGaplessPlayback" + android:defaultValue="true"/> + + <CheckBoxPreference + android:title="@string/settings.casting_stream_original" + android:summary="@string/settings.casting_stream_original_summary" + android:key="castStreamOriginal" + android:defaultValue="true"/> + + <CheckBoxPreference + android:title="@string/settings.casting_cache" + android:summary="@string/settings.casting_cache_summary" + android:key="castCache" + android:defaultValue="false"/> + </PreferenceCategory> +</PreferenceScreen>
\ No newline at end of file diff --git a/app/src/main/res/xml/settings_drawer.xml b/app/src/main/res/xml/settings_drawer.xml index a874881a..4b92737e 100644 --- a/app/src/main/res/xml/settings_drawer.xml +++ b/app/src/main/res/xml/settings_drawer.xml @@ -38,7 +38,15 @@ </PreferenceCategory> <PreferenceCategory - android:title="@string/button_bar.chat"> + android:title="@string/settings.other_title"> + + <ListPreference + android:title="@string/settings.open_to_tab" + android:summary="@string/settings.open_to_tab_summary" + android:key="openToTab" + android:entryValues="@array/defaultDrawerItemsDescriptions" + android:entries="@array/defaultDrawerItems" + android:defaultValue="Home"/> <github.daneren2005.dsub.view.SeekBarPreference android:title="@string/settings.chat_refresh" diff --git a/app/src/main/res/xml/settings_playback.xml b/app/src/main/res/xml/settings_playback.xml index 3c505b6e..566a8218 100644 --- a/app/src/main/res/xml/settings_playback.xml +++ b/app/src/main/res/xml/settings_playback.xml @@ -41,10 +41,17 @@ android:defaultValue="false"/> <CheckBoxPreference - android:title="@string/settings.play_now_after" - android:summary="@string/settings.play_now_after_summary" - android:key="playNowAfter" - android:defaultValue="true"/> + android:title="@string/settings.heads_up_notification" + android:summary="@string/settings.heads_up_notification_summary" + android:key="headsUpNotification" + android:defaultValue="false"/> + + <ListPreference + android:title="@string/settings.song_press_action" + android:key="songPressAction" + android:defaultValue="all" + android:entryValues="@array/songPressActionValues" + android:entries="@array/songPressActionNames"/> </PreferenceCategory> <PreferenceCategory @@ -59,16 +66,6 @@ </PreferenceCategory> <PreferenceCategory - android:title="@string/settings.casting"> - - <CheckBoxPreference - android:title="@string/settings.casting_proxy" - android:summary="@string/settings.casting_proxy_summary" - android:key="castProxy" - android:defaultValue="false"/> - </PreferenceCategory> - - <PreferenceCategory android:title="@string/settings.replay_gain"> <CheckBoxPreference diff --git a/build.gradle b/build.gradle index 095091b2..db35b661 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.2.3' + classpath 'com.android.tools.build:gradle:2.1.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/drawer_header_clean.psd b/drawer_header_clean.psd Binary files differnew file mode 100644 index 00000000..a708d49c --- /dev/null +++ b/drawer_header_clean.psd diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0c71e760..f9d370e8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Apr 10 15:27:10 PDT 2013 +#Tue Aug 16 16:00:21 PDT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip |