mirror of
https://github.com/nini22P/iris.git
synced 2026-06-02 09:29:36 +08:00
feat: supports FTP storage
This commit is contained in:
102
README.md
102
README.md
@@ -3,76 +3,80 @@
|
||||
# IRIS - A lightweight video player
|
||||
|
||||

|
||||
<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>`
|
||||
[](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
|
||||
|
||||
|
||||
106
README_CN.md
106
README_CN.md
@@ -3,76 +3,80 @@
|
||||
# IRIS - 轻量级视频播放器
|
||||
|
||||

|
||||
<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>`
|
||||
[](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) 文件。
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "进入全屏",
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
|
||||
90
lib/models/storages/ftp.dart
Normal file
90
lib/models/storages/ftp.dart
Normal 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}'))}';
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: () {
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
198
lib/widgets/dialogs/show_ftp_dialog.dart
Normal file
198
lib/widgets/dialogs/show_ftp_dialog.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
81
pubspec.lock
81
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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user