Browse Source

DC: impl file/folder create & upload

pull/18/head
Sun 4 years ago
parent
commit
7c9b58223f
  1. 5
      lib/apps/assistant/message.dart
  2. 103
      lib/apps/file/editor.dart
  3. 163
      lib/apps/file/list.dart
  4. 173
      lib/apps/file/models.dart
  5. 1
      lib/l10n/localizations.dart
  6. 2
      lib/l10n/localizations_en.dart
  7. 2
      lib/l10n/localizations_zh.dart
  8. 1
      lib/rpc.dart
  9. 6
      lib/utils/toast.dart
  10. 5
      lib/widgets/chat_message.dart
  11. 7
      pubspec.lock
  12. 1
      pubspec.yaml
  13. 2
      src/apps/file/mod.rs
  14. 224
      src/apps/file/models.rs
  15. 124
      src/apps/file/rpc.rs
  16. 14
      src/event.rs
  17. 5
      src/migrate/file.rs
  18. 16
      src/storage.rs

5
lib/apps/assistant/message.dart

@ -7,13 +7,13 @@ import 'package:open_file/open_file.dart'; @@ -7,13 +7,13 @@ import 'package:open_file/open_file.dart';
import 'package:esse/l10n/localizations.dart';
import 'package:esse/utils/adaptive.dart';
import 'package:esse/utils/file_image.dart';
import 'package:esse/utils/better_print.dart';
import 'package:esse/widgets/avatar.dart';
import 'package:esse/widgets/audio_player.dart';
import 'package:esse/widgets/shadow_dialog.dart';
import 'package:esse/global.dart';
import 'package:esse/apps/file/models.dart' show FileType, FileTypeExtension, parseFileType;
import 'package:esse/apps/assistant/models.dart';
class AssistantMessage extends StatelessWidget {
@ -107,7 +107,8 @@ class AssistantMessage extends StatelessWidget { @@ -107,7 +107,8 @@ class AssistantMessage extends StatelessWidget {
fileExsit = false;
fileImage = Image(image: AssetImage('assets/images/image_missing.png'), fit: BoxFit.cover);
} else {
fileImage = fileIcon(content, 36.0);
final params = parseFileType(content).params();
fileImage = Icon(params[0], color: params[1], size: 36.0);
}
return GestureDetector(
onTap: fileExsit

103
lib/apps/file/editor.dart

@ -1,38 +1,73 @@ @@ -1,38 +1,73 @@
import 'dart:io' show File;
import 'dart:convert' show jsonDecode, jsonEncode;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:flutter_quill/flutter_quill.dart' hide Text;
import 'package:esse/utils/adaptive.dart';
import 'package:esse/l10n/localizations.dart';
import 'package:esse/provider.dart';
import 'package:esse/global.dart';
import 'package:esse/rpc.dart';
import 'package:esse/apps/file/models.dart';
import 'package:esse/apps/file/list.dart';
class EditorPage extends StatefulWidget {
final FilePath path;
const EditorPage({Key? key, required this.path}) : super(key: key);
FilePath path;
final List<FilePath> parents;
EditorPage({Key? key, required this.path, required this.parents}) : super(key: key);
@override
_EditorPageState createState() => _EditorPageState();
}
class _EditorPageState extends State<EditorPage> {
QuillController _controller = QuillController.basic();
QuillController? _controller;
FToast? _fToast;
TextEditingController _nameController = TextEditingController();
bool _nameEdit = false;
String _filePath = '';
readFile() async {
try {
final s = await File(this._filePath).readAsString();
final doc = Document.fromJson(jsonDecode(s));
setState(() {
this._controller = QuillController(
document: doc, selection: const TextSelection.collapsed(offset: 0)
);
});
} catch (e) {
await File(this._filePath).create(recursive: true);
final doc = Document()..insert(0, '');
setState(() {
this._controller = QuillController(
document: doc, selection: const TextSelection.collapsed(offset: 0));
});
}
}
@override
initState() {
print("File editor initState...");
super.initState();
_nameController.text = widget.path.name();
_nameController.text = widget.path.showName();
this._filePath = Global.filePath + widget.path.did;
readFile();
this._fToast = FToast();
this._fToast!.init(context);
}
@override
Widget build(BuildContext context) {
final color = Theme.of(context).colorScheme;
final lang = AppLocalizations.of(context);
if (_controller == null) {
return Scaffold(body: Center(child: Text(lang.waiting)));
}
final color = Theme.of(context).colorScheme;
final isDesktop = isDisplayDesktop(context);
return Scaffold(
@ -40,7 +75,7 @@ class _EditorPageState extends State<EditorPage> { @@ -40,7 +75,7 @@ class _EditorPageState extends State<EditorPage> {
leading: isDesktop ? IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
final w = FilesList(path: FilePath.prev(widget.path));
final w = FilesList(path: widget.parents);
context.read<AccountProvider>().updateActivedWidget(w);
}
) : null,
@ -66,13 +101,24 @@ class _EditorPageState extends State<EditorPage> { @@ -66,13 +101,24 @@ class _EditorPageState extends State<EditorPage> {
),
const SizedBox(width: 10.0),
GestureDetector(
onTap: () {
if (_nameController.text.length > 0) {
// TODO update file name.
onTap: () async {
final name = _nameController.text.trim();
if (name.length > 0) {
final res = await httpPost(
Global.httpRpc,
'dc-file-update',
[widget.path.id, widget.path.root.toInt(), widget.path.parent,
FilePath.newPostName(name)]
);
if (res.isOk) {
widget.path = FilePath.fromList(res.params);
setState(() {
this._nameEdit = false;
});
} else {
print('change name error');
}
}
setState(() {
this._nameEdit = false;
});
},
child: Container(
width: 20.0,
@ -81,16 +127,36 @@ class _EditorPageState extends State<EditorPage> { @@ -81,16 +127,36 @@ class _EditorPageState extends State<EditorPage> {
const SizedBox(width: 8.0),
GestureDetector(
onTap: () => setState(() {
_nameController.text = widget.path.name();
_nameController.text = widget.path.showName();
this._nameEdit = false;
}),
child: Container(
width: 20.0, child: Icon(Icons.clear_rounded)),
),
])
: TextButton(child: Text(widget.path.name()),
: TextButton(child: Text(widget.path.showName()),
onPressed: () => setState(() { this._nameEdit = true; }),
),
actions: [
IconButton(icon: Icon(Icons.save_rounded), onPressed: () async {
final j = this._controller!.document.toDelta().toJson();
final s = jsonEncode(j);
await File(this._filePath).writeAsString(s);
this._fToast!.showToast(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 10.0),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(25.0),
color: Colors.green),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [Icon(Icons.check), const SizedBox(width: 12.0), Text(lang.saveOk)],
)),
gravity: ToastGravity.BOTTOM,
toastDuration: Duration(seconds: 2),
);
}),
const SizedBox(width: 10.0),
]
),
body: Column(
children: [
@ -99,15 +165,16 @@ class _EditorPageState extends State<EditorPage> { @@ -99,15 +165,16 @@ class _EditorPageState extends State<EditorPage> {
decoration: BoxDecoration(color: color.secondary),
padding: const EdgeInsets.all(10.0),
child: QuillToolbar.basic(
controller: _controller,
showAlignmentButtons: false,
controller: this._controller!,
showAlignmentButtons: true,
showLink: false,
)
),
Expanded(
child: Container(
padding: const EdgeInsets.all(10.0),
child: QuillEditor.basic(
controller: _controller,
controller: this._controller!,
readOnly: false, // true for view only mode
),
),

163
lib/apps/file/list.dart

@ -2,31 +2,64 @@ import 'package:flutter/material.dart'; @@ -2,31 +2,64 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:esse/utils/adaptive.dart';
import 'package:esse/utils/pick_file.dart';
import 'package:esse/l10n/localizations.dart';
import 'package:esse/widgets/button_text.dart';
import 'package:esse/widgets/input_text.dart';
import 'package:esse/widgets/shadow_dialog.dart';
import 'package:esse/provider.dart';
import 'package:esse/rpc.dart';
import 'package:esse/apps/file/models.dart';
import 'package:esse/apps/file/editor.dart';
class FilesList extends StatefulWidget {
FilePath path;
List<FilePath> path; // current file/folder.
FilesList({Key? key, required this.path}) : super(key: key);
@override
_FilesListState createState() => _FilesListState();
_FilesListState createState() => _FilesListState(this.path.last.root);
}
class _FilesListState extends State<FilesList> {
RootDirectory root; // check if root is changed.
bool _isDesktop = false;
List<FilePath> _children = []; // children if is folder.
_FilesListState(this.root);
@override
void initState() {
super.initState();
_loadDirectory();
rpc.addListener('dc-list', _dcList, false);
rpc.addListener('dc-file-create', _dcFileCreate, false);
rpc.addListener('dc-folder-create', _dcFolderCreate, false);
rpc.addListener('dc-file-upload', _dcFolderCreate, false);
_loadDirectory(widget.path.last);
}
_dcList(List params) {
this._children.clear();
params.forEach((param) {
this._children.add(FilePath.fromList(param));
});
setState(() {});
}
_dcFileCreate(List params) {
final newFile = FilePath.fromList(params);
_navigator(EditorPage(path: newFile, parents: widget.path));
}
_dcFolderCreate(List params) {
setState(() {
this._children.add(FilePath.fromList(params));
});
}
_loadDirectory() {
//
_loadDirectory(FilePath path) {
rpc.send('dc-list', [path.root.toInt(), path.id]);
}
_navigator(Widget w) {
@ -37,29 +70,29 @@ class _FilesListState extends State<FilesList> { @@ -37,29 +70,29 @@ class _FilesListState extends State<FilesList> {
}
}
_prevDirectory(int i) {
setState(() {
widget.path = List.generate(i+1, (j) => widget.path[j]);
_loadDirectory(widget.path.last);
});
}
_nextDirectory(FilePath path) {
setState(() {
widget.path = path;
_loadDirectory();
widget.path.add(path);
_loadDirectory(path);
});
}
List<Widget> _pathWidget(String root) {
List<Widget> widgets = [
InkWell(
onTap: () => _nextDirectory(FilePath.root(widget.path.root)),
child: Text(root,
style: TextStyle(fontSize: 14.0, color: Color(0xFFADB0BB)))
)
];
List<Widget> _pathWidget() {
List<Widget> widgets = [];
final n = widget.path.path.length;
final n = widget.path.length;
for (int i = 0; i < n; i++) {
final name = widget.path.path[i];
final current_path = List.generate(i+1, (i) => widget.path.path[i]);
widgets.add(InkWell(
onTap: () => _nextDirectory(FilePath(widget.path.root, current_path)),
child: Text('/'+FilePath.directoryName(name),
onTap: () => _prevDirectory(i),
child: Text('/'+widget.path[i].directoryName(),
style: TextStyle(fontSize: 14.0, color: Color(0xFFADB0BB)))
));
}
@ -68,14 +101,14 @@ class _FilesListState extends State<FilesList> { @@ -68,14 +101,14 @@ class _FilesListState extends State<FilesList> {
}
Widget _item(FilePath file) {
final trueName = file.name();
final trueName = file.showName();
final params = file.fileType().params();
return InkWell(
onTap: () {
if (file.isDirectory()) {
_nextDirectory(file);
} else if (file.isPost()) {
_navigator(EditorPage(path: file));
_navigator(EditorPage(path: file, parents: widget.path));
}
},
child: Container(
@ -100,15 +133,21 @@ class _FilesListState extends State<FilesList> { @@ -100,15 +133,21 @@ class _FilesListState extends State<FilesList> {
@override
Widget build(BuildContext context) {
if (widget.path.last.root != this.root) {
this.root = widget.path.last.root;
_loadDirectory(widget.path.last);
}
final color = Theme.of(context).colorScheme;
final lang = AppLocalizations.of(context);
this._isDesktop = isDisplayDesktop(context);
return Scaffold(
appBar: AppBar(
title: Text(lang.dataCenter + ' (${lang.wip})'),
title: Text(lang.dataCenter),
actions: [
widget.path.root == RootDirectory.Trash
if (widget.path.last.root != RootDirectory.Star)
widget.path.last.root == RootDirectory.Trash
? IconButton(
icon: Icon(Icons.delete_forever, color: Colors.red),
onPressed: () => showDialog(
@ -139,14 +178,21 @@ class _FilesListState extends State<FilesList> { @@ -139,14 +178,21 @@ class _FilesListState extends State<FilesList> {
),
color: const Color(0xFFEDEDED),
child: SizedBox(width: 40.0, child: Icon(Icons.add_rounded, color: color.primary)),
onSelected: (int value) {
onSelected: (int value) async {
final parent = widget.path.last;
if (value == 0) {
final path = FilePath.next(widget.path, "new document 0.quill.json");
_navigator(EditorPage(path: path));
rpc.send('dc-file-create',
[parent.root.toInt(), parent.id, FilePath.newPostName(lang.newPost)]
);
} else if (value == 1) {
// new folder
showShadowDialog(context, Icons.folder_rounded, lang.newFolder,
_CreateFolder(parent: parent), 20.0
);
} else if (value == 2) {
// upload file
final file = await pickFile();
if (file != null) {
rpc.send('dc-file-upload', [parent.root.toInt(), parent.id, file]);
}
}
},
itemBuilder: (context) {
@ -171,28 +217,13 @@ class _FilesListState extends State<FilesList> { @@ -171,28 +217,13 @@ class _FilesListState extends State<FilesList> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(children: this._pathWidget('/' + widget.path.root.params(lang)[1])),
Row(children: this._pathWidget()),
const SizedBox(height: 4.0),
Expanded(
child: GridView.extent(
maxCrossAxisExtent: 75.0,
childAspectRatio: 0.8,
children: <Widget> [
_item(FilePath.next(widget.path, 'myworks.dir')),
_item(FilePath.next(widget.path, 'ESSE-infos-public.dir')),
_item(FilePath.next(widget.path, 'personal.dir')),
_item(FilePath.next(widget.path, 'others.dir')),
_item(FilePath.next(widget.path, 'logo.jpg')),
_item(FilePath.next(widget.path, 'cat.png')),
_item(FilePath.next(widget.path, 'what-is-esse_en.doc')),
_item(FilePath.next(widget.path, '20210101-customers.xls')),
_item(FilePath.next(widget.path, 'product.pdf')),
_item(FilePath.next(widget.path, 'deck.ppt')),
_item(FilePath.next(widget.path, 'coder.md')),
_item(FilePath.next(widget.path, 'how-to-live-in-happy.mp4')),
_item(FilePath.next(widget.path, 'something_important')),
_item(FilePath.next(widget.path, 'car.quill.json')),
],
children: this._children.map((file) => _item(file)).toList()
),
)
]
@ -201,3 +232,43 @@ class _FilesListState extends State<FilesList> { @@ -201,3 +232,43 @@ class _FilesListState extends State<FilesList> {
);
}
}
class _CreateFolder extends StatelessWidget {
final FilePath parent;
TextEditingController _nameController = TextEditingController();
FocusNode _nameFocus = FocusNode();
_CreateFolder({Key? key, required this.parent}) : super(key: key);
@override
Widget build(BuildContext context) {
final color = Theme.of(context).colorScheme;
final lang = AppLocalizations.of(context);
_nameFocus.requestFocus();
return Column(
children: [
Container(
padding: EdgeInsets.only(bottom: 20.0),
child: InputText(
icon: Icons.folder_rounded,
text: lang.newFolder,
controller: _nameController,
focus: _nameFocus),
),
ButtonText(
text: lang.send,
action: () {
final name = _nameController.text.trim();
if (name.length == 0) {
return;
}
rpc.send('dc-folder-create',
[parent.root.toInt(), parent.id, FilePath.newFolderName(name)]
);
Navigator.pop(context);
}),
]
);
}
}

173
lib/apps/file/models.dart

@ -1,4 +1,6 @@ @@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:esse/utils/relative_time.dart';
import 'package:esse/l10n/localizations.dart';
const List<RootDirectory> ROOT_DIRECTORY = [
@ -7,6 +9,7 @@ const List<RootDirectory> ROOT_DIRECTORY = [ @@ -7,6 +9,7 @@ const List<RootDirectory> ROOT_DIRECTORY = [
RootDirectory.Image,
RootDirectory.Music,
RootDirectory.Video,
RootDirectory.Session,
RootDirectory.Trash,
];
@ -16,24 +19,74 @@ enum RootDirectory { @@ -16,24 +19,74 @@ enum RootDirectory {
Image,
Music,
Video,
Session,
Trash,
}
extension InnerServiceExtension on RootDirectory {
extension RootDirectoryExtension on RootDirectory {
List params(AppLocalizations lang) {
switch (this) {
case RootDirectory.Star:
return [Icons.star, lang.star, FilePath.root(RootDirectory.Star)];
return [Icons.star, lang.star,
[FilePath.root(RootDirectory.Star, lang.star)]];
case RootDirectory.Document:
return [Icons.description, lang.document, FilePath.root(RootDirectory.Document)];
return [Icons.description, lang.document,
[FilePath.root(RootDirectory.Document, lang.document)]];
case RootDirectory.Image:
return [Icons.image, lang.image, FilePath.root(RootDirectory.Image)];
return [Icons.image, lang.image,
[FilePath.root(RootDirectory.Image, lang.image)]];
case RootDirectory.Music:
return [Icons.music_note, lang.music, FilePath.root(RootDirectory.Music)];
return [Icons.music_note, lang.music,
[FilePath.root(RootDirectory.Music, lang.music)]];
case RootDirectory.Video:
return [Icons.play_circle_filled, lang.video, FilePath.root(RootDirectory.Video)];
return [Icons.play_circle_filled, lang.video,
[FilePath.root(RootDirectory.Video, lang.video)]];
case RootDirectory.Session:
return [Icons.sms, lang.sessions,
[FilePath.root(RootDirectory.Session, lang.sessions)]];
case RootDirectory.Trash:
return [Icons.auto_delete, lang.trash, FilePath.root(RootDirectory.Trash)];
return [Icons.auto_delete, lang.trash,
[FilePath.root(RootDirectory.Trash, lang.trash)]];
}
}
int toInt() {
switch (this) {
case RootDirectory.Star:
return 0;
case RootDirectory.Trash:
return 1;
case RootDirectory.Session:
return 2;
case RootDirectory.Document:
return 3;
case RootDirectory.Image:
return 4;
case RootDirectory.Music:
return 5;
case RootDirectory.Video:
return 6;
}
}
static RootDirectory fromInt(int a) {
switch (a) {
case 0:
return RootDirectory.Star;
case 1:
return RootDirectory.Trash;
case 2:
return RootDirectory.Session;
case 3:
return RootDirectory.Document;
case 4:
return RootDirectory.Image;
case 5:
return RootDirectory.Music;
case 6:
return RootDirectory.Video;
default:
return RootDirectory.Trash;
}
}
}
@ -100,7 +153,7 @@ extension FileTypeExtension on FileType { @@ -100,7 +153,7 @@ extension FileTypeExtension on FileType {
case FileType.Sheet:
return [Icons.table_chart_rounded, Color(0xFF4CAF50)];
case FileType.Word:
return [Icons.description_rounded, Color(0xFF1976d2)];
return [Icons.description_rounded, Color(0xFF0b335b)];
case FileType.Markdown:
return [Icons.description_rounded, Color(0xFF455A64)];
case FileType.Other:
@ -109,78 +162,80 @@ extension FileTypeExtension on FileType { @@ -109,78 +162,80 @@ extension FileTypeExtension on FileType {
}
}
class FilePath {
RootDirectory root;
List<String> path = [];
String get fullName => this.path.last;
FilePath.root(this.root);
FilePath(this.root, this.path);
static FilePath next(FilePath file, String name) {
final root = file.root;
List<String> path = List.from(file.path);
path.add(name);
return FilePath(root, path);
FileType parseFileType(String name) {
if (name.endsWith('.quill.json')) {
return FileType.Post;
}
static FilePath prev(FilePath file) {
final root = file.root;
List<String> path = List.from(file.path);
if (path.length == 0) {
return FilePath.root(root);
} else {
path.removeLast();
return FilePath(root, path);
final i = name.lastIndexOf('.');
if (i > 0) {
final suffix = name.substring(i + 1);
if (FILE_TYPES.containsKey(suffix)) {
return FILE_TYPES[suffix]!;
}
}
return FileType.Other;
}
static directoryName(String name) {
final i = name.lastIndexOf('.');
return name.substring(0, i);
class FilePath {
int id = 0;
String did = '';
int parent = 0;
RootDirectory root = RootDirectory.Trash;
String name = '';
bool starred = false;
RelativeTime time = RelativeTime();
FilePath.root(this.root, this.name);
static newPostName(String name) {
return name + '.quill.json';
}
void add(String next) {
this.path.add(next);
static newFolderName(String name) {
return name + '.dir';
}
String name() {
if (isDirectory()) {
final i = this.path.last.lastIndexOf('.');
return this.path.last.substring(0, i);
} else if (isPost()){
final i = this.path.last.lastIndexOf('.quill');
return this.path.last.substring(0, i);
String directoryName() {
final i = this.name.lastIndexOf('.');
if (i < 0) {
return this.name;
} else {
return this.path.last;
return this.name.substring(0, i);
}
}
FileType fileType() {
String showName() {
if (isDirectory()) {
return FileType.Folder;
}
if (isPost()) {
return FileType.Post;
final i = this.name.lastIndexOf('.');
return this.name.substring(0, i);
} else if (isPost()){
final i = this.name.lastIndexOf('.quill');
return this.name.substring(0, i);
} else {
return this.name;
}
}
final i = this.path.last.lastIndexOf('.');
if (i > 0) {
final suffix = this.path.last.substring(i + 1);
if (FILE_TYPES.containsKey(suffix)) {
return FILE_TYPES[suffix]!;
}
}
return FileType.Other;
FileType fileType() {
return parseFileType(this.name);
}
bool isPost() {
return this.path.last.endsWith('.quill.json');
return this.name.endsWith('.quill.json');
}
bool isDirectory() {
return this.path.last.endsWith('.dir');
return this.name.endsWith('.dir');
}
FilePath.fromList(List params) {
this.id = params[0];
this.did = params[1];
this.parent = params[2];
this.root = RootDirectoryExtension.fromInt(params[3]);
this.name = params[4];
this.starred = params[5];
this.time = RelativeTime.fromInt(params[6]);
}
}

1
lib/l10n/localizations.dart

@ -42,6 +42,7 @@ abstract class AppLocalizations { @@ -42,6 +42,7 @@ abstract class AppLocalizations {
// Common
String get title;
String get ok;
String get saveOk;
String get cancel;
String get next;
String get back;

2
lib/l10n/localizations_en.dart

@ -9,6 +9,8 @@ class AppLocalizationsEn extends AppLocalizations { @@ -9,6 +9,8 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get ok => 'OK';
@override
String get saveOk => 'save successfully';
@override
String get cancel => 'Cancel';
@override
String get next => 'Next';

2
lib/l10n/localizations_zh.dart

@ -9,6 +9,8 @@ class AppLocalizationsZh extends AppLocalizations { @@ -9,6 +9,8 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get ok => '确认';
@override
String get saveOk => '保存成功';
@override
String get cancel => '取消';
@override
String get next => '下一步';

1
lib/rpc.dart

@ -137,7 +137,6 @@ class WebSocketsNotifications { @@ -137,7 +137,6 @@ class WebSocketsNotifications {
print(response);
if (response["result"] != null &&
response["result"].length != 0 &&
response["method"] != null &&
response["gid"] != null
) {

6
lib/utils/toast.dart

@ -21,7 +21,7 @@ class _FadeToastState extends State<FadeToast> @@ -21,7 +21,7 @@ class _FadeToastState extends State<FadeToast>
_animation = Tween(begin: 0.0, end: 1.0).animate(_controller!);
_animation!.addStatusListener((status) {
if (status == AnimationStatus.completed) {
Future.delayed(Duration(seconds: 2), () {
Future.delayed(Duration(seconds: 1), () {
_controller!.reverse();
});
}
@ -54,7 +54,7 @@ void toast(BuildContext context, String text) { @@ -54,7 +54,7 @@ void toast(BuildContext context, String text) {
decoration: BoxDecoration(
color: Color(0xFFADB0BB).withOpacity(0.5),
borderRadius: BorderRadius.circular(25.0)),
height: 50.0,
height: 40.0,
margin: EdgeInsets.only(bottom: 40.0),
alignment: Alignment.center,
padding: const EdgeInsets.all(10.0),
@ -63,6 +63,6 @@ void toast(BuildContext context, String text) { @@ -63,6 +63,6 @@ void toast(BuildContext context, String text) {
backgroundColor: Colors.transparent,
elevation: 1000,
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: 4),
duration: Duration(seconds: 2),
));
}

5
lib/widgets/chat_message.dart

@ -8,7 +8,6 @@ import 'package:open_file/open_file.dart'; @@ -8,7 +8,6 @@ import 'package:open_file/open_file.dart';
import 'package:esse/l10n/localizations.dart';
import 'package:esse/utils/adaptive.dart';
import 'package:esse/utils/file_image.dart';
import 'package:esse/utils/better_print.dart';
import 'package:esse/widgets/avatar.dart';
import 'package:esse/widgets/audio_player.dart';
@ -19,6 +18,7 @@ import 'package:esse/global.dart'; @@ -19,6 +18,7 @@ import 'package:esse/global.dart';
import 'package:esse/apps/primitives.dart';
import 'package:esse/apps/chat/models.dart' show Request;
import 'package:esse/apps/group_chat/models.dart' show GroupType, GroupTypeExtension;
import 'package:esse/apps/file/models.dart' show FileType, FileTypeExtension, parseFileType;
import 'package:esse/apps/chat/provider.dart';
import 'package:esse/apps/group_chat/provider.dart';
@ -144,7 +144,8 @@ class ChatMessage extends StatelessWidget { @@ -144,7 +144,8 @@ class ChatMessage extends StatelessWidget {
fileExsit = false;
fileImage = Image(image: AssetImage('assets/images/image_missing.png'), fit: BoxFit.cover);
} else {
fileImage = fileIcon(message.content, 36.0);
final params = parseFileType(message.content).params();
fileImage = Icon(params[0], color: params[1], size: 36.0);
}
return GestureDetector(
onTap: fileExsit

7
pubspec.lock

@ -347,6 +347,13 @@ packages: @@ -347,6 +347,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
fluttertoast:
dependency: "direct main"
description:
name: fluttertoast
url: "https://pub.dartlang.org"
source: hosted
version: "8.0.8"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter

1
pubspec.yaml

@ -53,6 +53,7 @@ dependencies: @@ -53,6 +53,7 @@ dependencies:
percent_indicator: any
bottom_navy_bar: ^6.0.0
flutter_quill: ^2.0.6
fluttertoast: ^8.0.8
dev_dependencies:
flutter_test:

2
src/apps/file/mod.rs

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
mod models;
mod rpc;
pub(crate) use models::{FileId, FileType};
pub(crate) use models::{FileDid, RootDirectory};
pub(crate) use rpc::new_rpc_handler;

224
src/apps/file/models.rs

@ -1,23 +1,219 @@ @@ -1,23 +1,219 @@
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use tdn::types::{
primitive::Result,
rpc::{json, RpcParam},
};
use tdn_storage::local::{DStorage, DsValue};
#[derive(Serialize, Deserialize)]
pub(crate) enum FileType {
Dir,
File,
SessionFile,
ServiceFile,
#[derive(Serialize, Deserialize, Eq, PartialEq)]
pub(crate) enum RootDirectory {
Star,
Trash,
Session,
Document,
Image,
Music,
Video,
}
#[derive(Serialize, Deserialize)]
pub(crate) struct FileId([u8; 32]);
impl RootDirectory {
fn to_i64(&self) -> i64 {
match self {
RootDirectory::Star => 0,
RootDirectory::Trash => 1,
RootDirectory::Session => 2,
RootDirectory::Document => 3,
RootDirectory::Image => 4,
RootDirectory::Music => 5,
RootDirectory::Video => 6,
}
}
pub(crate) struct _File {
pub fn from_i64(i: i64) -> Self {
match i {
0 => RootDirectory::Star,
1 => RootDirectory::Trash,
2 => RootDirectory::Session,
3 => RootDirectory::Document,
4 => RootDirectory::Image,
5 => RootDirectory::Music,
6 => RootDirectory::Video,
_ => RootDirectory::Trash,
}
}
}
#[derive(Serialize, Deserialize, Default)]
pub(crate) struct FileDid([u8; 32]);
impl FileDid {
pub fn generate() -> Self {
Self(rand::thread_rng().gen::<[u8; 32]>())
}
pub fn to_hex(&self) -> String {
let mut hex = String::new();
hex.extend(self.0.iter().map(|byte| format!("{:02x?}", byte)));
hex
}
pub fn from_hex(s: &str) -> Result<Self> {
if s.len() != 64 {
return Err(anyhow::anyhow!("Hex is invalid!"));
}
let mut bytes = [0u8; 32];
for i in 0..32 {
let res = u8::from_str_radix(&s[2 * i..2 * i + 2], 16)?;
bytes[i] = res;
}
Ok(Self(bytes))
}
}
pub(crate) struct File {
pub id: i64,
pub file_id: [u8; 32],
pub parent: i64,
pub f_type: FileType,
pub did: FileDid,
pub parent: i64, // if root directory, parent is 0.
pub root: RootDirectory,
pub name: String,
pub desc: String,
pub device: Vec<i64>,
pub starred: bool,
//pub device: Vec<i64>,
pub datetime: i64,
}
impl File {
pub fn generate(root: RootDirectory, parent: i64, name: String) -> Self {
let did = FileDid::generate();
let start = SystemTime::now();
let datetime = start
.duration_since(UNIX_EPOCH)
.map(|s| s.as_secs())
.unwrap_or(0) as i64; // safe for all life.
Self {
did,
parent,
root,
name,
datetime,
id: 0,
starred: false,
}
}
pub fn storage_name(&self) -> String {
self.did.to_hex()
}
fn _read(&self) -> Vec<u8> {
todo!()
}
fn _write(&self, _bytes: &[u8]) -> Result<()> {
todo!()
}
pub fn to_rpc(&self) -> RpcParam {
json!([
self.id,
self.did.to_hex(),
self.parent,
self.root.to_i64(),
self.name,
self.starred,
self.datetime,
])
}
fn from_values(mut v: Vec<DsValue>) -> Self {
Self {
datetime: v.pop().unwrap().as_i64(),
starred: v.pop().unwrap().as_bool(),
name: v.pop().unwrap().as_string(),
root: RootDirectory::from_i64(v.pop().unwrap().as_i64()),
parent: v.pop().unwrap().as_i64(),
did: FileDid::from_hex(&v.pop().unwrap().as_string()).unwrap_or(Default::default()),
id: v.pop().unwrap().as_i64(),
}
}
pub fn get(db: &DStorage, id: &i64) -> Result<Self> {
let sql = format!(
"SELECT id, did, parent, root, name, starred, datetime FROM files WHERE id = {}",
id
);
let mut matrix = db.query(&sql)?;
if matrix.len() > 0 {
let values = matrix.pop().unwrap(); // safe unwrap()
return Ok(Self::from_values(values));
}
Err(anyhow!("file is missing"))
}
pub fn list(db: &DStorage, root: &RootDirectory, parent: &i64) -> Result<Vec<Self>> {
let sql = if root == &RootDirectory::Star {
format!(
"SELECT id, did, parent, root, name, starred, datetime FROM files WHERE starred = true AND root != {}",
RootDirectory::Trash.to_i64()
)
} else {
format!(
"SELECT id, did, parent, root, name, starred, datetime FROM files WHERE parent = {} AND root = {}",
parent, root.to_i64()
)
};
let matrix = db.query(&sql)?;
let mut files = vec![];
for values in matrix {
files.push(Self::from_values(values));
}
Ok(files)
}
pub fn insert(&mut self, db: &DStorage) -> Result<()> {
let sql = format!(
"INSERT INTO files (did, parent, root, name, starred, device, datetime) VALUES ('{}', {}, {}, '{}', {}, '', {})",
self.did.to_hex(),
self.parent,
self.root.to_i64(),
self.name,
self.starred,
self.datetime,
);
let id = db.insert(&sql)?;
self.id = id;
Ok(())
}
pub fn star(db: &DStorage, id: &i64, starred: bool) -> Result<()> {
let sql = format!("UPDATE files SET starred = {} WHERE id = {}", starred, id);
db.update(&sql)?;
Ok(())
}
pub fn trash(db: &DStorage, id: &i64) -> Result<()> {
let sql = format!(
"UPDATE files SET root = {} WHERE id = {}",
RootDirectory::Trash.to_i64(),
id
);
db.update(&sql)?;
Ok(())
}
pub fn update(&self, db: &DStorage) -> Result<()> {
let sql = format!(
"UPDATE files SET parent = {}, root = {}, name = '{}' WHERE id = {}",
self.parent,
self.root.to_i64(),
self.name,
self.id
);
db.update(&sql)?;
Ok(())
}
}

124
src/apps/file/rpc.rs

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
use std::path::PathBuf;
use std::sync::Arc;
use tdn::types::{
group::GroupId,
@ -6,16 +7,131 @@ use tdn::types::{ @@ -6,16 +7,131 @@ use tdn::types::{
};
use crate::rpc::RpcState;
use crate::storage::{copy_file, file_db, write_file};
use super::models::{File, RootDirectory};
pub(crate) fn new_rpc_handler(handler: &mut RpcHandler<RpcState>) {
handler.add_method("files-echo", |_, params, _| async move {
handler.add_method("dc-echo", |_, params, _| async move {
Ok(HandleResult::rpc(json!(params)))
});
handler.add_method(
"files-folder",
|_gid: GroupId, params: Vec<RpcParam>, _state: Arc<RpcState>| async move {
let _path = params[0].as_str().ok_or(RpcError::ParseError)?;
"dc-list",
|gid: GroupId, params: Vec<RpcParam>, state: Arc<RpcState>| async move {
let root = RootDirectory::from_i64(params[0].as_i64().ok_or(RpcError::ParseError)?);
let parent = params[1].as_i64().ok_or(RpcError::ParseError)?;
let db = file_db(state.layer.read().await.base(), &gid)?;
let files: Vec<RpcParam> = File::list(&db, &root, &parent)?
.iter()
.map(|p| p.to_rpc())
.collect();
Ok(HandleResult::rpc(json!(files)))
},
);
handler.add_method(
"dc-file-create",
|gid: GroupId, params: Vec<RpcParam>, state: Arc<RpcState>| async move {
let root = RootDirectory::from_i64(params[0].as_i64().ok_or(RpcError::ParseError)?);
let parent = params[1].as_i64().ok_or(RpcError::ParseError)?;
let name = params[2].as_str().ok_or(RpcError::ParseError)?.to_owned();
let base = state.group.read().await.base().clone();
let db = file_db(&base, &gid)?;
// genereate new file.
let mut file = File::generate(root, parent, name);
file.insert(&db)?;
// create file on disk.
let _ = write_file(&base, &gid, &file.storage_name(), &[]).await?;
Ok(HandleResult::rpc(file.to_rpc()))
},
);
handler.add_method(
"dc-file-upload",
|gid: GroupId, params: Vec<RpcParam>, state: Arc<RpcState>| async move {
let root = RootDirectory::from_i64(params[0].as_i64().ok_or(RpcError::ParseError)?);
let parent = params[1].as_i64().ok_or(RpcError::ParseError)?;
let path = params[2].as_str().ok_or(RpcError::ParseError)?;
let file_path = PathBuf::from(path);
let name = file_path
.file_name()
.ok_or(RpcError::ParseError)?
.to_str()
.ok_or(RpcError::ParseError)?
.to_owned();
let base = state.group.read().await.base().clone();
let db = file_db(&base, &gid)?;
let mut file = File::generate(root, parent, name);
file.insert(&db)?;
copy_file(&file_path, &base, &gid, &file.storage_name()).await?;
Ok(HandleResult::rpc(file.to_rpc()))
},
);
handler.add_method(
"dc-folder-create",
|gid: GroupId, params: Vec<RpcParam>, state: Arc<RpcState>| async move {
let root = RootDirectory::from_i64(params[0].as_i64().ok_or(RpcError::ParseError)?);
let parent = params[1].as_i64().ok_or(RpcError::ParseError)?;
let name = params[2].as_str().ok_or(RpcError::ParseError)?.to_owned();
// create new folder.
let db = file_db(state.layer.read().await.base(), &gid)?;
let mut file = File::generate(root, parent, name);
file.insert(&db)?;
Ok(HandleResult::rpc(file.to_rpc()))
},
);
handler.add_method(
"dc-file-update",
|gid: GroupId, params: Vec<RpcParam>, state: Arc<RpcState>| async move {
let id = params[0].as_i64().ok_or(RpcError::ParseError)?;
let root = RootDirectory::from_i64(params[1].as_i64().ok_or(RpcError::ParseError)?);
let parent = params[2].as_i64().ok_or(RpcError::ParseError)?;
let name = params[3].as_str().ok_or(RpcError::ParseError)?.to_owned();
let db = file_db(state.layer.read().await.base(), &gid)?;
let mut file = File::get(&db, &id)?;
file.root = root;
file.parent = parent;
file.name = name;
file.update(&db)?;
Ok(HandleResult::rpc(file.to_rpc()))
},
);
handler.add_method(
"dc-file-star",
|gid: GroupId, params: Vec<RpcParam>, state: Arc<RpcState>| async move {
let id = params[0].as_i64().ok_or(RpcError::ParseError)?;
let starred = params[1].as_bool().ok_or(RpcError::ParseError)?;
let db = file_db(state.layer.read().await.base(), &gid)?;
File::star(&db, &id, starred)?;
Ok(HandleResult::new())
},
);
handler.add_method(
"dc-file-trash",
|gid: GroupId, params: Vec<RpcParam>, state: Arc<RpcState>| async move {
let id = params[0].as_i64().ok_or(RpcError::ParseError)?;
// TODO trash a directory.
let db = file_db(state.layer.read().await.base(), &gid)?;
File::trash(&db, &id)?;
Ok(HandleResult::new())
},
);

14
src/event.rs

@ -23,7 +23,7 @@ use crate::migrate::consensus::{ @@ -23,7 +23,7 @@ use crate::migrate::consensus::{
use crate::apps::chat::rpc as chat_rpc;
use crate::apps::chat::{Friend, Message, NetworkMessage, Request};
use crate::apps::file::{FileId, FileType};
use crate::apps::file::{FileDid, RootDirectory};
use crate::rpc;
use crate::storage::{
account_db, chat_db, consensus_db, delete_avatar_sync, read_avatar_sync, write_avatar_sync,
@ -59,16 +59,16 @@ pub(crate) enum InnerEvent { @@ -59,16 +59,16 @@ pub(crate) enum InnerEvent {
/// Session's message delete.
SessionMessageDelete(EventId),
/// create a file.
/// params: file_id, file_parent_id, file_type, file_name, file_desc, device_addr.
FileCreate(FileId, FileId, FileType, String, String, PeerAddr),
/// params: file_id, file_parent_id, file_directory, file_name, file_desc, device_addr.
FileCreate(FileDid, FileDid, RootDirectory, String, String, PeerAddr),
/// update file info. file_id, file_name, file_desc.
FileUpdate(FileId, String, String),
FileUpdate(FileDid, String, String),
/// update file's parent id (move file to other directory).
FileParent(FileId, FileId),
FileParent(FileDid, FileDid),
/// backup file in new device.
FileBackup(FileId, PeerAddr),
FileBackup(FileDid, PeerAddr),
/// delete a file.
FileDelete(FileId),
FileDelete(FileDid),
}
/// Event that not update status. only change UI.

5
src/migrate/file.rs

@ -2,10 +2,11 @@ @@ -2,10 +2,11 @@
pub(super) const FILE_VERSIONS: [&str; 1] = [
"CREATE TABLE IF NOT EXISTS files(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
did TEXT NOT NULL,
parent INTEGER NOT NULL,
f_type INTEGER NOT NULL,
root INTEGER NOT NULL,
name TEXT NOT NULL,
desc TEXT NOT NULL,
starred INTEGER NOT NULL,
device TEXT NOT NULL,
datetime INTEGER NOT NULL);",
];

16
src/storage.rs

@ -57,6 +57,20 @@ pub(crate) async fn read_file(base: &PathBuf) -> Result<Vec<u8>> { @@ -57,6 +57,20 @@ pub(crate) async fn read_file(base: &PathBuf) -> Result<Vec<u8>> {
Ok(fs::read(base).await?)
}
pub(crate) async fn copy_file(
target: &PathBuf,
base: &PathBuf,
gid: &GroupId,
name: &str,
) -> Result<()> {
let mut path = base.clone();
path.push(gid.to_hex());
path.push(FILES_DIR);
path.push(name);
fs::copy(target, path).await?;
Ok(())
}
pub(crate) async fn write_file(
base: &PathBuf,
gid: &GroupId,
@ -347,7 +361,7 @@ pub(crate) fn chat_db(base: &PathBuf, gid: &GroupId) -> Result<DStorage> { @@ -347,7 +361,7 @@ pub(crate) fn chat_db(base: &PathBuf, gid: &GroupId) -> Result<DStorage> {
}
#[inline]
pub(crate) fn _file_db(base: &PathBuf, gid: &GroupId) -> Result<DStorage> {
pub(crate) fn file_db(base: &PathBuf, gid: &GroupId) -> Result<DStorage> {
let mut db_path = base.clone();
db_path.push(gid.to_hex());
db_path.push(FILE_DB);

Loading…
Cancel
Save