Merge branch 'nitehu-feature/cache-folder-setting' into develop
@ -27,12 +27,12 @@ ext.versions = [
|
||||
|
||||
junit4 : "4.12",
|
||||
junit5 : "5.3.1",
|
||||
mockito : "2.16.0",
|
||||
mockito : "3.5.5",
|
||||
mockitoKotlin : "1.5.0",
|
||||
kluent : "1.35",
|
||||
apacheCodecs : "1.10",
|
||||
testRunner : "1.0.1",
|
||||
robolectric : "3.8",
|
||||
robolectric : "4.4",
|
||||
dexter : "6.1.2",
|
||||
]
|
||||
|
||||
|
@ -69,6 +69,6 @@ jacoco {
|
||||
}
|
||||
|
||||
ext {
|
||||
jacocoExclude = []
|
||||
jacocoExclude = ['jdk.internal.*']
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ jacoco {
|
||||
|
||||
ext {
|
||||
// override it in the module
|
||||
jacocoExclude = []
|
||||
jacocoExclude = ['jdk.internal.*']
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
|
@ -99,7 +99,8 @@ ext {
|
||||
'**/R$*.class',
|
||||
'**/R.class',
|
||||
'**/BuildConfig.class',
|
||||
'**/di/**'
|
||||
'**/di/**',
|
||||
'jdk.internal.*'
|
||||
]
|
||||
}
|
||||
|
||||
@ -109,4 +110,5 @@ jacoco {
|
||||
|
||||
tasks.withType(Test) {
|
||||
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.featureflags.Feature;
|
||||
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.service.MediaPlayerController;
|
||||
import org.moire.ultrasonic.util.*;
|
||||
@ -37,7 +39,7 @@ public class SettingsFragment extends PreferenceFragment
|
||||
private ListPreference maxBitrateWifi;
|
||||
private ListPreference maxBitrateMobile;
|
||||
private ListPreference cacheSize;
|
||||
private EditTextPreference cacheLocation;
|
||||
private Preference cacheLocation;
|
||||
private ListPreference preloadCount;
|
||||
private ListPreference bufferLength;
|
||||
private ListPreference incrementTime;
|
||||
@ -85,7 +87,7 @@ public class SettingsFragment extends PreferenceFragment
|
||||
maxBitrateWifi = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI);
|
||||
maxBitrateMobile = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE);
|
||||
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);
|
||||
bufferLength = (ListPreference) findPreference(Constants.PREFERENCES_KEY_BUFFER_LENGTH);
|
||||
incrementTime = (ListPreference) findPreference(Constants.PREFERENCES_KEY_INCREMENT_TIME);
|
||||
@ -113,6 +115,7 @@ public class SettingsFragment extends PreferenceFragment
|
||||
setupClearSearchPreference();
|
||||
setupGaplessControlSettingsV14();
|
||||
setupFeatureFlagsPreferences();
|
||||
setupCacheLocationPreference();
|
||||
|
||||
// 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) {
|
||||
@ -155,8 +158,6 @@ public class SettingsFragment extends PreferenceFragment
|
||||
setHideMedia(sharedPreferences.getBoolean(key, false));
|
||||
} else if (Constants.PREFERENCES_KEY_MEDIA_BUTTONS.equals(key)) {
|
||||
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)) {
|
||||
setBluetoothPreferences(sharedPreferences.getBoolean(key, true));
|
||||
} 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() {
|
||||
Preference clearSearchPreference = findPreference(Constants.PREFERENCES_KEY_CLEAR_SEARCH_HISTORY);
|
||||
|
||||
@ -315,7 +350,6 @@ public class SettingsFragment extends PreferenceFragment
|
||||
maxBitrateWifi.setSummary(maxBitrateWifi.getEntry());
|
||||
maxBitrateMobile.setSummary(maxBitrateMobile.getEntry());
|
||||
cacheSize.setSummary(cacheSize.getEntry());
|
||||
cacheLocation.setSummary(cacheLocation.getText());
|
||||
preloadCount.setSummary(preloadCount.getEntry());
|
||||
bufferLength.setSummary(bufferLength.getEntry());
|
||||
incrementTime.setSummary(incrementTime.getEntry());
|
||||
@ -396,14 +430,16 @@ public class SettingsFragment extends PreferenceFragment
|
||||
if (!FileUtil.ensureDirectoryExistsAndIsReadWritable(dir)) {
|
||||
PermissionUtil.handlePermissionFailed(getActivity(), new PermissionUtil.PermissionRequestFinishedCallback() {
|
||||
@Override
|
||||
public void onPermissionRequestFinished() {
|
||||
public void onPermissionRequestFinished(boolean hasPermission) {
|
||||
String currentPath = settings.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION,
|
||||
FileUtil.getDefaultMusicDirectory(getActivity()).getPath());
|
||||
cacheLocation.setSummary(currentPath);
|
||||
cacheLocation.setText(currentPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
cacheLocation.setSummary(path);
|
||||
}
|
||||
|
||||
// Clear download queue.
|
||||
mediaPlayerControllerLazy.getValue().clear();
|
||||
|
@ -5,6 +5,8 @@ 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 android.util.Log;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@ -34,7 +36,7 @@ public class PermissionUtil {
|
||||
private static final String TAG = FileUtil.class.getSimpleName();
|
||||
|
||||
public interface PermissionRequestFinishedCallback {
|
||||
void onPermissionRequestFinished();
|
||||
void onPermissionRequestFinished(boolean hasPermission);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -55,27 +57,79 @@ public class PermissionUtil {
|
||||
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
|
||||
Context mainContext = MainActivity.getInstance();
|
||||
final Context mainContext = MainActivity.getInstance();
|
||||
|
||||
if ((PermissionChecker.checkSelfPermission(context, Manifest.permission.WRITE_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
|
||||
setCacheLocation(context, FileUtil.getDefaultMusicDirectory(context).getPath());
|
||||
requestPermission(mainContext, currentCachePath, callback);
|
||||
requestFailedPermission(mainContext, currentCachePath, callback);
|
||||
} else {
|
||||
setCacheLocation(context, FileUtil.getDefaultMusicDirectory(context).getPath());
|
||||
showWarning(mainContext, context.getString(R.string.permissions_message_box_title), context.getString(R.string.permissions_access_error), null);
|
||||
callback.onPermissionRequestFinished();
|
||||
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);
|
||||
}
|
||||
});
|
||||
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) {
|
||||
Util.getPreferences(context).edit()
|
||||
.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, cacheLocation)
|
||||
.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)
|
||||
.withPermissions(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
@ -86,14 +140,14 @@ public class PermissionUtil {
|
||||
if (report.areAllPermissionsGranted()) {
|
||||
Log.i(TAG, String.format("Permission granted to use cache directory %s", cacheLocation));
|
||||
setCacheLocation(context, cacheLocation);
|
||||
if (callback != null) callback.onPermissionRequestFinished();
|
||||
if (callback != null) callback.onPermissionRequestFinished(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (report.isAnyPermissionPermanentlyDenied()) {
|
||||
Log.i(TAG, String.format("Found permanently denied permission to use cache directory %s, offering settings", cacheLocation));
|
||||
showSettingsDialog(context);
|
||||
if (callback != null) callback.onPermissionRequestFinished();
|
||||
if (callback != null) callback.onPermissionRequestFinished(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -101,13 +155,13 @@ public class PermissionUtil {
|
||||
setCacheLocation(context, FileUtil.getDefaultMusicDirectory(context).getPath());
|
||||
showWarning(context, context.getString(R.string.permissions_message_box_title),
|
||||
context.getString(R.string.permissions_permission_missing), null);
|
||||
if (callback != null) callback.onPermissionRequestFinished();
|
||||
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), token);
|
||||
context.getString(R.string.permissions_rationale_description_failed), token);
|
||||
}
|
||||
}).withErrorListener(new PermissionRequestErrorListener() {
|
||||
@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 |
BIN
ultrasonic/src/main/res/drawable-hdpi/ic_folder_dark.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
ultrasonic/src/main/res/drawable-hdpi/ic_folder_light.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
ultrasonic/src/main/res/drawable-hdpi/ic_sd_storage_dark.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
ultrasonic/src/main/res/drawable-hdpi/ic_sd_storage_light.png
Normal file
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 |
BIN
ultrasonic/src/main/res/drawable-ldpi/ic_folder_dark.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
ultrasonic/src/main/res/drawable-ldpi/ic_folder_light.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
ultrasonic/src/main/res/drawable-ldpi/ic_sd_storage_dark.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
ultrasonic/src/main/res/drawable-ldpi/ic_sd_storage_light.png
Normal file
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 |
BIN
ultrasonic/src/main/res/drawable-mdpi/ic_folder_dark.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
ultrasonic/src/main/res/drawable-mdpi/ic_folder_light.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
ultrasonic/src/main/res/drawable-mdpi/ic_sd_storage_dark.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
ultrasonic/src/main/res/drawable-mdpi/ic_sd_storage_light.png
Normal file
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 |
BIN
ultrasonic/src/main/res/drawable-xhdpi/ic_folder_dark.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
ultrasonic/src/main/res/drawable-xhdpi/ic_folder_light.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
ultrasonic/src/main/res/drawable-xhdpi/ic_sd_storage_dark.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
ultrasonic/src/main/res/drawable-xhdpi/ic_sd_storage_light.png
Normal file
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 |
BIN
ultrasonic/src/main/res/drawable-xxhdpi/ic_folder_dark.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
ultrasonic/src/main/res/drawable-xxhdpi/ic_folder_light.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
ultrasonic/src/main/res/drawable-xxhdpi/ic_sd_storage_dark.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
ultrasonic/src/main/res/drawable-xxhdpi/ic_sd_storage_light.png
Normal file
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>
|
43
ultrasonic/src/main/res/layout/filepicker_dialog_main.xml
Normal file
@ -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.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_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_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.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">
|
||||
<item quantity="one">1 Titel</item>
|
||||
|
@ -400,10 +400,24 @@
|
||||
<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.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_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.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">
|
||||
<item quantity="one">1 canción</item>
|
||||
|
@ -400,10 +400,24 @@
|
||||
<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.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_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.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">
|
||||
<item quantity="one">Un titre</item>
|
||||
|
@ -400,10 +400,24 @@
|
||||
<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.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_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.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">
|
||||
<item quantity="one">1 dal</item>
|
||||
|
@ -400,10 +400,24 @@
|
||||
<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.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_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.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">
|
||||
<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.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_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_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.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">
|
||||
<item quantity="one">1 utwór</item>
|
||||
|
@ -400,10 +400,24 @@
|
||||
<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.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_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.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">
|
||||
<item quantity="one">1 música</item>
|
||||
|
@ -400,10 +400,24 @@
|
||||
<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.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_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.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">
|
||||
<item quantity="one">%d música</item>
|
||||
|
@ -404,10 +404,24 @@
|
||||
<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.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_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.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">
|
||||
<item quantity="one">1 song</item>
|
||||
|
@ -92,6 +92,11 @@
|
||||
<attr name="check_mark_on" format="reference"/>
|
||||
<attr name="button_check_custom" 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>
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="UltraSonicTheme" parent="Theme.AppCompat">
|
||||
<item name="color_background">@color/background_color_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="bookmark">@drawable/ic_menu_bookmark_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 name="UltraSonicTheme.Light" parent="Theme.AppCompat.Light">
|
||||
@ -89,6 +92,9 @@
|
||||
<item name="chat_send">@drawable/ic_menu_chat_send_light</item>
|
||||
<item name="bookmark">@drawable/ic_menu_bookmark_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>
|
||||
|
||||
</resources>
|
@ -210,7 +210,7 @@
|
||||
a:entryValues="@array/cacheSizeValues"
|
||||
a:key="cacheSize"
|
||||
a:title="@string/settings.cache_size"/>
|
||||
<EditTextPreference
|
||||
<Preference
|
||||
a:key="cacheLocation"
|
||||
a:title="@string/settings.cache_location"/>
|
||||
<ListPreference
|
||||
|