• 作者:老汪软件技巧
  • 发表时间:2024-11-01 11:01
  • 浏览量:

_Flutter 匠心千刃 | 批量文件重命名_Flutter 匠心千刃 | 批量文件重命名

匠心千刃是张风捷特烈通过 Flutter 打造的全平台工具产品。基于 fx 应用框架和 tolyui 视图框架构建的软件应用。

最近想要一个能够批量重命名 文件/文件夹 的工具,可以选择文件之后,一键将他们按照规则批量重命名。比如文章中的配图,通过 markdown 下链接载到本地,我想让它们根据 下载时间 来依次有序的重命名;比如 01、02 等:

重命名前重命名后

_Flutter 匠心千刃 | 批量文件重命名_Flutter 匠心千刃 | 批量文件重命名

_Flutter 匠心千刃 | 批量文件重命名_Flutter 匠心千刃 | 批量文件重命名

1. 应用交互

匠心千刃中,在 文件处理/文件重命名 界面,可以选择或拖拽文件夹到指定区域。界面会展示文件夹中的所有文件,绿色的箭头后面展示文件的新名字。并且支持 选择排序方式,这样可以适应更多的使用场景。在交互时,都可以实时更新文件预览的界面:

除此之外,你还可以用为新名字设置 前缀 和 后缀,让命名行为更有多样性:

点击头部的运行按钮,就可以将当前选中的所有文件重新命名,效果如下搜索:

2. 状态数据

当前界面交互过程中会发生变化的有:

状态量类型含义变化时机

_config

RenameConfig

配置参数

输入/排序

_files

List

文件展示列表

选择/拖拽/输入/排序

其中三个配置参数由 RenameConfig 类维护,SortType 表示排序的类型,通过枚举维护:

Flutter 匠心千刃 | 批量文件重命名_Flutter 匠心千刃 | 批量文件重命名_

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 方法获取格式化后的尺寸和时间:

Flutter 匠心千刃 | 批量文件重命名__Flutter 匠心千刃 | 批量文件重命名

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. 界面布局

匠心千刃的每个工具的布局,将以这个结果为蓝本。竖向排布,从上到下依次是:

Flutter 匠心千刃 | 批量文件重命名__Flutter 匠心千刃 | 批量文件重命名

由于整个结构是匠心千刃通用的,可以封装一个应用层的组件 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,
  });

在界面元素上:

每个文件条目的展示,可以封装为 RenameFileTile 组件。在构造入参中传入必要的数据信息:

Flutter 匠心千刃 | 批量文件重命名__Flutter 匠心千刃 | 批量文件重命名

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. 行为事件

在 一文中介绍过桌面端选择文件、以及拖拽文件的处理方式,这里就不赘述了。在选择文件的事件回调中,触发如下的 _onFileSelect 方法,来解析收集 FileDisplay 列表数据

void _onFileSelect(String path) async {
  bool isDirectory = await FileSystemEntity.isDirectory(path);
  if (isDirectory) {
    Directory dir = Directory(path);
    List 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(() {});
}

尾声

这样,从视图、数据、行为三个方面,就实现了文件批量重命名的功能。未来匠心千刃也会作为一个工具软件,供用户随意使用。更多精彩内容,敬请期待 ~