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

-
+`
```
[](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 - 轻量级视频播放器

-
+`
```
[](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: