1:基本页面展示
项目地址:https://github.com/NeverOvO/never_filling
2:功能介绍
此程序的主要目的是用来归档文件夹中的不同类型的文件,通过命令行来转移归档文件
(1)源文件与目标文件夹选择,支持拖动文件夹来选择
(2)点击 “检索文件地址目录” 即可展示出该文件夹下的文件,包含子文件夹的子文件,无论几层都可以解析出来。
(3)可以选择重名文件是否保留,如果选择保留的话,会在全部文件后面添加需要,例如 Text.txt ==> Text_1.txt
(4)点击 “操作已选择文件” 就会生成命令行语句并直接复制到剪切板。例如:
mv '/Users/XXX/本地文稿/never_filling/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png' '/Users/XXX/Downloads/未命名文件夹/app_icon_16.png' ;
(5)用户确认起始位置无误后,进入终端复制操作即可。
3:部分代码解析记录
3.1 新版本功能
(1)在2023年2月14日优化了重复文件名的体验,目前不在需要选择是否保留重名文件,而是全部进行保留,并在全部重名文件后添加时间戳来进行区别。
//重名文件将会直接保留,在后续会添加时间戳保证非重复。
command = "mv '${totalEnd.first[2]}/${totalEnd.first[0]}' '$toAdd/${totalEnd.first[0]}'";
endFileName.add(totalEnd.first[0]);
for(int i = 1;i<totalEnd.length ; i++){
if(endFileName.contains(totalEnd[i][0])){
command += " ; mv '${totalEnd[i][2]}/${totalEnd[i][0]}' '$toAdd/${totalEnd[i][0].toString().replaceAll(".", "_${DateTime.now().microsecondsSinceEpoch.toString()}.")}'";
}else{
command += " ; mv '${totalEnd[i][2]}/${totalEnd[i][0]}' '$toAdd/${totalEnd[i][0]}'";
endFileName.add(totalEnd[i][0]);
}
}
(2)新增了黑名单功能,采用sqflite_common_ffi: ^2.2.1+1 实现,能拥有更好的扩展性与持续性。使用鼠标右键点击检索后的文件列表确认即可加入黑名单,后续的检索中黑名单文件将不会出现在检索列表内,避免操作与重复劳动。核心代码:
//打开数据库代码
db = await databaseFactory.openDatabase("blackList.db");
//检查表是否存在
var sql ="SELECT * FROM sqlite_master WHERE TYPE = 'table' AND NAME = 'Black'";
var res = await db.rawQuery(sql);
var returnRes = res!=null && res.length > 0;
if(!returnRes){
await db.execute(
'''
CREATE TABLE Black (
id INTEGER PRIMARY KEY,
title TEXT,
time TEXT,
other TEXT
)
'''
);
}
//向数据库中插入数据
await db.insert('Black', {'title': fileLookupList![index][0] , 'time': DateTime.now().toString().split(".").first,'other' : ""});
3.2 旧版本功能代码
(0)该项目的全部依赖第三方库:
cupertino_icons: ^1.0.5
path_provider: ^2.0.11
file_picker: ^5.2.4
mime: ^1.0.3 #判断文件类型
desktop_drop: ^0.4.0 #文件拖动
(1)对于文件夹的判断
此处用到的库为:mime: ^1.0.3
lookupMimeType(fileSystemEntity.path) != null
即在看文件类型时,如果lookupMimeType值为null,那么可以判断为这个文件为文件夹,这个判断语句基本可以判断出大部分常用文件类型,完整的 “检索源文件夹目录” 核心代码:
await for(FileSystemEntity fileSystemEntity in fileList!){
if(lookupMimeType(fileSystemEntity.path) != null){
if(!fileSystemEntity.path.toString().split("/").last.startsWith(".")){
fileLookupType![lookupMimeType(fileSystemEntity.path)] = "true";
fileLookupList!.add([fileSystemEntity.path.toString().split("/").last,lookupMimeType(fileSystemEntity.path),fileSystemEntity.parent.path]);
total = fileLookupList!.length;
}
}
}
这里我选择将文件的路径与文件的实际名字分开保存,打算为后期的根本文件夹选择功能进行保留。
#目前已知问题,后期修复:
如果文件夹名字和文件名字相同,那么转移的时候会直接转移整个文件夹,内部子文件不受影响。
(2)文件夹选择与拖动选择
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(dialogTitle:"选择源文件夹",lockParentWindow: true);
file_picker: ^5.2.4
这是采用file_picker来打开一个文件选择窗口,是较为传统与常用的方式。
下面是拖动文件夹到APP内,来快速选择文件夹的方法:
desktop_drop: ^0.4.0
DropTarget(
onDragDone: (detail) {
setState(() {
if(lookupMimeType(detail.files.first.path) != null){
setState(() {
_errorFrom = true;
});
Future.delayed(const Duration(seconds: 2)).then((onValue) async{
setState(() {
_errorFrom = false;
});
});
return;
}else{
fromDirectoryPathController.text = detail.files.first.path;
setState(() {
_errorFrom = false;
});
}
});
},
onDragEntered: (detail) {
setState(() {
_dragging = true;
});
},
onDragExited: (detail) {
setState(() {
_dragging = false;
});
},
核心点就是在onDragDone方法中展示。
完整部分:
DropTarget(
onDragDone: (detail) {
setState(() {
if(lookupMimeType(detail.files.first.path) != null){
setState(() {
_errorFrom = true;
});
Future.delayed(const Duration(seconds: 2)).then((onValue) async{
setState(() {
_errorFrom = false;
});
});
return;
}else{
fromDirectoryPathController.text = detail.files.first.path;
setState(() {
_errorFrom = false;
});
}
});
},
onDragEntered: (detail) {
setState(() {
_dragging = true;
});
},
onDragExited: (detail) {
setState(() {
_dragging = false;
});
},
child: Container(
color: _dragging ? Colors.blue.withOpacity(0.4) : Colors.transparent,
child:Container(
padding: const EdgeInsets.fromLTRB(10, 20, 10, 20),
child: Row(
children: [
Expanded(
flex: 2,
child: TextField(
enabled: false,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(10, 0, 10, 0),
enabledBorder: UnderlineInputBorder(),
labelStyle: TextStyle(color: Colors.grey),
labelText: '请选择源文件夹地址或拖动到此处',
),
controller: fromDirectoryPathController,
autocorrect:false,
style: const TextStyle(color: Colors.black,fontSize: 11),
),
),
GestureDetector(
onTap: () async{
if(_filePicktrue){
_filePicktrue = false;
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(dialogTitle:"选择源文件夹",lockParentWindow: true);
if(selectedDirectory != null){
command = "";
total = 0;
fileLookupList!.clear();
fileLookupType!.clear();
fromDirectoryPathController.text = selectedDirectory;
setState(() {});
}
_filePicktrue = true;
}
},
behavior: HitTestBehavior.opaque,
child: Container(
decoration: const BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.all(Radius.circular(6.0)),
),
margin: const EdgeInsets.fromLTRB(10, 0, 10, 0),
alignment: Alignment.centerLeft,
padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
child: const Text("选择",style: TextStyle(color: Colors.white),),
),
),
],
),
),
),
),
(3)在终端内执行的文件移动语句生成:
这里我只写了macOS下的方法,理论上win和macOS对于命令行语句在写法上应该是不同的,所以该APP目前只能应用在macOS中,win后续等有空了再写。
List? totalEnd = [];
fileLookupType!.forEach((key, value) {
if(value == "true"){
totalEnd.addAll(fileLookupList!.where((element) => element[1] == key));
}
});
command = "";
if(totalEnd.isNotEmpty){
if(totalEnd.length == 1){
command = "mv '${totalEnd.first[2]}/${totalEnd.first[0]}' '${toDirectoryPathController.text}/${totalEnd.first[0]}'";
}else{
if(repeat == 1){
command = "mv '${totalEnd.first[2]}/${totalEnd.first[0]}' '${toDirectoryPathController.text}/${totalEnd.first[0].toString().replaceAll(".", "_0.")}'";
for(int i = 1;i<totalEnd.length ; i++){
command += " ; mv '${totalEnd[i][2]}/${totalEnd[i][0]}' '${toDirectoryPathController.text}/${totalEnd[i][0].toString().replaceAll(".", "_${i.toString()}.")}'";
}
}else{
command = "mv '${totalEnd.first[2]}/${totalEnd.first[0]}' '${toDirectoryPathController.text}/${totalEnd.first[0]}'";
for(int i = 1;i<totalEnd.length ; i++){
command += " ; mv '${totalEnd[i][2]}/${totalEnd[i][0]}' '${toDirectoryPathController.text}/${totalEnd[i][0]}'";
}
}
}
}
解释:先将需要移动的所有文件汇总,排除非选中项目,判断list是否大于1,来区别1项文件与多项文件时语句的不同拼接方法。
若用户选择保留重名文件的话,会在文件名称后面添加当前的编号.replaceAll(".", "_${i.toString()}.")。