diff options
Diffstat (limited to 'app')
138 files changed, 3406 insertions, 1114 deletions
diff --git a/app/build.gradle b/app/build.gradle index 12cd09fa..5798ad5e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,11 +8,15 @@ android { applicationId "github.daneren2005.dsub" minSdkVersion 14 targetSdkVersion 22 + versionCode 174 + versionName '5.1.5' + setProperty("archivesBaseName", "DSub $versionName") } buildTypes { release { minifyEnabled true proguardFiles 'proguard.cfg' + zipAlignEnabled true } } @@ -42,8 +46,8 @@ dependencies { 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/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index be941740..4293ba8e 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"/> 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..00e80517 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,9 +190,8 @@ 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 = (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.setBackgroundResource(R.drawable.abc_spinner_mtrl_am_alpha); } spinnerAdapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item); @@ -199,7 +199,7 @@ public class SubsonicActivity extends AppCompatActivity implements OnItemSelecte 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() { + restart(true); + } + protected void restart(boolean resumePosition) { Intent intent = new Intent(this, ((Object) this).getClass()); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 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)); + } 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) { 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..487ad83c 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; @@ -153,8 +154,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,19 +199,24 @@ 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); @@ -230,6 +247,8 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo openNowPlaying(); } }, 200); + + getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD); } bottomBar = findViewById(R.id.bottom_bar); @@ -330,6 +349,10 @@ 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); @@ -472,7 +495,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 +585,7 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo @Override public void closeNowPlaying() { slideUpPanel.setPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + isPanelClosing = true; } private SubsonicFragment getNewFragment(String fragmentType) { @@ -857,7 +881,6 @@ public class SubsonicFragmentActivity extends SubsonicActivity implements Downlo 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 +898,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/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..71e78e4b 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/EntryGridAdapter.java @@ -19,6 +19,7 @@ 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; @@ -46,7 +47,6 @@ public class EntryGridAdapter extends SectionAdapter<Entry> { private boolean largeAlbums; private boolean showArtist = false; private boolean removeFromPlaylist = false; - private boolean removeStarred = true; private View header; public EntryGridAdapter(Context context, List<Entry> entries, ImageLoader imageLoader, boolean largeCell) { @@ -136,9 +136,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 +148,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/MainAdapter.java b/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java index 8f1f1c38..473366fe 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/MainAdapter.java @@ -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..aeb98a6b 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/PodcastChannelAdapter.java @@ -126,7 +126,7 @@ public class PodcastChannelAdapter extends SectionAdapter<Serializable> implemen } menu.removeItem(R.id.menu_remove_playlist); - menu.removeItem(R.id.menu_unstar); + menu.removeItem(R.id.menu_star); } @Override 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..fbed4d7a 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SectionAdapter.java @@ -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..51d08a99 100644 --- a/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java +++ b/app/src/main/java/github/daneren2005/dsub/adapter/SettingsAdapter.java @@ -23,11 +23,16 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import java.util.ArrayList; +import java.util.Arrays; 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,26 +42,42 @@ 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); @@ -65,28 +86,37 @@ public class SettingsAdapter extends SectionAdapter<Setting> { 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 +135,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..fddafd65 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/Artist.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/Artist.java @@ -30,6 +30,8 @@ import java.util.List; */ 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 +40,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; } @@ -128,9 +138,9 @@ public class Artist implements Serializable { char rhs1 = rhs.charAt(0); if (Character.isDigit(lhs1) && !Character.isDigit(rhs1)) { - return 1; - } else if (Character.isDigit(rhs1) && !Character.isDigit(lhs1)) { return -1; + } else if (Character.isDigit(rhs1) && !Character.isDigit(lhs1)) { + return 1; } for (String article : ignoredArticles) { 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..cd84c0a3 100644 --- a/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java +++ b/app/src/main/java/github/daneren2005/dsub/domain/MusicDirectory.java @@ -32,7 +32,9 @@ import java.io.Serializable; import java.util.Collections; import java.util.Comparator; +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 +137,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 +158,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; 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/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..f3d675f0 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,11 @@ 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.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 +18,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 +29,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(); @@ -113,6 +129,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); @@ -238,9 +270,9 @@ public class MainFragment extends SelectRecyclerFragment<Integer> { private void getLogs() { try { final String version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; - new LoadingTask<File>(context) { + 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 +290,93 @@ 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; 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_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 +405,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..8e459996 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/NowPlayingFragment.java @@ -457,6 +457,10 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis 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 +478,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 +624,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 +635,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 +659,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 +685,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,7 +708,7 @@ 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); @@ -899,10 +906,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); } } @@ -1158,11 +1169,23 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis @Override public void onSongChanged(DownloadFile currentPlaying, int currentPlayingIndex) { this.currentPlaying = currentPlaying; + setupSubtitle(currentPlayingIndex); + } + + 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 +1222,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 +1358,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 +1380,18 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis break; } } + + @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..9a7823a5 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SearchFragment.java @@ -108,13 +108,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; } @@ -130,7 +130,7 @@ public class SearchFragment extends SubsonicFragment implements SectionAdapter.O @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case R.id.menu_search: + case R.id.menu_global_search: context.startSearch(currentQuery, false, null, false); return true; } 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..6890f572 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectArtistFragment.java @@ -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(); + } } } @@ -111,6 +113,7 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme 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 MusicDirectory.Entry(artist)); } @@ -163,7 +166,7 @@ public class SelectArtistFragment extends SelectRecyclerFragment<Artist> impleme public List<Artist> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { List<Artist> artists; 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 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..f4f0ac30 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) { @@ -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; @@ -384,16 +367,17 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Section } 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()); + if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_PLAY_NOW_AFTER, true) && (albumListType == null || "starred".equals(albumListType))) { + for(Entry song: entries) { + if(!song.isDirectory() && !song.isVideo()) { + songs.add(song); + } } + playNow(songs, entry, 0); } else { songs.add(entry); + playNow(songs); } - - playNow(songs); } } @@ -577,6 +561,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 +585,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 +651,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 +673,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 +701,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); @@ -953,70 +971,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) { @@ -1159,22 +1135,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 +1159,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 +1328,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..37fc21cc 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java @@ -251,7 +251,7 @@ public class SelectPodcastsFragment extends SelectRecyclerFragment<Serializable> } @Override - public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() { + public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final GridLayoutManager gridLayoutManager) { return new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { @@ -259,7 +259,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 +270,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/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..fc436a61 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; @@ -137,6 +138,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 +208,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); @@ -350,6 +353,8 @@ public class SettingsFragment extends PreferenceCompatFragment implements Shared if(theme != null) { theme.setSummary(theme.getEntry()); + } + if(openToTab != null) { openToTab.setSummary(openToTab.getEntry()); } @@ -664,8 +669,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..a7896763 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/SubsonicFragment.java @@ -221,6 +221,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; @@ -323,6 +326,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 +662,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 +677,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; } @@ -1691,7 +1697,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 +1904,10 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR } } + protected void toggleSelectedStarred() { + UpdateHelper.toggleStarred(context, getSelectedEntries()); + } + public abstract class RecursiveLoader extends LoadingTask<Boolean> { protected MusicService musicService; protected static final int MAX_SONGS = 500; 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/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/service/AutoMediaBrowserService.java b/app/src/main/java/github/daneren2005/dsub/service/AutoMediaBrowserService.java index d579ef54..35d419bb 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,13 @@ 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 static final int MAX_DOUBLE_LINE_ITEMS = 9; private DownloadService downloadService; private Handler handler = new Handler(); @@ -76,12 +85,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 +111,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 +126,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 +147,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 +167,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, MAX_DOUBLE_LINE_ITEMS, 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 +249,105 @@ 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<>(); + + int i = 0; + 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)); + i++; + if(i >= MAX_DOUBLE_LINE_ITEMS) { + break; + } + } + + 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 +370,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 +378,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..3be04cff 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/CachedMusicService.java @@ -22,6 +22,8 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.ListIterator; @@ -57,6 +59,7 @@ import github.daneren2005.dsub.util.SongDBHandler; import github.daneren2005.dsub.util.SyncUtil; import github.daneren2005.dsub.util.TimeLimitedCache; import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.UpdateHelper; import github.daneren2005.dsub.util.Util; import static github.daneren2005.dsub.domain.MusicDirectory.Entry; @@ -69,6 +72,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 +127,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 +158,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 +177,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 +194,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 +213,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 +246,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 +279,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 +296,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 +672,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); } @@ -1136,12 +1157,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/DownloadFile.java b/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java index 3febfaea..21fe3fd5 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadFile.java @@ -379,6 +379,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 +529,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..3eab2855 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/DownloadService.java @@ -116,6 +116,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; @@ -219,14 +220,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)) { @@ -600,6 +601,9 @@ public class DownloadService extends Service { } editor.commit(); } + public boolean isArtistRadio() { + return artistRadio; + } public synchronized void shuffle() { Collections.shuffle(downloadList); @@ -781,6 +785,8 @@ public class DownloadService extends Service { suggestedPlaylistName = null; suggestedPlaylistId = null; + + checkDownloads(); } public synchronized void remove(int which) { @@ -807,6 +813,8 @@ public class DownloadService extends Service { if(downloadFile == nextPlaying) { setNextPlaying(); } + + checkDownloads(); } public synchronized void removeBackground(DownloadFile downloadFile) { if (downloadFile == currentDownloading && downloadFile != currentPlaying && downloadFile != nextPlaying) { @@ -1372,6 +1380,7 @@ public class DownloadService extends Service { if (playerState == PAUSED) { lifecycleSupport.serializeDownloadQueue(); + checkAddBookmark(true); } boolean show = playerState == PlayerState.STARTED; @@ -1699,6 +1708,10 @@ public class DownloadService extends Service { nextPlayingTask.cancel(); nextPlayingTask = null; } + + if(nextPlayerState != IDLE) { + setNextPlayerState(IDLE); + } } if(remoteState == LOCAL) { @@ -2423,8 +2436,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 +2470,9 @@ public class DownloadService extends Service { if(found != null) { found.setBookmark(new Bookmark(position)); } + if(updateMetadata) { + onMetadataUpdate(METADATA_UPDATED_BOOKMARK); + } return null; } @@ -2482,9 +2501,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"); @@ -2571,6 +2590,11 @@ public class DownloadService extends Service { onMetadataUpdate(METADATA_UPDATED_STAR); } } + + @Override + public void starCommited(boolean starred) { + + } }); } public void toggleRating(int rating) { @@ -2600,7 +2624,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); } } @@ -2616,10 +2640,12 @@ public class DownloadService extends Service { 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 +2656,7 @@ public class DownloadService extends Service { onSongsChanged(); onSongProgress(); onStateUpdate(); + onMetadataUpdate(METADATA_UPDATED_ALL); } }); } else { @@ -2637,53 +2664,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,15 +2724,18 @@ 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) { @@ -2718,39 +2754,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/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..876b45fd 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; 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..3048a0db 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/OfflineMusicService.java @@ -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); } 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..7a2edf79 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); } @@ -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"); @@ -909,10 +947,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 +1295,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); } @@ -1462,6 +1499,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 +1533,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); 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/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/util/BackgroundTask.java b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java index 18f245d5..fce855fe 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) { } 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..9aebef83 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"; @@ -168,6 +169,9 @@ 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 OFFLINE_SCROBBLE_COUNT = "scrobbleCount"; public static final String OFFLINE_SCROBBLE_ID = "scrobbleID"; 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/Notifications.java b/app/src/main/java/github/daneren2005/dsub/util/Notifications.java index 375c9966..356ec552 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/Notifications.java +++ b/app/src/main/java/github/daneren2005/dsub/util/Notifications.java @@ -306,6 +306,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 = ""; @@ -346,6 +349,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..859c32dd 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java +++ b/app/src/main/java/github/daneren2005/dsub/util/SongDBHandler.java @@ -150,6 +150,14 @@ 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)); } 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..435c33c0 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); @@ -901,26 +912,38 @@ 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) { 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 +1245,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) 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..47c77cac 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 @@ -36,14 +36,18 @@ import android.os.Bundle; import android.provider.MediaStore; import android.support.v7.media.MediaRouter; +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; @@ -145,7 +149,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { DownloadFile downloadFile = downloadService.getCurrentPlaying(); if(downloadFile != null) { - MusicDirectory.Entry entry = downloadFile.getSong(); + Entry entry = downloadFile.getSong(); addCustomActions(entry, builder); builder.setActiveQueueItemId(entry.getId().hashCode()); } @@ -156,7 +160,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 +169,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 +193,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 +212,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()) @@ -244,7 +248,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { 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 +300,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()) { @@ -308,12 +312,12 @@ 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 +325,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 +333,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 +353,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(Entry entry) { - private void playSong(MusicDirectory.Entry entry) { - List<MusicDirectory.Entry> entries = new ArrayList<>(); + } + 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() { @@ -467,6 +520,7 @@ public class RemoteControlClientLP extends RemoteControlClientBase { } } + @Override public void onPlayFromMediaId (String mediaId, Bundle extras) { if(extras == null) { return; @@ -474,11 +528,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 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..b9e0bcce --- /dev/null +++ b/app/src/main/java/github/daneren2005/dsub/view/CardView.java @@ -0,0 +1,68 @@ +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()) { + Log.d(TAG, "Change to software"); + 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..a33746c4 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; + 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; } } 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..95f7479f 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,16 @@ 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; 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 +87,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,8 +110,7 @@ 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) { @@ -138,8 +145,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 +213,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 +268,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; } } 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_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/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_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/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-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/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_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/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_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/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_clean.psd b/app/src/main/res/drawable/drawer_header_clean.psd Binary files differnew file mode 100644 index 00000000..a708d49c --- /dev/null +++ b/app/src/main/res/drawable/drawer_header_clean.psd 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/layout-land/download.xml b/app/src/main/res/layout-land/download.xml index f3e39a5f..9451a587 100644 --- a/app/src/main/res/layout-land/download.xml +++ b/app/src/main/res/layout-land/download.xml @@ -27,7 +27,7 @@ android:layout_width="wrap_content" android:layout_height="fill_parent" android:layout_weight="1" - android:scaleType="fitStart"/> + android:scaleType="fitCenter"/> <include layout="@layout/download_playlist"/> diff --git a/app/src/main/res/layout-large-land/download.xml b/app/src/main/res/layout-large-land/download.xml index 8b252190..0b48384f 100644 --- a/app/src/main/res/layout-large-land/download.xml +++ b/app/src/main/res/layout-large-land/download.xml @@ -21,7 +21,7 @@ android:layout_width="0dp" android:layout_height="fill_parent" android:layout_weight="1" - android:scaleType="fitStart"/> + android:scaleType="fitCenter"/> <RelativeLayout android:orientation="vertical" android:id="@+id/download_control_layout" diff --git a/app/src/main/res/layout/abstract_fragment_activity.xml b/app/src/main/res/layout/abstract_fragment_activity.xml index d41b0115..6914b7e0 100644 --- a/app/src/main/res/layout/abstract_fragment_activity.xml +++ b/app/src/main/res/layout/abstract_fragment_activity.xml @@ -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" 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_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/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/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/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/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/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..1ede02c9 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> @@ -439,15 +425,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 +476,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 +491,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> @@ -642,6 +616,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 +631,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..fd13ee84 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> @@ -420,14 +406,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 +468,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 +482,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> diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4674ca32..157a6c33 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -28,8 +28,7 @@ <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> @@ -50,8 +49,7 @@ <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> @@ -114,19 +112,12 @@ <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.donate_dialog_message">Téléchargement illimité en supportant Subsonic.</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> @@ -424,15 +411,6 @@ <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> @@ -484,9 +462,7 @@ <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> @@ -501,8 +477,7 @@ <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.folder">Sélectionner un dossier</string> <string name="select_artist.all_folders">Tous les dossier</string> <string name="equalizer.label">Equaliseur</string> diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 60b127be..75962d48 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> @@ -473,6 +468,8 @@ <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 eszköz támogatja a fájltípust.</string> <string name="shuffle.title">Dalsorrend keverése</string> <string name="shuffle.startYear">Kezdő év:</string> @@ -480,15 +477,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 +505,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 +513,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> @@ -540,9 +531,7 @@ <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 +546,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> @@ -642,6 +630,7 @@ <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> <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..2ee7d5ad --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,638 @@ +<?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.play_now_after">Speel af - na</string> + <string name="settings.play_now_after_summary">Nu afspelen contextmenu speelt alles af na het geselecteerde item (net zoals de Subsonic website)</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" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Versie <xliff:g id="version_name">%s</xliff:g></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..69b7f692 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> @@ -477,15 +463,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 +514,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 +529,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> 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..3ee60fce 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> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index c4f80478..c8553dc0 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -37,6 +37,7 @@ <attr name="rating_good" format="reference"/> <attr name="radio" format="reference"/> <attr name="star_outline" format="reference"/> + <attr name="download" format="reference"/> <attr name="drawerItemsIcons" format="reference"/> <attr name="drawerHome" format="reference"/> <attr name="drawerLibrary" format="reference"/> @@ -55,6 +56,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..bdc5443a 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,7 @@ <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="sync.new_podcasts">New podcasts available</string> <string name="sync.new_playlists">New songs in playlists</string> @@ -291,6 +284,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> @@ -473,6 +468,8 @@ <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="shuffle.title">Shuffle By</string> <string name="shuffle.startYear">Start Year:</string> @@ -480,15 +477,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 +505,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 +513,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 +527,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 +547,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> @@ -643,6 +632,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..4915c012 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -38,6 +38,7 @@ <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="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,6 +63,8 @@ <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> @@ -101,6 +104,7 @@ <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="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 +128,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"> @@ -179,6 +192,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..601f5ec6 100644 --- a/app/src/main/res/xml/changelog.xml +++ b/app/src/main/res/xml/changelog.xml @@ -1,5 +1,50 @@ <?xml version="1.0" encoding="utf-8"?> <changelog> + <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..45766162 --- /dev/null +++ b/app/src/main/res/xml/settings_cast.xml @@ -0,0 +1,25 @@ +<?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"/> + </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..a4ea443e 100644 --- a/app/src/main/res/xml/settings_playback.xml +++ b/app/src/main/res/xml/settings_playback.xml @@ -59,16 +59,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 |