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/components/sliver_header.dart'; import 'package:tstor_ui/font/t_icons_icons.dart'; import 'package:path/path.dart' as p; import 'package:tstor_ui/screens/torrent_stats.dart'; import 'package:tstor_ui/utils/bytes.dart'; class FileViewScreen extends StatefulWidget { final String initialPath; const FileViewScreen({super.key, this.initialPath = "/"}); @override State createState() => _FileViewScreenState(); } class _FileViewScreenState extends State { 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>? listDirFuture; @override Widget build(BuildContext context) { return PopScope( canPop: false, onPopInvoked: (didPop) { cd(".."); }, child: Scaffold( appBar: AppBar( title: TextField( controller: pathController, onEditingComplete: () => cd(pathController.text), ), leading: IconButton( onPressed: () => cd(".."), icon: const Icon(Icons.arrow_upward), ), actions: [ IconButton( onPressed: () => Navigator.push( context, MaterialPageRoute(builder: (context) => const TorrentStatsScreen()), ), icon: const Icon(Icons.trending_up), ), 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: [ EntryHeaderSliver(entry: entry), SliverList.builder( itemCount: entries.length, itemBuilder: (context, index) { return DirEntry( entry: entries[index], onTap: (name, isFile) { if (!isFile) { cd(name); } }, ); }, ), ], ); }, ), ), ); } } List? _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 EntryHeaderSliver extends StatelessWidget { final Query$ListDir$fsEntry entry; const EntryHeaderSliver({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 HideableHeaderSliver( leading: const Icon(TIcons.bittorrent_bttold_logo), 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 HideableHeaderSliver( leading: const Icon(Icons.folder), body: Text(entry.name), ); } } }