mirror of
				https://github.com/johrpan/musicus_mobile.git
				synced 2025-10-26 18:57:25 +01:00 
			
		
		
		
	Add basic files selector
This required to add a platform channel with a method to get the toplevel storage devices. The app will need to request the WRITE_EXTERNAL_STORAGE permission later. Also the dependency on "path" was introduced.
This commit is contained in:
		
							parent
							
								
									02e283b8cc
								
							
						
					
					
						commit
						24a4911665
					
				
					 4 changed files with 213 additions and 2 deletions
				
			
		|  | @ -1,6 +1,8 @@ | ||||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     package="de.johrpan.musicus"> |     package="de.johrpan.musicus"> | ||||||
| 
 | 
 | ||||||
|  |     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||||
|  | 
 | ||||||
|     <application |     <application | ||||||
|         android:name="io.flutter.app.FlutterApplication" |         android:name="io.flutter.app.FlutterApplication" | ||||||
|         android:label="Musicus" |         android:label="Musicus" | ||||||
|  |  | ||||||
|  | @ -1,12 +1,39 @@ | ||||||
| package de.johrpan.musicus | package de.johrpan.musicus | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
|  | import androidx.core.content.ContextCompat | ||||||
| import io.flutter.embedding.android.FlutterActivity | import io.flutter.embedding.android.FlutterActivity | ||||||
| import io.flutter.embedding.engine.FlutterEngine | import io.flutter.embedding.engine.FlutterEngine | ||||||
|  | import io.flutter.plugin.common.MethodChannel | ||||||
| import io.flutter.plugins.GeneratedPluginRegistrant | import io.flutter.plugins.GeneratedPluginRegistrant | ||||||
| 
 | 
 | ||||||
| class MainActivity: FlutterActivity() { | class MainActivity : FlutterActivity() { | ||||||
|  |     private val CHANNEL = "de.johrpan.musicus/platform" | ||||||
|  | 
 | ||||||
|     override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { |     override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { | ||||||
|         GeneratedPluginRegistrant.registerWith(flutterEngine); |         GeneratedPluginRegistrant.registerWith(flutterEngine) | ||||||
|  | 
 | ||||||
|  |         MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> | ||||||
|  |             if (call.method == "getStorageRoots") { | ||||||
|  |                 result.success(getStorageRoots()) | ||||||
|  |             } else { | ||||||
|  |                 result.notImplemented() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun getStorageRoots(): ArrayList<String> { | ||||||
|  |         val result = ArrayList<String>() | ||||||
|  | 
 | ||||||
|  |         ContextCompat.getExternalFilesDirs(this, null).forEach { | ||||||
|  |             val path = it.absolutePath; | ||||||
|  |             val index = path.lastIndexOf("/Android/data/") | ||||||
|  | 
 | ||||||
|  |             if (index > 0) { | ||||||
|  |                 result.add(path.substring(0, index)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return result | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										181
									
								
								lib/selectors/files.dart
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								lib/selectors/files.dart
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,181 @@ | ||||||
|  | import 'dart:io'; | ||||||
|  | 
 | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter/services.dart'; | ||||||
|  | import 'package:path/path.dart' as path; | ||||||
|  | 
 | ||||||
|  | class FilesSelector extends StatefulWidget { | ||||||
|  |   @override | ||||||
|  |   _FilesSelectorState createState() => _FilesSelectorState(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class _FilesSelectorState extends State<FilesSelector> { | ||||||
|  |   static const platform = MethodChannel('de.johrpan.musicus/platform'); | ||||||
|  | 
 | ||||||
|  |   List<Directory> storageRoots = []; | ||||||
|  |   List<Directory> directories = []; | ||||||
|  |   List<FileSystemEntity> contents = []; | ||||||
|  |   Set<String> selectedPaths = {}; | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  | 
 | ||||||
|  |     platform.invokeListMethod<String>('getStorageRoots').then((sr) { | ||||||
|  |       setState(() { | ||||||
|  |         storageRoots = sr.map((path) => Directory(path)).toList(); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     Widget body; | ||||||
|  | 
 | ||||||
|  |     if (directories.isEmpty) { | ||||||
|  |       if (storageRoots != null) { | ||||||
|  |         body = ListView( | ||||||
|  |           children: storageRoots | ||||||
|  |               .map((dir) => ListTile( | ||||||
|  |                     leading: const Icon(Icons.storage), | ||||||
|  |                     title: Text(dir.path), | ||||||
|  |                     onTap: () { | ||||||
|  |                       setState(() { | ||||||
|  |                         directories.add(dir); | ||||||
|  |                       }); | ||||||
|  | 
 | ||||||
|  |                       openDirectory(dir); | ||||||
|  |                     }, | ||||||
|  |                   )) | ||||||
|  |               .toList(), | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         body = Container(); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       if (contents != null) { | ||||||
|  |         body = ListView( | ||||||
|  |           children: contents.map((fse) { | ||||||
|  |             Widget result; | ||||||
|  | 
 | ||||||
|  |             if (fse is Directory) { | ||||||
|  |               result = ListTile( | ||||||
|  |                 leading: const Icon(Icons.folder), | ||||||
|  |                 title: Text(path.basename(fse.path)), | ||||||
|  |                 onTap: () { | ||||||
|  |                   setState(() { | ||||||
|  |                     directories.add(fse); | ||||||
|  |                   }); | ||||||
|  | 
 | ||||||
|  |                   openDirectory(fse); | ||||||
|  |                 }, | ||||||
|  |               ); | ||||||
|  |             } else if (fse is File) { | ||||||
|  |               result = CheckboxListTile( | ||||||
|  |                 value: selectedPaths.contains(fse.path), | ||||||
|  |                 secondary: Icon(Icons.insert_drive_file), | ||||||
|  |                 title: Text(path.basename(fse.path)), | ||||||
|  |                 onChanged: (selected) { | ||||||
|  |                   setState(() { | ||||||
|  |                     if (selected) { | ||||||
|  |                       selectedPaths.add(fse.path); | ||||||
|  |                     } else { | ||||||
|  |                       selectedPaths.remove(fse.path); | ||||||
|  |                     } | ||||||
|  |                   }); | ||||||
|  |                 }, | ||||||
|  |               ); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return result; | ||||||
|  |           }).toList(), | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         body = Container(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return WillPopScope( | ||||||
|  |       child: Scaffold( | ||||||
|  |         appBar: AppBar( | ||||||
|  |           title: Text('Choose files'), | ||||||
|  |           actions: <Widget>[ | ||||||
|  |             FlatButton( | ||||||
|  |               child: Text('DONE'), | ||||||
|  |               onPressed: () { | ||||||
|  |                 Navigator.pop(context, selectedPaths); | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |         body: Column( | ||||||
|  |           children: <Widget>[ | ||||||
|  |             Material( | ||||||
|  |               elevation: 2.0, | ||||||
|  |               child: ListTile( | ||||||
|  |                 leading: directories.isNotEmpty ? IconButton( | ||||||
|  |                   icon: const Icon(Icons.arrow_upward), | ||||||
|  |                   onPressed: up, | ||||||
|  |                 ) : null, | ||||||
|  |                 title: Text(directories.isEmpty | ||||||
|  |                     ? 'Storage devices' | ||||||
|  |                     : directories.last.path), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |             Expanded( | ||||||
|  |               child: body, | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       onWillPop: () { | ||||||
|  |         if (directories.isNotEmpty) { | ||||||
|  |           up(); | ||||||
|  |           return Future.value(false); | ||||||
|  |         } else { | ||||||
|  |           return Future.value(true); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> openDirectory(Directory directory) async { | ||||||
|  |     setState(() { | ||||||
|  |       contents.clear(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     final fses = await directory.list().toList(); | ||||||
|  |     fses.sort((fse1, fse2) { | ||||||
|  |       int compareBasenames() => | ||||||
|  |           path.basename(fse1.path).compareTo(path.basename(fse2.path)); | ||||||
|  | 
 | ||||||
|  |       if (fse1 is Directory) { | ||||||
|  |         if (fse2 is Directory) { | ||||||
|  |           return compareBasenames(); | ||||||
|  |         } else { | ||||||
|  |           return -1; | ||||||
|  |         } | ||||||
|  |       } else if (fse2 is Directory) { | ||||||
|  |         return 1; | ||||||
|  |       } else { | ||||||
|  |         return compareBasenames(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     setState(() { | ||||||
|  |       contents = fses; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   void up() { | ||||||
|  |     if (directories.isNotEmpty) { | ||||||
|  |       setState(() { | ||||||
|  |         directories.removeLast(); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       if (directories.isNotEmpty) { | ||||||
|  |         openDirectory(directories.last); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -12,6 +12,7 @@ dependencies: | ||||||
|   flutter: |   flutter: | ||||||
|     sdk: flutter |     sdk: flutter | ||||||
|   moor_flutter: |   moor_flutter: | ||||||
|  |   path: | ||||||
|   rxdart: |   rxdart: | ||||||
| 
 | 
 | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Elias Projahn
						Elias Projahn