From 9228bbe0c2cf128faad67c64458a7e09168e49fd Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Sat, 7 Jun 2025 16:15:11 +0800 Subject: [PATCH] feat: supports FTP storage --- README.md | 102 ++++++------ README_CN.md | 106 ++++++------ lib/l10n/app_en.arb | 2 + lib/l10n/app_zh.arb | 2 + lib/main.dart | 7 + lib/models/storages/ftp.dart | 90 +++++++++++ lib/models/storages/storage.dart | 17 ++ lib/pages/storages/favorites.dart | 5 + lib/pages/storages/storages.dart | 12 +- lib/pages/storages/storages_list.dart | 30 +++- lib/utils/check_data_source_type.dart | 1 + lib/widgets/dialogs/show_ftp_dialog.dart | 198 +++++++++++++++++++++++ pubspec.lock | 81 ++++++++++ pubspec.yaml | 5 + 14 files changed, 550 insertions(+), 108 deletions(-) create mode 100644 lib/models/storages/ftp.dart create mode 100644 lib/widgets/dialogs/show_ftp_dialog.dart diff --git a/README.md b/README.md index 86bf8dd..3dd224d 100644 --- a/README.md +++ b/README.md @@ -3,76 +3,80 @@ # IRIS - A lightweight video player ![ci](https://github.com/nini22P/iris/actions/workflows/ci.yml/badge.svg) -Afdian +`Afdian``` [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/nini22p) English | [中文](./README_CN.md) ## Features -- [x] Base on [Media Kit](https://github.com/media-kit/media-kit) | [FVP](https://github.com/wang-bin/fvp), supports multiple video formats -- [x] Local storage and WebDAV support -- [x] Switchable subtitle and audio track -- [x] Playback queue support for random and repeat -- [x] Comprehensive gesture support +- [X] Base on [Media Kit](https://github.com/media-kit/media-kit) | [FVP](https://github.com/wang-bin/fvp), supports multiple video formats +- [X] Local storage, WebDAV and FTP support +- [X] Switchable subtitle and audio track +- [X] Playback queue support for random and repeat +- [X] Comprehensive gesture support ## Download ### Windows + - [IRIS-windows-installer.exe](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows-installer.exe) - [IRIS-windows.zip](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows.zip) ### Android -| Architecture | Download Link | -|-------------------|-------------------------------------------------------------------------| -| arm64-v8a | [IRIS-android-arm64-v8a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-arm64-v8a.apk) | -| armeabi-v7a | [IRIS-android-armeabi-v7a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-armeabi-v7a.apk) | -| x86_64 | [IRIS-android-x86_64.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-x86_64.apk) | + +| Architecture | Download Link | +| ------------ | ------------------------------------------------------------------------------------------------------------------ | +| arm64-v8a | [IRIS-android-arm64-v8a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-arm64-v8a.apk) | +| armeabi-v7a | [IRIS-android-armeabi-v7a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-armeabi-v7a.apk) | +| x86_64 | [IRIS-android-x86_64.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-x86_64.apk) | ## Keyboard and Gesture Controls ### Keyboard Controls -| Key | Description | -|----------------------|------------------------------------------| -| `Space` | Play / Pause / Select file | -| `Arrow Left` | Fast backward 10 seconds | -| `Arrow Right` | Fast forward 10 seconds | -| `Arrow Up` | Volume up | -| `Arrow Down` | Volume down | -| `Ctrl + Arrow Left` | Previous | -| `Ctrl + Arrow Right` | Next | -| `Ctrl + X` | Shuffle | -| `Ctrl + R` | Repeat | -| `Ctrl + V` | Video zoom | -| `Ctrl + M` | Volume mute | -| `S` | Subtitles and audio tracks | -| `P` | Play queue | -| `F` | Storages | -| `Ctrl + O` | Open file | -| `Ctrl + L` | Open link | -| `Ctrl + C` | Close currently media file | -| `Ctrl + H` | Play history | -| `Ctrl + P` | Settings | -| `+` | Step forward | -| `-` | Step backward | + +| Key | Description | +| ---------------------- | -------------------------------------------------- | +| `Space` | Play / Pause / Select file | +| `Arrow Left` | Fast backward 10 seconds | +| `Arrow Right` | Fast forward 10 seconds | +| `Arrow Up` | Volume up | +| `Arrow Down` | Volume down | +| `Ctrl + Arrow Left` | Previous | +| `Ctrl + Arrow Right` | Next | +| `Ctrl + X` | Shuffle | +| `Ctrl + R` | Repeat | +| `Ctrl + V` | Video zoom | +| `Ctrl + M` | Volume mute | +| `S` | Subtitles and audio tracks | +| `P` | Play queue | +| `F` | Storages | +| `Ctrl + O` | Open file | +| `Ctrl + L` | Open link | +| `Ctrl + C` | Close currently media file | +| `Ctrl + H` | Play history | +| `Ctrl + P` | Settings | +| `+` | Step forward | +| `-` | Step backward | | `Enter` | Enter full screen / Exit full screen / Select file | -| `F11` | Enter full screen / Exit full screen | -| `Esc` | Exit current Menu / Go back / Exit full screen | -| `F10` | Toggle always on top | -| `Alt + X` | Exit application | +| `F11` | Enter full screen / Exit full screen | +| `Esc` | Exit current Menu / Go back / Exit full screen | +| `F10` | Toggle always on top | +| `Alt + X` | Exit application | ### Gesture Controls -| Gesture | Description | -|---------------------------------|------------------------------------------| -| Tap | Select an item or open a menu | -| Double tap center | Play / Pause | -| Double tap left side | Fast backward 10 seconds | -| Double tap right side | Fast forward 10 seconds | -| Swipe left / right | Adjust playback progress | -| Swipe up / down on left side | Adjust screen brightness | -| Swipe up / down on right side | Adjust device volume | -| Long press | Start speed playback | -| Long press and swipe left / right | Adjust speed playback speed | + +| Gesture | Description | +| --------------------------------- | ----------------------------- | +| Tap | Select an item or open a menu | +| Double tap center | Play / Pause | +| Double tap left side | Fast backward 10 seconds | +| Double tap right side | Fast forward 10 seconds | +| Swipe left / right | Adjust playback progress | +| Swipe up / down on left side | Adjust screen brightness | +| Swipe up / down on right side | Adjust device volume | +| Long press | Start speed playback | +| Long press and swipe left / right | Adjust speed playback speed | ## Contribution diff --git a/README_CN.md b/README_CN.md index e72db70..68fb3ab 100644 --- a/README_CN.md +++ b/README_CN.md @@ -3,76 +3,80 @@ # IRIS - 轻量级视频播放器 ![ci](https://github.com/nini22P/iris/actions/workflows/ci.yml/badge.svg) -爱发电 +`爱发电``` [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/nini22p) [English](./README.md) | 中文 ## 特性 -- [x] 基于 [Media Kit](https://github.com/media-kit/media-kit) | [FVP](https://github.com/wang-bin/fvp),可播放多种视频格式 -- [x] 支持本地存储、WebDAV -- [x] 可切换字幕和音轨 -- [x] 播放队列支持随机和重复 -- [x] 完善的手势支持 +- [X] 基于 [Media Kit](https://github.com/media-kit/media-kit) | [FVP](https://github.com/wang-bin/fvp),可播放多种视频格式 +- [X] 支持本地存储、WebDAV 和 FTP +- [X] 可切换字幕和音轨 +- [X] 播放队列支持随机和重复 +- [X] 完善的手势支持 ## 下载 ### Windows + - [IRIS-windows-installer.exe](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows-installer.exe) - [IRIS-windows.zip](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows.zip) ### Android -| 设备架构 | 下载链接 | -|------------------|--------------------------------------------------------------------------| -| arm64-v8a | [IRIS-android-arm64-v8a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-arm64-v8a.apk) | -| armeabi-v7a | [IRIS-android-armeabi-v7a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-armeabi-v7a.apk) | -| x86_64 | [IRIS-android-x86_64.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-x86_64.apk) | + +| 设备架构 | 下载链接 | +| ----------- | ------------------------------------------------------------------------------------------------------------------ | +| arm64-v8a | [IRIS-android-arm64-v8a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-arm64-v8a.apk) | +| armeabi-v7a | [IRIS-android-armeabi-v7a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-armeabi-v7a.apk) | +| x86_64 | [IRIS-android-x86_64.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-x86_64.apk) | ## 键位和手势 ### 键位操作 -| 键位 | 描述 | -|----------------------|----------------------------------------| -| `Space` | 播放 / 暂停 / 选择文件 | -| `Arrow Left` | 快退 10 秒 | -| `Arrow Right` | 快进 10 秒 | -| `Arrow Up` | 提升音量 | -| `Arrow Down` | 降低音量 | -| `Ctrl + Arrow Left` | 上一个 | -| `Ctrl + Arrow Right` | 下一个 | -| `Ctrl + X` | 随机 | -| `Ctrl + R` | 重复 | -| `Ctrl + V` | 视频缩放 | -| `Ctrl + M` | 静音 | -| `S` | 字幕和音轨 | -| `P` | 播放队列 | -| `F` | 存储 | -| `Ctrl + O` | 打开文件 | -| `Ctrl + L` | 打开链接 | -| `Ctrl + C` | 关闭当前媒体文件 | -| `Ctrl + H` | 播放历史 | -| `Ctrl + P` | 设置 | -| `+` | 帧进 | -| `-` | 帧退 | -| `Enter` | 进入全屏 / 退出全屏 / 选择文件 | -| `F11` | 进入全屏 / 退出全屏 | -| `Esc` | 退出当前菜单 / 返回上一级 / 关闭全屏 | -| `F10` | 切换窗口置顶 | -| `Alt + X` | 退出应用 | + +| 键位 | 描述 | +| ---------------------- | ------------------------------------ | +| `Space` | 播放 / 暂停 / 选择文件 | +| `Arrow Left` | 快退 10 秒 | +| `Arrow Right` | 快进 10 秒 | +| `Arrow Up` | 提升音量 | +| `Arrow Down` | 降低音量 | +| `Ctrl + Arrow Left` | 上一个 | +| `Ctrl + Arrow Right` | 下一个 | +| `Ctrl + X` | 随机 | +| `Ctrl + R` | 重复 | +| `Ctrl + V` | 视频缩放 | +| `Ctrl + M` | 静音 | +| `S` | 字幕和音轨 | +| `P` | 播放队列 | +| `F` | 存储 | +| `Ctrl + O` | 打开文件 | +| `Ctrl + L` | 打开链接 | +| `Ctrl + C` | 关闭当前媒体文件 | +| `Ctrl + H` | 播放历史 | +| `Ctrl + P` | 设置 | +| `+` | 帧进 | +| `-` | 帧退 | +| `Enter` | 进入全屏 / 退出全屏 / 选择文件 | +| `F11` | 进入全屏 / 退出全屏 | +| `Esc` | 退出当前菜单 / 返回上一级 / 关闭全屏 | +| `F10` | 切换窗口置顶 | +| `Alt + X` | 退出应用 | ### 手势操作 -| 手势 | 描述 | -|--------------------|----------------------------------------| -| 单击 | 选择项目或打开菜单 | -| 双击屏幕中心 | 播放 / 暂停 | -| 双击屏幕左侧 | 快退 10 秒 | -| 双击屏幕右侧 | 快进 10 秒 | -| 左右滑动 | 调整播放进度 | -| 屏幕左侧上下滑动 | 调整屏幕亮度 | -| 屏幕右侧上下滑动 | 调整设备音量 | -| 长按 | 启动倍速播放 | -| 长按后左右滑动 | 调整倍速播放的速度 | + +| 手势 | 描述 | +| ---------------- | ------------------ | +| 单击 | 选择项目或打开菜单 | +| 双击屏幕中心 | 播放 / 暂停 | +| 双击屏幕左侧 | 快退 10 秒 | +| 双击屏幕右侧 | 快进 10 秒 | +| 左右滑动 | 调整播放进度 | +| 屏幕左侧上下滑动 | 调整屏幕亮度 | +| 屏幕右侧上下滑动 | 调整设备音量 | +| 长按 | 启动倍速播放 | +| 长按后左右滑动 | 调整倍速播放的速度 | ## 贡献 @@ -89,4 +93,4 @@ ## 许可证 -本项目采用 AGPLv3 许可证,详细信息请查看 [LICENSE](./LICENSE) 文件。 \ No newline at end of file +本项目采用 AGPLv3 许可证,详细信息请查看 [LICENSE](./LICENSE) 文件。 diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 76183af..a57b3c2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -4,6 +4,7 @@ "add": "Add", "add_favorite": "Add favorite", "add_folder": "Add folder", + "add_ftp_storage": "Add FTP storage", "add_local_storage": "Add local storage", "add_storage": "Add storage", "add_to_play_queue": "Add to play queue", @@ -32,6 +33,7 @@ "download_error": "Download error", "edit": "Edit", "edit_folder": "Edit folder", + "edit_ftp_storage": "Edit FTP storage", "edit_local_storage": "Edit local storage", "edit_webdav_storage": "Edit WebDAV storage", "enter_fullscreen": "Enter fullscreen", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 0b88630..9a22871 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -4,6 +4,7 @@ "add": "添加", "add_favorite": "添加收藏", "add_folder": "添加文件夹", + "add_ftp_storage": "添加 FTP 存储", "add_local_storage": "添加本地存储", "add_storage": "添加存储", "add_to_play_queue": "添加到播放队列", @@ -32,6 +33,7 @@ "download_error": "下载错误", "edit": "编辑", "edit_folder": "编辑文件夹", + "edit_ftp_storage": "编辑 FTP 存储", "edit_local_storage": "编辑本地存储", "edit_webdav_storage": "编辑 WebDAV 存储", "enter_fullscreen": "进入全屏", diff --git a/lib/main.dart b/lib/main.dart index c3442a5..1dc5245 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,6 +16,7 @@ import 'package:iris/utils/logger.dart'; import 'package:iris/utils/platform.dart'; import 'package:iris/utils/request_storage_permission.dart'; import 'package:media_kit/media_kit.dart'; +import 'package:media_stream/media_stream.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:saf_util/saf_util.dart'; import 'package:window_manager/window_manager.dart'; @@ -41,6 +42,9 @@ void main(List arguments) async { }, if (Platform.isAndroid) 'subtitleFontFile': 'assets/fonts/NotoSansCJKsc-Medium.otf', + 'global': { + 'log': 'debug', + } }); final appLinks = AppLinks(); @@ -70,6 +74,9 @@ void main(List arguments) async { }); } + MediaStream mediaStream = MediaStream(); + mediaStream.startServer(); + runApp(const StoreScope(child: MyApp())); } diff --git a/lib/models/storages/ftp.dart b/lib/models/storages/ftp.dart new file mode 100644 index 0000000..714bf5a --- /dev/null +++ b/lib/models/storages/ftp.dart @@ -0,0 +1,90 @@ +import 'dart:convert'; + +import 'package:iris/models/file.dart'; +import 'package:iris/models/storages/storage.dart'; +import 'package:iris/utils/check_content_type.dart'; +import 'package:iris/utils/find_subtitle.dart'; +import 'package:iris/utils/logger.dart'; +import 'package:pure_ftp/pure_ftp.dart'; + +Future> getFTPFiles( + FTPStorage storage, List path) async { + final username = storage.username.isEmpty ? 'anonymous' : storage.username; + + final client = FtpClient( + socketInitOptions: FtpSocketInitOptions( + host: storage.host, + port: int.tryParse(storage.port), + ), + authOptions: FtpAuthOptions( + username: username, + password: storage.password, + account: '', + ), + logCallback: null, + ); + + try { + await client.connect(); + await client.fs.changeDirectory(path.join('/').replaceFirst('//', '/')); + + final files = await client.fs.listDirectory(); + + await client.disconnect(); + + final baseUri = + 'http://localhost:8760/ftp?host=${storage.host}&port=${storage.port}&path=${path.join('/').replaceFirst('//', '/')}'; + + return await Future.wait(files.map( + (file) async => FileItem( + storageId: storage.id, + storageType: StorageType.ftp, + name: file.name, + uri: '$baseUri/${file.name}', + path: [...path, file.name], + isDir: file.isDirectory, + size: file.isDirectory ? 0 : file.info?.size ?? 0, + lastModified: file.info?.modifyTime != null + ? DateTime.tryParse(file.info!.modifyTime!) + : null, + type: file.isDirectory ? ContentType.dir : checkContentType(file.name), + subtitles: await findSubtitle( + files.map((file) => file.name).toList(), + file.name, + baseUri, + ), + ), + )); + } catch (error) { + logger('Error testing FTP: $error'); + return []; + } +} + +Future testFTP(FTPStorage storage) async { + final client = FtpClient( + socketInitOptions: FtpSocketInitOptions( + host: storage.host, + port: int.tryParse(storage.port), + ), + authOptions: FtpAuthOptions( + username: storage.username.isEmpty ? 'anonymous' : storage.username, + password: storage.password, + account: '', + ), + logCallback: null, + ); + + try { + await client.connect(); + await client.fs.listDirectory(); + await client.disconnect(); + return true; + } catch (error) { + logger('Error testing FTP: $error'); + return false; + } +} + +String getFTPAuth(FTPStorage storage) => + 'Basic ${base64Encode(utf8.encode('${storage.username.isEmpty ? 'anonymous' : storage.username}:${storage.password}'))}'; diff --git a/lib/models/storages/storage.dart b/lib/models/storages/storage.dart index b72659d..5511981 100644 --- a/lib/models/storages/storage.dart +++ b/lib/models/storages/storage.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:iris/models/file.dart'; +import 'package:iris/models/storages/ftp.dart'; import 'package:iris/models/storages/local.dart'; import 'package:iris/models/storages/webdav.dart'; import 'package:iris/utils/platform.dart'; @@ -21,6 +22,7 @@ enum StorageType { usb, sdcard, webdav, + ftp, } enum StorageOptions { @@ -64,6 +66,17 @@ sealed class Storage with _$Storage implements _Storage { required bool https, }) = WebDAVStorage; + factory Storage.ftp({ + required String id, + @Default(StorageType.ftp) StorageType type, + required String name, + required String host, + required List basePath, + required String port, + required String username, + required String password, + }) = FTPStorage; + factory Storage.fromJson(Map json) => _$StorageFromJson(json); @@ -81,6 +94,8 @@ sealed class Storage with _$Storage implements _Storage { } case StorageType.webdav: return await getWebDAVFiles(this as WebDAVStorage, path); + case StorageType.ftp: + return await getFTPFiles(this as FTPStorage, path); case StorageType.none: return []; } @@ -91,6 +106,8 @@ sealed class Storage with _$Storage implements _Storage { switch (type) { case StorageType.webdav: return getWebDAVAuth(this as WebDAVStorage); + case StorageType.ftp: + return getFTPAuth(this as FTPStorage); default: return null; } diff --git a/lib/pages/storages/favorites.dart b/lib/pages/storages/favorites.dart index f2a42b0..88ae02b 100644 --- a/lib/pages/storages/favorites.dart +++ b/lib/pages/storages/favorites.dart @@ -48,6 +48,11 @@ class Favorites extends HookWidget { } else if (storage is WebDAVStorage) { return Text( 'http${storage.https ? 's' : ''}://${storage.host}${favorites[index].path.join('/')}'); + } else if (storage is FTPStorage) { + return Text( + 'ftp://${storage.host}${favorites[index].path.join('/').replaceFirst('//', '/')}'); + } else { + return null; } }(), onTap: () { diff --git a/lib/pages/storages/storages.dart b/lib/pages/storages/storages.dart index 20467af..7e5b3f5 100644 --- a/lib/pages/storages/storages.dart +++ b/lib/pages/storages/storages.dart @@ -9,6 +9,7 @@ import 'package:iris/store/use_storage_store.dart'; import 'package:iris/utils/get_localizations.dart'; import 'package:iris/utils/path_conv.dart'; import 'package:iris/widgets/dialogs/show_folder_dialog.dart'; +import 'package:iris/widgets/dialogs/show_ftp_dialog.dart'; import 'package:iris/widgets/dialogs/show_webdav_dialog.dart'; import 'package:iris/pages/storages/storages_list.dart'; import 'package:iris/utils/platform.dart'; @@ -129,19 +130,26 @@ class Storages extends HookWidget { case StorageType.webdav: showWebDAVDialog(context); break; + case StorageType.ftp: + showFTPDialog(context); + break; case StorageType.none: break; } }, itemBuilder: (BuildContext context) { return [ + PopupMenuItem( + value: StorageType.internal, + child: Text(t.folder), + ), const PopupMenuItem( value: StorageType.webdav, child: Text('WebDAV'), ), PopupMenuItem( - value: StorageType.internal, - child: Text(t.folder), + value: StorageType.ftp, + child: Text('FTP'), ), ]; }, diff --git a/lib/pages/storages/storages_list.dart b/lib/pages/storages/storages_list.dart index 86ab0f5..48d2f8c 100644 --- a/lib/pages/storages/storages_list.dart +++ b/lib/pages/storages/storages_list.dart @@ -6,6 +6,7 @@ import 'package:iris/models/storages/storage.dart'; import 'package:iris/widgets/dialogs/show_folder_dialog.dart'; import 'package:iris/store/use_storage_store.dart'; import 'package:iris/utils/get_localizations.dart'; +import 'package:iris/widgets/dialogs/show_ftp_dialog.dart'; import 'package:iris/widgets/dialogs/show_webdav_dialog.dart'; import 'package:path/path.dart' as p; @@ -55,6 +56,11 @@ class StoragesList extends HookWidget { subtitle = 'http${storage.https ? 's' : ''}://${storage.host}${storage.basePath.join('/')}'; break; + case StorageType.ftp: + final storage = allStorages[index] as FTPStorage; + subtitle = + 'ftp://${storage.host}${storage.basePath.join('/')}'; + break; case StorageType.none: break; } @@ -79,12 +85,24 @@ class StoragesList extends HookWidget { onSelected: (value) { switch (value) { case StorageOptions.edit: - if (allStorages[index] is WebDAVStorage) { - showWebDAVDialog(context, - storage: allStorages[index] as WebDAVStorage); - } else if (allStorages[index] is LocalStorage) { - showFolderDialog(context, - storage: allStorages[index] as LocalStorage); + switch (allStorages[index].type) { + case StorageType.internal: + case StorageType.network: + case StorageType.usb: + case StorageType.sdcard: + showFolderDialog(context, + storage: allStorages[index] as LocalStorage); + break; + case StorageType.webdav: + showWebDAVDialog(context, + storage: allStorages[index] as WebDAVStorage); + break; + case StorageType.ftp: + showFTPDialog(context, + storage: allStorages[index] as FTPStorage); + break; + case StorageType.none: + break; } break; case StorageOptions.remove: diff --git a/lib/utils/check_data_source_type.dart b/lib/utils/check_data_source_type.dart index cf3310b..9b9c0ec 100644 --- a/lib/utils/check_data_source_type.dart +++ b/lib/utils/check_data_source_type.dart @@ -15,6 +15,7 @@ DataSourceType checkDataSourceType(FileItem file) { case StorageType.usb: return DataSourceType.file; case StorageType.webdav: + case StorageType.ftp: case StorageType.none: return DataSourceType.network; } diff --git a/lib/widgets/dialogs/show_ftp_dialog.dart b/lib/widgets/dialogs/show_ftp_dialog.dart new file mode 100644 index 0000000..a8a5cdb --- /dev/null +++ b/lib/widgets/dialogs/show_ftp_dialog.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:iris/models/storages/ftp.dart'; +import 'package:iris/models/storages/storage.dart'; +import 'package:iris/store/use_storage_store.dart'; +import 'package:iris/utils/get_localizations.dart'; +import 'package:uuid/uuid.dart'; + +Future showFTPDialog(BuildContext context, {FTPStorage? storage}) async => + await showDialog( + context: context, + builder: (BuildContext context) { + return FTPDialog(storage: storage); + }, + ); + +class FTPDialog extends HookWidget { + const FTPDialog({ + super.key, + this.storage, + }); + final FTPStorage? storage; + + @override + Widget build(BuildContext context) { + final t = getLocalizations(context); + final bool isEdit = + storage != null && (useStorageStore().state.storages.contains(storage)); + + final id = useMemoized(() => storage?.id ?? const Uuid().v4()); + final name = useState(storage?.name ?? ''); + final host = useState(storage?.host ?? ''); + final basePath = useState(storage?.basePath ?? ['/']); + final username = useState(storage?.username ?? ''); + final password = useState(storage?.password ?? ''); + + final isTested = useState(false); + + final TextEditingController portController = + useTextEditingController(text: storage?.port ?? '21'); + + void add() { + useStorageStore().addStorage( + FTPStorage( + id: id, + name: name.value, + host: host.value, + basePath: basePath.value, + port: portController.text, + username: username.value, + password: password.value, + ), + ); + } + + void update() { + useStorageStore().updateStorage( + useStorageStore().state.storages.indexOf(storage as Storage), + FTPStorage( + id: id, + name: name.value, + host: host.value, + basePath: basePath.value, + port: portController.text, + username: username.value, + password: password.value, + ), + ); + } + + void testConnection() async { + final bool isConnected = await testFTP(FTPStorage( + id: id, + name: name.value, + host: host.value, + basePath: basePath.value, + port: portController.text, + username: username.value, + password: password.value, + )); + isTested.value = isConnected; + } + + return AlertDialog( + title: Text(isEdit ? t.edit_ftp_storage : t.add_ftp_storage), + content: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Form( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: t.name, + ), + initialValue: name.value, + onChanged: (value) { + name.value = value.trim(); + isTested.value = false; + }, + ), + const SizedBox(height: 16.0), + TextFormField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: t.host, + ), + initialValue: host.value, + onChanged: (value) { + host.value = value.trim().split('//').last; + isTested.value = false; + }, + ), + const SizedBox(height: 16.0), + TextFormField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: t.path, + ), + initialValue: basePath.value.join('/'), + onChanged: (value) { + final trimmedValue = + value.trim().replaceAll(RegExp(r'^\/+|\/+$'), ''); + final finalPath = '/$trimmedValue'; + basePath.value = [finalPath]; + isTested.value = false; + }), + const SizedBox(height: 16.0), + TextFormField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: t.port, + ), + controller: portController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + onChanged: (value) { + isTested.value = false; + }, + ), + const SizedBox(height: 16.0), + TextFormField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: t.username, + ), + initialValue: username.value, + onChanged: (value) { + username.value = value.trim(); + isTested.value = false; + }, + ), + const SizedBox(height: 16.0), + TextFormField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: t.password, + ), + initialValue: password.value, + obscureText: true, + onChanged: (value) { + password.value = value.trim(); + isTested.value = false; + }, + ), + const SizedBox(height: 16.0), + ], + ), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'Cancel'), + child: Text(t.cancel), + ), + TextButton( + onPressed: testConnection, + child: Text(t.test_connection), + ), + TextButton( + onPressed: isTested.value + ? () { + Navigator.pop(context, 'OK'); + isEdit ? update() : add(); + } + : null, + child: Text(isEdit ? t.save : t.add), + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 444de36..458087a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.7.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "0511d6be23b007e95105ae023db599aea731df604608978dada7f9faf2637623" + url: "https://pub.dev" + source: hosted + version: "1.6.4" async: dependency: transitive description: @@ -161,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + charset: + dependency: transitive + description: + name: charset + sha256: "27802032a581e01ac565904ece8c8962564b1070690794f0072f6865958ce8b9" + url: "https://pub.dev" + source: hosted + version: "2.0.1" checked_yaml: dependency: transitive description: @@ -233,6 +249,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + cryptography: + dependency: transitive + description: + name: cryptography + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 + url: "https://pub.dev" + source: hosted + version: "2.7.0" csslib: dependency: transitive description: @@ -606,6 +630,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + http_methods: + dependency: transitive + description: + name: http_methods + sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566" + url: "https://pub.dev" + source: hosted + version: "1.1.1" http_multi_server: dependency: transitive description: @@ -798,6 +830,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.5" + media_stream: + dependency: "direct main" + description: + path: "." + ref: main + resolved-ref: aa5362427f201d2740d3ec6a251d3287b1a9ef69 + url: "https://github.com/nini22P/media_stream" + source: git + version: "0.0.1" meta: dependency: transitive description: @@ -822,6 +863,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.16.9" + mutex: + dependency: transitive + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" nested: dependency: transitive description: @@ -990,6 +1039,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" pool: dependency: transitive description: @@ -1038,6 +1095,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + pure_ftp: + dependency: "direct main" + description: + name: pure_ftp + sha256: be26500e86e49e2e39d16e62888eb4865f0491fc55a6c61bd5359cb888200e65 + url: "https://pub.dev" + source: hosted + version: "0.7.5" saf_util: dependency: "direct main" description: @@ -1158,6 +1223,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.2" + shelf_router: + dependency: transitive + description: + name: shelf_router + sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864 + url: "https://pub.dev" + source: hosted + version: "1.1.4" shelf_web_socket: dependency: transitive description: @@ -1171,6 +1244,14 @@ packages: description: flutter source: sdk version: "0.0.0" + smb_connect: + dependency: transitive + description: + name: smb_connect + sha256: "8d82b701f8ff8a736070f6eb5393fe983da1a0f4747a56c3a04bda68765b5e66" + url: "https://pub.dev" + source: hosted + version: "0.0.9" source_gen: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 61dbdc6..0ebab8c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,10 +51,15 @@ dependencies: video_player: ^2.9.2 wakelock_plus: ^1.2.10 popover: ^0.3.1 + pure_ftp: ^0.7.5 drives_windows: git: url: https://github.com/nini22P/drives_windows ref: main + media_stream: + git: + url: https://github.com/nini22P/media_stream + ref: main dev_dependencies: flutter_test: