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

435 阅读5分钟

匠心千刃 是张风捷特烈通过 Flutter 打造的 全平台 工具产品。基于 fx 应用框架和 tolyui 视图框架构建的软件应用。
最近想要一个能够批量重命名 文件/文件夹 的工具,可以选择文件之后,一键将他们按照规则批量重命名。比如文章中的配图,通过 markdown 下链接载到本地,我想让它们根据 下载时间 来依次有序的重命名;比如 0102 等:

重命名前重命名后

1. 应用交互

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


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

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


2. 状态数据

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

状态量类型含义变化时机
_configRenameConfig配置参数输入/排序
_filesList<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类维护,包括名称、修改日期、文件大小。在类总可以提供 sizeStrmodifiedStr 方法获取格式化后的尺寸和时间:

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 站 。