Merge branch 'nitehu-feature/cache-folder-setting' into develop
|
@ -27,12 +27,12 @@ ext.versions = [
|
||||||
|
|
||||||
junit4 : "4.12",
|
junit4 : "4.12",
|
||||||
junit5 : "5.3.1",
|
junit5 : "5.3.1",
|
||||||
mockito : "2.16.0",
|
mockito : "3.5.5",
|
||||||
mockitoKotlin : "1.5.0",
|
mockitoKotlin : "1.5.0",
|
||||||
kluent : "1.35",
|
kluent : "1.35",
|
||||||
apacheCodecs : "1.10",
|
apacheCodecs : "1.10",
|
||||||
testRunner : "1.0.1",
|
testRunner : "1.0.1",
|
||||||
robolectric : "3.8",
|
robolectric : "4.4",
|
||||||
dexter : "6.1.2",
|
dexter : "6.1.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,6 @@ jacoco {
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
jacocoExclude = []
|
jacocoExclude = ['jdk.internal.*']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ jacoco {
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
// override it in the module
|
// override it in the module
|
||||||
jacocoExclude = []
|
jacocoExclude = ['jdk.internal.*']
|
||||||
}
|
}
|
||||||
|
|
||||||
jacocoTestReport {
|
jacocoTestReport {
|
||||||
|
|
|
@ -99,7 +99,8 @@ ext {
|
||||||
'**/R$*.class',
|
'**/R$*.class',
|
||||||
'**/R.class',
|
'**/R.class',
|
||||||
'**/BuildConfig.class',
|
'**/BuildConfig.class',
|
||||||
'**/di/**'
|
'**/di/**',
|
||||||
|
'jdk.internal.*'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,4 +110,5 @@ jacoco {
|
||||||
|
|
||||||
tasks.withType(Test) {
|
tasks.withType(Test) {
|
||||||
jacoco.includeNoLocationClasses = true
|
jacoco.includeNoLocationClasses = true
|
||||||
|
jacoco.excludes += jacocoExclude
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@ import org.moire.ultrasonic.activity.ServerSettingsActivity;
|
||||||
import org.moire.ultrasonic.activity.SubsonicTabActivity;
|
import org.moire.ultrasonic.activity.SubsonicTabActivity;
|
||||||
import org.moire.ultrasonic.featureflags.Feature;
|
import org.moire.ultrasonic.featureflags.Feature;
|
||||||
import org.moire.ultrasonic.featureflags.FeatureStorage;
|
import org.moire.ultrasonic.featureflags.FeatureStorage;
|
||||||
|
import org.moire.ultrasonic.filepicker.FilePickerDialog;
|
||||||
|
import org.moire.ultrasonic.filepicker.OnFileSelectedListener;
|
||||||
import org.moire.ultrasonic.provider.SearchSuggestionProvider;
|
import org.moire.ultrasonic.provider.SearchSuggestionProvider;
|
||||||
import org.moire.ultrasonic.service.MediaPlayerController;
|
import org.moire.ultrasonic.service.MediaPlayerController;
|
||||||
import org.moire.ultrasonic.util.*;
|
import org.moire.ultrasonic.util.*;
|
||||||
|
@ -37,7 +39,7 @@ public class SettingsFragment extends PreferenceFragment
|
||||||
private ListPreference maxBitrateWifi;
|
private ListPreference maxBitrateWifi;
|
||||||
private ListPreference maxBitrateMobile;
|
private ListPreference maxBitrateMobile;
|
||||||
private ListPreference cacheSize;
|
private ListPreference cacheSize;
|
||||||
private EditTextPreference cacheLocation;
|
private Preference cacheLocation;
|
||||||
private ListPreference preloadCount;
|
private ListPreference preloadCount;
|
||||||
private ListPreference bufferLength;
|
private ListPreference bufferLength;
|
||||||
private ListPreference incrementTime;
|
private ListPreference incrementTime;
|
||||||
|
@ -85,7 +87,7 @@ public class SettingsFragment extends PreferenceFragment
|
||||||
maxBitrateWifi = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI);
|
maxBitrateWifi = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI);
|
||||||
maxBitrateMobile = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE);
|
maxBitrateMobile = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE);
|
||||||
cacheSize = (ListPreference) findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE);
|
cacheSize = (ListPreference) findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE);
|
||||||
cacheLocation = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION);
|
cacheLocation = findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION);
|
||||||
preloadCount = (ListPreference) findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT);
|
preloadCount = (ListPreference) findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT);
|
||||||
bufferLength = (ListPreference) findPreference(Constants.PREFERENCES_KEY_BUFFER_LENGTH);
|
bufferLength = (ListPreference) findPreference(Constants.PREFERENCES_KEY_BUFFER_LENGTH);
|
||||||
incrementTime = (ListPreference) findPreference(Constants.PREFERENCES_KEY_INCREMENT_TIME);
|
incrementTime = (ListPreference) findPreference(Constants.PREFERENCES_KEY_INCREMENT_TIME);
|
||||||
|
@ -113,6 +115,7 @@ public class SettingsFragment extends PreferenceFragment
|
||||||
setupClearSearchPreference();
|
setupClearSearchPreference();
|
||||||
setupGaplessControlSettingsV14();
|
setupGaplessControlSettingsV14();
|
||||||
setupFeatureFlagsPreferences();
|
setupFeatureFlagsPreferences();
|
||||||
|
setupCacheLocationPreference();
|
||||||
|
|
||||||
// After API26 foreground services must be used for music playback, and they must have a notification
|
// After API26 foreground services must be used for music playback, and they must have a notification
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
@ -155,8 +158,6 @@ public class SettingsFragment extends PreferenceFragment
|
||||||
setHideMedia(sharedPreferences.getBoolean(key, false));
|
setHideMedia(sharedPreferences.getBoolean(key, false));
|
||||||
} else if (Constants.PREFERENCES_KEY_MEDIA_BUTTONS.equals(key)) {
|
} else if (Constants.PREFERENCES_KEY_MEDIA_BUTTONS.equals(key)) {
|
||||||
setMediaButtonsEnabled(sharedPreferences.getBoolean(key, true));
|
setMediaButtonsEnabled(sharedPreferences.getBoolean(key, true));
|
||||||
} else if (Constants.PREFERENCES_KEY_CACHE_LOCATION.equals(key)) {
|
|
||||||
setCacheLocation(sharedPreferences.getString(key, ""));
|
|
||||||
} else if (Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS.equals(key)) {
|
} else if (Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS.equals(key)) {
|
||||||
setBluetoothPreferences(sharedPreferences.getBoolean(key, true));
|
setBluetoothPreferences(sharedPreferences.getBoolean(key, true));
|
||||||
} else if (Constants.PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY.equals(key)) {
|
} else if (Constants.PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY.equals(key)) {
|
||||||
|
@ -164,6 +165,40 @@ public class SettingsFragment extends PreferenceFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupCacheLocationPreference() {
|
||||||
|
cacheLocation.setSummary(settings.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION,
|
||||||
|
FileUtil.getDefaultMusicDirectory(getActivity()).getPath()));
|
||||||
|
|
||||||
|
cacheLocation.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
|
// If the user tries to change the cache location, we must first check to see if we have write access.
|
||||||
|
PermissionUtil.requestInitialPermission(getActivity(), new PermissionUtil.PermissionRequestFinishedCallback() {
|
||||||
|
@Override
|
||||||
|
public void onPermissionRequestFinished(boolean hasPermission) {
|
||||||
|
if (hasPermission) {
|
||||||
|
FilePickerDialog filePickerDialog = FilePickerDialog.Companion.createFilePickerDialog(getActivity());
|
||||||
|
filePickerDialog.setDefaultDirectory(FileUtil.getDefaultMusicDirectory(getActivity()).getPath());
|
||||||
|
filePickerDialog.setInitialDirectory(cacheLocation.getSummary().toString());
|
||||||
|
filePickerDialog.setOnFileSelectedListener(new OnFileSelectedListener() {
|
||||||
|
@Override
|
||||||
|
public void onFileSelected(File file, String path) {
|
||||||
|
SharedPreferences.Editor editor = cacheLocation.getEditor();
|
||||||
|
editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, path);
|
||||||
|
editor.commit();
|
||||||
|
|
||||||
|
setCacheLocation(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
filePickerDialog.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void setupClearSearchPreference() {
|
private void setupClearSearchPreference() {
|
||||||
Preference clearSearchPreference = findPreference(Constants.PREFERENCES_KEY_CLEAR_SEARCH_HISTORY);
|
Preference clearSearchPreference = findPreference(Constants.PREFERENCES_KEY_CLEAR_SEARCH_HISTORY);
|
||||||
|
|
||||||
|
@ -315,7 +350,6 @@ public class SettingsFragment extends PreferenceFragment
|
||||||
maxBitrateWifi.setSummary(maxBitrateWifi.getEntry());
|
maxBitrateWifi.setSummary(maxBitrateWifi.getEntry());
|
||||||
maxBitrateMobile.setSummary(maxBitrateMobile.getEntry());
|
maxBitrateMobile.setSummary(maxBitrateMobile.getEntry());
|
||||||
cacheSize.setSummary(cacheSize.getEntry());
|
cacheSize.setSummary(cacheSize.getEntry());
|
||||||
cacheLocation.setSummary(cacheLocation.getText());
|
|
||||||
preloadCount.setSummary(preloadCount.getEntry());
|
preloadCount.setSummary(preloadCount.getEntry());
|
||||||
bufferLength.setSummary(bufferLength.getEntry());
|
bufferLength.setSummary(bufferLength.getEntry());
|
||||||
incrementTime.setSummary(incrementTime.getEntry());
|
incrementTime.setSummary(incrementTime.getEntry());
|
||||||
|
@ -396,14 +430,16 @@ public class SettingsFragment extends PreferenceFragment
|
||||||
if (!FileUtil.ensureDirectoryExistsAndIsReadWritable(dir)) {
|
if (!FileUtil.ensureDirectoryExistsAndIsReadWritable(dir)) {
|
||||||
PermissionUtil.handlePermissionFailed(getActivity(), new PermissionUtil.PermissionRequestFinishedCallback() {
|
PermissionUtil.handlePermissionFailed(getActivity(), new PermissionUtil.PermissionRequestFinishedCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onPermissionRequestFinished() {
|
public void onPermissionRequestFinished(boolean hasPermission) {
|
||||||
String currentPath = settings.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION,
|
String currentPath = settings.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION,
|
||||||
FileUtil.getDefaultMusicDirectory(getActivity()).getPath());
|
FileUtil.getDefaultMusicDirectory(getActivity()).getPath());
|
||||||
cacheLocation.setSummary(currentPath);
|
cacheLocation.setSummary(currentPath);
|
||||||
cacheLocation.setText(currentPath);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
cacheLocation.setSummary(path);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear download queue.
|
// Clear download queue.
|
||||||
mediaPlayerControllerLazy.getValue().clear();
|
mediaPlayerControllerLazy.getValue().clear();
|
||||||
|
|
|
@ -5,6 +5,8 @@ import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
@ -34,7 +36,7 @@ public class PermissionUtil {
|
||||||
private static final String TAG = FileUtil.class.getSimpleName();
|
private static final String TAG = FileUtil.class.getSimpleName();
|
||||||
|
|
||||||
public interface PermissionRequestFinishedCallback {
|
public interface PermissionRequestFinishedCallback {
|
||||||
void onPermissionRequestFinished();
|
void onPermissionRequestFinished(boolean hasPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,18 +57,70 @@ public class PermissionUtil {
|
||||||
if (currentCachePath.compareTo(defaultCachePath) == 0) return;
|
if (currentCachePath.compareTo(defaultCachePath) == 0) return;
|
||||||
|
|
||||||
// We must get the context of the Main Activity for the dialogs, as this function may be called from a background thread where displaying dialogs is not available
|
// We must get the context of the Main Activity for the dialogs, as this function may be called from a background thread where displaying dialogs is not available
|
||||||
Context mainContext = MainActivity.getInstance();
|
final Context mainContext = MainActivity.getInstance();
|
||||||
|
|
||||||
if ((PermissionChecker.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_DENIED) ||
|
if ((PermissionChecker.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_DENIED) ||
|
||||||
(PermissionChecker.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PERMISSION_DENIED)) {
|
(PermissionChecker.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PERMISSION_DENIED)) {
|
||||||
// While we request permission, the Music Directory is temporarily reset to its default location
|
// While we request permission, the Music Directory is temporarily reset to its default location
|
||||||
setCacheLocation(context, FileUtil.getDefaultMusicDirectory(context).getPath());
|
setCacheLocation(context, FileUtil.getDefaultMusicDirectory(context).getPath());
|
||||||
requestPermission(mainContext, currentCachePath, callback);
|
requestFailedPermission(mainContext, currentCachePath, callback);
|
||||||
} else {
|
} else {
|
||||||
setCacheLocation(context, FileUtil.getDefaultMusicDirectory(context).getPath());
|
setCacheLocation(context, FileUtil.getDefaultMusicDirectory(context).getPath());
|
||||||
|
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
showWarning(mainContext, context.getString(R.string.permissions_message_box_title), context.getString(R.string.permissions_access_error), null);
|
showWarning(mainContext, context.getString(R.string.permissions_message_box_title), context.getString(R.string.permissions_access_error), null);
|
||||||
callback.onPermissionRequestFinished();
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
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()) {
|
||||||
|
Log.i(TAG, "Permission granted to read / write external storage");
|
||||||
|
if (callback != null) callback.onPermissionRequestFinished(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report.isAnyPermissionPermanentlyDenied()) {
|
||||||
|
Log.i(TAG, "Found permanently denied permission to read / write external storage, offering settings");
|
||||||
|
showSettingsDialog(context);
|
||||||
|
if (callback != null) callback.onPermissionRequestFinished(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "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<PermissionRequest> 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) {
|
||||||
|
Log.e(TAG, String.format("An error has occurred during checking permissions with Dexter: %s", error.toString()));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.check();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setCacheLocation(Context context, String cacheLocation) {
|
private static void setCacheLocation(Context context, String cacheLocation) {
|
||||||
|
@ -75,7 +129,7 @@ public class PermissionUtil {
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void requestPermission(final Context context, final String cacheLocation, final PermissionRequestFinishedCallback callback) {
|
private static void requestFailedPermission(final Context context, final String cacheLocation, final PermissionRequestFinishedCallback callback) {
|
||||||
Dexter.withContext(context)
|
Dexter.withContext(context)
|
||||||
.withPermissions(
|
.withPermissions(
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||||
|
@ -86,14 +140,14 @@ public class PermissionUtil {
|
||||||
if (report.areAllPermissionsGranted()) {
|
if (report.areAllPermissionsGranted()) {
|
||||||
Log.i(TAG, String.format("Permission granted to use cache directory %s", cacheLocation));
|
Log.i(TAG, String.format("Permission granted to use cache directory %s", cacheLocation));
|
||||||
setCacheLocation(context, cacheLocation);
|
setCacheLocation(context, cacheLocation);
|
||||||
if (callback != null) callback.onPermissionRequestFinished();
|
if (callback != null) callback.onPermissionRequestFinished(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (report.isAnyPermissionPermanentlyDenied()) {
|
if (report.isAnyPermissionPermanentlyDenied()) {
|
||||||
Log.i(TAG, String.format("Found permanently denied permission to use cache directory %s, offering settings", cacheLocation));
|
Log.i(TAG, String.format("Found permanently denied permission to use cache directory %s, offering settings", cacheLocation));
|
||||||
showSettingsDialog(context);
|
showSettingsDialog(context);
|
||||||
if (callback != null) callback.onPermissionRequestFinished();
|
if (callback != null) callback.onPermissionRequestFinished(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,13 +155,13 @@ public class PermissionUtil {
|
||||||
setCacheLocation(context, FileUtil.getDefaultMusicDirectory(context).getPath());
|
setCacheLocation(context, FileUtil.getDefaultMusicDirectory(context).getPath());
|
||||||
showWarning(context, context.getString(R.string.permissions_message_box_title),
|
showWarning(context, context.getString(R.string.permissions_message_box_title),
|
||||||
context.getString(R.string.permissions_permission_missing), null);
|
context.getString(R.string.permissions_permission_missing), null);
|
||||||
if (callback != null) callback.onPermissionRequestFinished();
|
if (callback != null) callback.onPermissionRequestFinished(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPermissionRationaleShouldBeShown(List<PermissionRequest> permissions, PermissionToken token) {
|
public void onPermissionRationaleShouldBeShown(List<PermissionRequest> permissions, PermissionToken token) {
|
||||||
showWarning(context, context.getString(R.string.permissions_rationale_title),
|
showWarning(context, context.getString(R.string.permissions_rationale_title),
|
||||||
context.getString(R.string.permissions_rationale_description), token);
|
context.getString(R.string.permissions_rationale_description_failed), token);
|
||||||
}
|
}
|
||||||
}).withErrorListener(new PermissionRequestErrorListener() {
|
}).withErrorListener(new PermissionRequestErrorListener() {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,287 @@
|
||||||
|
package org.moire.ultrasonic.filepicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Environment
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import java.io.File
|
||||||
|
import java.util.LinkedList
|
||||||
|
import kotlin.Comparator
|
||||||
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.util.Util
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter for the RecyclerView which handles listing, navigating and picking files
|
||||||
|
* @author this implementation is loosely based on the work of Yogesh Sundaresan,
|
||||||
|
* original license: http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*/
|
||||||
|
internal class FilePickerAdapter : RecyclerView.Adapter<FilePickerAdapter.FileListHolder> {
|
||||||
|
|
||||||
|
private var data: MutableList<FileListItem> = LinkedList()
|
||||||
|
var defaultDirectory: File = Environment.getExternalStorageDirectory()
|
||||||
|
var initialDirectory: File = Environment.getExternalStorageDirectory()
|
||||||
|
lateinit var selectedDirectoryChanged: (String, Boolean) -> Unit
|
||||||
|
var selectedDirectory: File = defaultDirectory
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var context: Context? = null
|
||||||
|
private var listerView: FilePickerView? = null
|
||||||
|
private var isRealDirectory: Boolean = false
|
||||||
|
|
||||||
|
private val physicalPaths: Array<String>
|
||||||
|
get() = arrayOf(
|
||||||
|
"/storage/sdcard0", "/storage/sdcard1", "/storage/extsdcard",
|
||||||
|
"/storage/sdcard0/external_sdcard", "/mnt/extsdcard", "/mnt/sdcard/external_sd",
|
||||||
|
"/mnt/external_sd", "/mnt/media_rw/sdcard1", "/removable/microsd", "/mnt/emmc",
|
||||||
|
"/storage/external_SD", "/storage/ext_sd", "/storage/removable/sdcard1",
|
||||||
|
"/data/sdext", "/data/sdext2", "/data/sdext3", "/data/sdext4", "/sdcard1",
|
||||||
|
"/sdcard2", "/storage/microsd", "/data/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
private var folderIcon: Drawable? = null
|
||||||
|
private var upIcon: Drawable? = null
|
||||||
|
private var sdIcon: Drawable? = null
|
||||||
|
|
||||||
|
constructor(defaultDir: File, view: FilePickerView) : this(view) {
|
||||||
|
this.defaultDirectory = defaultDir
|
||||||
|
selectedDirectory = defaultDir
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(view: FilePickerView) {
|
||||||
|
this.context = view.context
|
||||||
|
listerView = view
|
||||||
|
|
||||||
|
upIcon = Util.getDrawableFromAttribute(context, R.attr.filepicker_subdirectory_up)
|
||||||
|
folderIcon = Util.getDrawableFromAttribute(context, R.attr.filepicker_folder)
|
||||||
|
sdIcon = Util.getDrawableFromAttribute(context, R.attr.filepicker_sd_card)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
fileLister(initialDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileLister(currentDirectory: File) {
|
||||||
|
var fileList = LinkedList<FileListItem>()
|
||||||
|
var storages: List<File>? = null
|
||||||
|
var storagePaths: List<String>? = null
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||||
|
storages = context!!.getExternalFilesDirs(null).filterNotNull()
|
||||||
|
storagePaths = storages.map { i -> i.absolutePath }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDirectory.absolutePath == "/" ||
|
||||||
|
currentDirectory.absolutePath == "/storage" ||
|
||||||
|
currentDirectory.absolutePath == "/storage/emulated" ||
|
||||||
|
currentDirectory.absolutePath == "/mnt"
|
||||||
|
) {
|
||||||
|
isRealDirectory = false
|
||||||
|
fileList = if (
|
||||||
|
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT
|
||||||
|
) {
|
||||||
|
getKitKatStorageItems(storages!!)
|
||||||
|
} else {
|
||||||
|
getStorageItems()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isRealDirectory = true
|
||||||
|
val files = currentDirectory.listFiles()
|
||||||
|
files?.forEach { file ->
|
||||||
|
if (file.isDirectory) {
|
||||||
|
fileList.add(FileListItem(file, file.name, folderIcon!!))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data = LinkedList(fileList)
|
||||||
|
|
||||||
|
data.sortWith(
|
||||||
|
Comparator { f1, f2 ->
|
||||||
|
if (f1.file!!.isDirectory && f2.file!!.isDirectory)
|
||||||
|
f1.name.compareTo(f2.name, ignoreCase = true)
|
||||||
|
else if (f1.file!!.isDirectory && !f2.file!!.isDirectory)
|
||||||
|
-1
|
||||||
|
else if (!f1.file!!.isDirectory && f2.file!!.isDirectory)
|
||||||
|
1
|
||||||
|
else if (!f1.file!!.isDirectory && !f2.file!!.isDirectory)
|
||||||
|
f1.name.compareTo(f2.name, ignoreCase = true)
|
||||||
|
else
|
||||||
|
0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
selectedDirectory = currentDirectory
|
||||||
|
selectedDirectoryChanged.invoke(
|
||||||
|
if (isRealDirectory) selectedDirectory.absolutePath
|
||||||
|
else context!!.getString(R.string.filepicker_available_drives),
|
||||||
|
isRealDirectory
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add the "Up" navigation to the list
|
||||||
|
if (currentDirectory.absolutePath != "/" && isRealDirectory) {
|
||||||
|
// If we are on KitKat or later, only the default App folder is usable, so we can't
|
||||||
|
// navigate the SD card. Jump to the root if "Up" is selected.
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||||
|
if (storagePaths!!.indexOf(currentDirectory.absolutePath) > 0)
|
||||||
|
data.add(0, FileListItem(File("/"), "..", upIcon!!))
|
||||||
|
else
|
||||||
|
data.add(0, FileListItem(selectedDirectory.parentFile!!, "..", upIcon!!))
|
||||||
|
} else {
|
||||||
|
data.add(0, FileListItem(selectedDirectory.parentFile!!, "..", upIcon!!))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyDataSetChanged()
|
||||||
|
listerView!!.scrollToPosition(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStorageItems(): LinkedList<FileListItem> {
|
||||||
|
val fileList = LinkedList<FileListItem>()
|
||||||
|
var s = System.getenv("EXTERNAL_STORAGE")
|
||||||
|
if (!TextUtils.isEmpty(s)) {
|
||||||
|
val f = File(s!!)
|
||||||
|
fileList.add(FileListItem(f, f.name, sdIcon!!))
|
||||||
|
} else {
|
||||||
|
val paths = physicalPaths
|
||||||
|
for (path in paths) {
|
||||||
|
val f = File(path)
|
||||||
|
if (f.exists())
|
||||||
|
fileList.add(FileListItem(f, f.name, sdIcon!!))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s = System.getenv("SECONDARY_STORAGE")
|
||||||
|
if (s != null && !TextUtils.isEmpty(s)) {
|
||||||
|
val rawSecondaryStorages =
|
||||||
|
s.split(File.pathSeparator.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||||
|
for (path in rawSecondaryStorages) {
|
||||||
|
val f = File(path)
|
||||||
|
if (f.exists())
|
||||||
|
fileList.add(FileListItem(f, f.name, sdIcon!!))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileList
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getKitKatStorageItems(storages: List<File>): LinkedList<FileListItem> {
|
||||||
|
val fileList = LinkedList<FileListItem>()
|
||||||
|
if (storages.isNotEmpty()) {
|
||||||
|
for ((index, file) in storages.withIndex()) {
|
||||||
|
var path = file.absolutePath
|
||||||
|
path = path.replace("/Android/data/([a-zA-Z_][.\\w]*)/files".toRegex(), "")
|
||||||
|
if (index == 0) {
|
||||||
|
fileList.add(
|
||||||
|
FileListItem(
|
||||||
|
File(path),
|
||||||
|
context!!.getString(R.string.filepicker_internal, path),
|
||||||
|
sdIcon!!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
fileList.add(
|
||||||
|
FileListItem(
|
||||||
|
file,
|
||||||
|
context!!.getString(R.string.filepicker_default_app_folder, path),
|
||||||
|
sdIcon!!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileListHolder {
|
||||||
|
return FileListHolder(
|
||||||
|
LayoutInflater.from(context).inflate(
|
||||||
|
R.layout.filepicker_item_file_lister, listerView, false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: FileListHolder, position: Int) {
|
||||||
|
val actualFile = data[position]
|
||||||
|
|
||||||
|
holder.name.text = actualFile.name
|
||||||
|
holder.icon.setImageDrawable(actualFile.icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return data.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun goToDefault() {
|
||||||
|
fileLister(defaultDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createNewFolder() {
|
||||||
|
val view = View.inflate(context, R.layout.filepicker_dialog_create_folder, null)
|
||||||
|
val editText = view.findViewById<AppCompatEditText>(R.id.edittext)
|
||||||
|
val builder = AlertDialog.Builder(context!!)
|
||||||
|
.setView(view)
|
||||||
|
.setTitle(context!!.getString(R.string.filepicker_enter_folder_name))
|
||||||
|
.setPositiveButton(context!!.getString(R.string.filepicker_create)) { _, _ -> }
|
||||||
|
val dialog = builder.create()
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||||
|
val name = editText.text!!.toString()
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(name)) {
|
||||||
|
Util.toast(context!!, context!!.getString(R.string.filepicker_name_invalid))
|
||||||
|
} else {
|
||||||
|
val file = File(selectedDirectory, name)
|
||||||
|
|
||||||
|
if (file.exists()) {
|
||||||
|
Util.toast(context!!, context!!.getString(R.string.filepicker_already_exists))
|
||||||
|
} else {
|
||||||
|
dialog.dismiss()
|
||||||
|
if (file.mkdirs()) {
|
||||||
|
fileLister(file)
|
||||||
|
} else {
|
||||||
|
Util.toast(
|
||||||
|
context!!,
|
||||||
|
context!!.getString(R.string.filepicker_create_folder_failed)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inner class FileListItem(
|
||||||
|
fileParameter: File,
|
||||||
|
nameParameter: String,
|
||||||
|
iconParameter: Drawable
|
||||||
|
) {
|
||||||
|
var file: File? = fileParameter
|
||||||
|
var name: String = nameParameter
|
||||||
|
var icon: Drawable? = iconParameter
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inner class FileListHolder(
|
||||||
|
itemView: View
|
||||||
|
) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
|
||||||
|
|
||||||
|
var name: TextView = itemView.findViewById(R.id.name)
|
||||||
|
var icon: ImageView = itemView.findViewById(R.id.icon)
|
||||||
|
|
||||||
|
init {
|
||||||
|
itemView.findViewById<View>(R.id.layout).setOnClickListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View) {
|
||||||
|
val clickedFile = data[adapterPosition]
|
||||||
|
selectedDirectory = clickedFile.file!!
|
||||||
|
fileLister(clickedFile.file!!)
|
||||||
|
Log.d("FileLister", clickedFile.file!!.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package org.moire.ultrasonic.filepicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface.BUTTON_NEGATIVE
|
||||||
|
import android.content.DialogInterface.BUTTON_NEUTRAL
|
||||||
|
import android.content.DialogInterface.BUTTON_POSITIVE
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import org.moire.ultrasonic.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This dialog can be used to pick a file / folder from the filesystem.
|
||||||
|
* Currently only supports folders.
|
||||||
|
* @author this implementation is loosely based on the work of Yogesh Sundaresan,
|
||||||
|
* original license: http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*/
|
||||||
|
class FilePickerDialog {
|
||||||
|
|
||||||
|
private var alertDialog: AlertDialog? = null
|
||||||
|
private var filePickerView: FilePickerView? = null
|
||||||
|
private var onFileSelectedListener: OnFileSelectedListener? = null
|
||||||
|
private var currentPath: TextView? = null
|
||||||
|
private var newFolderButton: Button? = null
|
||||||
|
|
||||||
|
private constructor(context: Context) {
|
||||||
|
alertDialog = AlertDialog.Builder(context).create()
|
||||||
|
initialize(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(context: Context, themeResId: Int) {
|
||||||
|
alertDialog = AlertDialog.Builder(context, themeResId).create()
|
||||||
|
initialize(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initialize(context: Context) {
|
||||||
|
val view = LayoutInflater.from(context).inflate(R.layout.filepicker_dialog_main, null)
|
||||||
|
|
||||||
|
alertDialog!!.setView(view)
|
||||||
|
filePickerView = view.findViewById(R.id.file_list_view)
|
||||||
|
currentPath = view.findViewById(R.id.current_path)
|
||||||
|
|
||||||
|
newFolderButton = view.findViewById(R.id.filepicker_create_folder)
|
||||||
|
newFolderButton!!.setOnClickListener { filePickerView!!.createNewFolder() }
|
||||||
|
|
||||||
|
alertDialog!!.setTitle(context.getString(R.string.filepicker_select_folder))
|
||||||
|
|
||||||
|
alertDialog!!.setButton(BUTTON_POSITIVE, context.getString(R.string.filepicker_select)) {
|
||||||
|
dialogInterface, _ ->
|
||||||
|
dialogInterface.dismiss()
|
||||||
|
if (onFileSelectedListener != null)
|
||||||
|
onFileSelectedListener!!.onFileSelected(
|
||||||
|
filePickerView!!.selected, filePickerView!!.selected.absolutePath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
alertDialog!!.setButton(BUTTON_NEUTRAL, context.getString(R.string.filepicker_default)) {
|
||||||
|
_, _ ->
|
||||||
|
filePickerView!!.goToDefaultDirectory()
|
||||||
|
}
|
||||||
|
alertDialog!!.setButton(BUTTON_NEGATIVE, context.getString(R.string.common_cancel)) {
|
||||||
|
dialogInterface, _ ->
|
||||||
|
dialogInterface.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the FilePickerDialog
|
||||||
|
*/
|
||||||
|
fun show() {
|
||||||
|
filePickerView!!.start { currentDirectory, isRealPath ->
|
||||||
|
run {
|
||||||
|
currentPath?.text = currentDirectory
|
||||||
|
newFolderButton!!.isEnabled = isRealPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alertDialog!!.show()
|
||||||
|
alertDialog!!.getButton(BUTTON_NEUTRAL).setOnClickListener {
|
||||||
|
filePickerView!!.goToDefaultDirectory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener to know which file/directory is selected
|
||||||
|
*
|
||||||
|
* @param onFileSelectedListener Instance of the Listener
|
||||||
|
*/
|
||||||
|
fun setOnFileSelectedListener(onFileSelectedListener: OnFileSelectedListener) {
|
||||||
|
this.onFileSelectedListener = onFileSelectedListener
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the initial directory to show the list of files in that directory
|
||||||
|
*
|
||||||
|
* @param path String denoting to the directory
|
||||||
|
*/
|
||||||
|
fun setDefaultDirectory(path: String) {
|
||||||
|
filePickerView!!.setDefaultDirectory(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setInitialDirectory(path: String) {
|
||||||
|
filePickerView!!.setInitialDirectory(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Creates a default instance of FilePickerDialog
|
||||||
|
*
|
||||||
|
* @param context Context of the App
|
||||||
|
* @return Instance of FileListerDialog
|
||||||
|
*/
|
||||||
|
fun createFilePickerDialog(context: Context): FilePickerDialog {
|
||||||
|
return FilePickerDialog(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package org.moire.ultrasonic.filepicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RecyclerView containing the file list of a directory
|
||||||
|
* @author this implementation is loosely based on the work of Yogesh Sundaresan,
|
||||||
|
* original license: http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*/
|
||||||
|
internal class FilePickerView : RecyclerView {
|
||||||
|
|
||||||
|
private var adapter: FilePickerAdapter? = null
|
||||||
|
|
||||||
|
val selected: File
|
||||||
|
get() = adapter!!.selectedDirectory
|
||||||
|
|
||||||
|
constructor(context: Context) : super(context) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet?,
|
||||||
|
defStyle: Int
|
||||||
|
) : super(context, attrs, defStyle) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initialize() {
|
||||||
|
layoutManager = LinearLayoutManager(context, VERTICAL, false)
|
||||||
|
adapter = FilePickerAdapter(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start(selectedDirectoryChangedListener: (String, Boolean) -> Unit) {
|
||||||
|
setAdapter(adapter)
|
||||||
|
adapter?.selectedDirectoryChanged = selectedDirectoryChangedListener
|
||||||
|
adapter!!.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultDirectory(file: File) {
|
||||||
|
adapter!!.defaultDirectory = file
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultDirectory(path: String) {
|
||||||
|
setDefaultDirectory(File(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setInitialDirectory(path: String) {
|
||||||
|
adapter!!.initialDirectory = File(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun goToDefaultDirectory() {
|
||||||
|
adapter!!.goToDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createNewFolder() {
|
||||||
|
adapter!!.createNewFolder()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package org.moire.ultrasonic.filepicker
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
interface OnFileSelectedListener {
|
||||||
|
fun onFileSelected(file: File?, path: String?)
|
||||||
|
}
|
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="10dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatEditText
|
||||||
|
android:id="@+id/edittext"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:inputType="text"
|
||||||
|
android:maxLines="1" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
||||||
|
a:layout_width="match_parent"
|
||||||
|
a:layout_height="match_parent"
|
||||||
|
a:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
a:id="@+id/current_path"
|
||||||
|
a:layout_width="match_parent"
|
||||||
|
a:layout_height="wrap_content"
|
||||||
|
a:layout_alignParentTop="true"
|
||||||
|
a:layout_margin="20dp"
|
||||||
|
a:gravity="center_vertical"
|
||||||
|
a:text=""
|
||||||
|
a:textSize="18sp" />
|
||||||
|
|
||||||
|
<org.moire.ultrasonic.filepicker.FilePickerView
|
||||||
|
a:id="@+id/file_list_view"
|
||||||
|
a:layout_width="match_parent"
|
||||||
|
a:layout_height="match_parent"
|
||||||
|
a:layout_below="@id/current_path"
|
||||||
|
a:layout_above="@id/filepicker_create_folder"
|
||||||
|
a:scrollbars="vertical" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
a:id="@+id/filepicker_create_folder"
|
||||||
|
style="@style/Widget.AppCompat.Button.ButtonBar.AlertDialog"
|
||||||
|
a:layout_width="wrap_content"
|
||||||
|
a:layout_height="wrap_content"
|
||||||
|
a:layout_alignParentBottom="true"
|
||||||
|
a:layout_marginStart="10dp"
|
||||||
|
a:layout_marginLeft="10dp"
|
||||||
|
a:layout_marginTop="10dp"
|
||||||
|
a:layout_marginEnd="10dp"
|
||||||
|
a:layout_marginRight="10dp"
|
||||||
|
a:layout_marginBottom="10dp"
|
||||||
|
a:drawableStart="?attr/filepicker_create_new_folder"
|
||||||
|
a:drawableLeft="?attr/filepicker_create_new_folder"
|
||||||
|
a:drawablePadding="10dp"
|
||||||
|
a:gravity="start|center_vertical"
|
||||||
|
a:text="@string/filepicker.create_folder" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
|
||||||
|
a:id="@+id/layout"
|
||||||
|
a:padding="5dp"
|
||||||
|
a:layout_width="match_parent"
|
||||||
|
a:layout_height="wrap_content"
|
||||||
|
a:minHeight="?android:attr/listPreferredItemHeight"
|
||||||
|
a:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
a:id="@+id/icon"
|
||||||
|
a:layout_width="36dp"
|
||||||
|
a:layout_height="36dp"
|
||||||
|
a:layout_gravity="center_vertical" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
a:id="@+id/name"
|
||||||
|
a:layout_width="wrap_content"
|
||||||
|
a:layout_height="wrap_content"
|
||||||
|
a:layout_gravity="center_vertical"
|
||||||
|
a:layout_marginStart="20dp"
|
||||||
|
a:layout_marginLeft="20dp"
|
||||||
|
a:gravity="center_vertical"
|
||||||
|
a:text=""
|
||||||
|
a:textSize="18sp" />
|
||||||
|
</LinearLayout>
|
|
@ -399,10 +399,24 @@
|
||||||
<string name="permissions.message_box_title">Warning</string>
|
<string name="permissions.message_box_title">Warning</string>
|
||||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||||
<string name="permissions.rationale_title">Permission request</string>
|
<string name="permissions.rationale_title">Permission request</string>
|
||||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
<string name="permissions.rationale_description_failed">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||||
<string name="permissions.permanent_denial_description">Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value.</string>
|
<string name="permissions.permanent_denial_description">Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the default folder will be used as the cache location.</string>
|
||||||
<string name="permissions.open_settings">Open settings</string>
|
<string name="permissions.open_settings">Open settings</string>
|
||||||
|
<string name="permissions.rationale_description_initial">To be able to change the cache location, Ultrasonic needs read/write permission to the filesystem.</string>
|
||||||
|
|
||||||
|
<string name="filepicker.select_folder">Select a folder</string>
|
||||||
|
<string name="filepicker.create_folder">Create new folder</string>
|
||||||
|
<string name="filepicker.create_folder_failed">Failed to create new folder</string>
|
||||||
|
<string name="filepicker.internal">%1$s (Internal)</string>
|
||||||
|
<string name="filepicker.default_app_folder">Default app folder on %1$s (External)</string>
|
||||||
|
<string name="filepicker.enter_folder_name">Enter the folder name</string>
|
||||||
|
<string name="filepicker.create">Create</string>
|
||||||
|
<string name="filepicker.name_invalid">Please enter a valid folder name</string>
|
||||||
|
<string name="filepicker.already_exists">This folder already exists.\nPlease provide another name for the folder</string>
|
||||||
|
<string name="filepicker.select">Select</string>
|
||||||
|
<string name="filepicker.default">Use default</string>
|
||||||
|
<string name="filepicker.available_drives">Available drives:</string>
|
||||||
|
|
||||||
<plurals name="select_album_n_songs">
|
<plurals name="select_album_n_songs">
|
||||||
<item quantity="one">1 Titel</item>
|
<item quantity="one">1 Titel</item>
|
||||||
|
|
|
@ -400,10 +400,24 @@
|
||||||
<string name="permissions.message_box_title">Atención</string>
|
<string name="permissions.message_box_title">Atención</string>
|
||||||
<string name="permissions.permission_missing">Ultrasonic necesita permiso de lectura / escritura para el directorio caché de música. El directorio caché se restableció a su valor predeterminado.</string>
|
<string name="permissions.permission_missing">Ultrasonic necesita permiso de lectura / escritura para el directorio caché de música. El directorio caché se restableció a su valor predeterminado.</string>
|
||||||
<string name="permissions.rationale_title">Solicitud de permisos</string>
|
<string name="permissions.rationale_title">Solicitud de permisos</string>
|
||||||
<string name="permissions.rationale_description">Ultrasonic necesita permiso de lectura / escritura para el directorio caché de música. Por favor permite a Ultrasonic acceder al sistema de ficheros.</string>
|
<string name="permissions.rationale_description_failed">Ultrasonic necesita permiso de lectura / escritura para el directorio caché de música. Por favor permite a Ultrasonic acceder al sistema de ficheros.</string>
|
||||||
<string name="permissions.permanent_denial_title">Permisos denegados permanentemente</string>
|
<string name="permissions.permanent_denial_title">Permisos denegados permanentemente</string>
|
||||||
<string name="permissions.permanent_denial_description">Ultrasonic necesita acceso de lectura / escritura a la ubicación de la caché. Puedes otorgarlos en la configuración de la aplicación. Si rechazas esta solicitud, la ubicación de la caché se restablecerá a su valor predeterminado.</string>
|
<string name="permissions.permanent_denial_description">Ultrasonic necesita acceso de lectura / escritura a la ubicación de la caché. Puedes otorgarlos en la configuración de la aplicación. Si rechazas esta solicitud, la ubicación de la caché se restablecerá a su valor predeterminado.</string>
|
||||||
<string name="permissions.open_settings">Abrir configuración</string>
|
<string name="permissions.open_settings">Abrir configuración</string>
|
||||||
|
<string name="permissions.rationale_description_initial">To be able to change the cache location, Ultrasonic needs read/write permission to the filesystem.</string>
|
||||||
|
|
||||||
|
<string name="filepicker.select_folder">Select a folder</string>
|
||||||
|
<string name="filepicker.create_folder">Create new folder</string>
|
||||||
|
<string name="filepicker.create_folder_failed">Failed to create new folder</string>
|
||||||
|
<string name="filepicker.internal">%1$s (Internal)</string>
|
||||||
|
<string name="filepicker.default_app_folder">Default app folder on %1$s (External)</string>
|
||||||
|
<string name="filepicker.enter_folder_name">Enter the folder name</string>
|
||||||
|
<string name="filepicker.create">Create</string>
|
||||||
|
<string name="filepicker.name_invalid">Please enter a valid folder name</string>
|
||||||
|
<string name="filepicker.already_exists">This folder already exists.\nPlease provide another name for the folder</string>
|
||||||
|
<string name="filepicker.select">Select</string>
|
||||||
|
<string name="filepicker.default">Use default</string>
|
||||||
|
<string name="filepicker.available_drives">Available drives:</string>
|
||||||
|
|
||||||
<plurals name="select_album_n_songs">
|
<plurals name="select_album_n_songs">
|
||||||
<item quantity="one">1 canción</item>
|
<item quantity="one">1 canción</item>
|
||||||
|
|
|
@ -400,10 +400,24 @@
|
||||||
<string name="permissions.message_box_title">Warning</string>
|
<string name="permissions.message_box_title">Warning</string>
|
||||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||||
<string name="permissions.rationale_title">Permission request</string>
|
<string name="permissions.rationale_title">Permission request</string>
|
||||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
<string name="permissions.rationale_description_failed">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||||
<string name="permissions.permanent_denial_description">Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value.</string>
|
<string name="permissions.permanent_denial_description">Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the default folder will be used as the cache location.</string>
|
||||||
<string name="permissions.open_settings">Open settings</string>
|
<string name="permissions.open_settings">Open settings</string>
|
||||||
|
<string name="permissions.rationale_description_initial">To be able to change the cache location, Ultrasonic needs read/write permission to the filesystem.</string>
|
||||||
|
|
||||||
|
<string name="filepicker.select_folder">Select a folder</string>
|
||||||
|
<string name="filepicker.create_folder">Create new folder</string>
|
||||||
|
<string name="filepicker.create_folder_failed">Failed to create new folder</string>
|
||||||
|
<string name="filepicker.internal">%1$s (Internal)</string>
|
||||||
|
<string name="filepicker.default_app_folder">Default app folder on %1$s (External)</string>
|
||||||
|
<string name="filepicker.enter_folder_name">Enter the folder name</string>
|
||||||
|
<string name="filepicker.create">Create</string>
|
||||||
|
<string name="filepicker.name_invalid">Please enter a valid folder name</string>
|
||||||
|
<string name="filepicker.already_exists">This folder already exists.\nPlease provide another name for the folder</string>
|
||||||
|
<string name="filepicker.select">Select</string>
|
||||||
|
<string name="filepicker.default">Use default</string>
|
||||||
|
<string name="filepicker.available_drives">Available drives:</string>
|
||||||
|
|
||||||
<plurals name="select_album_n_songs">
|
<plurals name="select_album_n_songs">
|
||||||
<item quantity="one">Un titre</item>
|
<item quantity="one">Un titre</item>
|
||||||
|
|
|
@ -400,10 +400,24 @@
|
||||||
<string name="permissions.message_box_title">Figyelem</string>
|
<string name="permissions.message_box_title">Figyelem</string>
|
||||||
<string name="permissions.permission_missing">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.</string>
|
<string name="permissions.permission_missing">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.</string>
|
||||||
<string name="permissions.rationale_title">Jogosultság kérés</string>
|
<string name="permissions.rationale_title">Jogosultság kérés</string>
|
||||||
<string name="permissions.rationale_description">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.\nKérjük, engedélyezd a hozzáférést a fájlrendszerhez.</string>
|
<string name="permissions.rationale_description_failed">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.\nKérlek, adj hozzáférést az Ultrasonicnak a fájlrendszerhez.</string>
|
||||||
<string name="permissions.permanent_denial_title">A jogosultság visszautasítva</string>
|
<string name="permissions.permanent_denial_title">A jogosultság visszautasítva</string>
|
||||||
<string name="permissions.permanent_denial_description">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. Ez a beállítás az alkalmazásbeállítások között módosítható. Ha elutasítod ezt a kérést, a gyorsítótár helye visszaáll az alapbeállításra.</string>
|
<string name="permissions.permanent_denial_description">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. Ez a beállítás az alkalmazásbeállítások között módosítható. Ha elutasítod ezt a kérést, a gyorsítótár helye visszaáll az alapbeállításra.</string>
|
||||||
<string name="permissions.open_settings">Beállítások megnyitása</string>
|
<string name="permissions.open_settings">Beállítások megnyitása</string>
|
||||||
|
<string name="permissions.rationale_description_initial">A gyorsítótár helyének megváltoztatásához az Ultrasonicnak írás/olvasás hozzáférésre van szüksége a fájlrendszerhez.</string>
|
||||||
|
|
||||||
|
<string name="filepicker.select_folder">Mappa kiválasztása</string>
|
||||||
|
<string name="filepicker.create_folder">Új mappa létrehozása</string>
|
||||||
|
<string name="filepicker.create_folder_failed">Az új mappa létrehozása nem sikerült</string>
|
||||||
|
<string name="filepicker.internal">%1$s (Belső)</string>
|
||||||
|
<string name="filepicker.default_app_folder">Alapértelmezett alkalmazásmappa a %1$s tárolón (Külső)</string>
|
||||||
|
<string name="filepicker.enter_folder_name">A mappa neve</string>
|
||||||
|
<string name="filepicker.create">Létrehoz</string>
|
||||||
|
<string name="filepicker.name_invalid">Kérjük, adj meg egy érvényes mappanevet</string>
|
||||||
|
<string name="filepicker.already_exists">Ilyen nevű mappa már létezik.\nKérjük, adj meg más nevet.</string>
|
||||||
|
<string name="filepicker.select">Választ</string>
|
||||||
|
<string name="filepicker.default">Alapért.</string>
|
||||||
|
<string name="filepicker.available_drives">Elérhető tárolók:</string>
|
||||||
|
|
||||||
<plurals name="select_album_n_songs">
|
<plurals name="select_album_n_songs">
|
||||||
<item quantity="one">1 dal</item>
|
<item quantity="one">1 dal</item>
|
||||||
|
|
|
@ -400,10 +400,24 @@
|
||||||
<string name="permissions.message_box_title">Warning</string>
|
<string name="permissions.message_box_title">Warning</string>
|
||||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||||
<string name="permissions.rationale_title">Permission request</string>
|
<string name="permissions.rationale_title">Permission request</string>
|
||||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
<string name="permissions.rationale_description_failed">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||||
<string name="permissions.permanent_denial_description">Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value.</string>
|
<string name="permissions.permanent_denial_description">Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the default folder will be used as the cache location.</string>
|
||||||
<string name="permissions.open_settings">Open settings</string>
|
<string name="permissions.open_settings">Open settings</string>
|
||||||
|
<string name="permissions.rationale_description_initial">To be able to change the cache location, Ultrasonic needs read/write permission to the filesystem.</string>
|
||||||
|
|
||||||
|
<string name="filepicker.select_folder">Select a folder</string>
|
||||||
|
<string name="filepicker.create_folder">Create new folder</string>
|
||||||
|
<string name="filepicker.create_folder_failed">Failed to create new folder</string>
|
||||||
|
<string name="filepicker.internal">%1$s (Internal)</string>
|
||||||
|
<string name="filepicker.default_app_folder">Default app folder on %1$s (External)</string>
|
||||||
|
<string name="filepicker.enter_folder_name">Enter the folder name</string>
|
||||||
|
<string name="filepicker.create">Create</string>
|
||||||
|
<string name="filepicker.name_invalid">Please enter a valid folder name</string>
|
||||||
|
<string name="filepicker.already_exists">This folder already exists.\nPlease provide another name for the folder</string>
|
||||||
|
<string name="filepicker.select">Select</string>
|
||||||
|
<string name="filepicker.default">Use default</string>
|
||||||
|
<string name="filepicker.available_drives">Available drives:</string>
|
||||||
|
|
||||||
<plurals name="select_album_n_songs">
|
<plurals name="select_album_n_songs">
|
||||||
<item quantity="one">1 nummer</item>
|
<item quantity="one">1 nummer</item>
|
||||||
|
|
|
@ -400,10 +400,24 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników
|
||||||
<string name="permissions.message_box_title">Warning</string>
|
<string name="permissions.message_box_title">Warning</string>
|
||||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||||
<string name="permissions.rationale_title">Permission request</string>
|
<string name="permissions.rationale_title">Permission request</string>
|
||||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
<string name="permissions.rationale_description_failed">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||||
<string name="permissions.permanent_denial_description">Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value.</string>
|
<string name="permissions.permanent_denial_description">Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the default folder will be used as the cache location.</string>
|
||||||
<string name="permissions.open_settings">Open settings</string>
|
<string name="permissions.open_settings">Open settings</string>
|
||||||
|
<string name="permissions.rationale_description_initial">To be able to change the cache location, Ultrasonic needs read/write permission to the filesystem.</string>
|
||||||
|
|
||||||
|
<string name="filepicker.select_folder">Select a folder</string>
|
||||||
|
<string name="filepicker.create_folder">Create new folder</string>
|
||||||
|
<string name="filepicker.create_folder_failed">Failed to create new folder</string>
|
||||||
|
<string name="filepicker.internal">%1$s (Internal)</string>
|
||||||
|
<string name="filepicker.default_app_folder">Default app folder on %1$s (External)</string>
|
||||||
|
<string name="filepicker.enter_folder_name">Enter the folder name</string>
|
||||||
|
<string name="filepicker.create">Create</string>
|
||||||
|
<string name="filepicker.name_invalid">Please enter a valid folder name</string>
|
||||||
|
<string name="filepicker.already_exists">This folder already exists.\nPlease provide another name for the folder</string>
|
||||||
|
<string name="filepicker.select">Select</string>
|
||||||
|
<string name="filepicker.default">Use default</string>
|
||||||
|
<string name="filepicker.available_drives">Available drives:</string>
|
||||||
|
|
||||||
<plurals name="select_album_n_songs">
|
<plurals name="select_album_n_songs">
|
||||||
<item quantity="one">1 utwór</item>
|
<item quantity="one">1 utwór</item>
|
||||||
|
|
|
@ -400,10 +400,24 @@
|
||||||
<string name="permissions.message_box_title">Warning</string>
|
<string name="permissions.message_box_title">Warning</string>
|
||||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||||
<string name="permissions.rationale_title">Permission request</string>
|
<string name="permissions.rationale_title">Permission request</string>
|
||||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
<string name="permissions.rationale_description_failed">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||||
<string name="permissions.permanent_denial_description">Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value.</string>
|
<string name="permissions.permanent_denial_description">Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the default folder will be used as the cache location.</string>
|
||||||
<string name="permissions.open_settings">Open settings</string>
|
<string name="permissions.open_settings">Open settings</string>
|
||||||
|
<string name="permissions.rationale_description_initial">To be able to change the cache location, Ultrasonic needs read/write permission to the filesystem.</string>
|
||||||
|
|
||||||
|
<string name="filepicker.select_folder">Select a folder</string>
|
||||||
|
<string name="filepicker.create_folder">Create new folder</string>
|
||||||
|
<string name="filepicker.create_folder_failed">Failed to create new folder</string>
|
||||||
|
<string name="filepicker.internal">%1$s (Internal)</string>
|
||||||
|
<string name="filepicker.default_app_folder">Default app folder on %1$s (External)</string>
|
||||||
|
<string name="filepicker.enter_folder_name">Enter the folder name</string>
|
||||||
|
<string name="filepicker.create">Create</string>
|
||||||
|
<string name="filepicker.name_invalid">Please enter a valid folder name</string>
|
||||||
|
<string name="filepicker.already_exists">This folder already exists.\nPlease provide another name for the folder</string>
|
||||||
|
<string name="filepicker.select">Select</string>
|
||||||
|
<string name="filepicker.default">Use default</string>
|
||||||
|
<string name="filepicker.available_drives">Available drives:</string>
|
||||||
|
|
||||||
<plurals name="select_album_n_songs">
|
<plurals name="select_album_n_songs">
|
||||||
<item quantity="one">1 música</item>
|
<item quantity="one">1 música</item>
|
||||||
|
|
|
@ -400,10 +400,24 @@
|
||||||
<string name="permissions.message_box_title">Warning</string>
|
<string name="permissions.message_box_title">Warning</string>
|
||||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||||
<string name="permissions.rationale_title">Permission request</string>
|
<string name="permissions.rationale_title">Permission request</string>
|
||||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
<string name="permissions.rationale_description_failed">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||||
<string name="permissions.permanent_denial_description">Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value.</string>
|
<string name="permissions.permanent_denial_description">Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the default folder will be used as the cache location.</string>
|
||||||
<string name="permissions.open_settings">Open settings</string>
|
<string name="permissions.open_settings">Open settings</string>
|
||||||
|
<string name="permissions.rationale_description_initial">To be able to change the cache location, Ultrasonic needs read/write permission to the filesystem.</string>
|
||||||
|
|
||||||
|
<string name="filepicker.select_folder">Select a folder</string>
|
||||||
|
<string name="filepicker.create_folder">Create new folder</string>
|
||||||
|
<string name="filepicker.create_folder_failed">Failed to create new folder</string>
|
||||||
|
<string name="filepicker.internal">%1$s (Internal)</string>
|
||||||
|
<string name="filepicker.default_app_folder">Default app folder on %1$s (External)</string>
|
||||||
|
<string name="filepicker.enter_folder_name">Enter the folder name</string>
|
||||||
|
<string name="filepicker.create">Create</string>
|
||||||
|
<string name="filepicker.name_invalid">Please enter a valid folder name</string>
|
||||||
|
<string name="filepicker.already_exists">This folder already exists.\nPlease provide another name for the folder</string>
|
||||||
|
<string name="filepicker.select">Select</string>
|
||||||
|
<string name="filepicker.default">Use default</string>
|
||||||
|
<string name="filepicker.available_drives">Available drives:</string>
|
||||||
|
|
||||||
<plurals name="select_album_n_songs">
|
<plurals name="select_album_n_songs">
|
||||||
<item quantity="one">%d música</item>
|
<item quantity="one">%d música</item>
|
||||||
|
|
|
@ -404,10 +404,24 @@
|
||||||
<string name="permissions.message_box_title">Warning</string>
|
<string name="permissions.message_box_title">Warning</string>
|
||||||
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
|
||||||
<string name="permissions.rationale_title">Permission request</string>
|
<string name="permissions.rationale_title">Permission request</string>
|
||||||
<string name="permissions.rationale_description">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
<string name="permissions.rationale_description_failed">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
|
||||||
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
|
||||||
<string name="permissions.permanent_denial_description">Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the cache location will be reset to its default value.</string>
|
<string name="permissions.permanent_denial_description">Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the default folder will be used as the cache location.</string>
|
||||||
<string name="permissions.open_settings">Open settings</string>
|
<string name="permissions.open_settings">Open settings</string>
|
||||||
|
<string name="permissions.rationale_description_initial">To be able to change the cache location, Ultrasonic needs read/write permission to the filesystem.</string>
|
||||||
|
|
||||||
|
<string name="filepicker.select_folder">Select a folder</string>
|
||||||
|
<string name="filepicker.create_folder">Create new folder</string>
|
||||||
|
<string name="filepicker.create_folder_failed">Failed to create new folder</string>
|
||||||
|
<string name="filepicker.internal">%1$s (Internal)</string>
|
||||||
|
<string name="filepicker.default_app_folder">Default app folder on %1$s (External)</string>
|
||||||
|
<string name="filepicker.enter_folder_name">Enter the folder name</string>
|
||||||
|
<string name="filepicker.create">Create</string>
|
||||||
|
<string name="filepicker.name_invalid">Please enter a valid folder name</string>
|
||||||
|
<string name="filepicker.already_exists">This folder already exists.\nPlease provide another name for the folder</string>
|
||||||
|
<string name="filepicker.select">Select</string>
|
||||||
|
<string name="filepicker.default">Use default</string>
|
||||||
|
<string name="filepicker.available_drives">Available drives:</string>
|
||||||
|
|
||||||
<plurals name="select_album_n_songs">
|
<plurals name="select_album_n_songs">
|
||||||
<item quantity="one">1 song</item>
|
<item quantity="one">1 song</item>
|
||||||
|
|
|
@ -92,6 +92,11 @@
|
||||||
<attr name="check_mark_on" format="reference"/>
|
<attr name="check_mark_on" format="reference"/>
|
||||||
<attr name="button_check_custom" format="reference"/>
|
<attr name="button_check_custom" format="reference"/>
|
||||||
<attr name="color_background" format="reference"/>
|
<attr name="color_background" format="reference"/>
|
||||||
|
<attr name="filepicker_create_new_folder" format="reference"/>
|
||||||
|
<attr name="filepicker_folder" format="reference"/>
|
||||||
|
<attr name="filepicker_subdirectory_left" format="reference"/>
|
||||||
|
<attr name="filepicker_subdirectory_up" format="reference"/>
|
||||||
|
<attr name="filepicker_sd_card" format="reference"/>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="UltraSonicTheme" parent="Theme.AppCompat">
|
<style name="UltraSonicTheme" parent="Theme.AppCompat">
|
||||||
<item name="color_background">@color/background_color_dark</item>
|
<item name="color_background">@color/background_color_dark</item>
|
||||||
<item name="star_hollow">@drawable/ic_star_hollow_dark</item>
|
<item name="star_hollow">@drawable/ic_star_hollow_dark</item>
|
||||||
|
@ -44,6 +43,10 @@
|
||||||
<item name="chat_send">@drawable/ic_menu_chat_send_dark</item>
|
<item name="chat_send">@drawable/ic_menu_chat_send_dark</item>
|
||||||
<item name="bookmark">@drawable/ic_menu_bookmark_dark</item>
|
<item name="bookmark">@drawable/ic_menu_bookmark_dark</item>
|
||||||
<item name="button_check_custom">@drawable/btn_check_custom_dark</item>
|
<item name="button_check_custom">@drawable/btn_check_custom_dark</item>
|
||||||
|
<item name="filepicker_create_new_folder">@drawable/ic_create_new_folder_dark</item>
|
||||||
|
<item name="filepicker_folder">@drawable/ic_folder_dark</item>
|
||||||
|
<item name="filepicker_subdirectory_up">@drawable/ic_subdirectory_up_dark</item>
|
||||||
|
<item name="filepicker_sd_card">@drawable/ic_sd_storage_dark</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="UltraSonicTheme.Light" parent="Theme.AppCompat.Light">
|
<style name="UltraSonicTheme.Light" parent="Theme.AppCompat.Light">
|
||||||
|
@ -89,6 +92,9 @@
|
||||||
<item name="chat_send">@drawable/ic_menu_chat_send_light</item>
|
<item name="chat_send">@drawable/ic_menu_chat_send_light</item>
|
||||||
<item name="bookmark">@drawable/ic_menu_bookmark_light</item>
|
<item name="bookmark">@drawable/ic_menu_bookmark_light</item>
|
||||||
<item name="button_check_custom">@drawable/btn_check_custom_light</item>
|
<item name="button_check_custom">@drawable/btn_check_custom_light</item>
|
||||||
|
<item name="filepicker_create_new_folder">@drawable/ic_create_new_folder_light</item>
|
||||||
|
<item name="filepicker_folder">@drawable/ic_folder_light</item>
|
||||||
|
<item name="filepicker_subdirectory_up">@drawable/ic_subdirectory_up_light</item>
|
||||||
|
<item name="filepicker_sd_card">@drawable/ic_sd_storage_light</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -210,7 +210,7 @@
|
||||||
a:entryValues="@array/cacheSizeValues"
|
a:entryValues="@array/cacheSizeValues"
|
||||||
a:key="cacheSize"
|
a:key="cacheSize"
|
||||||
a:title="@string/settings.cache_size"/>
|
a:title="@string/settings.cache_size"/>
|
||||||
<EditTextPreference
|
<Preference
|
||||||
a:key="cacheLocation"
|
a:key="cacheLocation"
|
||||||
a:title="@string/settings.cache_location"/>
|
a:title="@string/settings.cache_location"/>
|
||||||
<ListPreference
|
<ListPreference
|
||||||
|
|