update
This commit is contained in:
parent
5591f145a9
commit
d8ee8a3a24
166 changed files with 15431 additions and 889 deletions
ui/lib/screens
55
ui/lib/screens/downloads.dart
Normal file
55
ui/lib/screens/downloads.dart
Normal file
|
@ -0,0 +1,55 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:tstor_ui/api/client.dart';
|
||||
import 'package:tstor_ui/api/torrent.graphql.dart';
|
||||
import 'package:tstor_ui/components/download.dart';
|
||||
|
||||
class DownloadsScreen extends StatefulWidget {
|
||||
const DownloadsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DownloadsScreen> createState() => _DownloadsScreenState();
|
||||
}
|
||||
|
||||
class _DownloadsScreenState extends State<DownloadsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: client.query$ListTorrents(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final torrents = snapshot.data!.parsedData!.torrents;
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: torrents.length,
|
||||
itemBuilder: (context, index) {
|
||||
final torrent = torrents[index];
|
||||
return ListTile(
|
||||
title: Text(torrent.name),
|
||||
subtitle: DownloadProgress(
|
||||
torrent.bytesCompleted, torrent.bytesCompleted + torrent.bytesMissing),
|
||||
trailing: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => client.mutate$MarkTorrentDownload(
|
||||
Options$Mutation$MarkTorrentDownload(
|
||||
variables: Variables$Mutation$MarkTorrentDownload(
|
||||
infohash: torrent.infohash,
|
||||
),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.download),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
352
ui/lib/screens/file_view.dart
Normal file
352
ui/lib/screens/file_view.dart
Normal file
|
@ -0,0 +1,352 @@
|
|||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue