feat: supports FTP storage

This commit is contained in:
22
2025-06-07 16:15:11 +08:00
parent a9da16fb98
commit 9228bbe0c2
14 changed files with 550 additions and 108 deletions

102
README.md
View File

@@ -3,76 +3,80 @@
# IRIS - A lightweight video player
![ci](https://github.com/nini22P/iris/actions/workflows/ci.yml/badge.svg)
<a href="https://afdian.com/a/nini22P"><img alt="Afdian" style="height: 30px;" src="https://pic1.afdiancdn.com/static/img/welcome/button-sponsorme.png"></a>
`<a href="https://afdian.com/a/nini22P"><img alt="Afdian" style="height: 30px;" src="https://pic1.afdiancdn.com/static/img/welcome/button-sponsorme.png">``</a>`
[![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

View File

@@ -3,76 +3,80 @@
# IRIS - 轻量级视频播放器
![ci](https://github.com/nini22P/iris/actions/workflows/ci.yml/badge.svg)
<a href="https://afdian.com/a/nini22P"><img alt="爱发电" style="height: 30px;" src="https://pic1.afdiancdn.com/static/img/welcome/button-sponsorme.png"></a>
`<a href="https://afdian.com/a/nini22P"><img alt="爱发电" style="height: 30px;" src="https://pic1.afdiancdn.com/static/img/welcome/button-sponsorme.png">``</a>`
[![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) 文件。
本项目采用 AGPLv3 许可证,详细信息请查看 [LICENSE](./LICENSE) 文件。

View File

@@ -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",

View File

@@ -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": "进入全屏",

View File

@@ -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<String> arguments) async {
},
if (Platform.isAndroid)
'subtitleFontFile': 'assets/fonts/NotoSansCJKsc-Medium.otf',
'global': {
'log': 'debug',
}
});
final appLinks = AppLinks();
@@ -70,6 +74,9 @@ void main(List<String> arguments) async {
});
}
MediaStream mediaStream = MediaStream();
mediaStream.startServer();
runApp(const StoreScope(child: MyApp()));
}

View File

@@ -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<List<FileItem>> getFTPFiles(
FTPStorage storage, List<String> 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<bool> 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}'))}';

View File

@@ -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<String> basePath,
required String port,
required String username,
required String password,
}) = FTPStorage;
factory Storage.fromJson(Map<String, dynamic> 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;
}

View File

@@ -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: () {

View File

@@ -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<StorageType>(
value: StorageType.internal,
child: Text(t.folder),
),
const PopupMenuItem<StorageType>(
value: StorageType.webdav,
child: Text('WebDAV'),
),
PopupMenuItem<StorageType>(
value: StorageType.internal,
child: Text(t.folder),
value: StorageType.ftp,
child: Text('FTP'),
),
];
},

View File

@@ -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:

View File

@@ -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;
}

View File

@@ -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<void> showFTPDialog(BuildContext context, {FTPStorage? storage}) async =>
await showDialog<void>(
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: <TextInputFormatter>[
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: <Widget>[
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),
),
],
);
}
}

View File

@@ -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:

View File

@@ -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: