Implemented file picker for cache folder settings

This commit is contained in:
Nite 2020-08-18 21:24:24 +02:00
parent 0ad1751a06
commit 5739b2bf4b
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
61 changed files with 830 additions and 36 deletions

View File

@ -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();

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -0,0 +1,7 @@
package org.moire.ultrasonic.filepicker
import java.io.File
interface OnFileSelectedListener {
fun onFileSelected(file: File?, path: String?)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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