2024-04-24 17:36:33 +00:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:graphql/client.dart';
|
|
|
|
import 'package:graphql_flutter/graphql_flutter.dart';
|
|
|
|
import 'package:tstor_ui/api/client.dart';
|
|
|
|
import 'package:tstor_ui/api/fs_entry.graphql.dart';
|
|
|
|
import 'package:tstor_ui/api/torrent.graphql.dart';
|
|
|
|
import 'package:tstor_ui/components/download.dart';
|
2024-04-27 11:00:34 +00:00
|
|
|
import 'package:tstor_ui/components/sliver_header.dart';
|
2024-04-24 17:36:33 +00:00
|
|
|
|
|
|
|
import 'package:tstor_ui/font/t_icons_icons.dart';
|
|
|
|
import 'package:path/path.dart' as p;
|
|
|
|
import 'package:tstor_ui/utils/bytes.dart';
|
|
|
|
|
|
|
|
class FileViewScreen extends StatefulWidget {
|
|
|
|
final String initialPath;
|
|
|
|
|
|
|
|
const FileViewScreen({super.key, this.initialPath = "/"});
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<FileViewScreen> createState() => _FileViewScreenState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _FileViewScreenState extends State<FileViewScreen> {
|
|
|
|
late String path;
|
|
|
|
|
|
|
|
late final TextEditingController pathController;
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
path = widget.initialPath;
|
|
|
|
pathController = TextEditingController(text: path);
|
|
|
|
listDirFuture = client.query$ListDir(
|
|
|
|
Options$Query$ListDir(
|
|
|
|
variables: Variables$Query$ListDir(path: path),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
super.initState();
|
|
|
|
}
|
|
|
|
|
|
|
|
void cd(String part) {
|
|
|
|
setState(() {
|
|
|
|
listDirFuture = null;
|
|
|
|
});
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
path = p.normalize(p.join(path, part));
|
|
|
|
pathController.text = path;
|
|
|
|
listDirFuture = client.query$ListDir(
|
|
|
|
Options$Query$ListDir(
|
|
|
|
variables: Variables$Query$ListDir(path: path),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void refresh() {
|
|
|
|
setState(() {
|
|
|
|
listDirFuture = null;
|
|
|
|
});
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
listDirFuture = client.query$ListDir(
|
|
|
|
Options$Query$ListDir(
|
|
|
|
variables: Variables$Query$ListDir(path: path),
|
|
|
|
fetchPolicy: FetchPolicy.noCache,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<QueryResult<Query$ListDir>>? listDirFuture;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Scaffold(
|
|
|
|
appBar: AppBar(
|
|
|
|
title: TextField(
|
|
|
|
controller: pathController,
|
|
|
|
onEditingComplete: () => cd(pathController.text),
|
|
|
|
),
|
|
|
|
leading: IconButton(
|
|
|
|
onPressed: () => cd(".."),
|
|
|
|
icon: const Icon(Icons.arrow_upward),
|
|
|
|
),
|
|
|
|
actions: [
|
|
|
|
IconButton(
|
|
|
|
icon: const Icon(Icons.refresh),
|
|
|
|
onPressed: refresh,
|
|
|
|
)
|
|
|
|
],
|
|
|
|
),
|
|
|
|
body: FutureBuilder(
|
|
|
|
key: GlobalKey(),
|
|
|
|
future: listDirFuture,
|
|
|
|
initialData: null,
|
|
|
|
builder: (context, snapshot) {
|
|
|
|
if (!snapshot.hasData) {
|
|
|
|
return const Center(child: CircularProgressIndicator());
|
|
|
|
}
|
|
|
|
|
|
|
|
final data = snapshot.data!;
|
|
|
|
|
|
|
|
if (data.exception != null) {
|
|
|
|
return Text("Error\n${data.exception.toString()}");
|
|
|
|
}
|
|
|
|
|
|
|
|
final entry = snapshot.data?.parsedData?.fsEntry;
|
|
|
|
if (entry == null) {
|
|
|
|
return const Center(child: Text("Entry not exists"));
|
|
|
|
}
|
|
|
|
|
|
|
|
final entries = _getEntries(entry);
|
|
|
|
|
|
|
|
if (entries == null || entries.isEmpty) {
|
|
|
|
return const Center(child: Text("Empty dir"));
|
|
|
|
}
|
|
|
|
|
|
|
|
return CustomScrollView(
|
|
|
|
slivers: [
|
2024-04-27 11:00:34 +00:00
|
|
|
EntryHeaderSliver(entry: entry),
|
2024-04-24 17:36:33 +00:00
|
|
|
SliverList.builder(
|
|
|
|
itemCount: entries.length,
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
return DirEntry(
|
|
|
|
entry: entries[index],
|
|
|
|
onTap: (name, isFile) {
|
|
|
|
if (!isFile) {
|
|
|
|
cd(name);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
List<Fragment$DirEntry>? _getEntries(Query$ListDir$fsEntry entry) {
|
|
|
|
switch (entry) {
|
|
|
|
case Query$ListDir$fsEntry$$ArchiveFS entry:
|
|
|
|
return entry.entries;
|
|
|
|
case Query$ListDir$fsEntry$$ResolverFS entry:
|
|
|
|
return entry.entries;
|
|
|
|
case Query$ListDir$fsEntry$$SimpleDir entry:
|
|
|
|
return entry.entries;
|
|
|
|
case Query$ListDir$fsEntry$$TorrentFS entry:
|
|
|
|
return entry.entries;
|
|
|
|
default:
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class DirEntry extends StatelessWidget {
|
|
|
|
final Fragment$DirEntry entry;
|
|
|
|
final void Function(String name, bool isFile) onTap;
|
|
|
|
|
|
|
|
const DirEntry({super.key, required this.entry, required this.onTap});
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
switch (entry) {
|
|
|
|
case Fragment$TorrentDir entry:
|
|
|
|
final completness = entry.torrent.bytesCompleted /
|
|
|
|
(entry.torrent.bytesCompleted + entry.torrent.bytesMissing);
|
|
|
|
|
|
|
|
return ListTile(
|
|
|
|
leading: const Icon(TIcons.bittorrent_bttold_logo),
|
|
|
|
title: Text(entry.name),
|
|
|
|
subtitle: Row(
|
|
|
|
mainAxisSize: MainAxisSize.max,
|
|
|
|
children: [
|
|
|
|
Text("${completness * 100}%"),
|
|
|
|
Expanded(child: LinearProgressIndicator(value: completness)),
|
|
|
|
IconButton(
|
|
|
|
onPressed: () => client.mutate$MarkTorrentDownload(
|
|
|
|
Options$Mutation$MarkTorrentDownload(
|
|
|
|
variables:
|
|
|
|
Variables$Mutation$MarkTorrentDownload(infohash: entry.torrent.infohash),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
icon: const Icon(Icons.download),
|
|
|
|
)
|
|
|
|
],
|
|
|
|
),
|
|
|
|
onTap: () => onTap(entry.name, false),
|
|
|
|
);
|
|
|
|
case Fragment$DirEntry$$SimpleDir entry:
|
|
|
|
return ListTile(
|
|
|
|
leading: const Icon(Icons.folder),
|
|
|
|
title: Text(entry.name),
|
|
|
|
onTap: () => onTap(entry.name, false),
|
|
|
|
);
|
|
|
|
case Fragment$DirEntry$$ArchiveFS entry:
|
|
|
|
return ListTile(
|
|
|
|
leading: const Icon(Icons.folder_zip),
|
|
|
|
title: Text(entry.name),
|
|
|
|
onTap: () => onTap(entry.name, false),
|
|
|
|
);
|
|
|
|
case Fragment$File entry:
|
|
|
|
return ListTile(
|
|
|
|
leading: const Icon(Icons.insert_drive_file),
|
|
|
|
title: Text(entry.name),
|
|
|
|
onTap: () => onTap(entry.name, true),
|
|
|
|
);
|
|
|
|
default:
|
|
|
|
return ListTile(
|
|
|
|
leading: const Icon(Icons.question_mark),
|
|
|
|
title: Text(entry.name),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-27 11:00:34 +00:00
|
|
|
class EntryHeaderSliver extends StatelessWidget {
|
2024-04-24 17:36:33 +00:00
|
|
|
final Query$ListDir$fsEntry entry;
|
|
|
|
|
2024-04-27 11:00:34 +00:00
|
|
|
const EntryHeaderSliver({super.key, required this.entry});
|
2024-04-24 17:36:33 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
switch (entry) {
|
|
|
|
case Query$ListDir$fsEntry$$TorrentFS entry:
|
|
|
|
final total = entry.torrent.bytesCompleted + entry.torrent.bytesMissing;
|
2024-04-27 11:00:34 +00:00
|
|
|
return HideableHeaderSliver(
|
|
|
|
leading: const Icon(TIcons.bittorrent_bttold_logo),
|
2024-04-24 17:36:33 +00:00
|
|
|
body: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
Text(entry.torrent.name),
|
|
|
|
Text("Size: ${total.bytesFormat()}"),
|
|
|
|
Text("InfoHash: ${entry.torrent.infohash}"),
|
|
|
|
DownloadProgress(entry.torrent.bytesCompleted, total)
|
|
|
|
],
|
|
|
|
),
|
|
|
|
actions: [
|
|
|
|
IconButton(
|
|
|
|
icon: const Icon(Icons.download),
|
|
|
|
onPressed: () => client.mutate$MarkTorrentDownload(
|
|
|
|
Options$Mutation$MarkTorrentDownload(
|
|
|
|
variables: Variables$Mutation$MarkTorrentDownload(
|
|
|
|
infohash: entry.torrent.infohash,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
],
|
|
|
|
);
|
|
|
|
|
|
|
|
default:
|
2024-04-27 11:00:34 +00:00
|
|
|
return HideableHeaderSliver(
|
|
|
|
leading: const Icon(Icons.folder),
|
2024-04-24 17:36:33 +00:00
|
|
|
body: Text(entry.name),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|