diff --git a/.github/dependabot.yml b/.github/dependabot.yml index eb12cbda..5fa213c4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,4 +8,8 @@ updates: - package-ecosystem: "gradle" # See documentation for possible values directory: "/" # Location of package manifests schedule: - interval: "weekly" + interval: "monthly" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch"] + diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Album.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Album.kt index 5dacd51b..801634c3 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Album.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Album.kt @@ -5,14 +5,19 @@ import java.util.Calendar data class Album( val id: String = "", + val parent: String = "", + val album: String = "", + val title: String = "", val name: String = "", + val discNumber: Int = 0, val coverArt: String = "", + val songCount: Int = 0, + val created: Calendar? = null, val artist: String = "", val artistId: String = "", - val songCount: Int = 0, val duration: Int = 0, - val created: Calendar? = null, val year: Int = 0, val genre: String = "", - @JsonProperty("song") val songList: List = emptyList() + @JsonProperty("song") val songList: List = emptyList(), + @JsonProperty("starred") val starredDate: String = "" ) diff --git a/dependencies.gradle b/dependencies.gradle index 33107526..82182134 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -3,13 +3,13 @@ ext.versions = [ targetSdk : 29, compileSdk : 29, // You need to run ./gradlew wrapper after updating the version - gradle : '7.0', + gradle : '7.2', navigation : "2.3.5", gradlePlugin : "4.2.2", androidxcore : "1.5.0", ktlint : "0.37.1", - ktlintGradle : "9.2.1", + ktlintGradle : "10.2.0", detekt : "1.18.1", jacoco : "0.8.7", preferences : "1.1.1", diff --git a/fastlane/metadata/android/en-US/changelogs/97.txt b/fastlane/metadata/android/en-US/changelogs/97.txt new file mode 100644 index 00000000..bd47354a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/97.txt @@ -0,0 +1,2 @@ +Bug fixes +- #571: Fixed media session null checks. diff --git a/fastlane/metadata/android/en-US/changelogs/98.txt b/fastlane/metadata/android/en-US/changelogs/98.txt new file mode 100644 index 00000000..f490f704 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/98.txt @@ -0,0 +1,15 @@ +Bug fixes +- #594: Added PlaybackComplete intent when a song playback is completed. +- #593: Fixed album lists. +- #602: Fix NPE. + +Enhancements +- #558: Video call can be static. +- #559: Add better offline Support. +- #568: Rework Downloader. +- #567: Use semantically correct API endpoint when streaming/downloading. +- #572: Moved drag handle to the left in the Now Playing list. +- #585: Added setting to disable Now Playing List sending for incompatible + bluetooth devices. +- #596: Added option whether to create a share on the server when sharing + songs. diff --git a/fastlane/metadata/android/es-ES/changelogs/97.txt b/fastlane/metadata/android/es-ES/changelogs/97.txt new file mode 100644 index 00000000..25abfb8e --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/97.txt @@ -0,0 +1,2 @@ +Correción de errores +- #571: Se comprueban los valores nulos en la sesión de medios. diff --git a/fastlane/metadata/android/es-ES/changelogs/98.txt b/fastlane/metadata/android/es-ES/changelogs/98.txt new file mode 100644 index 00000000..c53e8d13 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/98.txt @@ -0,0 +1,18 @@ +Correción de errores +- #594: Agregado un intent de PlaybackComplete cuando se completa la + reproducción de una canción. +- #593: Corregidas las listas de álbumes. +- #602: NPE corregido. + +Mejoras +- #558: La llamada a video puede ser estática. +- #559: Agregado un mejor soporte sin conexión. +- #568: Se ha reescrito el downloader. +- #567: Se utiliza el endpoint semánticamente correcto al realizar streaming + o descargar. +- #572: Se ha movido el botón de arrastre de canción hacia la izquierda en + la lista de reproducción. +- #585: Agregada una configuración para deshabilitar el envío de la Lista de + reproducción en curso para dispositivos Bluetooth incompatibles. +- #596: Se agregó la opción de crear un recurso compartido en el servidor al + compartir canciones. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3c4101c3..a0f7639f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle_scripts/kotlin-module-bootstrap.gradle b/gradle_scripts/kotlin-module-bootstrap.gradle index 1eb26ec6..fc84bbd8 100644 --- a/gradle_scripts/kotlin-module-bootstrap.gradle +++ b/gradle_scripts/kotlin-module-bootstrap.gradle @@ -32,9 +32,9 @@ ext { jacocoTestReport { reports { - html.enabled true - csv.enabled false - xml.enabled true + html.required = true + xml.required = false + csv.required = false } afterEvaluate { diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 6c676aa5..665c987e 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -9,8 +9,8 @@ android { defaultConfig { applicationId "org.moire.ultrasonic" - versionCode 95 - versionName "2.23.0" + versionCode 98 + versionName "2.24.0" minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index e9795cea..d4b7fda1 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -1,5 +1,5 @@ - + - - - - - - - - - - - - @@ -99,61 +66,6 @@ column="14"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -414,7 +172,7 @@ errorLine2=" ~~~~~~~~"> @@ -451,28 +209,6 @@ column="9"/> - - - - - - - - @@ -889,11 +625,11 @@ column="13"/> @@ -1202,11 +938,11 @@ column="13"/> @@ -1308,11 +1044,11 @@ column="13"/> @@ -1429,43 +1165,47 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - - - + + + + @@ -1509,18 +1249,7 @@ errorLine2=" ^"> - - - - @@ -1531,7 +1260,7 @@ errorLine2=" ^"> @@ -1542,7 +1271,7 @@ errorLine2=" ^"> @@ -1553,29 +1282,7 @@ errorLine2=" ^"> - - - - - - - - @@ -1586,7 +1293,7 @@ errorLine2=" ^"> @@ -1597,7 +1304,7 @@ errorLine2=" ^"> @@ -1705,7 +1412,7 @@ errorLine2=" ~~~~~~~~~"> @@ -1727,7 +1434,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1837,7 +1544,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2277,7 +1984,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2310,7 +2017,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -2325,61 +2032,6 @@ column="6"/> - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -2415,7 +2122,7 @@ @@ -2633,1650 +2340,4 @@ column="6"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index e80ca5f7..8b709fbd 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -27,7 +27,9 @@ android:theme="@style/NoActionBar" android:name=".app.UApp" android:label="@string/common.appname" - android:usesCleartextTraffic="true"> + android:usesCleartextTraffic="true" + android:supportsRtl="false" + tools:ignore="UnusedAttribute"> diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java index 57106064..c1fb0466 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java @@ -141,7 +141,6 @@ public class SettingsFragment extends PreferenceFragmentCompat sharingDefaultGreeting.setText(Settings.getShareGreeting()); setupClearSearchPreference(); - setupGaplessControlSettingsV14(); setupFeatureFlagsPreferences(); setupCacheLocationPreference(); setupBluetoothDevicePreferences(); @@ -221,8 +220,7 @@ public class SettingsFragment extends PreferenceFragmentCompat } private void setupCacheLocationPreference() { - cacheLocation.setSummary(settings.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, - FileUtil.getDefaultMusicDirectory().getPath())); + cacheLocation.setSummary(Settings.getCacheLocation()); cacheLocation.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override @@ -373,24 +371,6 @@ public class SettingsFragment extends PreferenceFragmentCompat } - private void setupGaplessControlSettingsV14() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { - PreferenceCategory playbackControlSettings = - findPreference(Constants.PREFERENCES_KEY_PLAYBACK_CONTROL_SETTINGS); - CheckBoxPreference gaplessPlaybackEnabled = - findPreference(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK); - - if (gaplessPlaybackEnabled != null) { - gaplessPlaybackEnabled.setChecked(false); - gaplessPlaybackEnabled.setEnabled(false); - - if (playbackControlSettings != null) { - playbackControlSettings.removePreference(gaplessPlaybackEnabled); - } - } - } - } - private void update() { theme.setSummary(theme.getEntry()); maxBitrateWifi.setSummary(maxBitrateWifi.getEntry()); @@ -412,8 +392,7 @@ public class SettingsFragment extends PreferenceFragmentCompat sharingDefaultExpiration.setSummary(sharingDefaultExpiration.getText()); sharingDefaultDescription.setSummary(sharingDefaultDescription.getText()); sharingDefaultGreeting.setSummary(sharingDefaultGreeting.getText()); - cacheLocation.setSummary(settings.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, - FileUtil.getDefaultMusicDirectory().getPath())); + cacheLocation.setSummary(Settings.getCacheLocation()); if (!mediaButtonsEnabled.isChecked()) { lockScreenEnabled.setChecked(false); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/PermissionUtil.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/PermissionUtil.java deleted file mode 100644 index 8ac36b07..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/PermissionUtil.java +++ /dev/null @@ -1,233 +0,0 @@ -package org.moire.ultrasonic.util; - -import android.Manifest; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import timber.log.Timber; - -import androidx.appcompat.app.AlertDialog; -import androidx.core.content.PermissionChecker; - -import com.karumi.dexter.Dexter; -import com.karumi.dexter.MultiplePermissionsReport; -import com.karumi.dexter.PermissionToken; -import com.karumi.dexter.listener.DexterError; -import com.karumi.dexter.listener.PermissionRequest; -import com.karumi.dexter.listener.PermissionRequestErrorListener; -import com.karumi.dexter.listener.multi.MultiplePermissionsListener; - -import org.moire.ultrasonic.R; - -import java.util.List; - -import static androidx.core.content.PermissionChecker.PERMISSION_DENIED; - - -/** - * Contains static functions for Permission handling - */ -public class PermissionUtil { - - private Context activityContext; - private final Context applicationContext; - - public PermissionUtil(Context context) { - applicationContext = context; - } - - public interface PermissionRequestFinishedCallback { - void onPermissionRequestFinished(boolean hasPermission); - } - - public void ForegroundApplicationStarted(Context context) { - this.activityContext = context; - } - - public void ForegroundApplicationStopped() { - activityContext = null; - } - - /** - * This function can be used to handle file access permission failures. - * - * It will check if the failure is because the necessary permissions aren't available, - * and it will request them, if necessary. - * - * @param callback callback function to execute after the permission request is finished - */ - public void handlePermissionFailed(final PermissionRequestFinishedCallback callback) { - String currentCachePath = Settings.getPreferences().getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory().getPath()); - String defaultCachePath = FileUtil.getDefaultMusicDirectory().getPath(); - - // Ultrasonic can do nothing about this error when the Music Directory is already set to the default. - if (currentCachePath.compareTo(defaultCachePath) == 0) return; - - if ((PermissionChecker.checkSelfPermission(applicationContext, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_DENIED) || - (PermissionChecker.checkSelfPermission(applicationContext, Manifest.permission.READ_EXTERNAL_STORAGE) == PERMISSION_DENIED)) { - // While we request permission, the Music Directory is temporarily reset to its default location - setCacheLocation(applicationContext, FileUtil.getDefaultMusicDirectory().getPath()); - // If the application is not running, we can't notify the user - if (activityContext == null) return; - requestFailedPermission(activityContext, currentCachePath, callback); - } else { - setCacheLocation(applicationContext, FileUtil.getDefaultMusicDirectory().getPath()); - // If the application is not running, we can't notify the user - if (activityContext != null) { - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - showWarning(activityContext, activityContext.getString(R.string.permissions_message_box_title), activityContext.getString(R.string.permissions_access_error), null); - } - }); - } - callback.onPermissionRequestFinished(false); - } - } - - /** - * This function requests permission to access the filesystem. - * It can be used to request the permission initially, e.g. when the user decides to use a non-default folder for the cache - * @param context context for the operation - * @param callback callback function to execute after the permission request is finished - */ - public static void requestInitialPermission(final Context context, final PermissionRequestFinishedCallback callback) { - Dexter.withContext(context) - .withPermissions( - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE) - .withListener(new MultiplePermissionsListener() { - @Override - public void onPermissionsChecked(MultiplePermissionsReport report) { - if (report.areAllPermissionsGranted()) { - Timber.i("Permission granted to read / write external storage"); - if (callback != null) callback.onPermissionRequestFinished(true); - return; - } - - if (report.isAnyPermissionPermanentlyDenied()) { - Timber.i("Found permanently denied permission to read / write external storage, offering settings"); - showSettingsDialog(context); - if (callback != null) callback.onPermissionRequestFinished(false); - return; - } - - Timber.i("At least one permission is missing to read / write external storage"); - showWarning(context, context.getString(R.string.permissions_message_box_title), - context.getString(R.string.permissions_rationale_description_initial), null); - if (callback != null) callback.onPermissionRequestFinished(false); - } - - @Override - public void onPermissionRationaleShouldBeShown(List permissions, PermissionToken token) { - showWarning(context, context.getString(R.string.permissions_rationale_title), - context.getString(R.string.permissions_rationale_description_initial), token); - } - }).withErrorListener(new PermissionRequestErrorListener() { - @Override - public void onError(DexterError error) { - Timber.e("An error has occurred during checking permissions with Dexter: %s", error.toString()); - } - }) - .check(); - } - - private static void setCacheLocation(Context context, String cacheLocation) { - Settings.getPreferences().edit() - .putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, cacheLocation) - .apply(); - } - - private static void requestFailedPermission(final Context context, final String cacheLocation, final PermissionRequestFinishedCallback callback) { - Dexter.withContext(context) - .withPermissions( - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE) - .withListener(new MultiplePermissionsListener() { - @Override - public void onPermissionsChecked(MultiplePermissionsReport report) { - if (report.areAllPermissionsGranted()) { - Timber.i("Permission granted to use cache directory %s", cacheLocation); - setCacheLocation(context, cacheLocation); - if (callback != null) callback.onPermissionRequestFinished(true); - return; - } - - if (report.isAnyPermissionPermanentlyDenied()) { - Timber.i("Found permanently denied permission to use cache directory %s, offering settings", cacheLocation); - showSettingsDialog(context); - if (callback != null) callback.onPermissionRequestFinished(false); - return; - } - - Timber.i("At least one permission is missing to use directory %s ", cacheLocation); - setCacheLocation(context, FileUtil.getDefaultMusicDirectory().getPath()); - showWarning(context, context.getString(R.string.permissions_message_box_title), - context.getString(R.string.permissions_permission_missing), null); - if (callback != null) callback.onPermissionRequestFinished(false); - } - - @Override - public void onPermissionRationaleShouldBeShown(List permissions, PermissionToken token) { - showWarning(context, context.getString(R.string.permissions_rationale_title), - context.getString(R.string.permissions_rationale_description_failed), token); - } - }).withErrorListener(new PermissionRequestErrorListener() { - @Override - public void onError(DexterError error) { - Timber.e("An error has occurred during checking permissions with Dexter: %s", error.toString()); - } - }) - .check(); - } - - private static void showSettingsDialog(final Context context) { - AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Dialog); - builder.setIcon(android.R.drawable.ic_dialog_alert); - builder.setTitle(context.getString(R.string.permissions_permanent_denial_title)); - builder.setMessage(context.getString(R.string.permissions_permanent_denial_description)); - - builder.setPositiveButton(context.getString(R.string.permissions_open_settings), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); - openSettings(context); - } - }); - - builder.setNegativeButton(context.getString(R.string.common_cancel), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - setCacheLocation(context, FileUtil.getDefaultMusicDirectory().getPath()); - dialog.cancel(); - } - }); - - builder.show(); - } - - private static void openSettings(Context context) { - Intent i = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - i.addCategory(Intent.CATEGORY_DEFAULT); - i.setData(Uri.parse("package:" + context.getPackageName())); - context.startActivity(i); - } - - private static void showWarning(Context context, String title, String text, final PermissionToken token) { - AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Dialog); - builder.setIcon(android.R.drawable.ic_dialog_alert); - builder.setTitle(title); - builder.setMessage(text); - builder.setPositiveButton(context.getString(R.string.common_ok), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); - if (token != null) token.continuePermissionRequest(); - } - }); - builder.show(); - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShareDetails.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShareDetails.java index 91d4986d..0cffa6f8 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShareDetails.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShareDetails.java @@ -10,6 +10,7 @@ import java.util.List; public class ShareDetails { public String Description; + public boolean ShareOnServer; public long Expiration; public List Entries; } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index a640ba1e..7226c3c4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -88,7 +88,7 @@ class NavigationActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { setUncaughtExceptionHandler() - permissionUtil.ForegroundApplicationStarted(this) + permissionUtil.onForegroundApplicationStarted(this) Util.applyTheme(this) super.onCreate(savedInstanceState) @@ -219,7 +219,7 @@ class NavigationActivity : AppCompatActivity() { nowPlayingEventDistributor.unsubscribe(nowPlayingEventListener) themeChangedEventDistributor.unsubscribe(themeChangedEventListener) imageLoaderProvider.clearImageLoader() - permissionUtil.ForegroundApplicationStopped() + permissionUtil.onForegroundApplicationStopped() } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { @@ -332,12 +332,7 @@ class NavigationActivity : AppCompatActivity() { PreferenceManager.setDefaultValues(this, R.xml.settings, false) val preferences = Settings.preferences if (!preferences.contains(Constants.PREFERENCES_KEY_CACHE_LOCATION)) { - val editor = preferences.edit() - editor.putString( - Constants.PREFERENCES_KEY_CACHE_LOCATION, - FileUtil.defaultMusicDirectory.path - ) - editor.apply() + Settings.cacheLocation = FileUtil.defaultMusicDirectory.path } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt index efcebe2e..0dac2654 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIAlbumConverter.kt @@ -16,7 +16,8 @@ fun Album.toDomainEntity(): MusicDirectory.Entry = MusicDirectory.Entry( duration = this@toDomainEntity.duration, created = this@toDomainEntity.created?.time, year = this@toDomainEntity.year, - genre = this@toDomainEntity.genre + genre = this@toDomainEntity.genre, + starred = this@toDomainEntity.starredDate.isNotEmpty() ) fun Album.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 97285d68..9cd0a050 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -63,7 +63,8 @@ class AlbumListFragment : GenericListFragment onItemClick(entry) }, { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, imageLoaderProvider.getImageLoader(), - onMusicFolderUpdate + onMusicFolderUpdate, + requireContext() ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt index 239fb237..c9dcbabe 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt @@ -7,15 +7,22 @@ package org.moire.ultrasonic.fragment +import android.content.Context +import android.graphics.drawable.Drawable import android.view.MenuItem import android.view.View import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.recyclerview.widget.RecyclerView +import java.lang.Exception import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.imageloader.ImageLoader +import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import org.moire.ultrasonic.util.Settings.shouldUseId3Tags +import org.moire.ultrasonic.util.Util +import timber.log.Timber /** * Creates a Row in a RecyclerView which contains the details of an Album @@ -25,13 +32,19 @@ class AlbumRowAdapter( onItemClick: (MusicDirectory.Entry) -> Unit, onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean, private val imageLoader: ImageLoader, - onMusicFolderUpdate: (String?) -> Unit + onMusicFolderUpdate: (String?) -> Unit, + context: Context, ) : GenericRowAdapter( onItemClick, onContextMenuClick, onMusicFolderUpdate ) { + private val starDrawable: Drawable = + Util.getDrawableFromAttribute(context, R.attr.star_full) + private val starHollowDrawable: Drawable = + Util.getDrawableFromAttribute(context, R.attr.star_hollow) + override var itemList = albumList // Set our layout files @@ -53,6 +66,8 @@ class AlbumRowAdapter( holder.details.setOnClickListener { onItemClick(entry) } holder.details.setOnLongClickListener { view -> createPopupMenu(view, listPosition) } holder.coverArtId = entry.coverArt + holder.star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable) + holder.star.setOnClickListener { onStarClick(entry, holder.star) } imageLoader.loadImage( holder.coverArt, entry, @@ -78,6 +93,7 @@ class AlbumRowAdapter( var artist: TextView = view.findViewById(R.id.album_artist) var details: LinearLayout = view.findViewById(R.id.row_album_details) var coverArt: ImageView = view.findViewById(R.id.album_coverart) + var star: ImageView = view.findViewById(R.id.album_star) var coverArtId: String? = null } @@ -87,4 +103,33 @@ class AlbumRowAdapter( override fun newViewHolder(view: View): RecyclerView.ViewHolder { return ViewHolder(view) } + + /** + * Handles the star / unstar action for an album + */ + private fun onStarClick(entry: MusicDirectory.Entry, star: ImageView) { + entry.starred = !entry.starred + star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable) + val musicService = getMusicService() + Thread { + val useId3 = shouldUseId3Tags + try { + if (entry.starred) { + musicService.star( + if (!useId3) entry.id else null, + if (useId3) entry.id else null, + null + ) + } else { + musicService.unstar( + if (!useId3) entry.id else null, + if (useId3) entry.id else null, + null + ) + } + } catch (all: Exception) { + Timber.e(all) + } + }.start() + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt index c0bd3191..38f42bb1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt @@ -14,7 +14,13 @@ import androidx.navigation.fragment.findNavController import com.google.android.material.switchmaterial.SwitchMaterial import com.google.android.material.textfield.TextInputLayout import com.skydoves.colorpickerview.ColorPickerDialog +import com.skydoves.colorpickerview.flag.BubbleFlag +import com.skydoves.colorpickerview.flag.FlagMode import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener +import java.io.IOException +import java.net.MalformedURLException +import java.net.URL +import java.util.Locale import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.BuildConfig @@ -32,18 +38,12 @@ import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.ErrorDialog import org.moire.ultrasonic.util.ModalBackgroundTask +import org.moire.ultrasonic.util.ServerColor import org.moire.ultrasonic.util.Util import retrofit2.Response import timber.log.Timber -import java.io.IOException -import java.net.MalformedURLException -import java.net.URL -import java.util.* -import com.skydoves.colorpickerview.flag.FlagMode - -import com.skydoves.colorpickerview.flag.BubbleFlag -import org.moire.ultrasonic.util.ServerColor +private const val DIALOG_PADDING = 12 /** * Displays a form where server settings can be created / edited @@ -170,14 +170,18 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { this.colorPickerView.setFlagView(bubbleFlag) } .attachAlphaSlideBar(false) - .setPositiveButton(getString(R.string.common_ok), + .setPositiveButton( + getString(R.string.common_ok), ColorEnvelopeListener { envelope, _ -> selectedColor = envelope.color - updateColor(envelope.color) }) + updateColor(envelope.color) + } + ) .setNegativeButton(getString(R.string.common_cancel)) { - dialogInterface, i -> dialogInterface.dismiss() + dialogInterface, i -> + dialogInterface.dismiss() } - .setBottomSpace(12) + .setBottomSpace(DIALOG_PADDING) .show() } } @@ -466,10 +470,9 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { } Util.showDialog( - activity, - android.R.drawable.ic_dialog_info, - R.string.settings_testing_ok, - dialogText + context = requireActivity(), + titleId = R.string.settings_testing_ok, + message = dialogText ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index f0ce17ba..b02372a4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -33,6 +33,7 @@ import android.widget.SeekBar import android.widget.SeekBar.OnSeekBarChangeListener import android.widget.TextView import android.widget.ViewFlipper +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.navigation.Navigation import com.mobeta.android.dslv.DragSortListView @@ -153,7 +154,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon return inflater.inflate(R.layout.current_playing, container, false) } - fun findViews(view: View) { + private fun findViews(view: View) { playlistFlipper = view.findViewById(R.id.current_playing_playlist_flipper) emptyTextView = view.findViewById(R.id.playlist_empty) songTitleTextView = view.findViewById(R.id.current_playing_song) @@ -209,7 +210,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon val nextButton: AutoRepeatButton = view.findViewById(R.id.button_next) val shuffleButton = view.findViewById(R.id.button_shuffle) val ratingLinearLayout = view.findViewById(R.id.song_rating) - if (!useFiveStarRating) ratingLinearLayout.visibility = View.GONE + if (!useFiveStarRating) ratingLinearLayout.isVisible = false hollowStar = Util.getDrawableFromAttribute(view.context, R.attr.star_hollow) fullStar = Util.getDrawableFromAttribute(context, R.attr.star_full) @@ -375,7 +376,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon mediaPlayerController.isShufflePlayEnabled = true } - visualizerViewLayout.visibility = View.GONE + visualizerViewLayout.isVisible = false VisualizerController.get().observe( requireActivity(), { visualizerController -> @@ -389,11 +390,9 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon LinearLayout.LayoutParams.MATCH_PARENT ) ) - if (!visualizerView.isActive) { - visualizerViewLayout.visibility = View.GONE - } else { - visualizerViewLayout.visibility = View.VISIBLE - } + + visualizerViewLayout.isVisible = visualizerView.isActive + visualizerView.setOnTouchListener { _, _ -> visualizerView.isActive = !visualizerView.isActive mediaPlayerController.showVisualization = visualizerView.isActive @@ -402,7 +401,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon isVisualizerAvailable = true } else { Timber.d("VisualizerController Observer.onChanged has no controller") - visualizerViewLayout.visibility = View.GONE + visualizerViewLayout.isVisible = false isVisualizerAvailable = false } } @@ -497,6 +496,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon val equalizerMenuItem = menu.findItem(R.id.menu_item_equalizer) val visualizerMenuItem = menu.findItem(R.id.menu_item_visualizer) val shareMenuItem = menu.findItem(R.id.menu_item_share) + val shareSongMenuItem = menu.findItem(R.id.menu_item_share_song) starMenuItem = menu.findItem(R.id.menu_item_star) val bookmarkMenuItem = menu.findItem(R.id.menu_item_bookmark_set) val bookmarkRemoveMenuItem = menu.findItem(R.id.menu_item_bookmark_delete) @@ -523,20 +523,27 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon } val mediaPlayerController = mediaPlayerController val downloadFile = mediaPlayerController.currentPlaying + if (downloadFile != null) { currentSong = downloadFile.song } + if (useFiveStarRating) starMenuItem.isVisible = false + if (currentSong != null) { starMenuItem.icon = if (currentSong!!.starred) fullStar else hollowStar + shareSongMenuItem.isVisible = true } else { starMenuItem.icon = hollowStar + shareSongMenuItem.isVisible = false } + if (mediaPlayerController.keepScreenOn) { screenOption?.setTitle(R.string.download_menu_screen_off) } else { screenOption?.setTitle(R.string.download_menu_screen_on) } + if (jukeboxOption != null) { jukeboxOption.isEnabled = jukeboxAvailable jukeboxOption.isVisible = jukeboxAvailable @@ -598,9 +605,8 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon when (menuItemId) { R.id.menu_show_artist -> { - if (entry == null) { - return false - } + if (entry == null) return false + if (Settings.shouldUseId3Tags) { bundle = Bundle() bundle.putString(Constants.INTENT_EXTRA_NAME_ID, entry.artistId) @@ -613,9 +619,8 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon return true } R.id.menu_show_album -> { - if (entry == null) { - return false - } + if (entry == null) return false + val albumId = if (Settings.shouldUseId3Tags) entry.albumId else entry.parent bundle = Bundle() bundle.putString(Constants.INTENT_EXTRA_NAME_ID, albumId) @@ -627,9 +632,8 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon return true } R.id.menu_lyrics -> { - if (entry == null) { - return false - } + if (entry == null) return false + bundle = Bundle() bundle.putString(Constants.INTENT_EXTRA_NAME_ARTIST, entry.artist) bundle.putString(Constants.INTENT_EXTRA_NAME_TITLE, entry.title) @@ -664,11 +668,9 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon R.id.menu_item_visualizer -> { val active = !visualizerView.isActive visualizerView.isActive = active - if (!visualizerView.isActive) { - visualizerViewLayout.visibility = View.GONE - } else { - visualizerViewLayout.visibility = View.VISIBLE - } + + visualizerViewLayout.isVisible = visualizerView.isActive + mediaPlayerController.showVisualization = visualizerView.isActive Util.toast( context, @@ -705,9 +707,8 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon return true } R.id.menu_item_star -> { - if (currentSong == null) { - return true - } + if (currentSong == null) return true + val isStarred = currentSong!!.starred val id = currentSong!!.id if (isStarred) { @@ -732,9 +733,8 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon return true } R.id.menu_item_bookmark_set -> { - if (currentSong == null) { - return true - } + if (currentSong == null) return true + val songId = currentSong!!.id val playerPosition = mediaPlayerController.playerPosition currentSong!!.bookmarkPosition = playerPosition @@ -755,9 +755,8 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon return true } R.id.menu_item_bookmark_delete -> { - if (currentSong == null) { - return true - } + if (currentSong == null) return true + val bookmarkSongId = currentSong!!.id currentSong!!.bookmarkPosition = 0 Thread { @@ -782,6 +781,15 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon shareHandler.createShare(this, entries, null, cancellationToken) return true } + R.id.menu_item_share_song -> { + if (currentSong == null) return true + + val entries: MutableList = ArrayList() + entries.add(currentSong) + + shareHandler.createShare(this, entries, null, cancellationToken) + return true + } else -> return false } } @@ -903,7 +911,9 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon onCurrentChanged() } }) - emptyTextView.visibility = if (list.isEmpty()) View.VISIBLE else View.GONE + + emptyTextView.isVisible = list.isEmpty() + currentRevision = mediaPlayerController.playListUpdateRevision when (mediaPlayerController.repeatMode) { RepeatMode.OFF -> repeatButton.setImageDrawable( @@ -1028,19 +1038,19 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon when (playerState) { PlayerState.STARTED -> { - pauseButton.visibility = View.VISIBLE - stopButton.visibility = View.GONE - startButton.visibility = View.GONE + pauseButton.isVisible = true + stopButton.isVisible = false + startButton.isVisible = false } PlayerState.DOWNLOADING, PlayerState.PREPARING -> { - pauseButton.visibility = View.GONE - stopButton.visibility = View.VISIBLE - startButton.visibility = View.GONE + pauseButton.isVisible = false + stopButton.isVisible = true + startButton.isVisible = false } else -> { - pauseButton.visibility = View.GONE - stopButton.visibility = View.GONE - startButton.visibility = View.VISIBLE + pauseButton.isVisible = false + stopButton.isVisible = false + startButton.isVisible = true } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerRowAdapter.kt index 1f9e4c42..eff72fd0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerRowAdapter.kt @@ -12,7 +12,6 @@ import android.widget.BaseAdapter import android.widget.ImageButton import android.widget.ImageView import android.widget.PopupMenu -import android.widget.RelativeLayout import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 239b3d6b..d30b5d00 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -485,11 +485,7 @@ class LocalMediaPlayer : KoinComponent { try { setNextPlayerState(PlayerState.PREPARED) if (Settings.gaplessPlayback && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && - ( - playerState === PlayerState.STARTED || - playerState === PlayerState.PAUSED - ) + (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) ) { mediaPlayer.setNextMediaPlayer(nextMediaPlayer) nextSetup = true @@ -536,6 +532,7 @@ class LocalMediaPlayer : KoinComponent { wakeLock.acquire(60000) val pos = cachedPosition Timber.i("Ending position %d of %d", pos, duration) + if (!isPartial || downloadFile.isWorkDone && abs(duration - pos) < 1000) { setPlayerState(PlayerState.COMPLETED) if (Settings.gaplessPlayback && @@ -555,6 +552,7 @@ class LocalMediaPlayer : KoinComponent { } return } + synchronized(this) { if (downloadFile.isWorkDone) { // Complete was called early even though file is fully buffered diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt index 57d70acf..883aeeac 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt @@ -7,6 +7,8 @@ import android.view.LayoutInflater import android.view.View import android.widget.CheckBox import android.widget.EditText +import android.widget.TextView +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import java.util.Locale @@ -31,9 +33,12 @@ import org.moire.ultrasonic.util.TimeSpanPicker class ShareHandler(val context: Context) { private var shareDescription: EditText? = null private var timeSpanPicker: TimeSpanPicker? = null + private var shareOnServerCheckBox: CheckBox? = null private var hideDialogCheckBox: CheckBox? = null private var noExpirationCheckBox: CheckBox? = null private var saveAsDefaultsCheckBox: CheckBox? = null + private var textViewComment: TextView? = null + private var textViewExpiration: TextView? = null private val pattern = Pattern.compile(":") fun createShare( @@ -62,15 +67,17 @@ class ShareHandler(val context: Context) { swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken ) { - val task: BackgroundTask = object : FragmentBackgroundTask( + val task: BackgroundTask = object : FragmentBackgroundTask( fragment.requireActivity(), true, swipe, cancellationToken ) { @Throws(Throwable::class) - override fun doInBackground(): Share { + override fun doInBackground(): Share? { val ids: MutableList = ArrayList() + + if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1) return null if (shareDetails.Entries.isEmpty()) { fragment.arguments?.getString(Constants.INTENT_EXTRA_NAME_ID)?.let { ids.add(it) @@ -80,23 +87,51 @@ class ShareHandler(val context: Context) { ids.add(id) } } + val musicService = getMusicService() var timeInMillis: Long = 0 + if (shareDetails.Expiration != 0L) { timeInMillis = shareDetails.Expiration } + val shares = musicService.createShare(ids, shareDetails.Description, timeInMillis) + return shares[0] } - override fun done(result: Share) { + override fun done(result: Share?) { + val intent = Intent(Intent.ACTION_SEND) intent.type = "text/plain" - intent.putExtra( - Intent.EXTRA_TEXT, - String.format(Locale.ROOT, "%s\n\n%s", Settings.shareGreeting, result.url) - ) + + if (result != null) { + // Created a share, send the URL + intent.putExtra( + Intent.EXTRA_TEXT, + String.format( + Locale.ROOT, "%s\n\n%s", Settings.shareGreeting, result.url + ) + ) + } else { + // Sending only text details + val textBuilder = StringBuilder() + textBuilder.appendLine(Settings.shareGreeting) + + if (!shareDetails.Entries[0].title.isNullOrEmpty()) + textBuilder.append(context.resources.getString(R.string.common_title)) + .append(": ").appendLine(shareDetails.Entries[0].title) + if (!shareDetails.Entries[0].artist.isNullOrEmpty()) + textBuilder.append(context.resources.getString(R.string.common_artist)) + .append(": ").appendLine(shareDetails.Entries[0].artist) + if (!shareDetails.Entries[0].album.isNullOrEmpty()) + textBuilder.append(context.resources.getString(R.string.common_album)) + .append(": ").append(shareDetails.Entries[0].album) + + intent.putExtra(Intent.EXTRA_TEXT, textBuilder.toString()) + } + fragment.activity?.startActivity( Intent.createChooser( intent, @@ -119,24 +154,45 @@ class ShareHandler(val context: Context) { if (layout != null) { shareDescription = layout.findViewById(R.id.share_description) as EditText hideDialogCheckBox = layout.findViewById(R.id.hide_dialog) as CheckBox + shareOnServerCheckBox = layout.findViewById(R.id.share_on_server) as CheckBox noExpirationCheckBox = layout.findViewById( R.id.timeSpanDisableCheckBox ) as CheckBox saveAsDefaultsCheckBox = layout.findViewById(R.id.save_as_defaults) as CheckBox timeSpanPicker = layout.findViewById(R.id.date_picker) as TimeSpanPicker + textViewComment = layout.findViewById(R.id.textViewComment) as TextView + textViewExpiration = layout.findViewById(R.id.textViewExpiration) as TextView } + + if (shareDetails.Entries.size == 1) { + // For single songs the sharing may be done by text only + shareOnServerCheckBox?.setOnCheckedChangeListener { _, _ -> + updateVisibility() + } + + shareOnServerCheckBox?.isChecked = Settings.shareOnServer + } else { + shareOnServerCheckBox?.isVisible = false + } + updateVisibility() + val builder = AlertDialog.Builder(fragment.context) builder.setTitle(R.string.share_set_share_options) - builder.setPositiveButton(R.string.common_save) { _, _ -> + + builder.setPositiveButton(R.string.menu_share) { _, _ -> if (!noExpirationCheckBox!!.isChecked) { val timeSpan: TimeSpan = timeSpanPicker!!.timeSpan val now = TimeSpan.getCurrentTime() shareDetails.Expiration = now.add(timeSpan).totalMilliseconds } + shareDetails.Description = shareDescription!!.text.toString() + shareDetails.ShareOnServer = shareOnServerCheckBox!!.isChecked + if (hideDialogCheckBox!!.isChecked) { Settings.shouldAskForShareDetails = false } + if (saveAsDefaultsCheckBox!!.isChecked) { val timeSpanType: String = timeSpanPicker!!.timeSpanType val timeSpanAmount: Int = timeSpanPicker!!.timeSpanAmount @@ -145,22 +201,29 @@ class ShareHandler(val context: Context) { String.format("%d:%s", timeSpanAmount, timeSpanType) else "" Settings.defaultShareDescription = shareDetails.Description + Settings.shareOnServer = shareDetails.ShareOnServer } + share(fragment, shareDetails, swipe, cancellationToken) } + builder.setNegativeButton(R.string.common_cancel) { dialog, _ -> dialog.cancel() } + builder.setView(layout) builder.setCancelable(true) + timeSpanPicker!!.setTimeSpanDisableText(context.resources.getString(R.string.no_expiration)) noExpirationCheckBox!!.setOnCheckedChangeListener { _, b -> timeSpanPicker!!.isEnabled = !b } + val defaultDescription = Settings.defaultShareDescription val timeSpan = Settings.defaultShareExpiration + val split = pattern.split(timeSpan) if (split.size == 2) { val timeSpanAmount = split[0].toInt() @@ -178,8 +241,25 @@ class ShareHandler(val context: Context) { noExpirationCheckBox!!.isChecked = true timeSpanPicker!!.isEnabled = false } + shareDescription!!.setText(defaultDescription) builder.create() builder.show() } + + private fun updateVisibility() { + if (!shareOnServerCheckBox!!.isVisible || shareOnServerCheckBox!!.isChecked) { + noExpirationCheckBox?.isVisible = true + timeSpanPicker?.isVisible = true + shareDescription?.isVisible = true + textViewComment?.isVisible = true + textViewExpiration?.isVisible = true + } else { + noExpirationCheckBox?.isVisible = false + timeSpanPicker?.isVisible = false + shareDescription?.isVisible = false + textViewComment?.isVisible = false + textViewExpiration?.isVisible = false + } + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt index 135ed3d2..45bde7fa 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt @@ -104,6 +104,7 @@ object Constants { const val PREFERENCES_KEY_ASK_FOR_SHARE_DETAILS = "sharingAlwaysAskForDetails" const val PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION = "sharingDefaultDescription" const val PREFERENCES_KEY_DEFAULT_SHARE_GREETING = "sharingDefaultGreeting" + const val PREFERENCES_KEY_SHARE_ON_SERVER = "sharingCreateOnServer" const val PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION = "sharingDefaultExpiration" const val PREFERENCES_KEY_SHOW_ALL_SONGS_BY_ARTIST = "showAllSongsByArtist" const val PREFERENCES_KEY_USE_FIVE_STAR_RATING = "use_five_star_rating" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt index af5690e6..f9a03051 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt @@ -253,9 +253,8 @@ object FileUtil { @JvmStatic val musicDirectory: File get() { - val path = Settings.preferences - .getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultMusicDirectory.path) - val dir = File(path!!) + val path = Settings.cacheLocation + val dir = File(path) val hasAccess = ensureDirectoryExistsAndIsReadWritable(dir) if (!hasAccess) permissionUtil.value.handlePermissionFailed(null) return if (hasAccess) dir else defaultMusicDirectory diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/PermissionUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/PermissionUtil.kt new file mode 100644 index 00000000..92813226 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/PermissionUtil.kt @@ -0,0 +1,259 @@ +package org.moire.ultrasonic.util + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Handler +import android.os.Looper +import androidx.core.content.PermissionChecker +import com.karumi.dexter.Dexter +import com.karumi.dexter.MultiplePermissionsReport +import com.karumi.dexter.PermissionToken +import com.karumi.dexter.listener.PermissionRequest +import com.karumi.dexter.listener.multi.MultiplePermissionsListener +import org.moire.ultrasonic.R +import org.moire.ultrasonic.util.FileUtil.defaultMusicDirectory +import timber.log.Timber + +/** + * Contains static functions for Permission handling + */ +class PermissionUtil(private val applicationContext: Context) { + private var activityContext: Context? = null + + interface PermissionRequestFinishedCallback { + fun onPermissionRequestFinished(hasPermission: Boolean) + } + + fun onForegroundApplicationStarted(context: Context?) { + activityContext = context + } + + fun onForegroundApplicationStopped() { + activityContext = null + } + + /** + * This function can be used to handle file access permission failures. + * + * It will check if the failure is because the necessary permissions aren't available, + * and it will request them, if necessary. + * + * @param callback callback function to execute after the permission request is finished + */ + fun handlePermissionFailed(callback: PermissionRequestFinishedCallback?) { + val currentCachePath = Settings.cacheLocation + val defaultCachePath = defaultMusicDirectory.path + + // Ultrasonic can do nothing about this error when the Music Directory is already set to the default. + if (currentCachePath.compareTo(defaultCachePath) == 0) return + + if (PermissionChecker.checkSelfPermission( + applicationContext, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) == PermissionChecker.PERMISSION_DENIED || + PermissionChecker.checkSelfPermission( + applicationContext, + Manifest.permission.READ_EXTERNAL_STORAGE + ) == PermissionChecker.PERMISSION_DENIED + ) { + // While we request permission, the Music Directory is temporarily reset to its default location + Settings.cacheLocation = defaultMusicDirectory.path + // If the application is not running, we can't notify the user + if (activityContext == null) return + requestFailedPermission(activityContext!!, currentCachePath, callback) + } else { + Settings.cacheLocation = defaultMusicDirectory.path + // If the application is not running, we can't notify the user + if (activityContext != null) { + Handler(Looper.getMainLooper()).post { + showWarning( + activityContext!!, + activityContext!!.getString(R.string.permissions_message_box_title), + activityContext!!.getString(R.string.permissions_access_error), + null + ) + } + } + callback?.onPermissionRequestFinished(false) + } + } + + companion object { + /** + * This function requests permission to access the filesystem. + * It can be used to request the permission initially, e.g. when the user decides to + * use a non-default folder for the cache + * @param context context for the operation + * @param callback callback function to execute after the permission request is finished + */ + @JvmStatic + fun requestInitialPermission( + context: Context, + callback: PermissionRequestFinishedCallback? + ) { + Dexter.withContext(context) + .withPermissions( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + .withListener(object : MultiplePermissionsListener { + override fun onPermissionsChecked(report: MultiplePermissionsReport) { + if (report.areAllPermissionsGranted()) { + Timber.i("R/W permission granted for external storage") + callback?.onPermissionRequestFinished(true) + return + } + if (report.isAnyPermissionPermanentlyDenied) { + Timber.i( + "R/W permission is permanently denied for external storage" + ) + showSettingsDialog(context) + callback?.onPermissionRequestFinished(false) + return + } + Timber.i("R/W permission is missing for external storage") + showWarning( + context, + context.getString(R.string.permissions_message_box_title), + context.getString(R.string.permissions_rationale_description_initial), + null + ) + callback?.onPermissionRequestFinished(false) + } + + override fun onPermissionRationaleShouldBeShown( + permissions: List, + token: PermissionToken + ) { + showWarning( + context, + context.getString(R.string.permissions_rationale_title), + context.getString(R.string.permissions_rationale_description_initial), + token + ) + } + }).withErrorListener { error -> + Timber.e( + "An error has occurred during checking permissions with Dexter: %s", + error.toString() + ) + } + .check() + } + + private fun requestFailedPermission( + context: Context, + cacheLocation: String?, + callback: PermissionRequestFinishedCallback? + ) { + Dexter.withContext(context) + .withPermissions( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + .withListener(object : MultiplePermissionsListener { + override fun onPermissionsChecked(report: MultiplePermissionsReport) { + if (report.areAllPermissionsGranted()) { + Timber.i("Permission granted to use cache directory %s", cacheLocation) + + if (cacheLocation != null) { + Settings.cacheLocation = cacheLocation + } + callback?.onPermissionRequestFinished(true) + return + } + if (report.isAnyPermissionPermanentlyDenied) { + Timber.i( + "R/W permission for cache directory %s was permanently denied", + cacheLocation + ) + showSettingsDialog(context) + callback?.onPermissionRequestFinished(false) + return + } + Timber.i( + "At least one permission is missing to use directory %s ", + cacheLocation + ) + Settings.cacheLocation = defaultMusicDirectory.path + showWarning( + context, context.getString(R.string.permissions_message_box_title), + context.getString(R.string.permissions_permission_missing), null + ) + callback?.onPermissionRequestFinished(false) + } + + override fun onPermissionRationaleShouldBeShown( + permissions: List, + token: PermissionToken + ) { + showWarning( + context, + context.getString(R.string.permissions_rationale_title), + context.getString(R.string.permissions_rationale_description_failed), + token + ) + } + }).withErrorListener { error -> + Timber.e( + "An error has occurred during checking permissions with Dexter: %s", + error.toString() + ) + } + .check() + } + + private fun showSettingsDialog(ctx: Context) { + + val builder = Util.createDialog( + context = ctx, + android.R.drawable.ic_dialog_alert, + ctx.getString(R.string.permissions_permanent_denial_title), + ctx.getString(R.string.permissions_permanent_denial_description) + ) + + builder.setPositiveButton(ctx.getString(R.string.permissions_open_settings)) { + dialog, _ -> + dialog.cancel() + openSettings(ctx) + } + + builder.setNegativeButton(ctx.getString(R.string.common_cancel)) { dialog, _ -> + Settings.cacheLocation = defaultMusicDirectory.path + dialog.cancel() + } + + builder.show() + } + + private fun openSettings(context: Context) { + val i = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + i.addCategory(Intent.CATEGORY_DEFAULT) + i.data = Uri.parse("package:" + context.packageName) + context.startActivity(i) + } + + private fun showWarning( + context: Context, + title: String, + text: String, + token: PermissionToken? + ) { + + val builder = Util.createDialog( + context = context, + android.R.drawable.ic_dialog_alert, + title, + text + ) + + builder.setPositiveButton(context.getString(R.string.common_ok)) { dialog, _ -> + dialog.cancel() + token?.continuePermissionRequest() + } + builder.show() + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ServerColor.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ServerColor.kt index 9f34d02a..eb3853cc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ServerColor.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ServerColor.kt @@ -5,6 +5,8 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils import org.moire.ultrasonic.R +private const val LUMINANCE_LIMIT = 0.25 + object ServerColor { fun getBackgroundColor(context: Context, serverColor: Int?): Int { return serverColor ?: ContextCompat.getColor( @@ -17,10 +19,10 @@ object ServerColor { context, Util.getResourceFromAttribute(context, R.attr.colorOnPrimary) ) val luminance = ColorUtils.calculateLuminance(serverColor) - return if (luminance < 0.25) { + return if (luminance < LUMINANCE_LIMIT) { ContextCompat.getColor(context, R.color.selected_menu_dark) } else { ContextCompat.getColor(context, R.color.selected_menu_light) } } -} \ No newline at end of file +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index 24f97fa9..baf64f56 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -102,6 +102,23 @@ object Settings { return if (preloadCount == -1) Int.MAX_VALUE else preloadCount } + @JvmStatic + var cacheLocation: String + get() { + return preferences.getString( + Constants.PREFERENCES_KEY_CACHE_LOCATION, + FileUtil.defaultMusicDirectory.path + )!! + } + set(location) { + val editor = preferences.edit() + editor.putString( + Constants.PREFERENCES_KEY_CACHE_LOCATION, + location + ) + editor.apply() + } + @JvmStatic val cacheSizeMB: Int get() { @@ -376,6 +393,21 @@ object Settings { ) } + var shareOnServer: Boolean + get() { + val preferences = preferences + return preferences.getBoolean(Constants.PREFERENCES_KEY_SHARE_ON_SERVER, true)!! + } + set(shareOnServer) { + val preferences = preferences + val editor = preferences.edit() + editor.putBoolean( + Constants.PREFERENCES_KEY_SHARE_ON_SERVER, + shareOnServer + ) + editor.apply() + } + var defaultShareExpiration: String get() { val preferences = preferences diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index 52905159..3c5d2b91 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -80,6 +80,7 @@ object Util { private const val EVENT_META_CHANGED = "org.moire.ultrasonic.EVENT_META_CHANGED" private const val EVENT_PLAYSTATE_CHANGED = "org.moire.ultrasonic.EVENT_PLAYSTATE_CHANGED" private const val CM_AVRCP_PLAYSTATE_CHANGED = "com.android.music.playstatechanged" + private const val CM_AVRCP_PLAYBACK_COMPLETE = "com.android.music.playbackcomplete" private const val CM_AVRCP_METADATA_CHANGED = "com.android.music.metachanged" // Used by hexEncode() @@ -392,17 +393,30 @@ object Util { // The AlertDialog requires an Activity context, app context is not enough // See https://stackoverflow.com/questions/5436822/ - fun showDialog(context: Context?, icon: Int, titleId: Int, message: String?) { - AlertDialog.Builder(context) + fun createDialog( + context: Context?, + icon: Int = android.R.drawable.ic_dialog_info, + title: String, + message: String? + ): AlertDialog.Builder { + return AlertDialog.Builder(context) .setIcon(icon) - .setTitle(titleId) + .setTitle(title) .setMessage(message) .setPositiveButton(R.string.common_ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() } - .show() + } + + fun showDialog( + context: Context, + icon: Int = android.R.drawable.ic_dialog_info, + titleId: Int, + message: String? + ) { + createDialog(context, icon, context.getString(titleId, ""), message).show() } @JvmStatic @@ -519,61 +533,14 @@ object Util { listSize: Int, id: Int ) { - if (!Settings.shouldSendBluetoothNotifications) { - return - } + if (!Settings.shouldSendBluetoothNotifications) return + var song: MusicDirectory.Entry? = null val avrcpIntent = Intent(CM_AVRCP_METADATA_CHANGED) if (currentPlaying != null) song = currentPlaying.song - if (song == null) { - avrcpIntent.putExtra("track", "") - avrcpIntent.putExtra("track_name", "") - avrcpIntent.putExtra("artist", "") - avrcpIntent.putExtra("artist_name", "") - avrcpIntent.putExtra("album", "") - avrcpIntent.putExtra("album_name", "") - avrcpIntent.putExtra("album_artist", "") - avrcpIntent.putExtra("album_artist_name", "") + fillIntent(avrcpIntent, song, playerPosition, id, listSize) - if (Settings.shouldSendBluetoothAlbumArt) { - avrcpIntent.putExtra("coverart", null as Parcelable?) - avrcpIntent.putExtra("cover", null as Parcelable?) - } - - avrcpIntent.putExtra("ListSize", 0.toLong()) - avrcpIntent.putExtra("id", 0.toLong()) - avrcpIntent.putExtra("duration", 0.toLong()) - avrcpIntent.putExtra("position", 0.toLong()) - } else { - val title = song.title - val artist = song.artist - val album = song.album - val duration = song.duration - - avrcpIntent.putExtra("track", title) - avrcpIntent.putExtra("track_name", title) - avrcpIntent.putExtra("artist", artist) - avrcpIntent.putExtra("artist_name", artist) - avrcpIntent.putExtra("album", album) - avrcpIntent.putExtra("album_name", album) - avrcpIntent.putExtra("album_artist", artist) - avrcpIntent.putExtra("album_artist_name", artist) - - if (Settings.shouldSendBluetoothAlbumArt) { - val albumArtFile = FileUtil.getAlbumArtFile(song) - avrcpIntent.putExtra("coverart", albumArtFile.absolutePath) - avrcpIntent.putExtra("cover", albumArtFile.absolutePath) - } - - avrcpIntent.putExtra("position", playerPosition.toLong()) - avrcpIntent.putExtra("id", id.toLong()) - avrcpIntent.putExtra("ListSize", listSize.toLong()) - - if (duration != null) { - avrcpIntent.putExtra("duration", duration.toLong()) - } - } context.sendBroadcast(avrcpIntent) } @@ -586,54 +553,87 @@ object Util { id: Int, playerPosition: Int ) { - if (!Settings.shouldSendBluetoothNotifications) { - return - } + if (!Settings.shouldSendBluetoothNotifications) return + if (newSong != null) { - val avrcpIntent = Intent(CM_AVRCP_PLAYSTATE_CHANGED) - val title = newSong.title - val artist = newSong.artist - val album = newSong.album - val duration = newSong.duration + val avrcpIntent = Intent( + if (state == PlayerState.COMPLETED) CM_AVRCP_PLAYBACK_COMPLETE + else CM_AVRCP_PLAYSTATE_CHANGED + ) - avrcpIntent.putExtra("track", title) - avrcpIntent.putExtra("track_name", title) - avrcpIntent.putExtra("artist", artist) - avrcpIntent.putExtra("artist_name", artist) - avrcpIntent.putExtra("album", album) - avrcpIntent.putExtra("album_name", album) - avrcpIntent.putExtra("album_artist", artist) - avrcpIntent.putExtra("album_artist_name", artist) + fillIntent(avrcpIntent, newSong, playerPosition, id, listSize) - if (Settings.shouldSendBluetoothAlbumArt) { - val albumArtFile = FileUtil.getAlbumArtFile(newSong) - avrcpIntent.putExtra("coverart", albumArtFile.absolutePath) - avrcpIntent.putExtra("cover", albumArtFile.absolutePath) - } - - avrcpIntent.putExtra("position", playerPosition.toLong()) - avrcpIntent.putExtra("id", id.toLong()) - avrcpIntent.putExtra("ListSize", listSize.toLong()) - - if (duration != null) { - avrcpIntent.putExtra("duration", duration.toLong()) - } - - when (state) { - PlayerState.STARTED -> avrcpIntent.putExtra("playing", true) - PlayerState.STOPPED, PlayerState.PAUSED, - PlayerState.COMPLETED -> avrcpIntent.putExtra( - "playing", - false - ) - else -> return // No need to broadcast. + if (state != PlayerState.COMPLETED) { + when (state) { + PlayerState.STARTED -> avrcpIntent.putExtra("playing", true) + PlayerState.STOPPED, + PlayerState.PAUSED -> avrcpIntent.putExtra("playing", false) + else -> return // No need to broadcast. + } } context.sendBroadcast(avrcpIntent) } } + private fun fillIntent( + intent: Intent, + song: MusicDirectory.Entry?, + playerPosition: Int, + id: Int, + listSize: Int + ) { + if (song == null) { + intent.putExtra("track", "") + intent.putExtra("track_name", "") + intent.putExtra("artist", "") + intent.putExtra("artist_name", "") + intent.putExtra("album", "") + intent.putExtra("album_name", "") + intent.putExtra("album_artist", "") + intent.putExtra("album_artist_name", "") + + if (Settings.shouldSendBluetoothAlbumArt) { + intent.putExtra("coverart", null as Parcelable?) + intent.putExtra("cover", null as Parcelable?) + } + + intent.putExtra("ListSize", 0.toLong()) + intent.putExtra("id", 0.toLong()) + intent.putExtra("duration", 0.toLong()) + intent.putExtra("position", 0.toLong()) + } else { + val title = song.title + val artist = song.artist + val album = song.album + val duration = song.duration + + intent.putExtra("track", title) + intent.putExtra("track_name", title) + intent.putExtra("artist", artist) + intent.putExtra("artist_name", artist) + intent.putExtra("album", album) + intent.putExtra("album_name", album) + intent.putExtra("album_artist", artist) + intent.putExtra("album_artist_name", artist) + + if (Settings.shouldSendBluetoothAlbumArt) { + val albumArtFile = FileUtil.getAlbumArtFile(song) + intent.putExtra("coverart", albumArtFile.absolutePath) + intent.putExtra("cover", albumArtFile.absolutePath) + } + + intent.putExtra("position", playerPosition.toLong()) + intent.putExtra("id", id.toLong()) + intent.putExtra("ListSize", listSize.toLong()) + + if (duration != null) { + intent.putExtra("duration", duration.toLong()) + } + } + } + /** * * Broadcasts the given player state as the one being set. diff --git a/ultrasonic/src/main/res/drawable/thumb.xml b/ultrasonic/src/main/res/drawable/thumb.xml index 078a12c1..52b0f8dc 100644 --- a/ultrasonic/src/main/res/drawable/thumb.xml +++ b/ultrasonic/src/main/res/drawable/thumb.xml @@ -9,8 +9,8 @@ android:bottomLeftRadius="44dp" /> + android:paddingStart="22dp" + android:paddingEnd="22dp" /> diff --git a/ultrasonic/src/main/res/layout-land/current_playing.xml b/ultrasonic/src/main/res/layout-land/current_playing.xml index 2235bb65..a3028829 100644 --- a/ultrasonic/src/main/res/layout-land/current_playing.xml +++ b/ultrasonic/src/main/res/layout-land/current_playing.xml @@ -110,8 +110,8 @@ a:layout_width="fill_parent" a:layout_height="60dip" a:layout_gravity="bottom|center_horizontal" - a:layout_marginLeft="60dip" - a:layout_marginRight="60dip" + a:layout_marginStart="60dip" + a:layout_marginEnd="60dip" a:background="@color/translucent" a:orientation="vertical"/> diff --git a/ultrasonic/src/main/res/layout/album_list_item.xml b/ultrasonic/src/main/res/layout/album_list_item.xml index 7959aa3d..e3e971d1 100644 --- a/ultrasonic/src/main/res/layout/album_list_item.xml +++ b/ultrasonic/src/main/res/layout/album_list_item.xml @@ -15,7 +15,6 @@ a:layout_height="64dp" a:layout_gravity="center_horizontal|center_vertical" a:layout_marginStart="6dp" - a:layout_marginLeft="6dp" a:layout_marginTop="6dp" a:scaleType="fitCenter" a:src="@drawable/unknown_album" @@ -28,22 +27,21 @@ a:layout_width="0dp" a:layout_height="74dp" a:layout_marginStart="10dp" - a:layout_marginLeft="10dp" a:drawablePadding="6dip" a:gravity="center_vertical" a:minHeight="56dip" a:orientation="vertical" - a:paddingLeft="3dip" - a:paddingRight="3dip" + a:paddingStart="3dip" + a:paddingEnd="3dip" a:textAppearance="?android:attr/textAppearanceMedium" - app:layout_constraintEnd_toStartOf="@+id/guideline2" + app:layout_constraintEnd_toStartOf="@+id/album_star" app:layout_constraintLeft_toRightOf="@+id/album_coverart" app:layout_constraintStart_toEndOf="@+id/album_coverart" app:layout_constraintTop_toTopOf="parent"> - - - - + tools:src="@drawable/ic_star_hollow_dark" /> diff --git a/ultrasonic/src/main/res/layout/album_list_item_legacy.xml b/ultrasonic/src/main/res/layout/album_list_item_legacy.xml index e4129ff1..f8e244a2 100644 --- a/ultrasonic/src/main/res/layout/album_list_item_legacy.xml +++ b/ultrasonic/src/main/res/layout/album_list_item_legacy.xml @@ -9,17 +9,17 @@ a:id="@+id/album_coverart" a:layout_width="64dp" a:layout_height="64dp" - a:layout_gravity="left|center_vertical" - a:paddingLeft="3dip" /> + a:layout_gravity="start|center_vertical" + a:paddingStart="3dip" /> + a:layout_gravity="start|center_vertical" + a:paddingStart="6dip" + a:paddingEnd="3dip"> + a:paddingEnd="3dip" /> diff --git a/ultrasonic/src/main/res/layout/appwidget4x1.xml b/ultrasonic/src/main/res/layout/appwidget4x1.xml index 8234544e..d46d1858 100644 --- a/ultrasonic/src/main/res/layout/appwidget4x1.xml +++ b/ultrasonic/src/main/res/layout/appwidget4x1.xml @@ -38,8 +38,8 @@ a:fadingEdge="horizontal" a:fadingEdgeLength="20dip" a:minHeight="16sp" - a:paddingLeft="4dip" - a:paddingRight="4dip" + a:paddingStart="4dip" + a:paddingEnd="4dip" a:paddingTop="4dip" a:singleLine="true" a:gravity="center_horizontal" @@ -57,8 +57,8 @@ a:fadingEdgeLength="10dip" a:minHeight="12sp" a:paddingBottom="4dip" - a:paddingLeft="4dip" - a:paddingRight="4dip" + a:paddingStart="4dip" + a:paddingEnd="4dip" a:singleLine="true" a:gravity="center_horizontal" a:text="Artist" diff --git a/ultrasonic/src/main/res/layout/appwidget4x2.xml b/ultrasonic/src/main/res/layout/appwidget4x2.xml index dbee8569..51c4bcbf 100644 --- a/ultrasonic/src/main/res/layout/appwidget4x2.xml +++ b/ultrasonic/src/main/res/layout/appwidget4x2.xml @@ -40,8 +40,8 @@ a:fadingEdge="horizontal" a:fadingEdgeLength="20dip" a:minHeight="16sp" - a:paddingLeft="4dip" - a:paddingRight="4dip" + a:paddingStart="4dip" + a:paddingEnd="4dip" a:paddingTop="4dip" a:singleLine="true" a:gravity="center_horizontal" @@ -58,8 +58,8 @@ a:fadingEdge="horizontal" a:fadingEdgeLength="10dip" a:minHeight="12sp" - a:paddingLeft="4dip" - a:paddingRight="4dip" + a:paddingStart="4dip" + a:paddingEnd="4dip" a:singleLine="true" a:gravity="center_horizontal" a:text="Artist" @@ -75,8 +75,8 @@ a:fadingEdgeLength="10dip" a:minHeight="12sp" a:paddingBottom="4dip" - a:paddingLeft="4dip" - a:paddingRight="4dip" + a:paddingStart="4dip" + a:paddingEnd="4dip" a:singleLine="true" a:gravity="center_horizontal" a:text="Album" diff --git a/ultrasonic/src/main/res/layout/appwidget4x3.xml b/ultrasonic/src/main/res/layout/appwidget4x3.xml index 411dba03..ff103795 100644 --- a/ultrasonic/src/main/res/layout/appwidget4x3.xml +++ b/ultrasonic/src/main/res/layout/appwidget4x3.xml @@ -42,8 +42,8 @@ a:fadingEdge="horizontal" a:fadingEdgeLength="20dip" a:minHeight="16sp" - a:paddingLeft="5dip" - a:paddingRight="5dip" + a:paddingStart="5dip" + a:paddingEnd="5dip" a:singleLine="true" a:textColor="@color/appwidget_text" a:textSize="16sp" @@ -61,7 +61,7 @@ a:fadingEdgeLength="10dip" a:minHeight="12sp" a:paddingBottom="2dip" - a:paddingLeft="5dip" + a:paddingStart="5dip" a:singleLine="true" a:text="Artist" a:layout_gravity="center_horizontal" diff --git a/ultrasonic/src/main/res/layout/appwidget4x4.xml b/ultrasonic/src/main/res/layout/appwidget4x4.xml index b5aa402b..a7975b1d 100644 --- a/ultrasonic/src/main/res/layout/appwidget4x4.xml +++ b/ultrasonic/src/main/res/layout/appwidget4x4.xml @@ -43,8 +43,8 @@ a:fadingEdge="horizontal" a:fadingEdgeLength="20dip" a:minHeight="16sp" - a:paddingLeft="5dip" - a:paddingRight="5dip" + a:paddingStart="5dip" + a:paddingEnd="5dip" a:singleLine="true" a:textColor="@color/appwidget_text" a:textSize="16sp" @@ -62,7 +62,7 @@ a:fadingEdgeLength="10dip" a:minHeight="12sp" a:paddingBottom="2dip" - a:paddingLeft="5dip" + a:paddingStart="5dip" a:singleLine="true" a:text="Artist" a:layout_gravity="center_horizontal" diff --git a/ultrasonic/src/main/res/layout/artist_list_item.xml b/ultrasonic/src/main/res/layout/artist_list_item.xml index 26a4d7a1..527523c8 100644 --- a/ultrasonic/src/main/res/layout/artist_list_item.xml +++ b/ultrasonic/src/main/res/layout/artist_list_item.xml @@ -16,9 +16,7 @@ a:minWidth="56dip" a:minHeight="56dip" a:paddingStart="8dip" - a:paddingLeft="8dip" a:paddingEnd="8dip" - a:paddingRight="8dip" a:text="A" a:textAppearance="?android:attr/textAppearanceLarge" a:textColor="@color/cyan" /> @@ -30,11 +28,8 @@ a:layout_gravity="center_horizontal|center_vertical" a:layout_marginTop="8dp" a:layout_marginStart="2dp" - a:layout_marginLeft="2dp" a:layout_marginEnd="10dp" - a:layout_marginRight="10dp" a:layout_toEndOf="@+id/row_section" - a:layout_toRightOf="@+id/row_section" a:scaleType="fitCenter" a:src="@drawable/ic_contact_picture" app:shapeAppearanceOverlay="@style/roundedImageView" /> @@ -44,13 +39,12 @@ a:layout_width="fill_parent" a:layout_height="wrap_content" a:layout_toEndOf="@+id/artist_coverart" - a:layout_toRightOf="@+id/artist_coverart" a:drawablePadding="6dip" a:gravity="center_vertical" a:minHeight="56dip" - a:paddingLeft="3dip" - a:paddingRight="3dip" - a:layout_marginRight="12dp" + a:paddingStart="3dip" + a:paddingEnd="3dip" a:layout_marginEnd="12dp" + a:textAppearance="?android:attr/textAppearanceMedium" /> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/chat_item.xml b/ultrasonic/src/main/res/layout/chat_item.xml index 3dad7870..8497995d 100644 --- a/ultrasonic/src/main/res/layout/chat_item.xml +++ b/ultrasonic/src/main/res/layout/chat_item.xml @@ -20,8 +20,8 @@ a:id="@+id/chat_username" a:layout_width="wrap_content" a:layout_height="wrap_content" - a:layout_marginLeft="6dip" - a:layout_marginRight="6dip" + a:layout_marginStart="6dip" + a:layout_marginEnd="6dip" a:ellipsize="marquee" a:singleLine="true" a:textIsSelectable="true" @@ -43,7 +43,7 @@ a:id="@+id/chat_time" a:layout_width="wrap_content" a:layout_height="wrap_content" - a:layout_marginLeft="6dip" + a:layout_marginStart="6dip" a:singleLine="true" a:textIsSelectable="true" a:textAppearance="?android:attr/textAppearanceMedium" @@ -53,8 +53,8 @@ a:id="@+id/chat_message" a:layout_width="wrap_content" a:layout_height="wrap_content" - a:layout_marginLeft="6dip" - a:layout_marginRight="6dip" + a:layout_marginStart="6dip" + a:layout_marginEnd="6dip" a:textIsSelectable="true" a:linksClickable="true" a:singleLine="false" diff --git a/ultrasonic/src/main/res/layout/chat_item_reverse.xml b/ultrasonic/src/main/res/layout/chat_item_reverse.xml index 39ca0b0f..a7684a43 100644 --- a/ultrasonic/src/main/res/layout/chat_item_reverse.xml +++ b/ultrasonic/src/main/res/layout/chat_item_reverse.xml @@ -17,7 +17,7 @@ a:id="@+id/chat_username" a:layout_width="wrap_content" a:layout_height="wrap_content" - a:layout_marginRight="6dip" + a:layout_marginEnd="6dip" a:gravity="center_vertical|right" a:layout_gravity="right" a:ellipsize="marquee" @@ -38,7 +38,7 @@ a:id="@+id/chat_time" a:layout_width="wrap_content" a:layout_height="wrap_content" - a:layout_marginLeft="6dip" + a:layout_marginStart="6dip" a:singleLine="true" a:gravity="center_vertical|right" a:textIsSelectable="true" @@ -49,8 +49,8 @@ a:id="@+id/chat_message" a:layout_width="wrap_content" a:layout_height="wrap_content" - a:layout_marginLeft="6dip" - a:layout_marginRight="6dip" + a:layout_marginStart="6dip" + a:layout_marginEnd="6dip" a:linksClickable="true" a:singleLine="false" a:autoLink="all" diff --git a/ultrasonic/src/main/res/layout/current_playing.xml b/ultrasonic/src/main/res/layout/current_playing.xml index 43c5d843..806ceb04 100644 --- a/ultrasonic/src/main/res/layout/current_playing.xml +++ b/ultrasonic/src/main/res/layout/current_playing.xml @@ -110,8 +110,8 @@ a:layout_height="60dip" a:layout_gravity="center" a:background="@color/translucent" - a:layout_marginLeft="80dip" - a:layout_marginRight="80dip" + a:layout_marginStart="80dip" + a:layout_marginEnd="80dip" a:orientation="vertical" /> diff --git a/ultrasonic/src/main/res/layout/equalizer.xml b/ultrasonic/src/main/res/layout/equalizer.xml index a1f3ecba..63aea6d2 100644 --- a/ultrasonic/src/main/res/layout/equalizer.xml +++ b/ultrasonic/src/main/res/layout/equalizer.xml @@ -36,8 +36,8 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="20dip" - android:paddingLeft="40dip" - android:paddingRight="40dip"/> + android:paddingStart="40dip" + android:paddingEnd="40dip"/> diff --git a/ultrasonic/src/main/res/layout/equalizer_bar.xml b/ultrasonic/src/main/res/layout/equalizer_bar.xml index 6b75b623..b76d8e75 100644 --- a/ultrasonic/src/main/res/layout/equalizer_bar.xml +++ b/ultrasonic/src/main/res/layout/equalizer_bar.xml @@ -24,7 +24,7 @@ a:layout_height="wrap_content" a:layout_marginTop="8dp" a:layout_alignParentRight="true" - a:layout_toRightOf="@+id/equalizer.frequency" + a:layout_toEndOf="@+id/equalizer.frequency" /> diff --git a/ultrasonic/src/main/res/layout/generic_text_list_item.xml b/ultrasonic/src/main/res/layout/generic_text_list_item.xml index 05b31407..560f794a 100644 --- a/ultrasonic/src/main/res/layout/generic_text_list_item.xml +++ b/ultrasonic/src/main/res/layout/generic_text_list_item.xml @@ -6,6 +6,6 @@ a:layout_height="wrap_content" a:textAppearance="?android:attr/textAppearanceMedium" a:gravity="center_vertical" - a:paddingLeft="3dip" - a:paddingRight="3dip" + a:paddingStart="3dip" + a:paddingEnd="3dip" a:minHeight="50dip"/> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/jukebox_volume.xml b/ultrasonic/src/main/res/layout/jukebox_volume.xml index 6e00ed96..81adf6ad 100644 --- a/ultrasonic/src/main/res/layout/jukebox_volume.xml +++ b/ultrasonic/src/main/res/layout/jukebox_volume.xml @@ -14,8 +14,8 @@ a:textColor="#ffffffff" a:shadowColor="#bb000000" a:shadowRadius="2.75" - a:paddingLeft="32dp" - a:paddingRight="32dp" + a:paddingStart="32dp" + a:paddingEnd="32dp" a:paddingBottom="12dp" /> diff --git a/ultrasonic/src/main/res/layout/lyrics.xml b/ultrasonic/src/main/res/layout/lyrics.xml index 6880d374..0495dd9c 100644 --- a/ultrasonic/src/main/res/layout/lyrics.xml +++ b/ultrasonic/src/main/res/layout/lyrics.xml @@ -28,8 +28,8 @@ a:gravity="center_horizontal" a:layout_width="fill_parent" a:layout_height="wrap_content" - a:paddingLeft="10dip" - a:paddingRight="10dip" + a:paddingStart="10dip" + a:paddingEnd="10dip" a:paddingTop="10dip" a:paddingBottom="4dip" /> @@ -40,8 +40,8 @@ a:gravity="center_horizontal" a:layout_width="fill_parent" a:layout_height="wrap_content" - a:paddingLeft="10dip" - a:paddingRight="10dip" + a:paddingStart="10dip" + a:paddingEnd="10dip" /> diff --git a/ultrasonic/src/main/res/layout/main_buttons.xml b/ultrasonic/src/main/res/layout/main_buttons.xml index bc27ea5b..ef1395c7 100644 --- a/ultrasonic/src/main/res/layout/main_buttons.xml +++ b/ultrasonic/src/main/res/layout/main_buttons.xml @@ -5,95 +5,95 @@ + a:paddingStart="6dip" a:paddingEnd="6dip" a:minHeight="40dip" /> + a:paddingStart="6dip" a:paddingEnd="6dip" a:minHeight="40dip" /> + a:paddingStart="6dip" a:paddingEnd="6dip" a:minHeight="40dip" /> + a:paddingStart="6dip" a:paddingEnd="6dip" a:minHeight="40dip" /> + a:paddingStart="6dip" a:paddingEnd="6dip" a:minHeight="40dip" /> + a:paddingStart="6dip" a:paddingEnd="6dip" a:minHeight="40dip" /> + a:paddingStart="6dip" a:paddingEnd="6dip" a:minHeight="40dip" /> + a:paddingStart="6dip" a:paddingEnd="6dip" a:minHeight="40dip" /> + a:paddingStart="6dip" a:paddingEnd="6dip" a:minHeight="40dip" /> + a:paddingStart="6dip" a:paddingEnd="6dip" a:minHeight="40dip" /> diff --git a/ultrasonic/src/main/res/layout/media_buttons.xml b/ultrasonic/src/main/res/layout/media_buttons.xml index 92f02e91..a9eef057 100644 --- a/ultrasonic/src/main/res/layout/media_buttons.xml +++ b/ultrasonic/src/main/res/layout/media_buttons.xml @@ -4,8 +4,8 @@ a:layout_width="fill_parent" a:layout_height="wrap_content" a:orientation="horizontal" - a:layout_marginLeft="12dp" - a:layout_marginRight="12dp" > + a:layout_marginStart="12dp" + a:layout_marginEnd="12dp" > @@ -30,14 +29,13 @@ android:layout_gravity="center_vertical" android:layout_weight="1.0" android:orientation="vertical" - android:paddingLeft="11.0dip" android:paddingStart="11.0dip"> + a:layout_marginStart="12dp" + a:layout_marginEnd="12dp" > + a:layout_marginStart="12dp" + a:layout_marginEnd="12dp" > @@ -31,7 +31,7 @@ a:layout_width="wrap_content" a:layout_height="wrap_content" a:layout_alignParentRight="true" - a:layout_marginRight="12dip" + a:layout_marginEnd="12dip" a:text="@string/util.no_time" a:textAppearance="?android:attr/textAppearanceSmall" /> diff --git a/ultrasonic/src/main/res/layout/playlist_list_item.xml b/ultrasonic/src/main/res/layout/playlist_list_item.xml index 3bcc8489..410837ad 100644 --- a/ultrasonic/src/main/res/layout/playlist_list_item.xml +++ b/ultrasonic/src/main/res/layout/playlist_list_item.xml @@ -11,8 +11,8 @@ android:layout_weight="1" android:textAppearance="?android:attr/textAppearanceMedium" android:gravity="left|center_vertical" - android:paddingLeft="6dip" - android:paddingRight="6dip" + android:paddingStart="6dip" + android:paddingEnd="6dip" android:minHeight="50dip"/> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/search_buttons.xml b/ultrasonic/src/main/res/layout/search_buttons.xml index 89742b70..66b82755 100644 --- a/ultrasonic/src/main/res/layout/search_buttons.xml +++ b/ultrasonic/src/main/res/layout/search_buttons.xml @@ -24,7 +24,7 @@ a:textStyle="bold" a:background="#ff555555" a:gravity="center_vertical" - a:paddingLeft="4dp"/> + a:paddingStart="4dp"/> + a:paddingStart="4dp"/> + a:paddingStart="4dp"/> @@ -18,9 +18,9 @@ a:id="@+id/select_album_title" a:layout_width="wrap_content" a:layout_height="wrap_content" - a:layout_toRightOf="@+id/select_album_art" + a:layout_toEndOf="@+id/select_album_art" a:ellipsize="end" - a:paddingRight="4dip" + a:paddingEnd="4dip" a:paddingTop="10dip" a:singleLine="true" a:textAppearance="?android:attr/textAppearanceMedium"/> @@ -30,9 +30,9 @@ a:layout_width="wrap_content" a:layout_height="wrap_content" a:layout_below="@+id/select_album_title" - a:layout_toRightOf="@+id/select_album_art" + a:layout_toEndOf="@+id/select_album_art" a:ellipsize="end" - a:paddingRight="4dip" + a:paddingEnd="4dip" a:singleLine="true" a:textAppearance="?android:attr/textAppearanceSmall"/> @@ -41,9 +41,9 @@ a:layout_width="wrap_content" a:layout_height="wrap_content" a:layout_below="@+id/select_album_artist" - a:layout_toRightOf="@+id/select_album_art" + a:layout_toEndOf="@+id/select_album_art" a:ellipsize="end" - a:paddingRight="4dip" + a:paddingEnd="4dip" a:singleLine="true" a:textAppearance="?android:attr/textAppearanceSmall"/> @@ -52,9 +52,9 @@ a:layout_width="wrap_content" a:layout_height="wrap_content" a:layout_below="@+id/select_album_genre" - a:layout_toRightOf="@+id/select_album_art" + a:layout_toEndOf="@+id/select_album_art" a:ellipsize="end" - a:paddingRight="4dip" + a:paddingEnd="4dip" a:singleLine="true" a:textAppearance="?android:attr/textAppearanceSmall"/> @@ -63,9 +63,9 @@ a:layout_width="wrap_content" a:layout_height="wrap_content" a:layout_below="@+id/select_album_year" - a:layout_toRightOf="@+id/select_album_art" + a:layout_toEndOf="@+id/select_album_art" a:ellipsize="none" - a:paddingRight="4dip" + a:paddingEnd="4dip" a:singleLine="true" a:textAppearance="?android:attr/textAppearanceSmall"/> @@ -74,9 +74,9 @@ a:layout_width="wrap_content" a:layout_height="wrap_content" a:layout_below="@+id/select_album_song_count" - a:layout_toRightOf="@+id/select_album_art" + a:layout_toEndOf="@+id/select_album_art" a:ellipsize="none" - a:paddingRight="4dip" + a:paddingEnd="4dip" a:singleLine="true" a:textAppearance="?android:attr/textAppearanceSmall"/> diff --git a/ultrasonic/src/main/res/layout/select_folder_header.xml b/ultrasonic/src/main/res/layout/select_folder_header.xml index 68538369..6eefae31 100644 --- a/ultrasonic/src/main/res/layout/select_folder_header.xml +++ b/ultrasonic/src/main/res/layout/select_folder_header.xml @@ -6,7 +6,7 @@ a:minHeight="?android:attr/listPreferredItemHeight" a:orientation="horizontal" a:paddingBottom="2dip" - a:paddingLeft="6dp" + a:paddingStart="6dp" a:paddingTop="2dip" a:background="?android:attr/selectableItemBackground" a:clickable="true" @@ -27,7 +27,7 @@ a:id="@+id/select_folder_title" a:layout_width="fill_parent" a:layout_height="wrap_content" - a:layout_marginLeft="10dip" + a:layout_marginStart="10dip" a:layout_marginTop="6dip" a:text="@string/select_artist.folder" a:textAppearance="?android:attr/textAppearanceLarge" /> @@ -36,7 +36,7 @@ a:id="@+id/select_folder_name" a:layout_width="fill_parent" a:layout_height="wrap_content" - a:layout_marginLeft="10dip" + a:layout_marginStart="10dip" a:textAppearance="?android:attr/textAppearanceSmall" /> diff --git a/ultrasonic/src/main/res/layout/server_edit.xml b/ultrasonic/src/main/res/layout/server_edit.xml index 18e1c923..289b371c 100644 --- a/ultrasonic/src/main/res/layout/server_edit.xml +++ b/ultrasonic/src/main/res/layout/server_edit.xml @@ -131,7 +131,6 @@ a:layout_width="0dp" a:layout_height="wrap_content" a:layout_marginStart="5dp" - a:layout_marginLeft="5dp" a:text="@string/settings.title.allow_self_signed_certificate" app:layout_constraintBottom_toTopOf="@id/edit_ldap_title" app:layout_constraintStart_toStartOf="parent" @@ -142,9 +141,7 @@ a:layout_width="0dp" a:layout_height="wrap_content" a:layout_marginStart="8dp" - a:layout_marginLeft="8dp" a:layout_marginEnd="5dp" - a:layout_marginRight="5dp" a:layout_marginBottom="8dp" a:checked="false" app:layout_constraintBottom_toTopOf="@id/edit_ldap_title" @@ -158,7 +155,6 @@ a:layout_width="0dp" a:layout_height="wrap_content" a:layout_marginStart="5dp" - a:layout_marginLeft="5dp" a:text="@string/settings.title.enable_ldap_users_support" app:layout_constraintBottom_toTopOf="@id/edit_ldap_description" app:layout_constraintStart_toStartOf="parent" @@ -170,7 +166,6 @@ a:layout_width="0dp" a:layout_height="wrap_content" a:layout_marginStart="5dp" - a:layout_marginLeft="5dp" a:text="@string/settings.summary.enable_ldap_users_support" app:layout_constraintBottom_toTopOf="@id/edit_jukebox" app:layout_constraintEnd_toStartOf="@id/edit_ldap" @@ -182,8 +177,6 @@ a:layout_width="wrap_content" a:layout_height="wrap_content" a:layout_marginEnd="5dp" - a:layout_marginRight="5dp" - a:layout_marginLeft="8dp" a:layout_marginStart="8dp" a:checked="false" app:layout_constraintBottom_toBottomOf="@id/edit_ldap_description" @@ -197,7 +190,6 @@ a:layout_width="0dp" a:layout_height="wrap_content" a:layout_marginStart="5dp" - a:layout_marginLeft="5dp" a:layout_marginTop="8dp" a:text="@string/jukebox.is_default" app:layout_constraintStart_toStartOf="parent" @@ -208,10 +200,8 @@ a:layout_width="0dp" a:layout_height="wrap_content" a:layout_marginStart="8dp" - a:layout_marginLeft="8dp" a:layout_marginTop="8dp" a:layout_marginEnd="5dp" - a:layout_marginRight="5dp" a:checked="false" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/edit_jukebox_title" diff --git a/ultrasonic/src/main/res/layout/server_row.xml b/ultrasonic/src/main/res/layout/server_row.xml index 451ba8e9..537f3043 100644 --- a/ultrasonic/src/main/res/layout/server_row.xml +++ b/ultrasonic/src/main/res/layout/server_row.xml @@ -55,7 +55,7 @@ a:layout_centerVertical="true" a:layout_gravity="end" a:layout_marginEnd="15dp" - a:layout_marginRight="15dp" + a:focusable="false" a:src="?attr/more_vert" tools:src="@drawable/ic_more_vert_dark" diff --git a/ultrasonic/src/main/res/layout/share_details.xml b/ultrasonic/src/main/res/layout/share_details.xml index 6790f366..26d3e8bb 100644 --- a/ultrasonic/src/main/res/layout/share_details.xml +++ b/ultrasonic/src/main/res/layout/share_details.xml @@ -10,6 +10,14 @@ a:layout_width="wrap_content" a:layout_height="wrap_content"> + + @@ -38,7 +38,7 @@ a:layout_gravity="left|center_vertical" a:layout_weight="1" a:ellipsize="middle" - a:paddingLeft="4dip" + a:paddingStart="4dip" a:singleLine="true" a:textAppearance="?android:attr/textAppearanceSmall"/> diff --git a/ultrasonic/src/main/res/layout/song_details.xml b/ultrasonic/src/main/res/layout/song_details.xml index c2ed8e16..7cab9d5f 100644 --- a/ultrasonic/src/main/res/layout/song_details.xml +++ b/ultrasonic/src/main/res/layout/song_details.xml @@ -4,7 +4,7 @@ a:layout_height="wrap_content" a:layout_gravity="center_vertical" a:layout_weight="1" - a:layout_marginLeft="4dp" + a:layout_marginStart="4dp" a:orientation="vertical"> @@ -39,7 +39,7 @@ a:layout_height="wrap_content" a:layout_gravity="right|center_vertical" a:drawablePadding="6dip" - a:paddingRight="6dip"/> + a:paddingEnd="6dip"/> @@ -64,8 +64,8 @@ a:layout_width="wrap_content" a:layout_height="wrap_content" a:layout_gravity="right|center_vertical" - a:paddingLeft="3dip" - a:paddingRight="9dip" + a:paddingStart="3dip" + a:paddingEnd="9dip" a:singleLine="true" a:textAppearance="?android:attr/textAppearanceSmall"/> diff --git a/ultrasonic/src/main/res/layout/song_list_item.xml b/ultrasonic/src/main/res/layout/song_list_item.xml index 94d7384a..820bc3bb 100644 --- a/ultrasonic/src/main/res/layout/song_list_item.xml +++ b/ultrasonic/src/main/res/layout/song_list_item.xml @@ -10,9 +10,7 @@ a:id="@+id/song_drag" a:layout_width="wrap_content" a:layout_height="fill_parent" - a:paddingLeft="5dip" a:paddingStart="5dip" - a:paddingRight="0dip" a:paddingEnd="0dip" a:background="@android:color/transparent" a:focusable="false" @@ -25,7 +23,7 @@ a:layout_height="fill_parent" a:checkMark="?attr/button_check_custom" a:gravity="center_vertical" - a:paddingRight="4dip"/> + a:paddingEnd="4dip"/> @@ -81,7 +79,7 @@ a:id="@+id/song_five_star_5" a:layout_width="10dip" a:layout_height="fill_parent" - a:layout_marginRight="8dip" + a:layout_marginEnd="8dip" a:background="@android:color/transparent" a:focusable="false" a:gravity="center_vertical" @@ -97,7 +95,7 @@ a:background="@android:color/transparent" a:focusable="false" a:gravity="center_vertical" - a:paddingRight="8dip" + a:paddingEnd="8dip" a:src="?attr/star_hollow" /> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/time_span_dialog.xml b/ultrasonic/src/main/res/layout/time_span_dialog.xml index 30b9be60..920a87b6 100644 --- a/ultrasonic/src/main/res/layout/time_span_dialog.xml +++ b/ultrasonic/src/main/res/layout/time_span_dialog.xml @@ -44,7 +44,7 @@ android:layout_height="wrap_content" android:id="@+id/timeSpanSpinner" android:layout_alignBottom="@+id/timeSpanEditText" - android:layout_toRightOf="@+id/timeSpanEditText"/> + android:layout_toEndOf="@+id/timeSpanEditText"/> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/update_playlist.xml b/ultrasonic/src/main/res/layout/update_playlist.xml index 57664f99..cc33e81e 100644 --- a/ultrasonic/src/main/res/layout/update_playlist.xml +++ b/ultrasonic/src/main/res/layout/update_playlist.xml @@ -12,7 +12,7 @@ android:id="@+id/get_playlist_name_label" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="4dp" + android:layout_marginStart="4dp" android:textSize="20sp" android:text="@string/common.name" /> @@ -34,7 +34,7 @@ android:id="@+id/get_playlist_comment_label" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="4dp" + android:layout_marginStart="4dp" android:textSize="20sp" android:text="@string/common.comment" /> @@ -56,7 +56,7 @@ android:id="@+id/get_playlist_public_label" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="4dp" + android:layout_marginStart="4dp" android:textSize="20sp" android:text="@string/common.public" /> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/video_details.xml b/ultrasonic/src/main/res/layout/video_details.xml index 08ac014e..2ea9473c 100644 --- a/ultrasonic/src/main/res/layout/video_details.xml +++ b/ultrasonic/src/main/res/layout/video_details.xml @@ -19,8 +19,8 @@ a:layout_gravity="left|center_vertical" a:layout_weight="1" a:ellipsize="end" - a:paddingLeft="4dip" - a:paddingRight="2dip" + a:paddingStart="4dip" + a:paddingEnd="2dip" a:singleLine="true" a:textAppearance="?android:attr/textAppearanceMedium"/> @@ -29,8 +29,8 @@ a:layout_width="wrap_content" a:layout_height="wrap_content" a:layout_gravity="right|center_vertical" - a:paddingLeft="2dip" - a:paddingRight="6dip" + a:paddingStart="2dip" + a:paddingEnd="6dip" a:singleLine="true" a:textAppearance="?android:attr/textAppearanceSmall"/> diff --git a/ultrasonic/src/main/res/layout/video_list_item.xml b/ultrasonic/src/main/res/layout/video_list_item.xml index 0066fb2d..5851bbca 100644 --- a/ultrasonic/src/main/res/layout/video_list_item.xml +++ b/ultrasonic/src/main/res/layout/video_list_item.xml @@ -10,7 +10,7 @@ a:layout_width="wrap_content" a:layout_height="fill_parent" a:gravity="center_vertical" - a:paddingLeft="1dip" + a:paddingStart="1dip" a:visibility="gone" /> \ No newline at end of file diff --git a/ultrasonic/src/main/res/menu/nowplaying.xml b/ultrasonic/src/main/res/menu/nowplaying.xml index ae1e1e32..83530690 100644 --- a/ultrasonic/src/main/res/menu/nowplaying.xml +++ b/ultrasonic/src/main/res/menu/nowplaying.xml @@ -14,6 +14,11 @@ a:icon="?attr/star_hollow" app:showAsAction="ifRoom|withText" a:title="@string/download.menu_star"/> + - + Načítám… Chyba sítě. Ověřte adresu serveru nebo zkuste později. @@ -17,7 +17,7 @@ Právě hraje Podcasty Není registrován žádný podcast kanál - Podcast + Podcasty Playlisty Hledat Poslat zprávu @@ -301,7 +301,9 @@ Očekává jména hlavních adresářů obsahující jména umělců Procházet za použití ID3 tagů Používat metodu ID3 tagů místo jmen na základě adresářové struktury - Video + Obrázek umělce v seznamu umělců + Zobrazí obrázek umělce v náhledu umělců pokud je dostupný + Video Obnovení náhledu .5 sekundy 1 sekunda @@ -321,14 +323,13 @@ 0.00 GB 0 KB 0.00 MB - -:-- + -:-- 0:00 Ťuknutím vybrat hudbu SD karta nedostupná Chybí SD karta Výchozí popis sdílení Sdílení - Vždy se dotazovat na popis a čas vypršení při vytváření sdílení Vždy se dotazovat na detaily Výchozí čas vypršení Dialog opět nezobrazovat @@ -379,7 +380,6 @@ Zachovat soubory Smazat soubory Smazat soubory logů. - Ultrasonic nemá přístup k odkládacím souborům hudby. Umístění odkládacího adresáře bylo změněno na výchozí hodnotu. Varování Ultrasonic vyžaduje práva čtení/zápisu do hudebního odkládacího adresáře. Umístění odkládacího adresáře bylo změněno na výchozí hodnotu. diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index 9e766b76..0182cf3e 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -1,5 +1,5 @@ - + Lade… Ein Netzwerkfehler ist aufgetreten. Bitte die Serveradresse prüfen oder später noch einmal versuchen. @@ -298,7 +298,7 @@ Annehmen, dass der Ordner der obersten Ebene der Name des Albumkünstlers ist Durchsuchen von ID3-Tags Nutze ID3 Tag Methode anstatt Dateisystem-Methode - Film + Film Aktualisierungsinterval .5 Sekunden 1 Sekunde @@ -318,14 +318,13 @@ 0.00 GB 0 KB 0.00 MB - -:-- + -:-- 0:00 Berühren, um Musik auszuwählen SD Karte nicht verfügbar Keine SD Karte Standard Beschreibung einer Freigabe Teilen - Beim erstellen einer Freigabe immer nach Beschreibung und Ablaufdatum fragen Immer nach Details fragen Standard Ablaufzeit Dialog nicht wieder anzeigen @@ -362,35 +361,6 @@ Künstler zeigen Mehrere Jahre Server hinzufügen - - %d Titel - %d Titel - - - %d Titel - %d Titel - - - %d Titel - %d Titel - - - %d Titel - %d Titel - - - %d Titel - %d Titel - - - %d Titel - %d Titel - - - %d Titel - %d Titel - - Allgemeiner API Fehler: %1$s Keine Nachricht vom Server erhalten @@ -401,7 +371,7 @@ Angeforderte Daten nicht gefunden. Der Testzeitraum ist abgelaufen. Inkompatible Versionen. Bitte die Ultrasonic App aktualisieren. - Inkompatible Versionen. Bitte den subsonic Server aktualisieren.. + Inkompatible Versionen. Bitte den subsonic Server aktualisieren. Funktionseinstellungem diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 8ba9dc7a..c176ee3f 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -28,7 +28,9 @@ Listas de reproducción Buscar Enviar un mensaje + Álbum Ultrasonic + Artista Cancelar Comentario Confirmar @@ -48,6 +50,7 @@ Reproducción aleatoria Public Guardar + Título Desanclar Varios artistas Quieres eliminar %1$s @@ -279,6 +282,8 @@ Configuración de la búsqueda Enviar la carátula del álbum vía Bluetooth (Puede causar que las notificaciones Bluetooth fallen) Carátula del Álbum vía Bluetooth + La lista de reproducción actual no se enviará a los dispositivos conectados. Esto puede restaurar la compatibilidad con dispositivos AVRCP 1.3, cuando la visualización de la pista actual no se actualiza + Desactivar el envío de la lista de reproducción actual Enviar notificaciones de reproducción vía Bluetooth Enviar notificaciones Bluetooth Administrar servidores @@ -310,7 +315,7 @@ Permir certificado HTTPS autofirmado Forzar autenticación de contraseña plana Esto obliga a la aplicación a enviar siempre la contraseña sin cifrar. - Útil si el servidor Subsonic no admite la nueva API de autenticación para los usuarios.. + Útil si el servidor Subsonic no admite la nueva API de autenticación para los usuarios. Usar carpetas para el nombre del artista Se asume que la carpeta en el nivel mal alto es el nombre del artista del álbum Navegar usando las etiquetas ID3 @@ -344,11 +349,13 @@ No hay tarjeta SD Descripción predeterminada al compartir Compartiendo - Preguntar siempre por la descripción y caducidad cuando se crea un compartido + Preguntar siempre por la descripción y caducidad al crear un recurso compartido en el servidor Preguntar siempre los detalles Tiempo de caducidad predeterminado No mostrar el diálogo de nuevo Configurar las opciones de compartir + Crear recurso compartido en el servidor + Compartir creará un recurso compartido en el servidor y compartirá su URL. Si está desactivado, solo se comparten los detalles de la canción Sin caducidad Alternar lista de reproducción Configurar marcador @@ -371,6 +378,7 @@ Tiempo de caducidad \"%s\" fue eliminado/a de la lista de reproducción Compartir lista de reproducción + Compartir la canción actual Saludo predeterminado para los compartidos Echa un vistazo a esta música que te comparto desde %s Compartir canciones vía @@ -395,6 +403,7 @@ Mantener archivos Borrar archivos Archivos de registro eliminados. + Descargando medios en segundo plano… Ultrasonic no puede acceder a la caché de los ficheros de música. La ubicación de la caché se restableció a la ruta predeterminada. Atención diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index 7dacf48b..9b75c926 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -344,7 +344,6 @@ Aucune carte SD Description par défaut de la collection partagée Répartition - Toujours demander pour la description et l\'expiration lors de la création d\'une collection partagée Toujours demander pour plus de détails Temps d\'expiration par défaut Ne montre pas de dialogue à nouveau @@ -395,7 +394,6 @@ Conserver les fichiers Supprimer les fichiers Fichiers de log supprimés - Ultrasonic ne peut pas accéder au cache. Le répertoire de cache a été réinitialisé sur le chemin par défaut. Attention Ultrasonic requiert les droits de lecture/écriture sur le répertoire de cache. Le répertoire de cache a été réinitialisé à la valeur par défaut. diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index b208ad28..f1aa2a85 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -1,5 +1,5 @@ - + Betöltés… 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! @@ -315,7 +315,7 @@ ID3 Tag módszer használata a fájlredszer alapú mód helyett. Előadó képének megjelenítése Az előadó listában megjeleníti a képeket, amennyiben elérhetőek - Videó + Videó Nézet frissítési gyakorisága .5 másodperc 1 másodperc @@ -335,14 +335,13 @@ 0.00 GB 0 KB 0.00 MB - -:-- + -:-- 0:00 Érintse meg a zene kiválasztásához Az SD kártya nem elérhető! Nincs SD kártya! Megosztás alapértelmezett leírása Megosztás - Megosztás létrehozásakor mindig kérje be a leírást és a lejárati időt. Mindig kérdezzen rá a részletekre Alapértelmezett lejárati idő A párbeszédablak ne jelenjen meg többé @@ -393,7 +392,6 @@ Fájlok megtartása Fájlok törlése Naplófájlok törölve. - Az Ultrasonic nem éri el a zenei fájl gyorsítótárat. A gyorsítótár helye visszaállítva az alapbeállításra. Figyelem Az Ultrasonic működéséhez írás/olvasás hozzáférés szükséges a zenei fájl gyorsítótárhoz. A gyorsítótár helye visszaállítva az alapbeállításra. diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index 6a5d46dd..a37ab73c 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -1,5 +1,5 @@ - + Caricamento… Si è verificato un errore di rete. Si prega di controllare l\'indirizzo del server o riprovare più tardi. @@ -290,7 +290,7 @@ Presumi che la cartella superiore sia il nome dell\'artista dell\'album Sfoglia Utilizzando Tag ID3 Usa metodi tag ID3 invece dei metodi basati sul filesystem - Video + Video .5 secondo 1 secondo 1.5 secondi @@ -307,7 +307,7 @@ 0.00 GB 0 KB 0.00 MB - -:-- + -:-- 0:00 Tocca per selezionare musica Scheda SD non disponibile @@ -324,9 +324,5 @@ Commenta \"%s\" è stato rimosso dalla playlist Condividi canzoni via - - 1 canzone - %d canzoni - Il periodo di prova è terminato. diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index 976fdfd7..0876e987 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -1,5 +1,5 @@ - + Bezig met laden… Er is een netwerkfout opgetreden. Controleer het serveradres of probeer het later opnieuw. @@ -28,7 +28,9 @@ Afspeellijsten Zoeken Bericht versturen + Album Ultrasonic + Artiest Annuleren Reageren Bevestigen @@ -48,6 +50,7 @@ Willekeurig afspelen Openbaar Opslaan + Titel Losmaken Verschillende artiesten Wil je %1$s verwijderen? @@ -110,7 +113,9 @@ Favorieten Nummers Video\'s + Om in Ultrasonic naar je eigen muziek te luisteren, heb je je eigen server nodig. \n\n➤ Maar als je de app eerst wilt uitproberen, dan kun je voorlopig de demoserver gebruiken. \n\n➤ In de instellingen kun je je eigen server instellen. Welkom! + Open de instellingen Over Algemeen Afspeellijst %s verwijderd @@ -123,7 +128,7 @@ Offline media Er is een netwerkfout opgetreden. Bezig met opnieuw proberen; poging %1$d van %2$d. %d artiesten opgehaald. - Bezig met uitlezen van server... + Bezig met uitlezen van server… Klaar! Afspeellijsten Informatie bijwerken @@ -148,7 +153,7 @@ Map kiezen Geen genres gevonden Geen opgeslagen afspeellijsten op server - Bezig met verbinden met server; even geduld... + Bezig met verbinden met server; even geduld… Uiterlijk Bufferduur Uitgeschakeld @@ -277,6 +282,8 @@ Zoekinstellingen Albumhoezen versturen via bluetooth (dit kan leiden tot mislukte bluetoothmeldingen) Albumhoezen versturen via bluetooth + De lijst ‘Nu aan het afspelen’ wordt niet gedeeld met verbonden apparaten. Hierdoor wordt de comptabiliteit met AVCRP 1.3-apparaten hersteld als het huidige nummer niet wordt bijgewerkt. + ‘Nu aan het afspelen’-lijst niet delen Afspeelmeldingen sturen via bluetooth Bluetoothmelding sturen Manage Servers @@ -315,7 +322,7 @@ ID3-labels gebruiken in plaats van systeemlabels Artiestfoto tonen op artiestenlijst Toont de artiestfoto op de artiestenlijst (indien beschikbaar) - Video + Video Verversen 0,5 seconden 1 seconde @@ -335,7 +342,7 @@ 0,00 GB 0 KB 0,00 MB - -:-- + -:-- 0:00 Druk om muziek te selecteren SD-kaart niet beschikbaar @@ -347,6 +354,8 @@ Standaard vervaltijd Niet meer tonen Deelopties instellen + Delen op server + Delen deelt een item op de server en maakt een deel-url aan. Schakel uit om alleen de nummerinformatie te delen. Geen vervaldatum Afspeellijst tonen/verbergen Bladwijzer instellen @@ -369,6 +378,7 @@ Tijd tot verval \"%s\" is verwijderd uit de afspeellijst Afspeellijst delen + Huidig nummer delen Standaard deelbericht Hé, luister eens naar de muziek die ik heb gedeeld via %s Nummers delen via @@ -389,10 +399,11 @@ Foutopsporingsopties Foutopsporingslogboek bijhouden De logboeken worden opgeslagen in %1$s/%2$s - Er staan %1$s logboeken in de map \'%3$s\' met een omvang van om en nabij de %2$s MB. Wil je deze behouden? + Er staan %1$s logboeken in de map ‘%3$s’ met een omvang van om en nabij de %2$s MB. Wil je deze behouden? Behouden Verwijderen De logboeken zijn verwijderd. + Bezig met downloaden van media op de achtergrond… Ultrasonic heeft geen toegang tot de muziekcache. De cachelocatie is teruggezet op de standaardlocatie. Waarschuwing @@ -430,6 +441,7 @@ Authenticatie Geavanceerde instellingen Eén of meerdere functies zijn uitgeschakeld omdat de server ze niet ondersteunt.\nVoer deze test later opnieuw uit. + Demoserver %d nummer @@ -445,19 +457,19 @@ %d los te maken nummer geselecteerd. - %dlos te maken nummers geselecteerd. + %d los te maken nummers geselecteerd. - %d nummer toegevoegd aan einde van afspeelwachtrij. - %d nummers toegevoegd aan einde van afspeelwachtrij. + %d nummer toegevoegd aan het einde van afspeelwachtrij. + %d nummers toegevoegd aan het einde van afspeelwachtrij. - %d nummer ingevoegd na huidige nummer. - %d nummers ingevoegd na huidige nummer. + %d nummer ingevoegd na het huidige nummer. + %d nummers ingevoegd na het huidige nummer. - Nog %d dag over van proefperiode - Nog %d dagen over van proefperiode + Nog %d dag over van de proefperiode + Nog %d dagen over van de proefperiode diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 310d6862..e794e991 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -1,5 +1,5 @@ - + Ładowanie… Wystąpił błąd sieci. Proszę sprawdzić adres serwera i spróbować później. @@ -298,7 +298,7 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników Zakłada, że folder najwyższego poziomu jest nazwą artysty albumu Przeglądaj używając tagów ID3 Używa metod z tagów ID3 zamiast metod opartych na systemie plików - Wideo + Wideo Odświeżanie widoku co pół sekundy co 1 sekundę @@ -318,14 +318,13 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników 0.00 GB 0 KB 0.00 MB - -:-- + -:-- 0:00 Dotknij, aby wybrać muzykę Karta SD jest niedostępna Brak karty SD Domyślne ustawienia udostępniania Udostępnianie - Zawsze pytaj o opis i czas wygaśnięcia podczas tworzenia udostępnienia Zawsze pytaj o szczegóły Domyślny czas wygaśnięcia Nie wyświetlaj więcej tego okna @@ -370,40 +369,40 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników %d utworów - %d utwór zaznaczony do przypięcia - %d utwory zaznaczone do przypięcia - %d utworów zaznaczonych do przypięcia - %d utworów zaznaczonych do przypięcia + %d utwór zaznaczony do przypięcia. + %d utwory zaznaczone do przypięcia. + %d utworów zaznaczonych do przypięcia. + %d utworów zaznaczonych do przypięcia. - %d utwór zaznaczony do pobrania - %d utwory zaznaczone do pobrania - %d utworów zaznaczonych do pobrania - %d utworów zaznaczonych do pobrania + %d utwór zaznaczony do pobrania. + %d utwory zaznaczone do pobrania. + %d utworów zaznaczonych do pobrania. + %d utworów zaznaczonych do pobrania. - %d utwór zaznaczony do odpięcia - %d utwory zaznaczone do odpięcia - %d utworów zaznaczonych do odpięcia - %d utworów zaznaczonych do odpięcia + %d utwór zaznaczony do odpięcia. + %d utwory zaznaczone do odpięcia. + %d utworów zaznaczonych do odpięcia. + %d utworów zaznaczonych do odpięcia. - %d utwór dodany na koniec kolejki odtwarzania - %d utwory dodane na koniec kolejki odtwarzania - %dutworów dodanych na koniec kolejki odtwarzania - %d utworów dodanych na koniec kolejki odtwarzania + %d utwór dodany na koniec kolejki odtwarzania. + %d utwory dodane na koniec kolejki odtwarzania. + %d utworów dodanych na koniec kolejki odtwarzania. + %d utworów dodanych na koniec kolejki odtwarzania. - %d utwór wstawiony po bieżącym utworze - %d utwory wstawione po bieżącym utworze - %d utworów wstawionych po bieżącym utworze - %d utworów wstawionych po bieżącym utworze + %d utwór wstawiony po bieżącym utworze. + %d utwory wstawione po bieżącym utworze. + %d utworów wstawionych po bieżącym utworze. + %d utworów wstawionych po bieżącym utworze. %d dzień pozostał do zakończenia okresu próbnego %d dni pozostały do zakończenia okresu próbnego - %d dni pozostało do zakończenia okresu próbnego - %d dni pozostało do zakończenia okresu próbnego + %d dni pozostało do zakończenia okresu próbnego + %d dni pozostało do zakończenia okresu próbnego diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index 2bb36b5b..41dd11fa 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -206,14 +206,14 @@ 5 minutos 1 hora Classificar Músicas por Álbum - Classificar músicas pelo número do álbum e faixas. + Classificar músicas pelo número do álbum e faixas Mostrar Taxa de Bits e Sufixo de Arquivo Adicionar o nome do artista com a taxa de bits e sufixo do arquivo Mostrar Downloads na Reprodução Transição para atividade de download quando iniciar reprodução Reprodução sem Interrupção Ativar reprodução sem interrupção - Esconder músicas de outros aplicativos. + Esconder arquivos de músicas de outros aplicativos Esconder de Outros Será efetivado na próxima vez que o Android procurar por músicas em seu celular. Intervalo de Salto @@ -231,9 +231,9 @@ 64 Kbps 80 Kbps 96 Kbps - Máx. de Taxa de Bits - Celular + Taxa Máxima de Bits - Celular Ilimitado - Máx. de Taxa de Bits - Wi-Fi + Taxa Máxima de Bits - Wi-Fi Máximo de Músicas Obedecer aos botões do celular, fones e botões de mídia do Bluetooth Botões de Mídia @@ -258,9 +258,9 @@ 5 músicas Ilimitado Retomar ao Inserir Fone de Ouvido - O aplicativo retomará a reprodução em pausa na inserção dos fones de ouvido no dispositivo. + O aplicativo retomará a reprodução em pausa na inserção dos fones de ouvido no dispositivo Lembre-se de configurar usuário e senha nos serviços Scrobble do servidor - Registre Minhas Músicas + Registrar Minhas Músicas 1 10 100 @@ -279,6 +279,8 @@ Configurações de Pesquisa Enviar a arte do álbum via Bluetooth (Pode causar falhas nas notificações do Bluetooth) Arte do Álbum via Bluetooth + A Lista Tocando Agora não será enviada aos dispositivos conectados. Isso pode restaurar a compatibilidade com dispositivos AVRCP 1.3 quando a exibição da trilha atual não é atualizada + Desativar Envio da Lista Tocando Agora Enviar notificações de reprodução via Bluetooth Notificações via Bluetooth Gerenciar Servidores @@ -344,7 +346,6 @@ Sem cartão SD Descrição Padrão do Compartilhamento Compartilhamento - Sempre perguntar pela descrição e expiração ao criar um compartilhamento Sempre Perguntar por Detalhes Tempo Padrão para Expirar Não mostrar este diálogo novamente @@ -395,6 +396,7 @@ Manter arquivos Excluir arquivos Arquivos de log excluídos. + Baixado mídia em segundo plano… O Ultrasonic não pôde acessar o cache dos arquivos de música. O local do cache foi redefinido para o caminho padrão. Atenção @@ -419,9 +421,9 @@ Usar o padrão Unidades disponíveis: - Servidores configurados + Servidores Configurados Quer realmente excluir o servidor? - Gerenciar servidor + Gerenciar Servidor Adicionar Servidor Quer realmente sair e descartar as alterações? Este campo é necessário @@ -447,20 +449,20 @@ %d músicas selecionadas para serem baixadas. - %d música selecionada para ser desafixada. + %d música selecionada para ser desfixada. %d músicas selecionadas para serem desfixadas. - %d música adicionada ao fim da fila. - %d músicas adicionadas ao fim da fila. + %d música adicionada ao final da playlist. + %d músicas adicionadas ao final da playlist. - %d música inserida após a atual. - %d músicas inseridas após a atual. + %d música adicionada após a atual. + %d músicas adicionadas após a atual. - Resta %d dia para o fim do período de teste - Restam %d dias para o fim do período de teste + %d dia restante do período de teste + %d dias restantes do período de teste @@ -478,8 +480,8 @@ Sinalização de Recursos Usar Classif. 5 Estrelas para Músicas - Use o sistema de classificação de 5 estrelas para músicas - em vez de simplesmente estrelar/não estrelar itens. + Usar o sistema de classificação de 5 estrelas para músicas + em vez de simplesmente estrelar/não estrelar itens diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index 8af20295..3706c9ee 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -1,5 +1,5 @@ - + Carregando… Ocorreu um erro de rede. Verifique o endereço do servidor ou tente mais tarde. @@ -298,7 +298,7 @@ Assumir que a pasta mais acima é o nome do artista Navegar Usando Etiquetas ID3 Usa as etiquetas ID3 ao invés do sistema de ficheiros - Vídeo + Vídeo Atualização do Ecrã .5 segundos 1 segundo @@ -318,14 +318,13 @@ 0.00 GB 0 KB 0.00 MB - —:—— + —:—— 0:00 Toque para selecionar a música Cartão SD indisponível Sem cartão SD Texto Padrão Compartilhamento - Sempre pergunta pela descrição e prazo ao criar um compartilhamento Sempre Perguntar por Detalhes Tempo Padrão para Expirar Não mostrar este diálogo novamente @@ -371,24 +370,24 @@ %d músicas selecionadas para serem fixadas. - %d música selecionada para descarregar. - %d músicas selecionadas para serem descarregadas. + %d música selecionada para ser baixada. + %d músicas selecionadas para serem baixadas. - %d música selecionada para ser desafixada. + %d música selecionada para ser desfixada. %d músicas selecionadas para serem desfixadas. - %d música adicionada ao fim da fila. - %d músicas adicionadas ao fim da fila. + %d música adicionada ao final da playlist. + %d músicas adicionadas ao final da playlist. - %d música inserida após a atual. - %d músicas inseridas após a atual. + %d música adicionada após a atual. + %d músicas adicionadas após a atual. - Resta %d dia para o fim do período de teste - Restam %d dias para o fim do período de teste + %d dia restante do período de teste + %d dias restantes do período de teste diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 5d9b725a..2c4091ea 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -344,7 +344,6 @@ Нет SD-карты Описание общего ресурса Поделиться - Всегда запрашивайте описание и срок действия при создании ресурса Всегда спрашивайте подробности Время истечения по умолчанию Больше не показывать диалог @@ -395,7 +394,6 @@ Сохранить файлы Удалить файлы Удаленные файлы журналов. - Ultrasonic не может получить доступ к кэшу музыкальных файлов. Местоположение кэша было сброшено на путь по умолчанию. Внимание Ultrasonic требуется разрешение на чтение/запись в директории музыкального кэша. Каталог кэша был сброшен на значение по умолчанию. diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index 7f5bed5d..00860b89 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -1,11 +1,12 @@ - + 加载中… 发生网络错误。请检查服务器地址或稍后重试。 服务端 api v%1$s 不支持此功能。 此软件需要连接网络,请打开Wi-Fi或移动网络。 未找到资源,请检查服务器地址。 + 未知回复内容,请检查服务器地址 HTTPS 证书错误: %1$s. SSL连接异常。请检查服务器证书。 请稍等… @@ -14,6 +15,13 @@ 聊天 Ultrasonic 主页 正在播放 + 播放 + 暂停 + 重复播放 + 随机播放 + 停止 + 下一首 + 上一首 播客 没有已注册的播客频道 播客 @@ -31,16 +39,26 @@ 名称 确定 固定 + 暂停 + 播放 最后一首 下一首 + 上一首 现在播放 随机播放 + 公开 保存 取消固定 群星 确定要删除 %1$s吗 书签已删除。 + 书签设置为 %s。 空的播放列表 + 不允许远程控制. 请在您的服务器上的 Users > Settings 打开点唱机模式。 + 关闭远程控制,音乐将在手机上播放 + 离线模式不支持远程控制 + 打开远程控制,音乐将在服务端播放。 + 远程控制不支持,请升级您的 Subsonic服务器。 均衡器 关闭 Jukebox 开启 Jukebox @@ -49,19 +67,29 @@ 保存播放列表 关闭屏幕常亮 开启屏幕常亮 + 显示专辑 随机 + 已随机排列播放列表 + 可视化 + 缓冲中 下载中 - %s + 随机播放 已成功保存播放列表。 + 保存播放列表失败,请重试。 输入播放列表名称: + 正在保存播放列表 - \"%s\"… 保存播放列表 循环播放所有 关闭循环播放 循环播放 + 关闭可视化 + 打开可视化 开启 均衡器 选择预设 错误 file:///android_asset/html/en/index.html + 默认自动点唱机 找不到歌词 按艺术家排序 按名称排序 @@ -81,8 +109,11 @@ 收藏夹 歌曲 视频 + 使用Ultrasonic来播放您的音乐,需要您自己的 服务器。\n\n➤ 如果您想试试此应用, 可以添加试用服务器。 \n\n➤ 可在 设置中编辑服务器配置信息。 欢迎! + 点击前往设置 关于 + 公共 已删除播放列表 %s 播放列表删除失败%s 退出 @@ -92,10 +123,13 @@ 媒体库 离线媒体 发生网络错误,正在重试 %1$d of %2$d. + 有 %d 位艺术家。 正在加载服务器。 正在加载服务器。完成! 播放列表 更新信息 + 已更新此播放列表信息 - %s + 更新播放列表信息失败 - %s 请稍等… 专辑 艺人 @@ -106,6 +140,8 @@ 歌曲 搜索 找不到歌曲 + 已选择 %d 首曲目。 + 未选择 %d 首曲目。 警告:网络不可用 错误:没有SD卡 播放所有 @@ -113,7 +149,9 @@ 选择文件夹 找不到流派 服务器上没有保存的播放列表 + 服务器连接中,请稍等。 外观 + 缓冲长度 已禁用 1 秒 10 秒 @@ -147,9 +185,18 @@ 8 GB 9 GB 不限制 + 音乐缓存 + 聊天消息刷新时间间隔 清空书签 + 歌曲播放完毕后清除书签 清空播放列表 + 所有歌曲播放完毕后清空播放列表 清空搜索历史 + 连接失败 + 默认专辑 + 默认艺术家 + 默认音乐 + 目录缓存时间 已禁用 1 分钟 10 分钟 @@ -157,7 +204,20 @@ 30 分钟 5 分钟 1 小时 + 按光盘排序歌曲 + 按光盘编号和曲目编号对歌曲列表进行排序 + 展示比特率和文件后缀 + 在艺术家姓名后追加比特率和文件后缀 + 无缝播放 + 启用无缝播放 + 隐藏来自其他应用的音乐 + 隐藏其他来源 + 在安卓系统下次扫描音乐时生效。 快进间隔 + 请填写有效的URL。 + 请填写有效用户名 (请去除尾部空格)。 + 最大专辑 + 最大艺术家 112 Kbps 128 Kbps 160 Kbps @@ -171,6 +231,10 @@ 最大比特率-移动网络 不限制 最大比特率-WIFI + 最大歌曲 + 响应手机、耳机和蓝牙设备的媒体按钮 + 媒体按钮 + 网络超时 105 秒 120 秒 15 秒 @@ -182,6 +246,7 @@ 通知 网络 其他设置 + 播放控制设置 预加载歌曲数量 1 首歌 10 首歌 @@ -189,6 +254,9 @@ 3 首歌 5 首歌 不限制 + 插入耳机时恢复播放 + 应用将在有线耳机插入设备时恢复已暂停的播放。 + 请记得在服务器上的 Scrobble 服务中设置您的用户名和密码 1 10 100 @@ -205,20 +273,49 @@ 75 搜索记录已清除 搜索设置 + 通过蓝牙发送专辑封面(可能导致蓝牙通知失败) + 通过蓝牙发送专辑封面 + 现在播放列表不会发送到已连接的设备。 当前曲目显示未更新时,这可能会恢复AVRCP 1.3的设备的兼容性。 + 禁用发送正在播放列表 + 通过蓝牙发送播放通知 + 发送蓝牙通知 管理服务器 服务器地址 名称 密码 删除服务器 + 从服务器下载缩放图像而不是全尺寸(节省数据流量) + 服务器端专辑图片缩放 + 未启用 用户名 + 锁屏显示控制器 + 在锁定屏幕上显示播放控件 显示通知 总是显示通知 + 当播放列表有音乐时,总是在通知栏显示播放信息 + 在状态栏中显示正在播放通知 + 显示正在播放 + 在所有活动页面显示正在播放信息 + 显示曲目编号 + 显示歌曲时包括曲目编号 测试连接 连接正常 连接正常, 服务器未授权。 + Light + Dark + Black 主题 允许自签名 HTTPS 证书 - 视频 + 强制原始密码认证 + 这会强制应用始终以未加密的方式发送密码。 + 如果 Subsonic 服务器不支持新的用户身份验证 API,则很有用。 + 将艺术家名称作为文件夹 + 将艺术家名称作为顶层文件夹名 + 使用 ID3 标签浏览 + 使用 ID3 标签方法而不是基于文件系统的方法 + 在艺术家列表中显示艺术家图片 + 如果可用,在艺术家列表中显示艺术家图片 + 视频 刷新视图 .5 秒 1 秒 @@ -232,18 +329,33 @@ 5 秒 仅在连接到 WIFI 时使用流媒体 仅使用 WIFI + %1$s%2$s + %d kbps 0 B 0.00 GB 0 KB 0.00 MB + -:-- 0:00 + 轻触选择音乐 SD 卡不可用 没有 SD 卡 + 默认分享说明 + 分享 + 始终询问详细信息 + 默认有效期 + 不再显示此对话框 + 设置分享选项 + 无期限 切换播放列表 设为书签 删除书签 收藏 清空播放列表 + 分享 + 服务器上没有可用的共享 + 删除分享 %s + 删除分享失败 %s 毫秒 分钟 @@ -251,27 +363,64 @@ 禁用 已禁用 + 保存为默认 评论 + 有效期 %s已从播放列表中移除 分享播放列表 + 默认分享问候语 + 看看我从 %s 分享的这首音乐 + 分享歌曲通过 分享 + %s 的所有歌曲 + 按艺术家显示所有歌曲 + 在艺术家视图中添加新条目以访问艺术家的所有歌曲 + 显示艺术家 + albumArt + Multiple Years + 连接蓝牙设备时恢复播放 + 断开蓝牙设备时暂停播放 + 所有蓝牙设备 + 仅音频 (A2DP) 设备 已禁用 + 启用蓝牙设备上的单键播放/暂停 + 当播放/暂停无法正常工作时,启用此功能可能对较旧的蓝牙设备有所帮助 + 调试选项 + 将调试日志写入文件 + 日志文件可在 %1$s/%2$s 获取 + %3$s 目录中有 %1$s 个日志文件占用了 ~%2$s MB 空间。您想保留这些吗? + 保留文件 删除文件 删除日志文件 + 在后台下载媒体… + Ultrasonic 无法访问音乐文件缓存,缓存位置已重置为默认路径。 警告 + Ultrasonic 需要对音乐缓存目录的读/写权限,缓存位置已重置为默认路径。 + 需要权限 + Ultrasonic 需要对音乐缓存目录具有读/写权限。\n请允许 Ultrasonic 访问文件系统。 + Ultrasonic 需要对音乐缓存目录具有读/写权限。您可以在应用程序设置中授予该权限,否则将以默认路径作为缓存目录。 打开设置 + 为了更改缓存位置,Ultrasonic 需要对文件系统具有读/写权限。 + 选择文件夹 创建文件夹 无法创建文件夹 + %1$s (内置) + 默认应用文件夹 %1$s (外置) 输入文件夹名称 创建 请输入一个有效的文件夹名称 + 该文件夹已存在。\n请为该文件夹提供另一个名称 选择 + 使用默认值 + 可用驱动器: + 配置服务器 您确定要删除此服务器吗? 编辑服务器 添加服务器 + 您确定要离开并丢弃您的更改吗? 此项必填 编辑 删除 @@ -279,11 +428,47 @@ 下移 认证 高级设置 + 一项或多项功能被禁用,因为此服务器不支持它们。\您可以随时再次运行此测试。 + 试用服务器 + + + %d 首曲目 + + + 已选择 %d 首歌曲进行固定。 + + + 已选择要下载 %d 首歌曲。 + + + 已选择 %d 首歌曲取消固定。 + + + 已将 %d 首歌曲添加到播放队列的末尾。 + + + 在当前歌曲之后插入了 %d 首歌曲。 + + + 试用期还剩 %d 天 + + + + 一般api错误: %1$s + 服务器未发送任何信息 + LDAP用户不支持以token形式授权连接。 用户名或密码错误 + 授权失败,请在 Subsonic server 检查用户权限。 缺少必需的参数。 未找到请求的数据。 试用期结束 版本不兼容,请升级 Ultrasonic 应用。 不兼容的版本。请升级Subsonic 服务。 - + + 特性标志 + 为歌曲使用五星评分 + 对歌曲使用五星级评级系统 +而不是简单地为项目加星标/取消星标。 + + diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 9ce97deb..9bcf4a28 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -28,7 +28,9 @@ Playlists Search Send a message + Album Ultrasonic + Artist Cancel Comment Confirm @@ -48,6 +50,7 @@ Play Shuffled Public Save + Title Unpin Various Artists Do you want to delete %1$s @@ -350,11 +353,13 @@ No SD card Default Share Description Sharing - Always ask for description and expiration when creating a share + Always ask for description and expiration when creating a share on the server Always Ask For Details Default Expiration Time Do not show dialog again Set Share Options + Create share on the server + Sharing will create a share on the server and share its URL. If disabled, only the song details are shared No Expiration Toggle Playlist Set Bookmark @@ -377,6 +382,7 @@ Time To Expiration \"%s\" was removed from playlist Share Playlist + Share Current Song Default Share Greeting Check out this music I shared from %s Share songs via diff --git a/ultrasonic/src/main/res/values/styles.xml b/ultrasonic/src/main/res/values/styles.xml index 9076219b..d1d05648 100644 --- a/ultrasonic/src/main/res/values/styles.xml +++ b/ultrasonic/src/main/res/values/styles.xml @@ -15,7 +15,7 @@ 14sp true center_vertical - 16dp + 16dp true end diff --git a/ultrasonic/src/main/res/xml/appwidget_info_4x1.xml b/ultrasonic/src/main/res/xml/appwidget_info_4x1.xml index 7993869d..df494cb8 100644 --- a/ultrasonic/src/main/res/xml/appwidget_info_4x1.xml +++ b/ultrasonic/src/main/res/xml/appwidget_info_4x1.xml @@ -1,9 +1,11 @@ \ No newline at end of file + xmlns:tools="http://schemas.android.com/tools" + a:minWidth="250dp" + a:minHeight="40dp" + a:updatePeriodMillis="0" + a:resizeMode="none" + a:previewImage="@drawable/preview4x1" + a:widgetCategory="home_screen|keyguard" + a:initialLayout="@layout/appwidget4x1" + tools:ignore="UnusedAttribute" /> \ No newline at end of file diff --git a/ultrasonic/src/main/res/xml/appwidget_info_4x2.xml b/ultrasonic/src/main/res/xml/appwidget_info_4x2.xml index 9f3a9078..be14efdc 100644 --- a/ultrasonic/src/main/res/xml/appwidget_info_4x2.xml +++ b/ultrasonic/src/main/res/xml/appwidget_info_4x2.xml @@ -1,9 +1,11 @@ \ No newline at end of file + xmlns:tools="http://schemas.android.com/tools" + a:minWidth="250dp" + a:minHeight="110dp" + a:updatePeriodMillis="0" + a:resizeMode="none" + a:previewImage="@drawable/preview4x2" + a:widgetCategory="home_screen|keyguard" + a:initialLayout="@layout/appwidget4x2" + tools:ignore="UnusedAttribute" /> \ No newline at end of file diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index 101d2ac1..a97aaff8 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -180,18 +180,15 @@ - - + +