localsend二次开发自动监听文件夹实现自动发送 链接到标题

1. 项目需求 链接到标题

我想增加一个自动在局域网内互传文件的功能。核心功能是监听本地电脑中的某个文件夹,当文件夹中有新文件时自动广播发送给其他的客户端,所以需要进行以下逻辑的实现:

  • 在localsend界面中增加两个配置选项,一个是监听文件夹路径,另一个是启用监听文件夹
  • 在localsend软件中增加监听逻辑,当启用监听文件夹后,只要文件夹中有新增的文件就将他自动发送给局域网其他设备

2.项目架构基础与分层 链接到标题

2.1 什么是前端、后端? 链接到标题

  • 前端:用户直接看到和操作的界面,比如App的设置页面、按钮、输入框等。
  • 后端:在用户看不到的地方,负责处理数据、业务逻辑,比如监听文件夹、自动广播文件等。

2.2 项目常见的分层 链接到标题

以Flutter项目为例,通常分为以下几层:

层级 作用说明 你本次涉及的文件举例
UI层(视图) 展示界面,响应用户操作 settings_tab.dart
状态管理层 管理和同步App的各种设置、状态 settings_provider.dart
数据模型层 定义数据结构,描述App的各种状态和数据 settings_state.dart
持久化层 负责数据的本地存储和读取 persistence_provider.dart
业务逻辑层 处理核心功能,比如监听文件夹、广播文件等 auto_broadcast_folder_provider.dartsend_provider.dart
工具/服务层 封装底层功能,比如文件夹监听服务 directory_watcher_service.dart
启动入口层 应用启动和初始化 main.dart

2.3 流程图 链接到标题

flowchart TD
    A[用户在设置页面选择文件夹/开关监听] --> B[设置项保存到本地]
    B --> C[状态管理层监听到设置变化]
    C --> D[业务逻辑层决定是否启动监听服务]
    D --> E[工具层开始监听文件夹]
    E --> F[检测到新文件]
    F --> G[业务逻辑层自动广播文件到所有设备]
    G --> H[用户的其他设备收到文件]

2.4 文字版流程 链接到标题

  1. 用户在App设置页面选择监听文件夹路径,并打开“启用监听”开关;
  2. 这些设置通过状态管理层保存到本地;
  3. 业务逻辑层(Provider)监听到设置变化,决定是否启动监听服务;
  4. 工具层(服务类)开始监听指定文件夹;
  5. 一旦有新文件出现,业务逻辑层自动将文件广播到所有在线设备。

3.前端代码的实现 链接到标题

在标准的 Flutter 项目中。每加一个新设置项,都要让数据模型、持久化、Provider、UI四层都认识它,才能保证功能完整、数据一致、界面可用。

所以我们需要对settings_state.dartpersistence_provider.dartsettings_provider.dartsettings_tab.dart四个文件进行修改,以下是各个文件的职责与关系。

3.1 UI层:settings_tab.dart 链接到标题

  • 作用:在设置页面中,新增了“监听文件夹路径”和“启用监听文件夹”两个设置项,允许用户选择文件夹并启用/关闭自动广播。
  • 内容:展示所有设置项的控件(如开关、按钮、输入框),并通过 Provider 读写 SettingsState。
  • 为什么要改:你要让用户能在界面上看到和操作“监听文件夹路径”和“启用监听”这两个新设置项。
// 监听文件夹路径设置项
_SettingsEntry(
  label: '监听文件夹路径',
  child: TextButton(
    onPressed: () async {
      // 弹出文件夹选择对话框
      final directory = await pickDirectoryPath();
      if (directory != null) {
        // 保存用户选择的文件夹路径
        await ref.notifier(settingsProvider).setAutoBroadcastFolderPath(directory);
      }
    },
    child: Padding(
      child: Text(
        vm.settings.autoBroadcastFolderPath ?? '未设置', // 显示当前设置的路径
      ),
    ),
  ),
),
// 启用监听文件夹开关
_BooleanEntry(
  label: '启用监听文件夹',
  value: vm.settings.autoBroadcastEnabled,
  onChanged: (b) async {
    // 保存用户的开关选择
    await ref.notifier(settingsProvider).setAutoBroadcastEnabled(b);
  },
),

3.2 状态管理层:settings_provider.dart 链接到标题

  • 作用:负责管理和持久化设置项,包括监听文件夹的开关和路径。
    当设置项变化时,自动同步到本地存储,并通知相关Provider。
  • 内容:用 Notifier/Provider 管理 SettingsState,提供各种 setter 方法,调用 persistence_provider 进行持久化,并通过 copyWith 更新状态。
  • 为什么要改:你要让 UI 能通过 Provider 读写这两个新设置项,所以要加对应的 setter,并在初始化时从 persistence_provider 读取。
// 设置是否启用监听文件夹
Future<void> setAutoBroadcastEnabled(bool enabled) async {
  await _persistence.setAutoBroadcastEnabled(enabled); // 持久化到本地
  state = state.copyWith(autoBroadcastEnabled: enabled); // 更新内存状态
}

// 设置监听文件夹路径
Future<void> setAutoBroadcastFolderPath(String? path) async {
  await _persistence.setAutoBroadcastFolderPath(path); // 持久化到本地
  state = state.copyWith(autoBroadcastFolderPath: path); // 更新内存状态
}

3.3 数据模型层:settings_state.dart 链接到标题

  • 作用:定义了SettingsState,新增了两个字段用于保存监听文件夹的开关和路径。
  • 内容:包含所有设置项的字段(如主题、语言、端口、保存目录等),以及构造函数。
  • 为什么要改:你要新增“监听文件夹路径”和“启用监听”这两个设置项,必须在 SettingsState 里加字段,否则整个设置系统都不会有这两个数据。
final bool autoBroadcastEnabled; // 是否启用监听文件夹
final String? autoBroadcastFolderPath; // 监听的文件夹路径

3.4 持久化层:persistence_provider.dart 链接到标题

  • 作用:负责将设置项持久化到本地(如手机/电脑的本地文件),包括监听文件夹的开关和路径。
  • 内容:定义了各种 get/set 方法,把 SettingsState 的每个字段和本地存储的 key 关联起来。
  • 为什么要改:你新增了两个设置项,需要能保存和读取它们的值,所以要加对应的 get/set 方法和 key。
// 获取是否启用监听文件夹
bool getAutoBroadcastEnabled() {
  return _prefs.getBool(_autoBroadcastEnabledKey) ?? false;
}
// 设置是否启用监听文件夹
Future<void> setAutoBroadcastEnabled(bool enabled) async {
  await _prefs.setBool(_autoBroadcastEnabledKey, enabled);
}

// 获取监听文件夹路径
String? getAutoBroadcastFolderPath() {
  return _prefs.getString(_autoBroadcastFolderPathKey);
}
// 设置监听文件夹路径
Future<void> setAutoBroadcastFolderPath(String? path) async {
  if (path == null) {
    await _prefs.remove(_autoBroadcastFolderPathKey);
  } else {
    await _prefs.setString(_autoBroadcastFolderPathKey, path);
  }
}

异常报错处理:

代码添加后在persistence_provider.dart文件夹中存在报错提示。

解决办法:

​ 根本原因是 SettingsState 的 copyWith 方法没有包含 autoBroadcastEnabled 和 autoBroadcastFolderPath 这两个新加的字段。

​ 这是因为 SettingsState 使用了 dart_mappable 自动生成 copyWith,而新增字段后没有重新生成映射代码。

需要执行以下命令:

flutter pub run build_runner build --delete-conflicting-outputs

3.5 界面效果展示 链接到标题

添加完成后,运行项目可以看到已经增加了两个配置项

image-20250523173233897

4.广播发送代码实现 链接到标题

4.1 业务逻辑层:auto_broadcast_folder_provider.dart 链接到标题

作用:根据设置项决定是否监听文件夹,并在检测到新文件时广播。

  • updateFromSettings根据设置项决定是否监听文件夹。
  • onFileDetected回调会触发文件广播。
class AutoBroadcastFolderProvider {
  final DirectoryWatcherService _watcherService = DirectoryWatcherService(); // 文件夹监听服务
  void Function(String filePath)? onFileDetected; // 新文件回调

  // 根据设置项更新监听状态
  void updateFromSettings(bool enabled, String? folderPath) {
    _watcherService.stopWatching(); // 先停止之前的监听
    if (enabled && folderPath != null && onFileDetected != null) {
      // 启用监听
      _watcherService.startWatching(folderPath, (filePath) {
        onFileDetected!(filePath); // 检测到新文件,调用回调
      });
    }
  }
}

// Provider注册
final autoBroadcastFolderProvider = Provider<AutoBroadcastFolderProvider>((ref) {
  final provider = AutoBroadcastFolderProvider();
  provider.onFileDetected = (filePath) {
    // 检测到新文件,调用发送Provider广播文件
    ref.notifier(sendProvider).broadcastFileToAllDevices(filePath);
  };
  // 读取设置项,决定是否监听
  final settings = ref.read(settingsProvider);
  provider.updateFromSettings(settings.autoBroadcastEnabled, settings.autoBroadcastFolderPath);
  return provider;
});

4.2工具/服务层:directory_watcher_service.dart 链接到标题

作用:封装了对文件夹的监听逻辑,利用DirectoryWatcher库监听新文件。底层实现文件夹监听,发现新文件时通知上层。

class DirectoryWatcherService {
  DirectoryWatcher? _watcher;
  StreamSubscription<WatchEvent>? _subscription;
  String? _currentPath;

  // 开始监听文件夹
  void startWatching(String folderPath, OnFileDetected onFileDetected) {
    stopWatching(); // 先停止之前的监听
    _currentPath = folderPath;
    _watcher = DirectoryWatcher(folderPath); // 创建监听器
    _subscription = _watcher!.events.listen((event) {
      // 只处理新增文件事件
      if (event.type == ChangeType.ADD && File(event.path).existsSync()) {
        onFileDetected(event.path); // 回调通知新文件
      }
    });
  }

  // 停止监听
  void stopWatching() {
    _subscription?.cancel();
    _subscription = null;
    _watcher = null;
    _currentPath = null;
  }
}

4.3 业务逻辑层:send_provider.dart 链接到标题

作用:实现广播文件到所有设备的功能。

  • 遍历所有在线设备,依次发送新文件。
// 广播文件到所有在线设备
Future<void> broadcastFileToAllDevices(String filePath) async {
  final devices = ref.read(nearbyDevicesProvider).devices.values.toList(); // 获取所有在线设备
  if (devices.isEmpty) return; // 没有设备则返回
  final file = await CrossFile.fromPath(filePath); // 构造文件对象
  for (final device in devices) {
    // 依次发送文件到每个设备
    await startSession(
      target: device,
      files: [file],
      background: true, // 后台发送
    );
  }
}

4.4 启动入口层:main.dart 链接到标题

作用:应用启动时初始化Provider,确保监听服务随App启动。

Future<void> main(List<String> args) async {
  // ...初始化代码...
  runApp(RefenaScope.withContainer(
    container: container,
    child: TranslationProvider(
      child: const LocalSendApp(),
    ),
  ));
}

class LocalSendApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final ref = context.ref;
    // 初始化时加载自动广播Provider,确保监听服务启动
    ref.read(autoBroadcastFolderProvider);
    // ...其他UI代码...
  }
}

4.5 应用效果展示 链接到标题

image-20250527152705540

全文完,请大家专注我们的B站视频号和微信公众号,如果对编译过程有疑问或者想与大家一起讨论开源软件的定制化,请大家加入我们的QQ群畅所欲言。

B站二维码 微信公众号二维码 QQ群二维码