Compare commits
16 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6b1bb83e4 |
||
|
|
ef4c1cad27 |
||
|
|
30cc7f663c |
||
| 49aa6e8048 | |||
| 1943344585 | |||
| cd0971bf14 | |||
| 3a0154deea | |||
| 2a7350af89 | |||
| 46ccf94b13 | |||
| 728c6068cd | |||
| 3b54d1c917 | |||
| 1f2ae7f1d9 | |||
|
|
9748ccf592 |
||
| d312d1fca8 | |||
| 34c4bcc831 | |||
| 50684b57f5 |
|
|
@ -23,12 +23,17 @@ This is a native music player for Android devices that connects to Jellyfin medi
|
|||
* Playback history reporting
|
||||
* Filter content by library
|
||||
|
||||
# Requisites
|
||||
- A Jellfin server. See how to setup one [here](https://jellyfin.org/docs/general/quick-start/).
|
||||
- Android 4.4 or later
|
||||
|
||||
# Issues
|
||||
|
||||
Since this was a small project intended mainly for myself, there are some things I haven't resolved yet. I would appreciate pull requests to fix any of these issues!
|
||||
|
||||
* Artist sorting isn't available through the API
|
||||
* Playlists and favorites will not update automatically when changed
|
||||
* Playlists and favorites will not update automatically when changed ([#5](https://github.com/adrianvic/jamfish/issues/5))
|
||||
* App may crash on really low end devices due exceeding the maximum bitmap memory ([#4](https://github.com/adrianvic/jamfish/issues/4))
|
||||
|
||||
# Future Plans
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ android {
|
|||
minSdk 19
|
||||
targetSdk 33
|
||||
|
||||
versionCode 1
|
||||
versionName '1.4.0'
|
||||
versionCode 2
|
||||
versionName '1.4.1'
|
||||
|
||||
// for SDK < 19
|
||||
multiDexEnabled true
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary true
|
||||
}
|
||||
|
|
@ -105,8 +107,9 @@ dependencies {
|
|||
implementation 'com.android.support:multidex:1.0.3'
|
||||
implementation 'com.mlegy.redscreenofdeath:red-screen-of-death:0.1.3'
|
||||
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||
// old version of retrofit to work with api < 21
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.6.4'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
|
||||
|
|
@ -117,4 +120,10 @@ dependencies {
|
|||
implementation 'com.github.bumptech.glide:annotations:4.12.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.12.0'
|
||||
|
||||
// for supporting legacy android versions:
|
||||
implementation('com.squareup.okhttp3:okhttp:3.12.13')
|
||||
|
||||
implementation "androidx.multidex:multidex:2.0.1"
|
||||
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
|
@ -49,6 +51,7 @@
|
|||
<activity
|
||||
android:name="org.adrianvictor.geleia.activities.SettingsActivity"
|
||||
android:label="@string/action_settings" />
|
||||
<activity android:name="org.adrianvictor.geleia.activities.DirectoryPickerActivity"/>
|
||||
<activity
|
||||
android:name="org.adrianvictor.geleia.activities.AboutActivity"
|
||||
android:label="@string/action_about" />
|
||||
|
|
@ -64,7 +67,7 @@
|
|||
android:name="org.adrianvictor.geleia.views.shortcuts.AppShortcutLauncherActivity"
|
||||
android:launchMode="singleInstance"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<activity android:name="org.adrianvictor.geleia.activities.UnreachableActivity" />
|
||||
<service android:name="org.adrianvictor.geleia.service.DownloadService" />
|
||||
<service android:name="org.adrianvictor.geleia.service.LoginService" />
|
||||
<service android:name="org.adrianvictor.geleia.service.MusicService" />
|
||||
|
|
@ -136,6 +139,12 @@
|
|||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="remove" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import android.content.Context;
|
|||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
import androidx.room.Room;
|
||||
|
||||
import org.adrianvictor.geleia.database.JellyDatabase;
|
||||
|
|
@ -14,6 +17,7 @@ import org.adrianvictor.geleia.util.PreferenceUtil;
|
|||
import org.adrianvictor.geleia.views.shortcuts.DynamicShortcutManager;
|
||||
import com.melegy.redscreenofdeath.RedScreenOfDeath;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.jellyfin.apiclient.interaction.AndroidDevice;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.interaction.VolleyHttpClient;
|
||||
|
|
@ -22,7 +26,16 @@ import org.jellyfin.apiclient.interaction.http.IAsyncHttpClient;
|
|||
import org.jellyfin.apiclient.logging.AndroidLogger;
|
||||
import org.jellyfin.apiclient.logging.ILogger;
|
||||
|
||||
public class App extends Application {
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.Registry;
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import java.io.InputStream;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
import java.security.Security;
|
||||
|
||||
public class App extends MultiDexApplication {
|
||||
private static App app;
|
||||
|
||||
private static JellyDatabase database;
|
||||
|
|
@ -30,8 +43,14 @@ public class App extends Application {
|
|||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// Initializing stuff for older Android APIs compatibility
|
||||
Security.insertProviderAt(Conscrypt.newProvider(), 1); // To have SSL 1.2 on API < 19
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); // To load vectors on API < 19
|
||||
|
||||
super.onCreate();
|
||||
|
||||
initializeGlide(this);
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
RedScreenOfDeath.init(this);
|
||||
}
|
||||
|
|
@ -40,7 +59,7 @@ public class App extends Application {
|
|||
database = createDatabase(this);
|
||||
apiClient = createApiClient(this);
|
||||
|
||||
if (database.userDao().getUsers().size() == 0) {
|
||||
if (database.userDao().getUsers().isEmpty()) {
|
||||
PreferenceUtil.getInstance(this).setServer(null);
|
||||
PreferenceUtil.getInstance(this).setUser(null);
|
||||
}
|
||||
|
|
@ -76,6 +95,7 @@ public class App extends Application {
|
|||
IDevice device = new AndroidDevice(deviceId, deviceName);
|
||||
EventListener eventListener = new EventListener();
|
||||
|
||||
|
||||
return new ApiClient(httpClient, logger, server, appName, appVersion, device, eventListener);
|
||||
}
|
||||
|
||||
|
|
@ -90,4 +110,15 @@ public class App extends Application {
|
|||
public static App getInstance() {
|
||||
return app;
|
||||
}
|
||||
|
||||
private void initializeGlide(@NonNull Context context) {
|
||||
// This OkHttpClient is now created with Conscrypt as the SSL provider.
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
|
||||
// Manually create and register Glide's OkHttp component.
|
||||
OkHttpUrlLoader.Factory factory = new OkHttpUrlLoader.Factory(client);
|
||||
|
||||
// Ensure Glide is initialized and then register the component.
|
||||
Glide.get(context).getRegistry().replace(GlideUrl.class, InputStream.class, factory);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import android.view.View;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.adrianvictor.geleia.service.notifications.ErrorNotification;
|
||||
import org.adrianvictor.geleia.util.NavigationUtil;
|
||||
import org.adrianvictor.geleia.util.PreferenceUtil;
|
||||
import org.adrianvictor.geleia.databinding.ActivityAboutBinding;
|
||||
|
|
@ -102,7 +103,7 @@ public class AboutActivity extends AbsBaseActivity implements View.OnClickListen
|
|||
try {
|
||||
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
ErrorNotification.show(context, e.getMessage());
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
package org.adrianvictor.geleia.activities;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class DirectoryPickerActivity extends AppCompatActivity {
|
||||
|
||||
public static final String EXTRA_RESULT_PATH = "result_path";
|
||||
|
||||
private File rootDir;
|
||||
private File currentDir;
|
||||
|
||||
private ArrayAdapter<File> adapter;
|
||||
private TextView pathView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
rootDir = Environment.getExternalStorageDirectory();
|
||||
currentDir = rootDir;
|
||||
|
||||
LinearLayout layout = new LinearLayout(this);
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
pathView = new TextView(this);
|
||||
pathView.setPadding(24, 24, 24, 24);
|
||||
|
||||
ListView listView = new ListView(this);
|
||||
|
||||
Button selectButton = new Button(this);
|
||||
selectButton.setText("Select this folder");
|
||||
|
||||
layout.addView(pathView);
|
||||
layout.addView(listView, new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||
layout.addView(selectButton);
|
||||
|
||||
setContentView(layout);
|
||||
|
||||
adapter = new ArrayAdapter<File>(this,
|
||||
android.R.layout.simple_list_item_1, new ArrayList<>()) {
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
TextView tv = (TextView) super.getView(position, convertView, parent);
|
||||
File f = getItem(position);
|
||||
|
||||
File parentFile = currentDir.getParentFile();
|
||||
|
||||
if (parentFile != null && parentFile.equals(f)) {
|
||||
tv.setText("..");
|
||||
} else {
|
||||
tv.setText(f.getName());
|
||||
}
|
||||
|
||||
return tv;
|
||||
}
|
||||
};
|
||||
|
||||
listView.setAdapter(adapter);
|
||||
|
||||
listView.setOnItemClickListener((p, v, pos, id) -> {
|
||||
File dir = adapter.getItem(pos);
|
||||
if (dir != null) openDir(dir);
|
||||
});
|
||||
|
||||
selectButton.setOnClickListener(v -> {
|
||||
Intent result = new Intent();
|
||||
result.putExtra(EXTRA_RESULT_PATH, currentDir.getAbsolutePath());
|
||||
setResult(RESULT_OK, result);
|
||||
finish();
|
||||
});
|
||||
|
||||
openDir(rootDir);
|
||||
}
|
||||
|
||||
private void openDir(File dir) {
|
||||
if (!dir.exists() || !dir.canRead()) return;
|
||||
|
||||
File[] dirs = dir.listFiles(f ->
|
||||
f.isDirectory() && f.canRead()
|
||||
);
|
||||
|
||||
if (dirs == null) return;
|
||||
|
||||
adapter.clear();
|
||||
|
||||
File parent = dir.getParentFile();
|
||||
if (parent != null && parent.getAbsolutePath().startsWith(rootDir.getAbsolutePath())) {
|
||||
adapter.add(parent);
|
||||
}
|
||||
|
||||
Arrays.sort(dirs);
|
||||
|
||||
for (File f : dirs) {
|
||||
adapter.add(f);
|
||||
}
|
||||
|
||||
currentDir = dir;
|
||||
pathView.setText(dir.getAbsolutePath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
File parent = currentDir.getParentFile();
|
||||
if (parent != null &&
|
||||
parent.getAbsolutePath().startsWith(rootDir.getAbsolutePath())) {
|
||||
openDir(parent);
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,11 +13,14 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.afollestad.materialcab.attached.AttachedCab;
|
||||
import com.afollestad.materialcab.attached.AttachedCabKt;
|
||||
import org.adrianvictor.geleia.activities.base.AbsMusicContentActivity;
|
||||
import org.adrianvictor.geleia.fragments.OfflineFragment;
|
||||
import org.adrianvictor.geleia.interfaces.CabHolder;
|
||||
import org.adrianvictor.geleia.util.NavigationUtil;
|
||||
import org.adrianvictor.geleia.util.PreferenceUtil;
|
||||
import org.adrianvictor.geleia.util.ThemeUtil;
|
||||
import org.adrianvictor.geleia.databinding.ActivityMainContentBinding;
|
||||
|
|
@ -42,6 +45,7 @@ public class MainActivity extends AbsMusicContentActivity implements CabHolder {
|
|||
private ActivityMainContentBinding contentBinding;
|
||||
private NavigationDrawerHeaderBinding navigationBinding;
|
||||
private boolean onLogout;
|
||||
private boolean pendingShowOffline = false;
|
||||
|
||||
@Nullable
|
||||
private AttachedCab cab;
|
||||
|
|
@ -97,6 +101,15 @@ public class MainActivity extends AbsMusicContentActivity implements CabHolder {
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateOffline() {
|
||||
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
|
||||
NavigationUtil.startUnreachable(this);
|
||||
} else {
|
||||
pendingShowOffline = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
|
@ -108,6 +121,16 @@ public class MainActivity extends AbsMusicContentActivity implements CabHolder {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (pendingShowOffline) {
|
||||
setCurrentFragment(OfflineFragment.newInstance());
|
||||
pendingShowOffline = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setCurrentFragment(Fragment fragment) {
|
||||
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, fragment, null).commit();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,6 +93,9 @@ public class SearchActivity extends AbsMusicContentActivity implements SearchVie
|
|||
public void onStateOnline() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateOffline() {}
|
||||
|
||||
private void setUpToolBar() {
|
||||
binding.toolbar.setBackgroundColor(PreferenceUtil.getInstance(this).getPrimaryColor());
|
||||
setSupportActionBar(binding.toolbar);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ public class SettingsActivity extends AbsBaseActivity {
|
|||
public static class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private ActivityResultLauncher<Intent> dirPickerLauncher;
|
||||
private ActivityResultLauncher<Intent> legacyDirPickerLauncher;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
|
|
@ -71,6 +72,25 @@ public class SettingsActivity extends AbsBaseActivity {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
legacyDirPickerLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == RESULT_OK && result.getData() != null) {
|
||||
String path = result.getData().getStringExtra(
|
||||
DirectoryPickerActivity.EXTRA_RESULT_PATH
|
||||
);
|
||||
|
||||
if (path != null) {
|
||||
SharedPreferences.Editor editor =
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit();
|
||||
editor.putString(PreferenceUtil.LOCATION_DOWNLOAD, path);
|
||||
editor.apply();
|
||||
invalidateSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -161,8 +181,13 @@ public class SettingsActivity extends AbsBaseActivity {
|
|||
}
|
||||
|
||||
private void openDirectoryPicker() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
dirPickerLauncher.launch(intent);
|
||||
} else {
|
||||
Intent intent = new Intent(requireContext(), DirectoryPickerActivity.class);
|
||||
legacyDirPickerLauncher.launch(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
package org.adrianvictor.geleia.activities;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.adrianvictor.geleia.App;
|
||||
import org.adrianvictor.geleia.R;
|
||||
|
|
@ -15,10 +20,41 @@ import org.adrianvictor.geleia.util.PreferenceUtil;
|
|||
import java.util.List;
|
||||
|
||||
public class SplashActivity extends AbsBaseActivity {
|
||||
|
||||
private final BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, @NonNull Intent intent) {
|
||||
if (intent.getAction() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (intent.getAction()) {
|
||||
case LoginService.STATE_ONLINE:
|
||||
NavigationUtil.startMain(context);
|
||||
finish();
|
||||
break;
|
||||
case LoginService.STATE_OFFLINE:
|
||||
NavigationUtil.startUnreachable(context);
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(LoginService.STATE_ONLINE);
|
||||
filter.addAction(LoginService.STATE_OFFLINE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||
} else {
|
||||
registerReceiver(receiver, filter);
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_splash);
|
||||
}
|
||||
|
||||
|
|
@ -35,13 +71,20 @@ public class SplashActivity extends AbsBaseActivity {
|
|||
User user = App.getDatabase().userDao().getUser(PreferenceUtil.getInstance(this).getUser());
|
||||
List<User> available = App.getDatabase().userDao().getUsers();
|
||||
|
||||
if (user == null && available.size() != 0) {
|
||||
if (user == null && !available.isEmpty()) {
|
||||
NavigationUtil.startSelect(this);
|
||||
finish();
|
||||
} else if (user == null) {
|
||||
NavigationUtil.startLogin(this);
|
||||
finish();
|
||||
} else {
|
||||
startService(new Intent(this, LoginService.class));
|
||||
new Handler().postDelayed(() -> NavigationUtil.startMain(this), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unregisterReceiver(receiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package org.adrianvictor.geleia.activities;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.adrianvictor.geleia.R;
|
||||
import org.adrianvictor.geleia.activities.base.AbsThemeActivity;
|
||||
import org.adrianvictor.geleia.util.NavigationUtil;
|
||||
|
||||
public class UnreachableActivity extends AbsThemeActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_unreachable);
|
||||
}
|
||||
|
||||
public void onSelectClick(View view) {
|
||||
NavigationUtil.startSelect(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ public abstract class AbsBaseActivity extends AbsThemeActivity {
|
|||
.setPositiveButton(R.string.disable, (dialog, id) -> requestBatteryOptimization());
|
||||
|
||||
new Handler().postDelayed(builder::show, 2000);
|
||||
} else if (permissions.size() != 0 && ActivityCompat.shouldShowRequestPermissionRationale(this, permissions.get(0))) {
|
||||
} else if (!permissions.isEmpty() && ActivityCompat.shouldShowRequestPermissionRationale(this, permissions.get(0))) {
|
||||
builder.setMessage(getPermissionMessage())
|
||||
.setTitle(R.string.permissions_denied)
|
||||
.setPositiveButton(R.string.action_grant, (dialog, id) -> requestPermissions());
|
||||
|
|
@ -127,8 +127,11 @@ public abstract class AbsBaseActivity extends AbsThemeActivity {
|
|||
private boolean checkBatteryOptimization() {
|
||||
String packageName = getPackageName();
|
||||
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return pm.isIgnoringBatteryOptimizations(packageName);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
|
|
@ -138,11 +141,13 @@ public abstract class AbsBaseActivity extends AbsThemeActivity {
|
|||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
private boolean checkPermissions() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
for (String permission : permissions) {
|
||||
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
package org.adrianvictor.geleia.activities.base;
|
||||
|
||||
import static org.adrianvictor.geleia.adapter.CustomFragmentStatePagerAdapter.TAG;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.adrianvictor.geleia.App;
|
||||
import org.adrianvictor.geleia.fragments.OfflineFragment;
|
||||
import org.adrianvictor.geleia.interfaces.StateListener;
|
||||
import org.adrianvictor.geleia.service.LoginService;
|
||||
import org.adrianvictor.geleia.util.NavigationUtil;
|
||||
|
|
@ -24,7 +29,7 @@ public abstract class AbsMusicContentActivity extends AbsMusicPanelActivity impl
|
|||
onStateOnline();
|
||||
break;
|
||||
case LoginService.STATE_OFFLINE:
|
||||
NavigationUtil.startLogin(context);
|
||||
onStateOffline();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +44,11 @@ public abstract class AbsMusicContentActivity extends AbsMusicPanelActivity impl
|
|||
filter.addAction(LoginService.STATE_ONLINE);
|
||||
filter.addAction(LoginService.STATE_OFFLINE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||
} else {
|
||||
registerReceiver(receiver, filter);
|
||||
}
|
||||
|
||||
if (App.getApiClient() == null) {
|
||||
startService(new Intent(this, LoginService.class));
|
||||
|
|
@ -51,24 +60,14 @@ public abstract class AbsMusicContentActivity extends AbsMusicPanelActivity impl
|
|||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (App.getApiClient() == null) {
|
||||
startService(new Intent(this, LoginService.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unregisterReceiver(receiver);
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatePolling() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateOffline() {
|
||||
}
|
||||
public void onStatePolling() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,11 @@ public class AlbumDetailActivity extends AbsMusicContentActivity implements Pale
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateOffline() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOffsetChanged (AppBarLayout appBarLayout, int verticalOffset) {
|
||||
float headerAlpha = Math.max(0, Math.min(1, 1 + (2 * (float) verticalOffset / headerViewHeight)));
|
||||
|
|
@ -231,6 +236,6 @@ public class AlbumDetailActivity extends AbsMusicContentActivity implements Pale
|
|||
binding.durationText.setText(MusicUtil.getReadableDurationString(MusicUtil.getTotalDuration(this, album.songs)));
|
||||
binding.albumYearText.setText(MusicUtil.getYearString(album.year));
|
||||
|
||||
if (album.songs.size() != 0) adapter.swapDataSet(album.songs);
|
||||
if (!album.songs.isEmpty()) adapter.swapDataSet(album.songs);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,11 @@ public class ArtistDetailActivity extends AbsMusicContentActivity implements Pal
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateOffline() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
|
||||
float headerAlpha = Math.max(0, Math.min(1, 1 + (2 * (float) verticalOffset / headerViewHeight)));
|
||||
|
|
@ -258,7 +263,7 @@ public class ArtistDetailActivity extends AbsMusicContentActivity implements Pal
|
|||
binding.albumCountText.setText(MusicUtil.getAlbumCountString(this, artist.albums.size()));
|
||||
binding.durationText.setText(MusicUtil.getReadableDurationString(MusicUtil.getTotalDuration(this, artist.songs)));
|
||||
|
||||
if (artist.songs.size() != 0) songAdapter.swapDataSet(artist.songs);
|
||||
if (artist.albums.size() != 0) albumAdapter.swapDataSet(artist.albums);
|
||||
if (!artist.songs.isEmpty()) songAdapter.swapDataSet(artist.songs);
|
||||
if (!artist.albums.isEmpty()) albumAdapter.swapDataSet(artist.albums);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,11 @@ public class GenreDetailActivity extends AbsMusicContentActivity implements CabH
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateOffline() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View createContentView() {
|
||||
binding = ActivityGenreDetailBinding.inflate(getLayoutInflater());
|
||||
|
|
|
|||
|
|
@ -71,6 +71,11 @@ public class PlaylistDetailActivity extends AbsMusicContentActivity implements C
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateOffline() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View createContentView() {
|
||||
binding = ActivityPlaylistDetailBinding.inflate(getLayoutInflater());
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ public abstract class CustomFragmentStatePagerAdapter extends PagerAdapter {
|
|||
@Override
|
||||
public Parcelable saveState() {
|
||||
Bundle state = null;
|
||||
if (mSavedState.size() > 0) {
|
||||
if (!mSavedState.isEmpty()) {
|
||||
state = new Bundle();
|
||||
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
|
||||
mSavedState.toArray(fss);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
package org.adrianvictor.geleia.adapter;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.adrianvictor.geleia.R;
|
||||
import org.adrianvictor.geleia.model.Song;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DownloadsAdapter extends RecyclerView.Adapter<DownloadsAdapter.ViewHolder> {
|
||||
private final List<Song> mSongs;
|
||||
private final int mLayoutId;
|
||||
|
||||
public DownloadsAdapter(int layoutId) {
|
||||
mLayoutId = layoutId;
|
||||
this.mSongs = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void swapDataSet(List<Song> newSongs) {
|
||||
mSongs.clear();
|
||||
if (newSongs != null) {
|
||||
mSongs.addAll(newSongs);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DownloadsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull DownloadsAdapter.ViewHolder holder, int position) {
|
||||
final Song song = mSongs.get(position);
|
||||
|
||||
holder.title.setText(song.title);
|
||||
holder.artist.setText(song.artistName);
|
||||
|
||||
// TODO: Load album cover into holder.cover using Glide
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mSongs.size();
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public final TextView title;
|
||||
public final TextView artist;
|
||||
public final ImageView cover;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
title = itemView.findViewById(R.id.title);
|
||||
artist = itemView.findViewById(R.id.text);
|
||||
cover = itemView.findViewById(R.id.image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,9 @@ import androidx.fragment.app.Fragment;
|
|||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
|
||||
import org.adrianvictor.geleia.activities.UnreachableActivity;
|
||||
import org.adrianvictor.geleia.fragments.OfflineFragment;
|
||||
import org.adrianvictor.geleia.fragments.library.DownloadsFragment;
|
||||
import org.adrianvictor.geleia.fragments.library.FavoritesFragment;
|
||||
import org.adrianvictor.geleia.model.Category;
|
||||
import org.adrianvictor.geleia.fragments.library.AlbumsFragment;
|
||||
|
|
@ -157,7 +160,8 @@ public class MusicLibraryPagerAdapter extends FragmentPagerAdapter {
|
|||
ARTISTS(ArtistsFragment.class),
|
||||
GENRES(GenresFragment.class),
|
||||
PLAYLISTS(PlaylistsFragment.class),
|
||||
FAVORITES(FavoritesFragment.class);
|
||||
FAVORITES(FavoritesFragment.class),
|
||||
DOWNLOADS(DownloadsFragment.class);
|
||||
|
||||
private final Class<? extends Fragment> mFragmentClass;
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ public interface CacheDao {
|
|||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
void insertCache(Cache cache);
|
||||
|
||||
@Query("SELECT * FROM cache")
|
||||
List<Cache> getAll();
|
||||
|
||||
@Query("SELECT * FROM songs LEFT JOIN cache USING(id) WHERE songs.id IN (:ids)")
|
||||
List<Song> getSongs(List<String> ids);
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public class SongShareDialog extends DialogFragment {
|
|||
final String currentlyListening = getString(R.string.currently_listening_to_x_by_x, song.title, song.artistName);
|
||||
return new MaterialDialog.Builder(requireActivity())
|
||||
.title(R.string.what_do_you_want_to_share)
|
||||
.items(getString(R.string.the_audio_file), "\u201C" + currentlyListening + "\u201D")
|
||||
.items(getString(R.string.the_audio_file), "“" + currentlyListening + "”")
|
||||
.itemsCallback((materialDialog, view, i, charSequence) -> {
|
||||
switch (i) {
|
||||
case 0:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package org.adrianvictor.geleia.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.adrianvictor.geleia.R;
|
||||
|
||||
public class OfflineFragment extends Fragment {
|
||||
public static OfflineFragment newInstance() {
|
||||
return new OfflineFragment();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_offline, container, false);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package org.adrianvictor.geleia.fragments.library;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
|
||||
import org.adrianvictor.geleia.App;
|
||||
import org.adrianvictor.geleia.adapter.DownloadsAdapter;
|
||||
|
||||
import org.adrianvictor.geleia.database.Cache;
|
||||
import org.adrianvictor.geleia.model.Song;
|
||||
import org.adrianvictor.geleia.model.SortMethod;
|
||||
import org.adrianvictor.geleia.model.SortOrder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DownloadsFragment extends AbsLibraryPagerRecyclerViewCustomGridSizeFragment<DownloadsAdapter, GridLayoutManager, Void> {
|
||||
@NonNull
|
||||
@Override
|
||||
protected DownloadsAdapter createAdapter() {
|
||||
return new DownloadsAdapter(getItemLayoutRes());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected GridLayoutManager createLayoutManager() {
|
||||
return new GridLayoutManager(getActivity(), getGridSize());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Void createQuery() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadItems(int index) {
|
||||
new Thread(() -> {
|
||||
List<Cache> cachedEntries = App.getDatabase().cacheDao().getAll();
|
||||
|
||||
List<String> songIds = new ArrayList<>();
|
||||
|
||||
for (Cache entry : cachedEntries) {
|
||||
songIds.add(entry.id);
|
||||
}
|
||||
|
||||
final List<Song> downloadedSongs = App.getDatabase().cacheDao().getSongs(songIds);
|
||||
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
getAdapter().swapDataSet(downloadedSongs);
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
@Override
|
||||
protected int loadGridSize() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveGridSize(int gridColumns) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int loadGridSizeLand() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveGridSizeLand(int gridColumns) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveUsePalette(boolean usePalette) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean loadUsePalette() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUsePalette(boolean usePalette) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setGridSize(int gridSize) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SortMethod loadSortMethod() {
|
||||
return SortMethod.ADDED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveSortMethod(SortMethod sortMethod) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSortMethod(SortMethod sortMethod) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SortOrder loadSortOrder() {
|
||||
return SortOrder.ASCENDING;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveSortOrder(SortOrder sortOrder) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSortOrder(SortOrder sortOrder) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -273,7 +273,7 @@ public class MusicPlayerRemote {
|
|||
|
||||
public static boolean playNext(Song song) {
|
||||
if (musicService != null && musicService.queueManager != null) {
|
||||
if (getPlayingQueue().size() > 0) {
|
||||
if (!getPlayingQueue().isEmpty()) {
|
||||
musicService.queueManager.addSong(getPosition() + 1, song);
|
||||
} else {
|
||||
List<Song> queue = new ArrayList<>();
|
||||
|
|
@ -290,7 +290,7 @@ public class MusicPlayerRemote {
|
|||
|
||||
public static boolean playNext(@NonNull List<Song> songs) {
|
||||
if (musicService != null && musicService.queueManager != null) {
|
||||
if (getPlayingQueue().size() > 0) {
|
||||
if (!getPlayingQueue().isEmpty()) {
|
||||
musicService.queueManager.addSongs(getPosition() + 1, songs);
|
||||
} else {
|
||||
openQueue(songs, 0, false);
|
||||
|
|
@ -306,7 +306,7 @@ public class MusicPlayerRemote {
|
|||
|
||||
public static boolean enqueue(Song song) {
|
||||
if (musicService != null && musicService.queueManager != null) {
|
||||
if (getPlayingQueue().size() > 0) {
|
||||
if (!getPlayingQueue().isEmpty()) {
|
||||
musicService.queueManager.addSong(song);
|
||||
} else {
|
||||
List<Song> queue = new ArrayList<>();
|
||||
|
|
@ -323,7 +323,7 @@ public class MusicPlayerRemote {
|
|||
|
||||
public static boolean enqueue(@NonNull List<Song> songs) {
|
||||
if (musicService != null && musicService.queueManager != null) {
|
||||
if (getPlayingQueue().size() > 0) {
|
||||
if (!getPlayingQueue().isEmpty()) {
|
||||
musicService.queueManager.addSongs(songs);
|
||||
} else {
|
||||
openQueue(songs, 0, false);
|
||||
|
|
|
|||
|
|
@ -29,10 +29,10 @@ public class Album implements Parcelable {
|
|||
this.title = itemDto.getName();
|
||||
this.year = itemDto.getProductionYear() != null ? itemDto.getProductionYear() : 0;
|
||||
|
||||
if (itemDto.getAlbumArtists().size() != 0) {
|
||||
if (!itemDto.getAlbumArtists().isEmpty()) {
|
||||
this.artistId = itemDto.getAlbumArtists().get(0).getId();
|
||||
this.artistName = itemDto.getAlbumArtists().get(0).getName();
|
||||
} else if (itemDto.getArtistItems().size() != 0) {
|
||||
} else if (!itemDto.getArtistItems().isEmpty()) {
|
||||
this.artistId = itemDto.getArtistItems().get(0).getId();
|
||||
this.artistName = itemDto.getArtistItems().get(0).getName();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ public enum Category {
|
|||
ARTISTS(R.string.artists),
|
||||
GENRES(R.string.genres),
|
||||
PLAYLISTS(R.string.playlists),
|
||||
FAVORITES(R.string.favorites);
|
||||
FAVORITES(R.string.favorites),
|
||||
DOWNLOADS(R.string.downloads);
|
||||
|
||||
@StringRes
|
||||
public final int title;
|
||||
|
|
|
|||
|
|
@ -67,10 +67,10 @@ public class Song implements Parcelable {
|
|||
this.albumId = itemDto.getAlbumId();
|
||||
this.albumName = itemDto.getAlbum();
|
||||
|
||||
if (itemDto.getArtistItems().size() != 0) {
|
||||
if (!itemDto.getArtistItems().isEmpty()) {
|
||||
this.artistId = itemDto.getArtistItems().get(0).getId();
|
||||
this.artistName = itemDto.getArtistItems().get(0).getName();
|
||||
} else if (itemDto.getAlbumArtists().size() != 0) {
|
||||
} else if (!itemDto.getAlbumArtists().isEmpty()) {
|
||||
this.artistId = itemDto.getAlbumArtists().get(0).getId();
|
||||
this.artistName = itemDto.getAlbumArtists().get(0).getName();
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ public class Song implements Parcelable {
|
|||
|
||||
this.supportsTranscoding = source.getSupportsTranscoding();
|
||||
|
||||
if (source.getMediaStreams() != null && source.getMediaStreams().size() != 0) {
|
||||
if (source.getMediaStreams() != null && !source.getMediaStreams().isEmpty()) {
|
||||
MediaStream stream = source.getMediaStreams().get(0);
|
||||
|
||||
this.codec = stream.getCodec();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import android.app.Service;
|
|||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import org.adrianvictor.geleia.App;
|
||||
|
|
@ -11,6 +13,7 @@ import org.adrianvictor.geleia.BuildConfig;
|
|||
import org.adrianvictor.geleia.database.Cache;
|
||||
import org.adrianvictor.geleia.model.Song;
|
||||
import org.adrianvictor.geleia.service.notifications.DownloadNotification;
|
||||
import org.adrianvictor.geleia.service.notifications.ErrorNotification;
|
||||
import org.adrianvictor.geleia.util.MusicUtil;
|
||||
import org.adrianvictor.geleia.util.PreferenceUtil;
|
||||
|
||||
|
|
@ -23,8 +26,8 @@ import java.util.List;
|
|||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public class DownloadService extends Service {
|
||||
public static final String TAG = DownloadService.class.getSimpleName();
|
||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||
public static final String ACTION_START = PACKAGE_NAME + ".action.start";
|
||||
public static final String ACTION_CANCEL = PACKAGE_NAME + ".action.cancel";
|
||||
|
|
@ -33,6 +36,8 @@ public class DownloadService extends Service {
|
|||
private ExecutorService executor;
|
||||
private DownloadNotification notification;
|
||||
|
||||
private static final Object lock = new Object();
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
|
@ -55,6 +60,7 @@ public class DownloadService extends Service {
|
|||
break;
|
||||
case DownloadService.ACTION_START:
|
||||
List<Song> songs = intent.getParcelableArrayListExtra(EXTRA_SONGS);
|
||||
assert songs != null;
|
||||
for (Song song : songs) {
|
||||
download(song);
|
||||
notification.start(song);
|
||||
|
|
@ -78,20 +84,25 @@ public class DownloadService extends Service {
|
|||
|
||||
String location = PreferenceUtil.getInstance(App.getInstance()).getLocationDownload();
|
||||
DocumentFile root;
|
||||
if (location.equals(getApplicationContext().getCacheDir().toString())) {
|
||||
root = DocumentFile.fromFile(new File(location));
|
||||
} else {
|
||||
if (location.startsWith("content://")) {
|
||||
root = DocumentFile.fromTreeUri(this, Uri.parse(location));
|
||||
} else {
|
||||
root = DocumentFile.fromFile(new File(location));
|
||||
}
|
||||
|
||||
DocumentFile artist = root.findFile(MusicUtil.ascii(song.artistName));
|
||||
DocumentFile artist;
|
||||
DocumentFile album;
|
||||
|
||||
synchronized (lock) {
|
||||
artist = root.findFile(MusicUtil.ascii(song.artistName));
|
||||
if (artist == null) {
|
||||
artist = root.createDirectory(MusicUtil.ascii(song.artistName));
|
||||
}
|
||||
DocumentFile album = artist.findFile(MusicUtil.ascii(song.albumName));
|
||||
album = artist.findFile(MusicUtil.ascii(song.albumName));
|
||||
if (album == null) {
|
||||
album = artist.createDirectory(MusicUtil.ascii(song.albumName));
|
||||
}
|
||||
}
|
||||
|
||||
String fileName = song.discNumber + "." + song.trackNumber + " - " + MusicUtil.ascii(song.title) + "." + song.container;
|
||||
DocumentFile audio = album.createFile("audio/" + song.container, fileName);
|
||||
|
|
@ -116,7 +127,8 @@ public class DownloadService extends Service {
|
|||
App.getDatabase().cacheDao().insertCache(new Cache(song));
|
||||
notification.stop(song);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Failed to download song: " + song.title, e);
|
||||
ErrorNotification.show(this, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,9 +46,19 @@ public class LoginService extends Service {
|
|||
|
||||
if (user == null) {
|
||||
Toast.makeText(this, context.getResources().getString(R.string.error_unexpected), Toast.LENGTH_SHORT).show();
|
||||
sendBroadcast(new Intent(STATE_OFFLINE));
|
||||
return;
|
||||
}
|
||||
|
||||
if (App.getApiClient() == null) {
|
||||
try {
|
||||
App.createApiClient(context);
|
||||
} catch (Exception e) {
|
||||
sendBroadcast(new Intent(STATE_OFFLINE));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
App.getApiClient().ChangeServerLocation(user.server);
|
||||
App.getApiClient().SetAuthenticationInfo(user.token, user.id);
|
||||
App.getApiClient().GetSystemInfoAsync(new Response<SystemInfo>() {
|
||||
|
|
|
|||
|
|
@ -1,114 +1,134 @@
|
|||
package org.adrianvictor.geleia.service.notifications;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import org.adrianvictor.geleia.R;
|
||||
import org.adrianvictor.geleia.activities.MainActivity;
|
||||
import org.adrianvictor.geleia.model.Song;
|
||||
import org.adrianvictor.geleia.service.DownloadService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static android.content.Context.NOTIFICATION_SERVICE;
|
||||
|
||||
public class DownloadNotification {
|
||||
private static final String CHANNEL_ID = DownloadNotification.class.getSimpleName();
|
||||
private static final int NOTIFICATION_ID = 2;
|
||||
|
||||
private static final String CHANNEL_ID = "download_channel";
|
||||
private final int ID = 2;
|
||||
private final Context context;
|
||||
private final NotificationManager notificationManager;
|
||||
private final List<Song> queue = Collections.synchronizedList(new LinkedList<>());
|
||||
|
||||
private final List<Song> songs;
|
||||
|
||||
private int current;
|
||||
private int maximum;
|
||||
private long totalSize;
|
||||
private long downloadedSize;
|
||||
private int lastPercentage = -1;
|
||||
|
||||
public DownloadNotification(Context context) {
|
||||
this.notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
|
||||
this.context = context;
|
||||
|
||||
this.songs = new ArrayList<>();
|
||||
}
|
||||
|
||||
public synchronized void start(Song song) {
|
||||
this.songs.add(song);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
createNotificationChannel();
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
context.getString(R.string.download_channel_name),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void update(int current, int maximum) {
|
||||
this.current += current;
|
||||
this.maximum += maximum;
|
||||
|
||||
Intent action = new Intent(context, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
PendingIntent clickIntent = PendingIntent.getActivity(context, 0, action, PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
Intent cancel = new Intent(context, DownloadService.class).setAction(DownloadService.ACTION_CANCEL);
|
||||
PendingIntent pendingCancel = PendingIntent.getService(context, 0, cancel, PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
|
||||
for (Song item : songs.stream().limit(5).collect(Collectors.toList())) {
|
||||
style.addLine(item.title);
|
||||
public void start(Song song) {
|
||||
queue.add(song);
|
||||
if (queue.size() == 1) { // This is the first song of a new batch
|
||||
totalSize = 0;
|
||||
downloadedSize = 0;
|
||||
lastPercentage = -1;
|
||||
notificationManager.notify(ID, getNotification());
|
||||
} else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
|
||||
// For KitKat, update for every new song to show correct count.
|
||||
notificationManager.notify(ID, getNotification());
|
||||
}
|
||||
}
|
||||
|
||||
public void update(long downloaded, long total) {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
|
||||
return; // No progress updates for KitKat
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
totalSize += total;
|
||||
downloadedSize += downloaded;
|
||||
|
||||
int percentage = 0;
|
||||
if (totalSize > 0) {
|
||||
percentage = (int) ((downloadedSize * 100) / totalSize);
|
||||
}
|
||||
|
||||
if (percentage > lastPercentage) {
|
||||
lastPercentage = percentage;
|
||||
notificationManager.notify(ID, getNotification());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void stop(Song song) {
|
||||
queue.remove(song);
|
||||
if (queue.isEmpty()) {
|
||||
notificationManager.cancel(ID);
|
||||
} else {
|
||||
// Update notification to show new queue size.
|
||||
// On KitKat, this is the only update after a download finishes.
|
||||
notificationManager.notify(ID, getNotification());
|
||||
}
|
||||
}
|
||||
|
||||
public void cancelAll() {
|
||||
queue.clear();
|
||||
notificationManager.cancel(ID);
|
||||
}
|
||||
|
||||
private Notification getNotification() {
|
||||
Intent intent = new Intent(context, DownloadService.class);
|
||||
intent.setAction(DownloadService.ACTION_CANCEL);
|
||||
|
||||
int flags = 0;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
flags |= PendingIntent.FLAG_IMMUTABLE;
|
||||
}
|
||||
PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, flags);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentIntent(clickIntent)
|
||||
.setContentTitle(String.format(context.getString(R.string.downloading_x_songs), songs.size()))
|
||||
.setProgress(this.maximum, this.current, false)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.addAction(R.drawable.ic_close_white_24dp, context.getString(R.string.action_cancel), pendingCancel)
|
||||
.setStyle(style)
|
||||
.setShowWhen(false);
|
||||
.setContentTitle(context.getString(R.string.downloading_songs))
|
||||
.setOngoing(true)
|
||||
.addAction(android.R.drawable.ic_menu_close_clear_cancel, context.getString(android.R.string.cancel), pendingIntent);
|
||||
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
||||
String contentText = context.getResources().getQuantityString(R.plurals.downloading_s_songs, queue.size(), queue.size());
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.notification_download);
|
||||
remoteViews.setTextViewText(R.id.notification_download_title, contentText);
|
||||
int progress = lastPercentage > 0 ? lastPercentage : 0;
|
||||
remoteViews.setProgressBar(R.id.notification_download_progress, 100, progress, totalSize == 0 && downloadedSize == 0);
|
||||
builder.setCustomContentView(remoteViews);
|
||||
} else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
|
||||
int progress = lastPercentage > 0 ? lastPercentage : 0;
|
||||
builder.setProgress(100, progress, totalSize == 0 && downloadedSize == 0);
|
||||
builder.setContentText(contentText);
|
||||
} else { // KitKat
|
||||
builder.setContentText(contentText);
|
||||
}
|
||||
|
||||
public synchronized void stop(Song song) {
|
||||
if (song != null) {
|
||||
songs.remove(song);
|
||||
} else {
|
||||
songs.clear();
|
||||
}
|
||||
|
||||
if (songs.size() != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
current = 0;
|
||||
maximum = 0;
|
||||
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notificationManager.deleteNotificationChannel(CHANNEL_ID);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private void createNotificationChannel() {
|
||||
NotificationChannel notificationChannel = notificationManager.getNotificationChannel(CHANNEL_ID);
|
||||
|
||||
if (notificationChannel == null) {
|
||||
notificationChannel = new NotificationChannel(CHANNEL_ID, context.getString(R.string.action_download), NotificationManager.IMPORTANCE_LOW);
|
||||
|
||||
notificationChannel.setDescription(context.getString(R.string.playing_notification_description));
|
||||
notificationChannel.enableLights(false);
|
||||
notificationChannel.enableVibration(false);
|
||||
|
||||
notificationManager.createNotificationChannel(notificationChannel);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package org.adrianvictor.geleia.service.notifications;
|
||||
|
||||
import static android.content.Context.NOTIFICATION_SERVICE;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import org.adrianvictor.geleia.R;
|
||||
|
||||
public class ErrorNotification {
|
||||
private static final String CHANNEL_ID = ErrorNotification.class.getSimpleName();
|
||||
private static final int NOTIFICATION_ID = 3;
|
||||
|
||||
private ErrorNotification() {}
|
||||
|
||||
public static void show(Context context, String error) {
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
|
||||
createNotificationChannel(notificationManager);
|
||||
|
||||
NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle();
|
||||
style.setBigContentTitle("Error:").bigText(error);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_bug_report_white_24dp)
|
||||
.setContentTitle(context.getString(R.string.error_notification_title))
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setStyle(style)
|
||||
.setContentText("Expand the notification for details.");
|
||||
}
|
||||
|
||||
public static void createNotificationChannel(NotificationManager notificationManager) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = notificationManager.getNotificationChannel(CHANNEL_ID);
|
||||
|
||||
if (channel == null) {
|
||||
channel = new NotificationChannel(CHANNEL_ID, "Errors", NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setDescription("Displays all sorts of errors.");
|
||||
channel.enableLights(false);
|
||||
channel.enableVibration(true);
|
||||
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package org.adrianvictor.geleia.service.playback;
|
|||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
|
@ -141,12 +142,18 @@ public class LocalPlayer implements Playback {
|
|||
|
||||
private List<MediaItem> createMediaItems(List<Song> queue) {
|
||||
return queue.stream().map(song -> {
|
||||
File audio = new File(MusicUtil.getFileUri(song));
|
||||
Uri uri = Uri.fromFile(audio);
|
||||
String fileUri = MusicUtil.getFileUri(song);
|
||||
Uri uri;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && fileUri.startsWith("content://")) {
|
||||
uri = Uri.parse(fileUri);
|
||||
} else {
|
||||
File audio = new File(fileUri);
|
||||
uri = Uri.fromFile(audio);
|
||||
|
||||
if (!audio.exists()) {
|
||||
uri = Uri.parse(MusicUtil.getTranscodeUri(song));
|
||||
}
|
||||
}
|
||||
|
||||
List<String> containers = PreferenceUtil.getInstance(context).getDirectPlayCodecs().stream()
|
||||
.map(codec -> codec.container.toLowerCase(Locale.ROOT))
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import androidx.annotation.ColorInt;
|
|||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
|
||||
|
|
@ -91,18 +92,22 @@ public class ImageUtil {
|
|||
}
|
||||
|
||||
public static Drawable getTintedVectorDrawable(@NonNull Context context, @DrawableRes int resId, @ColorInt int color) {
|
||||
Drawable drawable = getVectorDrawable(context.getResources(), resId, context.getTheme());
|
||||
final Drawable drawable = AppCompatResources.getDrawable(context, resId);
|
||||
|
||||
DrawableCompat.setTintMode(drawable, PorterDuff.Mode.SRC_IN);
|
||||
DrawableCompat.setTint(drawable, color);
|
||||
if (drawable != null) {
|
||||
Drawable wrappedDrawable = DrawableCompat.wrap(drawable).mutate();
|
||||
|
||||
return drawable;
|
||||
DrawableCompat.setTintMode(wrappedDrawable, PorterDuff.Mode.SRC_IN);
|
||||
DrawableCompat.setTint(wrappedDrawable, color);
|
||||
return wrappedDrawable;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static Drawable getVectorDrawable(@NonNull Context context, @DrawableRes int resId) {
|
||||
return getVectorDrawable(context.getResources(), resId, context.getTheme());
|
||||
return AppCompatResources.getDrawable(context, resId);
|
||||
}
|
||||
|
||||
public static Drawable resolveDrawable(@NonNull Context context, @AttrRes int drawableAttr) {
|
||||
TypedArray a = context.obtainStyledAttributes(new int[]{drawableAttr});
|
||||
Drawable drawable = a.getDrawable(0);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package org.adrianvictor.geleia.util;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
|
@ -18,6 +19,7 @@ import org.adrianvictor.geleia.model.Codec;
|
|||
import org.adrianvictor.geleia.model.Genre;
|
||||
import org.adrianvictor.geleia.model.Song;
|
||||
|
||||
import org.adrianvictor.geleia.service.notifications.ErrorNotification;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.dto.UserItemDataDto;
|
||||
|
|
@ -49,7 +51,7 @@ public class MusicUtil {
|
|||
List<Codec> codecs = preferenceUtil.getDirectPlayCodecs();
|
||||
Stream<String> values = codecs.stream().map(codec -> codec.value);
|
||||
|
||||
if (codecs.size() != 0) {
|
||||
if (!codecs.isEmpty()) {
|
||||
builder.append("&Container=").append(values.collect(Collectors.joining(",")));
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +83,8 @@ public class MusicUtil {
|
|||
public static String getFileUri(Song song) {
|
||||
String location = PreferenceUtil.getInstance(App.getInstance()).getLocationDownload();
|
||||
File root = new File(location, "music");
|
||||
if (!location.equals(App.getInstance().getCacheDir().toString())) {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !location.equals(App.getInstance().getCacheDir().toString())) {
|
||||
Uri uri = Uri.parse(location);
|
||||
return new File(uri.getPath()).getAbsolutePath();
|
||||
}
|
||||
|
|
@ -102,7 +105,7 @@ public class MusicUtil {
|
|||
try {
|
||||
return new Intent();
|
||||
} catch (IllegalArgumentException e) {
|
||||
e.printStackTrace();
|
||||
ErrorNotification.show(context, e.getMessage());
|
||||
Toast.makeText(context, R.string.error_share_file, Toast.LENGTH_SHORT).show();
|
||||
return new Intent();
|
||||
}
|
||||
|
|
@ -110,7 +113,7 @@ public class MusicUtil {
|
|||
|
||||
@NonNull
|
||||
public static String getArtistInfoString(@NonNull final Context context, @NonNull final Artist artist) {
|
||||
return artist.genres.size() != 0 ? artist.genres.get(0).name : "";
|
||||
return !artist.genres.isEmpty() ? artist.genres.get(0).name : "";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import androidx.core.util.Pair;
|
|||
import org.adrianvictor.geleia.activities.LoginActivity;
|
||||
import org.adrianvictor.geleia.activities.MainActivity;
|
||||
import org.adrianvictor.geleia.activities.SelectActivity;
|
||||
import org.adrianvictor.geleia.activities.UnreachableActivity;
|
||||
import org.adrianvictor.geleia.model.Album;
|
||||
import org.adrianvictor.geleia.model.Artist;
|
||||
import org.adrianvictor.geleia.model.Genre;
|
||||
|
|
@ -61,6 +62,13 @@ public class NavigationUtil {
|
|||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static void startUnreachable(Context context) {
|
||||
final Intent intent = new Intent(context, UnreachableActivity.class);
|
||||
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static void startSelect(Context context) {
|
||||
final Intent intent = new Intent(context, SelectActivity.class);
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public class PlaylistUtil {
|
|||
PlaylistCreationRequest request = new PlaylistCreationRequest();
|
||||
request.setUserId(App.getApiClient().getCurrentUserId());
|
||||
request.setName(name);
|
||||
if (ids.size() != 0) request.setItemIdList(ids);
|
||||
if (!ids.isEmpty()) request.setItemIdList(ids);
|
||||
App.getApiClient().CreatePlaylist(request, new Response<>());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -448,7 +448,6 @@ public final class PreferenceUtil {
|
|||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@SuppressWarnings("SimplifyStreamApiCallChains")
|
||||
public void setCategories(List<Category> categories) {
|
||||
List<String> values = categories.stream().map(category -> {
|
||||
return category.select ? category.toString() : category.toString().toLowerCase();
|
||||
|
|
|
|||
|
|
@ -10,23 +10,27 @@ import org.adrianvictor.geleia.util.ThemeUtil;
|
|||
import org.adrianvictor.geleia.R;
|
||||
|
||||
public class IconImageView extends AppCompatImageView {
|
||||
|
||||
public IconImageView(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public IconImageView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public IconImageView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
if (context == null) return;
|
||||
setColorFilter(ThemeUtil.getColorResource(context, R.attr.iconColor), PorterDuff.Mode.SRC_IN);
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
if (getContext() == null) return;
|
||||
setColorFilter(ThemeUtil.getColorResource(getContext(), R.attr.iconColor), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public class DynamicShortcutManager {
|
|||
}
|
||||
|
||||
public void initDynamicShortcuts() {
|
||||
if (shortcutManager.getDynamicShortcuts().size() == 0) {
|
||||
if (shortcutManager.getDynamicShortcuts().isEmpty()) {
|
||||
shortcutManager.setDynamicShortcuts(getDefaultShortcuts());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,4 @@
|
|||
<vector android:height="192dp" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="192dp"
|
||||
xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:pathData="m12,9.8425c-1.1866,0 -2.1575,0.9709 -2.1575,2.1575s0.9709,2.1575 2.1575,2.1575 2.1575,-0.9709 2.1575,-2.1575 -0.9709,-2.1575 -2.1575,-2.1575zM12,1c-6.072,0 -11,4.928 -11,11s4.928,11 11,11 11,-4.928 11,-11 -4.928,-11 -11,-11zM12,7.05c2.739,0 4.95,2.211 4.95,4.95s-2.211,4.95 -4.95,4.95 -4.95,-2.211 -4.95,-4.95 2.211,-4.95 4.95,-4.95z" android:strokeWidth="2.1575">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="25.2" android:endY="25.1996"
|
||||
android:startX="1" android:startY="0.99960005" android:type="linear">
|
||||
<item android:color="#FFAA5CC3" android:offset="0"/>
|
||||
<item android:color="#FF00A4DC" android:offset="1"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</selector>
|
||||
|
|
@ -19,21 +19,22 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<ImageView
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="40dp"
|
||||
android:layout_marginBottom="40dp"
|
||||
app:srcCompat="@drawable/ic_launcher_nodpi"
|
||||
tools:ignore="ContentDescription"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/username_layout" />
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:id="@+id/username_layout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
|
|
@ -43,27 +44,24 @@
|
|||
app:boxCornerRadiusBottomStart="24dp"
|
||||
app:boxCornerRadiusTopEnd="24dp"
|
||||
app:boxCornerRadiusTopStart="24dp"
|
||||
app:errorEnabled="true"
|
||||
app:boxStrokeColor="?android:textColorSecondary"
|
||||
app:endIconMode="clear_text"
|
||||
app:endIconTint="?android:textColorSecondary"
|
||||
app:boxStrokeColor="?android:textColorSecondary"
|
||||
app:hintTextColor="?android:textColorSecondary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/password_layout">
|
||||
app:errorEnabled="true"
|
||||
app:hintTextColor="?android:textColorSecondary">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:hint="@string/username" />
|
||||
android:hint="@string/username"
|
||||
android:inputType="textNoSuggestions" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:id="@+id/password_layout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
|
|
@ -73,27 +71,24 @@
|
|||
app:boxCornerRadiusBottomStart="24dp"
|
||||
app:boxCornerRadiusTopEnd="24dp"
|
||||
app:boxCornerRadiusTopStart="24dp"
|
||||
app:errorEnabled="true"
|
||||
app:boxStrokeColor="?android:textColorSecondary"
|
||||
app:endIconMode="password_toggle"
|
||||
app:endIconTint="?android:textColorSecondary"
|
||||
app:boxStrokeColor="?android:textColorSecondary"
|
||||
app:hintTextColor="?android:textColorSecondary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/server_layout">
|
||||
app:errorEnabled="true"
|
||||
app:hintTextColor="?android:textColorSecondary">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:hint="@string/password" />
|
||||
android:hint="@string/password"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:id="@+id/server_layout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
|
|
@ -103,21 +98,18 @@
|
|||
app:boxCornerRadiusBottomStart="24dp"
|
||||
app:boxCornerRadiusTopEnd="24dp"
|
||||
app:boxCornerRadiusTopStart="24dp"
|
||||
app:errorEnabled="true"
|
||||
app:boxStrokeColor="?android:textColorSecondary"
|
||||
app:endIconMode="clear_text"
|
||||
app:endIconTint="?android:textColorSecondary"
|
||||
app:boxStrokeColor="?android:textColorSecondary"
|
||||
app:hintTextColor="?android:textColorSecondary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/login">
|
||||
app:errorEnabled="true"
|
||||
app:hintTextColor="?android:textColorSecondary">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/server"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:hint="@string/server" />
|
||||
android:hint="@string/server"
|
||||
android:inputType="textUri" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
|
@ -129,10 +121,7 @@
|
|||
android:layout_marginTop="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/login"
|
||||
app:cornerRadius="24dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/select" />
|
||||
app:cornerRadius="24dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/select"
|
||||
|
|
@ -141,12 +130,11 @@
|
|||
android:layout_marginHorizontal="32dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/select"
|
||||
android:backgroundTint="?cardBackgroundColor"
|
||||
android:text="@string/select"
|
||||
app:cornerRadius="24dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
|||
32
app/src/main/res/layout/activity_unreachable.xml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context="org.adrianvictor.geleia.activities.UnreachableActivity">
|
||||
|
||||
<include layout="@layout/status_bar" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_margin="16dp"
|
||||
android:clipToPadding="false"
|
||||
android:isScrollContainer="true">
|
||||
|
||||
<include layout="@layout/fragment_offline" />
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:onClick="onSelectClick"
|
||||
android:text="@string/change_server" />
|
||||
</LinearLayout>
|
||||
|
|
@ -82,6 +82,50 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- <LinearLayout-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:background="?attr/rectSelector"-->
|
||||
<!-- android:clickable="true"-->
|
||||
<!-- android:focusable="true"-->
|
||||
<!-- android:gravity="center_vertical"-->
|
||||
<!-- android:minHeight="@dimen/item_height"-->
|
||||
<!-- android:orientation="horizontal"-->
|
||||
<!-- android:paddingLeft="16dp"-->
|
||||
<!-- android:paddingRight="16dp">-->
|
||||
|
||||
<!-- <org.adrianvictor.geleia.views.IconImageView-->
|
||||
<!-- android:id="@+id/icon_license"-->
|
||||
<!-- android:layout_width="24dp"-->
|
||||
<!-- android:layout_height="24dp"-->
|
||||
<!-- app:srcCompat="@android:drawable/ic_lock_idle_lock"-->
|
||||
<!-- tools:ignore="ContentDescription" />-->
|
||||
|
||||
<!-- <LinearLayout-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:layout_marginStart="32dp"-->
|
||||
<!-- android:orientation="vertical"-->
|
||||
<!-- android:paddingBottom="8dp"-->
|
||||
<!-- android:paddingTop="8dp">-->
|
||||
|
||||
<!-- <TextView-->
|
||||
<!-- android:layout_width="wrap_content"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:text="@string/license"-->
|
||||
<!-- android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />-->
|
||||
|
||||
<!-- <TextView-->
|
||||
<!-- android:id="@+id/app_license"-->
|
||||
<!-- android:layout_width="wrap_content"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:textAppearance="@style/TextAppearance.AppCompat.Caption"-->
|
||||
<!-- tools:text="@string/application_license" />-->
|
||||
|
||||
<!-- </LinearLayout>-->
|
||||
|
||||
<!-- </LinearLayout>-->
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/app_source"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -113,6 +157,14 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/application_license" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:layout_margin="20dp"
|
||||
android:background="@drawable/card_server"
|
||||
android:elevation="4dp"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@
|
|||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/dividerColor" />
|
||||
android:layout_marginBottom="10dp"
|
||||
android:background="?attr/dividerColor"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -76,6 +77,12 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:background="?attr/dividerColor" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
@ -134,6 +141,7 @@
|
|||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:background="?attr/dividerColor" />
|
||||
|
||||
<LinearLayout
|
||||
|
|
@ -186,6 +194,7 @@
|
|||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:background="?attr/dividerColor" />
|
||||
|
||||
<LinearLayout
|
||||
|
|
@ -238,6 +247,7 @@
|
|||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:background="?attr/dividerColor" />
|
||||
|
||||
<LinearLayout
|
||||
|
|
@ -298,6 +308,7 @@
|
|||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:background="?attr/dividerColor" />
|
||||
|
||||
<LinearLayout
|
||||
|
|
|
|||
13
app/src/main/res/layout/downloads_fragment.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.adrianvictor.geleia.fragments.library.DownloadsFragment">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:layout_width="409dp"
|
||||
android:layout_height="729dp"
|
||||
tools:layout_editor_absoluteX="1dp"
|
||||
tools:layout_editor_absoluteY="1dp" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
40
app/src/main/res/layout/fragment_offline.xml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:layout_margin="32dp"
|
||||
tools:context=".fragments.OfflineFragment">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/offline_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/sad_face"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Display3" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/offline_icon"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/oops"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/error_title"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/server_is_unreachable"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
|
||||
|
||||
</RelativeLayout>
|
||||
23
app/src/main/res/layout/notification_download.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_download_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Compat.Notification.Title"
|
||||
tools:text="Downloading 2 songs" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/notification_download_progress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 40 KiB |
|
|
@ -235,5 +235,22 @@
|
|||
<string name="error_empty_username">Please fill in your username.</string>
|
||||
<string name="dkanada_summary">Forking Phonograph and making Gelli</string>
|
||||
<string name="adrianvictor">Adrian Victor</string>
|
||||
<string name="error_notification_title">An error occurred in Jamfish.</string>
|
||||
<string name="offline">You are offline.</string>
|
||||
<string name="change_server">Change server</string>
|
||||
<string name="oops">Oops! I did it again...</string>
|
||||
<string name="sad_face">;(</string>
|
||||
<string name="server_is_unreachable">Sorry, but we couldn\'t reach this server right now.</string>
|
||||
<string name="license">License</string>
|
||||
<string name="app_license">GNU General Public License v3.0</string>
|
||||
<string name="application_license">GNU General Public License v3.0</string>
|
||||
|
||||
<string name="download_channel_name">Downloads</string>
|
||||
<string name="downloading_songs">Downloading songs</string>
|
||||
<string name="downloads">Downloads</string>
|
||||
<plurals name="downloading_s_songs">
|
||||
<item quantity="one">Downloading %d song</item>
|
||||
<item quantity="other">Downloading %d songs</item>
|
||||
</plurals>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1 +1,16 @@
|
|||
This is a native music player for Android devices that connects to Jellyfin media servers. The code is based on Gelli's archived repository, which is based on an old version of Phonograph. Jamfish is made for personal use, but contributions are welcome! Please open an issue to discuss larger changes before submitting a pull request.
|
||||
|
||||
Features
|
||||
- Basic library navigation
|
||||
- Download songs to internal storage individually or through batch actions
|
||||
- Gapless playback
|
||||
- Sort albums and songs by different fields
|
||||
- Search media for partial matches
|
||||
- Media service integration with notification
|
||||
- Favorites and playlists
|
||||
- Playback history reporting
|
||||
- Filter content by library
|
||||
|
||||
Requisites:
|
||||
- A Jellfin server
|
||||
- Android 4.4 or later
|
||||
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 650 KiB |
|
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 340 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |