mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
File browser support
This commit is contained in:
@@ -61,6 +61,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.LibraryArtistFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.LibraryArtistsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFilesFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.LibraryVideosFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||
@@ -191,6 +192,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
lateinit var _fragLibraryArtists: LibraryArtistsFragment;
|
||||
lateinit var _fragLibraryArtist: LibraryArtistFragment;
|
||||
lateinit var _fragLibraryVideos: LibraryVideosFragment;
|
||||
lateinit var _fragLibraryFiles: LibraryFilesFragment;
|
||||
|
||||
lateinit var _fragBrowser: BrowserFragment;
|
||||
|
||||
@@ -368,6 +370,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
_fragLibraryArtists = LibraryArtistsFragment.newInstance();
|
||||
_fragLibraryArtist = LibraryArtistFragment.newInstance();
|
||||
_fragLibraryVideos = LibraryVideosFragment.newInstance();
|
||||
_fragLibraryFiles = LibraryFilesFragment.newInstance();
|
||||
|
||||
_fragBrowser = BrowserFragment.newInstance();
|
||||
|
||||
@@ -505,6 +508,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
_fragLibraryArtists.topBar = _fragTopBarNavigation;
|
||||
_fragLibraryArtist.topBar = _fragTopBarNavigation;
|
||||
_fragLibraryVideos.topBar = _fragTopBarNavigation;
|
||||
_fragLibraryFiles.topBar = _fragTopBarNavigation;
|
||||
|
||||
_fragBrowser.topBar = _fragTopBarNavigation;
|
||||
|
||||
@@ -1314,6 +1318,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
LibraryArtistsFragment::class -> _fragLibraryArtists as T;
|
||||
LibraryArtistFragment::class -> _fragLibraryArtist as T;
|
||||
LibraryVideosFragment::class -> _fragLibraryVideos as T;
|
||||
LibraryFilesFragment::class -> _fragLibraryFiles as T;
|
||||
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +45,17 @@ class LocalClient: IPlatformClient {
|
||||
try {
|
||||
val uri = Uri.parse(url);
|
||||
return ContentResolver.SCHEME_CONTENT == uri.scheme
|
||||
&& MediaStore.AUTHORITY == uri.authority;
|
||||
&& (
|
||||
MediaStore.AUTHORITY == uri.authority ||
|
||||
uri.authority == "com.android.externalstorage.documents"
|
||||
)
|
||||
}
|
||||
catch(ex: MalformedURLException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
val audioExtensions = listOf(".mp3", ".wav", ".flac", ".mp4a", ".m4a");
|
||||
override fun getContentDetails(url: String): IPlatformContentDetails {
|
||||
val uri = Uri.parse(url);
|
||||
|
||||
@@ -60,6 +65,12 @@ class LocalClient: IPlatformClient {
|
||||
else if("video" in uri.pathSegments) {
|
||||
return StateLibrary.getVideoTrack(url) ?: throw Exception("Failed to find ${url}");
|
||||
}
|
||||
else if(uri.toString().contains("com.android.externalstorage.documents")) {
|
||||
if(audioExtensions.any { uri.lastPathSegment?.lowercase()?.endsWith(it) ?: false })
|
||||
return StateLibrary.getAudioTrack(url) ?: throw Exception("Failed to find ${url}");
|
||||
else
|
||||
return StateLibrary.getVideoTrack(url) ?: throw Exception("Failed to find ${url}");
|
||||
}
|
||||
else
|
||||
throw Exception("Unknown content url [${url}]");
|
||||
}
|
||||
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
package com.futo.platformplayer.fragment.mainactivity.main
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.states.FileEntry
|
||||
import com.futo.platformplayer.states.StateLibrary
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.NoResultsView
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
|
||||
class LibraryFilesFragment : MainFragment() {
|
||||
override val isMainView : Boolean = true;
|
||||
override val isTab: Boolean = true;
|
||||
override val hasBottomBar: Boolean get() = true;
|
||||
|
||||
|
||||
var view: FragView? = null;
|
||||
|
||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val view = FragView(this, inflater);
|
||||
this.view = view;
|
||||
return view;
|
||||
}
|
||||
|
||||
override fun onShown(parameter: Any?, isBack: Boolean) {
|
||||
super.onShown(parameter, isBack)
|
||||
}
|
||||
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
super.onShownWithView(parameter, isBack);
|
||||
view?.onShown();
|
||||
}
|
||||
|
||||
override fun onDestroyMainView() {
|
||||
view = null;
|
||||
super.onDestroyMainView();
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = LibraryFilesFragment().apply {}
|
||||
}
|
||||
|
||||
class FragView : FeedView<LibraryFilesFragment, FileEntry, FileEntry, IPager<FileEntry>, FileViewHolder> {
|
||||
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||
|
||||
val navStack = mutableListOf<FileStack>()
|
||||
var buttonUp: BigButton? = null;
|
||||
var buttonAdd: BigButton? = null;
|
||||
|
||||
constructor(fragment: LibraryFilesFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||
}
|
||||
|
||||
fun onShown() {
|
||||
loadTop();
|
||||
}
|
||||
fun loadTop() {
|
||||
val initialDirectories = StateLibrary.instance.getFileDirectories();
|
||||
if(initialDirectories.size == 0) {
|
||||
setEmptyPager(true);
|
||||
setPager(EmptyPager());
|
||||
buttonAdd?.let {
|
||||
it.isVisible = false;
|
||||
}
|
||||
buttonUp?.let {
|
||||
it.isVisible = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
setEmptyPager(false);
|
||||
navStack.clear();
|
||||
navStack.add(FileStack("", initialDirectories));
|
||||
openDirectory(navStack.last());
|
||||
}
|
||||
fun leaveDirectory() {
|
||||
if(navStack.size > 1) {
|
||||
navStack.removeLast();
|
||||
openDirectory(navStack.last());
|
||||
}
|
||||
else {}
|
||||
}
|
||||
fun openDirectory(stack: FileStack, addToStack: Boolean = false) {
|
||||
if(addToStack)
|
||||
navStack.add(stack);
|
||||
|
||||
buttonAdd?.let {
|
||||
it.isVisible = navStack.size < 2
|
||||
}
|
||||
buttonUp?.let {
|
||||
it.isVisible = navStack.size > 1;
|
||||
}
|
||||
setPager(AdhocPager<FileEntry>({ listOf(); }, stack.files));
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
override fun getEmptyPagerView(): View? {
|
||||
return NoResultsView(context, "No Directories Added",
|
||||
"To see files in Grayjay you have to add directories to view",
|
||||
R.drawable.ic_library, listOf(
|
||||
BigButton(context, "Add Directory", "Select a directory to add", R.drawable.ic_add, {
|
||||
StateLibrary.instance.addFileDirectory {
|
||||
loadTop();
|
||||
};
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<FileEntry>): InsertedViewAdapterWithLoader<FileViewHolder> {
|
||||
val buttonUp = BigButton(fragment.requireContext(), "Go up", "Go up a directory", R.drawable.ic_move_up) {
|
||||
if(navStack.size > 1)
|
||||
leaveDirectory();
|
||||
}
|
||||
val buttonAdd = BigButton(fragment.requireContext(), "Add Directory", "Select a directory to add", R.drawable.ic_add) {
|
||||
StateLibrary.instance.addFileDirectory {
|
||||
loadTop();
|
||||
};
|
||||
}
|
||||
this.buttonUp = buttonUp;
|
||||
this.buttonAdd = buttonAdd;
|
||||
return InsertedViewAdapterWithLoader(context, arrayListOf(buttonUp), arrayListOf(buttonAdd),
|
||||
childCountGetter = { dataset.size },
|
||||
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(dataset[position]); },
|
||||
childViewHolderFactory = { viewGroup, _ ->
|
||||
val holder = FileViewHolder(viewGroup);
|
||||
holder.onClick.subscribe { c ->
|
||||
if (c != null) {
|
||||
if(c.isDirectory) {
|
||||
openDirectory(FileStack(c.path, c.getSubFiles()), true);
|
||||
} else {
|
||||
fragment.navigate<VideoDetailFragment>(c.path)
|
||||
}
|
||||
}
|
||||
};
|
||||
holder.onDelete.subscribe { c ->
|
||||
if(c != null) {
|
||||
StateLibrary.instance.deleteFileDirectory(c.path);
|
||||
loadTop();
|
||||
}
|
||||
}
|
||||
return@InsertedViewAdapterWithLoader holder;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override fun updateSpanCount(){ }
|
||||
|
||||
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager {
|
||||
val glmResults = GridLayoutManager(context, 1)
|
||||
|
||||
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||
rightMargin = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
8.0f,
|
||||
context.resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
|
||||
return glmResults
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LibraryAlbumsFragmentsView";
|
||||
}
|
||||
}
|
||||
class FileStack(
|
||||
val path: String,
|
||||
val files: List<FileEntry>
|
||||
)
|
||||
|
||||
class FileViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<FileEntry>(
|
||||
LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_file,
|
||||
_viewGroup, false)) {
|
||||
|
||||
val onClick = Event1<FileEntry?>();
|
||||
val onDelete = Event1<FileEntry?>();
|
||||
|
||||
protected var _file: FileEntry? = null;
|
||||
protected val _imageThumbnail: ImageView
|
||||
protected val _buttonDelete: ImageButton;
|
||||
protected val _textName: TextView
|
||||
protected val _textMetadata: TextView
|
||||
|
||||
init {
|
||||
_imageThumbnail = _view.findViewById(R.id.image_thumbnail);
|
||||
_textName = _view.findViewById(R.id.text_name);
|
||||
_textMetadata = _view.findViewById(R.id.text_metadata);
|
||||
_buttonDelete = _view.findViewById(R.id.button_delete);
|
||||
|
||||
_view.setOnClickListener { onClick.emit(_file) };
|
||||
_buttonDelete.setOnClickListener { onDelete.emit(_file) }
|
||||
}
|
||||
|
||||
|
||||
override fun bind(file: FileEntry) {
|
||||
_file = file;
|
||||
_imageThumbnail?.let {
|
||||
if(file.isDirectory)
|
||||
it.setImageResource(R.drawable.ic_library);
|
||||
else {
|
||||
Glide.with(it)
|
||||
.load(file.thumbnail)
|
||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||
.into(it)
|
||||
}
|
||||
};
|
||||
_buttonDelete.isVisible = file.removable;
|
||||
|
||||
_textName.text = file.name;
|
||||
if(file.isDirectory)
|
||||
_textMetadata.text = "Directory";
|
||||
else
|
||||
_textMetadata.text = "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+4
-2
@@ -53,10 +53,12 @@ class LibraryFragment : MainFragment() {
|
||||
fun setPermissionResultAudio(access: Boolean) {
|
||||
allowedMusic = access;
|
||||
view?.setMusicPermissions(access);
|
||||
StateApp.instance.hasMediaStoreAudioPermission = (access);
|
||||
}
|
||||
fun setPermissionResultVideo(access: Boolean) {
|
||||
allowedVideo = access;
|
||||
view?.setVideoPermissions(access);
|
||||
StateApp.instance.hasMediaStoreVideoPermission = (access);
|
||||
}
|
||||
|
||||
fun requestPermissionMusic() {
|
||||
@@ -161,9 +163,9 @@ class LibraryFragment : MainFragment() {
|
||||
fragment.navigate<PlaylistsFragment>();
|
||||
}
|
||||
buttonFiles.onClick.subscribe {
|
||||
UIDialogs.appToast("This is gonna require a bit more work..");
|
||||
fragment.navigate<LibraryFilesFragment>()
|
||||
}
|
||||
buttonFiles.setButtonEnabled(false);
|
||||
//buttonFiles.setButtonEnabled(false);
|
||||
setMusicPermissions(allowMusic ?: false);
|
||||
setVideoPermissions(allowVideo ?: false);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,9 @@ class StateApp {
|
||||
privateModeChanged.emit(privateMode);
|
||||
}
|
||||
|
||||
var hasMediaStoreAudioPermission: Boolean = false;
|
||||
var hasMediaStoreVideoPermission: Boolean = false;
|
||||
|
||||
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
|
||||
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
||||
if(isValidStorageUri(context, generalUri))
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package com.futo.platformplayer.states
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.provider.MediaStore.Audio.Artists
|
||||
import android.provider.MediaStore.Images.ImageColumns
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.Thumbnail
|
||||
@@ -21,11 +26,10 @@ import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Playlist
|
||||
import com.futo.platformplayer.states.Album.Companion
|
||||
import com.futo.platformplayer.states.Album.Companion.TAG
|
||||
import com.futo.platformplayer.states.StateLibrary.Companion.getAudioTrack
|
||||
import com.futo.platformplayer.states.StateLibrary.Companion.videoFromCursor
|
||||
import com.google.protobuf.Empty
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringArrayStorage
|
||||
import java.io.File
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
@@ -33,8 +37,56 @@ import java.time.ZoneOffset
|
||||
|
||||
class StateLibrary {
|
||||
|
||||
private val _files = FragmentedStorage.get<StringArrayStorage>("libraryFiles")
|
||||
|
||||
|
||||
fun getFileDirectories(): List<FileEntry> {
|
||||
val context = StateApp.instance.contextOrNull ?: return listOf();
|
||||
return _files.getAllValues().map {
|
||||
if(it.startsWith("content://")) {
|
||||
val uri = it.toUri();
|
||||
val docFile = DocumentFile.fromTreeUri(context, uri) ?: return@map null;
|
||||
//val access = context.contentResolver.persistedUriPermissions.any { it.uri == uri && it.isReadPermission }
|
||||
if(!docFile.isDirectory) {
|
||||
_files.remove(it);
|
||||
return@map null;
|
||||
}
|
||||
if(docFile == null)
|
||||
return@map null;
|
||||
return@map FileEntry.fromFile(docFile).apply { this.removable = true }
|
||||
}
|
||||
else
|
||||
FileEntry.fromPath(it);
|
||||
}.filterNotNull();
|
||||
}
|
||||
fun deleteFileDirectory(path: String) {
|
||||
_files.remove(path);
|
||||
_files.save();
|
||||
}
|
||||
fun addFileDirectory(onAdded: ((entry: FileEntry) -> Unit)? = null): Boolean {
|
||||
if(!StateApp.instance.isMainActive)
|
||||
return false;
|
||||
val mainActivity = StateApp.instance.contextOrNull as MainActivity? ?: return false;
|
||||
|
||||
StateApp.instance.requestDirectoryAccess(mainActivity, "Select Directory",
|
||||
"Select a directory you would like to make accessible to Grayjay", null, {
|
||||
if(it != null) {
|
||||
mainActivity.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_WRITE_URI_PERMISSION.or(Intent.FLAG_GRANT_READ_URI_PERMISSION));
|
||||
try {
|
||||
val file = DocumentFile.fromTreeUri(mainActivity, it) ?: return@requestDirectoryAccess;
|
||||
val dir = FileEntry.fromFile(file);
|
||||
_files.add(dir.path);
|
||||
_files.save();
|
||||
onAdded?.invoke(dir);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Something went wrong converting requested directory", ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
fun getAlbums(): List<Album> {
|
||||
return Album.getAlbums();
|
||||
}
|
||||
@@ -133,11 +185,41 @@ class StateLibrary {
|
||||
MediaStore.Audio.Media.BUCKET_DISPLAY_NAME //7
|
||||
);
|
||||
|
||||
fun getDocumentTrack(url: String): IPlatformContentDetails? {
|
||||
if(!url.contains("com.android.externalstorage.documents"))
|
||||
return null;
|
||||
val docFile = DocumentFile.fromSingleUri(StateApp.instance.context, url.toUri()) ?: return null;
|
||||
|
||||
val contentUri = docFile.uri.toString();
|
||||
|
||||
val mimeType = MimeTypeMap.getFileExtensionFromUrl(contentUri);
|
||||
|
||||
if(docFile.name != null) {
|
||||
if (StateApp.instance.hasMediaStoreAudioPermission && mimeType.startsWith("audio/")) {
|
||||
val aud = findAudioByName(docFile.name!!);
|
||||
if (aud != null)
|
||||
return aud;
|
||||
}
|
||||
if (StateApp.instance.hasMediaStoreVideoPermission && mimeType.startsWith("video/")) {
|
||||
val vid = findVideoByName(docFile.name!!);
|
||||
if (vid != null)
|
||||
return vid;
|
||||
}
|
||||
}
|
||||
|
||||
return LocalVideoDetails(
|
||||
PlatformID("FILE", contentUri, null, 0, -1),
|
||||
docFile.name ?: docFile.uri.toString(), Thumbnails(arrayOf(
|
||||
Thumbnail(docFile.uri.toString(), 0)
|
||||
)), PlatformAuthorLink.UNKNOWN, contentUri, 0, mimeType, null);
|
||||
}
|
||||
|
||||
fun getAudioTrack(url: String): IPlatformContentDetails? {
|
||||
val uri = Uri.parse(url);
|
||||
val id = uri.lastPathSegment?.toLongOrNull();
|
||||
if(id == null)
|
||||
return null;
|
||||
if(id == null) {
|
||||
return getDocumentTrack(url);
|
||||
}
|
||||
|
||||
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||
if(resolver == null) {
|
||||
@@ -152,11 +234,25 @@ class StateLibrary {
|
||||
return null;
|
||||
return audioFromCursor(cursor);
|
||||
}
|
||||
fun findAudioByName(name: String): IPlatformContentDetails? {
|
||||
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||
if(resolver == null) {
|
||||
Logger.w(TAG, "Audio contentResolver not found");
|
||||
return null;
|
||||
}
|
||||
val cursor = resolver?.query(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, StateLibrary.PROJECTION_MEDIA, "${MediaStore.Audio.Media.DISPLAY_NAME} = ?", arrayOf(name),
|
||||
null) ?: return null;
|
||||
cursor.moveToFirst();
|
||||
if(cursor.isAfterLast)
|
||||
return null;
|
||||
return audioFromCursor(cursor);
|
||||
}
|
||||
fun getVideoTrack(url: String): IPlatformContentDetails? {
|
||||
val uri = Uri.parse(url);
|
||||
val id = uri.lastPathSegment?.toLongOrNull();
|
||||
if(id == null)
|
||||
return null;
|
||||
return getDocumentTrack(url);
|
||||
|
||||
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||
if(resolver == null) {
|
||||
@@ -171,6 +267,20 @@ class StateLibrary {
|
||||
return null;
|
||||
return videoFromCursor(cursor);
|
||||
}
|
||||
fun findVideoByName(name: String): IPlatformContentDetails? {
|
||||
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||
if(resolver == null) {
|
||||
Logger.w(TAG, "Album contentResolver not found");
|
||||
return null;
|
||||
}
|
||||
val cursor = resolver?.query(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, StateLibrary.PROJECTION_VIDEO, "${MediaStore.Video.Media.DISPLAY_NAME} = ?", arrayOf(name),
|
||||
null) ?: return null;
|
||||
cursor.moveToFirst();
|
||||
if(cursor.isAfterLast)
|
||||
return null;
|
||||
return videoFromCursor(cursor);
|
||||
}
|
||||
|
||||
fun audioFromCursor(cursor: Cursor): IPlatformVideoDetails {
|
||||
val id = cursor.getString(0);
|
||||
@@ -456,4 +566,46 @@ class Album {
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FileEntry(
|
||||
val path: String,
|
||||
val name: String,
|
||||
val isDirectory: Boolean = false,
|
||||
val thumbnail: String? = null,
|
||||
|
||||
var removable: Boolean = false
|
||||
) {
|
||||
|
||||
fun getSubFiles(): List<FileEntry> {
|
||||
if(isDirectory) {
|
||||
if(path.startsWith("content://"))
|
||||
return DocumentFile.fromTreeUri(StateApp.instance.context, path.toUri())?.listFiles()
|
||||
?.map { fromFile(it) } ?: return listOf();
|
||||
return File(path).listFiles()
|
||||
.map { fromFile(it) }
|
||||
}
|
||||
return listOf();
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromPath(path: String): FileEntry {
|
||||
/*
|
||||
val cursor = StateApp.instance.context.contentResolver.query(path.toUri(), null, null, null, null);
|
||||
cursor?.moveToFirst();
|
||||
val fileName = cursor?.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
||||
cursor?.close();
|
||||
return FileEntry(path, fileName, );
|
||||
*/
|
||||
val file = File(path);
|
||||
return FileEntry(file.path, file.name, file.isDirectory);
|
||||
}
|
||||
fun fromFile(file: File): FileEntry {
|
||||
return FileEntry(file.path, file.name, file.isDirectory);
|
||||
}
|
||||
fun fromFile(file: DocumentFile): FileEntry {
|
||||
return FileEntry(file.uri.toString(), file.name ?: "", file.isDirectory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
app:buttonIcon="@drawable/ic_videocam"
|
||||
app:buttonText="Videos"
|
||||
app:buttonSubText="All artists known on this phone"
|
||||
app:buttonSubText="All local videos on this phone"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
@@ -79,7 +79,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="Library UI is temporary, and will be replaced"
|
||||
android:text=""
|
||||
android:textColor="#AAAAAA" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:id="@+id/root"
|
||||
android:clickable="true">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image_thumbnail"
|
||||
android:layout_height="50dp"
|
||||
android:layout_width="50dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:shapeAppearanceOverlay="@style/roundedCorners_4dp"
|
||||
app:srcCompat="@drawable/placeholder_video_thumbnail"
|
||||
android:background="@drawable/video_thumbnail_outline"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="13dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_light"
|
||||
tools:text="Legendary grant recipient: Marvin Wißfeld Very Long Title That is Long"
|
||||
android:maxLines="2"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/button_trash"
|
||||
app:layout_constraintBottom_toTopOf="@id/text_metadata"
|
||||
android:layout_marginStart="10dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_metadata"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="10dp"
|
||||
android:textColor="@color/gray_e0"
|
||||
android:fontFamily="@font/inter_extra_light"
|
||||
tools:text="3 videos"
|
||||
android:maxLines="1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_name"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintRight_toLeftOf="@id/button_delete"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginStart="10dp" />
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_delete"
|
||||
android:layout_width="34dp"
|
||||
android:layout_height="34dp"
|
||||
app:srcCompat="@drawable/ic_trash"
|
||||
android:scaleType="fitCenter"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginEnd="10dp"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
Reference in New Issue
Block a user