匠心千刃 是张风捷特烈通过 Flutter 打造的 全平台 工具产品。基于 fx 应用框架和 tolyui 视图框架构建的软件应用。
最近想要一个能够批量重命名 文件/文件夹 的工具,可以选择文件之后,一键将他们按照规则批量重命名。比如文章中的配图,通过 markdown 下链接载到本地,我想让它们根据 下载时间
来依次有序的重命名;比如 01
、02
等:
重命名前 | 重命名后 |
---|---|
1. 应用交互
匠心千刃中,在 文件处理/文件重命名
界面,可以选择或拖拽文件夹到指定区域。界面会展示文件夹中的所有文件,绿色的箭头后面展示文件的新名字。并且支持 选择排序方式
,这样可以适应更多的使用场景。在交互时,都可以实时更新文件预览的界面:
除此之外,你还可以用为新名字设置 前缀
和 后缀
,让命名行为更有多样性:
点击头部的运行按钮,就可以将当前选中的所有文件重新命名,效果如下搜索:
2. 状态数据
当前界面交互过程中会发生变化的有:
状态量 | 类型 | 含义 | 变化时机 |
---|---|---|---|
_config | RenameConfig | 配置参数 | 输入/排序 |
_files | List<FileDisplay> | 文件展示列表 | 选择/拖拽/输入/排序 |
其中三个配置参数由 RenameConfig
类维护,SortType
表示排序的类型,通过枚举维护:
import 'package:path/path.dart' as p;
enum SortType {
modified,
name,
size,
}
class RenameConfig {
final String prefix;
final String stuff;
final SortType sortType;
const RenameConfig({
this.prefix = '',
this.stuff = '',
this.sortType = SortType.modified,
});
String calcName(int index, String path) {
return '$prefix${(index + 1).toString().padLeft(2, '0')}$stuff${p.extension(path)}';
}
RenameConfig copyWith({
String? prefix,
String? stuff,
SortType? sortType,
}) {
return RenameConfig(
prefix: prefix ?? this.prefix,
stuff: stuff ?? this.stuff,
sortType: sortType ?? this.sortType,
);
}
}
文件信息展示条目中的数据,通过 FileDisplay
l类维护,包括名称、修改日期、文件大小。在类总可以提供 sizeStr
和 modifiedStr
方法获取格式化后的尺寸和时间:
import 'package:path/path.dart' as p;
import 'package:intl/intl.dart';
DateFormat _format = DateFormat('yyyy/MM/dd HH:mm:ss');
class FileDisplay {
String get name => p.basename(path);
final int size;
final DateTime modified;
final String path;
FileDisplay({
required this.size,
required this.modified,
required this.path,
});
String get sizeStr {
if (size < 1024) {
return "${size}B";
} else if (size < 1024 * 1024) {
return "${(size / 1024).toStringAsFixed(2)}KB";
} else if (size < 1024 * 1024 * 1024) {
return "${(size / 1024 / 1024).toStringAsFixed(2)}MB";
} else {
return "${(size / 1024 / 1024 / 1024).toStringAsFixed(2)}GB";
}
}
String get modifiedStr => _format.format(modified);
}
3. 界面布局
匠心千刃的每个工具的布局,将以这个结果为蓝本。竖向排布,从上到下依次是:
- 工具的名称:展示工具名字。
- 工具的操作区:负责表单交互,和执行功能。以及一些通用的操作按钮。
- 展示区:负责展示工具交互过程中的界面。
- 信息区:介绍根据的基本使用,以及一些操作提示信息、耗时展示、异常展示等。
由于整个结构是匠心千刃通用的,可以封装一个应用层的组件 BladeToolScaffold
,负责整体界面结构的搭建。这样所有的工具界面复用一个结构,有利于后期的维护,以及统一修改样式。应用层的组件,一开始可以不用封装的太细致,随着功能需求的增加,可以逐步迭代,目前处理一下区域划分:
class BladeToolScaffold extends StatelessWidget {
final String title;
final Widget header;
final Widget? bottom;
final Widget body;
final Widget footer;
const BladeToolScaffold({
super.key,
required this.title,
required this.header,
required this.body,
this.bottom,
required this.footer,
});
在界面元素上:
- 下拉选择菜单使用了 tolyui 中的 TolyDropMenu 组件。
- 输入框使用 tolyui 中的 TolyInput 组件。
每个文件条目的展示,可以封装为 RenameFileTile
组件。在构造入参中传入必要的数据信息:
class RenameFileTile extends StatelessWidget {
final FileDisplay file;
final int index;
final RenameConfig config;
const RenameFileTile({
super.key,
required this.file,
required this.index,
required this.config,
});
@override
Widget build(BuildContext context) {
const TextStyle greyStyle = TextStyle(fontSize: 12, color: Colors.grey);
const TextStyle newNameStyle = TextStyle(color: Colors.orange);
String newName = config.calcName(index, file.path);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
const Icon(CupertinoIcons.doc_checkmark_fill, color: Colors.grey, size: 20),
const SizedBox(width: 8),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: greyStyle),
Row(
children: [
Text(file.modifiedStr, style: greyStyle),
const Spacer(),
Text(file.sizeStr, style: greyStyle),
],
),
Row(
children: [
const Icon(Icons.arrow_forward, color: Colors.green, size: 12),
const SizedBox(width: 4),
Text(newName, style: newNameStyle),
],
),
],
),
),
],
),
);
}
}
桌面端屏幕较宽,可以使用 GridView
组件构建网格列表:
return GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 400,
childAspectRatio: 4
),
itemCount: _files.length,
itemBuilder: (_, index) {
return RenameFileTile(
file: _files[index],
index: index,
config: _config,
);
},
);
4. 行为事件
在 《Flutter&Rust#06 - 图片二维码识别》 一文中介绍过桌面端选择文件、以及拖拽文件的处理方式,这里就不赘述了。在选择文件的事件回调中,触发如下的 _onFileSelect
方法,来解析收集 FileDisplay
列表数据
void _onFileSelect(String path) async {
bool isDirectory = await FileSystemEntity.isDirectory(path);
if (isDirectory) {
Directory dir = Directory(path);
List<FileSystemEntity> entities = dir.listSync();
for (FileSystemEntity entity in entities) {
if (entity is File) {
FileStat stat = await entity.stat();
_files.add(FileDisplay(size: stat.size, modified: stat.modified, path: entity.path));
sortFiles();
}
}
setState(() {});
}
}
sortFiles
方法根据 _config.sortType
的类型,决定排序的方式:
void sortFiles() {
_files.sort((a, b) {
return switch (_config.sortType) {
SortType.modified => a.modified.compareTo(b.modified),
SortType.name => a.name.compareTo(b.name),
SortType.size => a.size.compareTo(b.size),
};
});
}
在修改 _config
的三处回调中,只需要通过 RenameConfig#copyWith
更新配置对象,触发更新即可。
/// 输入后缀事件
void _onStuffChange(String value) {
setState(() {
_config = _config.copyWith(stuff: value);
});
}
/// 输入前缀事件
void _onPrefixChange(String value) {
setState(() {
_config = _config.copyWith(prefix: value);
});
}
/// 切换排序方式事件
void _onSelectSort(int index) {
_config = _config.copyWith(sortType: SortType.values[index]);
sortFiles();
setState(() {});
}
尾声
这样,从视图、数据、行为三个方面,就实现了文件批量重命名的功能。未来匠心千刃也会作为一个工具软件,供用户随意使用。更多精彩内容,敬请期待 ~
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。