tstor/ui/lib/screens/file_view.dart
royalcat d8ee8a3a24
All checks were successful
docker / build-docker (linux/amd64) (push) Successful in 1m34s
docker / build-docker (linux/386) (push) Successful in 1m37s
docker / build-docker (linux/arm64/v8) (push) Successful in 7m37s
docker / build-docker (linux/arm64) (push) Successful in 7m44s
docker / build-docker (linux/arm/v7) (push) Successful in 8m12s
update
2024-04-24 20:36:33 +03:00

352 lines
9.5 KiB
Dart

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';
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: [
EntryInfoSliver(entry: entry),
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),
);
}
}
}
class EntryInfoSliver extends StatelessWidget {
final Query$ListDir$fsEntry entry;
const EntryInfoSliver({super.key, required this.entry});
@override
Widget build(BuildContext context) {
switch (entry) {
case Query$ListDir$fsEntry$$TorrentFS entry:
final total = entry.torrent.bytesCompleted + entry.torrent.bytesMissing;
return EntryInfoHeader(
icon: TIcons.bittorrent_bttold_logo,
title: Text(entry.torrent.name),
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:
return EntryInfoHeader(
icon: Icons.folder,
title: Text(entry.name),
body: Text(entry.name),
);
}
}
}
class EntryInfoHeader extends StatelessWidget {
final IconData icon;
final Widget title;
final Widget body;
final List<Widget>? actions;
const EntryInfoHeader({
super.key,
required this.icon,
required this.title,
required this.body,
this.actions,
});
@override
Widget build(BuildContext context) {
return SliverPersistentHeader(
floating: true,
pinned: false,
delegate: EntryInfoSliverHeaderDelegate(icon: icon, title: title, body: body),
);
}
}
class EntryInfoSliverHeaderDelegate extends SliverPersistentHeaderDelegate {
final IconData icon;
final Widget title;
final Widget body;
final List<Widget>? actions;
final double size;
const EntryInfoSliverHeaderDelegate({
required this.icon,
required this.title,
required this.body,
this.actions,
this.size = 150,
});
@override
double get maxExtent => size;
@override
double get minExtent => size;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => true;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final content = [
Icon(icon, size: 50),
Expanded(child: body),
];
if (actions != null && actions!.isNotEmpty) {
content.add(ButtonBar(children: actions!));
}
final appBarTheme = AppBarTheme.of(context);
final colorScheme = Theme.of(context).colorScheme;
final onTop = (shrinkOffset == 0);
return Material(
color:
onTop ? appBarTheme.backgroundColor ?? colorScheme.surface : colorScheme.surfaceContainer,
elevation: onTop ? 0 : appBarTheme.elevation ?? 3,
surfaceTintColor: appBarTheme.surfaceTintColor ?? colorScheme.surfaceTint,
child: ClipRect(
child: SizedBox(
height: maxExtent,
child: Column(
children: [
const Spacer(),
Row(
children: content,
),
const Spacer(),
const Divider(
height: 1,
thickness: 1,
),
],
),
),
),
);
}
}