GopherShy/lib/gopher_browser.dart

232 lines
7.4 KiB
Dart

import 'package:flutter/material.dart';
import 'package:gophershy/gopherlib.dart';
import 'package:gophershy/gopher_widgets.dart';
class _GopherBrowserState extends State<GopherBrowser> {
// TODO: when is this updated? how is the widget.initialUrl at play?
// TODO: We must have button: Menu! to attempt to render any text file as menu
// Because when we visit from direct url we don't know what file type we get
// Other detection methods could be applied later
// This may not be the most common use case so maybe hide it in some hayburger menu
String currentUrl = "gopher://treebrary.org";
// Directory listing doesn't have to end with / so we can never
// be sure whether we're visiting menu or not.
// When true, we try to parse anything as menu
bool forceMenu = false;
/// Only links to session history, will be much bigger than [_loadedStack]
// Probably remove from here
final List<ParsedGopherItem> _urlStack = List<ParsedGopherItem>.empty(growable: true);
@override
void initState() {
super.initState();
print("Started with: ${widget.initialUrl}");
_currentItem = ParsedGopherItem.parseUrl(widget.initialUrl);
}
late ParsedGopherItem _currentItem;
void gotoUrl(String link) {
print("GoTo: $link from: $currentUrl");
if (link == currentUrl) return;
setState(() {
print("prev: Submitted: $link");
_currentItem = ParsedGopherItem.parseUrl(link);
_urlStack.add(_currentItem);
currentUrl = link;
});
}
@override
Widget build(BuildContext context) {
print("GopherBrowseState: $currentUrl");
return Builder(builder: (context) {
return Column(
children: [
TextField(
onSubmitted: gotoUrl,
),
Expanded(
child: GopherLoader(
key: ValueKey(currentUrl),
forceMenu: forceMenu,
parsedGopherItem: _currentItem,
)),
],
);
});
}
}
class GopherBrowser extends StatefulWidget {
final String initialUrl;
const GopherBrowser({super.key, required this.initialUrl});
@override
State<GopherBrowser> createState() => _GopherBrowserState();
}
class _GopherLoaderState extends State<GopherLoader> {
Future<LoadedGopherItem>? _data;
final List<LoadedGopherItem> _loadedHistory =
List<LoadedGopherItem>.empty(growable: true);
@override
void initState() {
super.initState();
_data = widget.parsedGopherItem.load(forceMenu: widget.forceMenu).then(
(value) {
print("Pushed beacuse of initState");
return _onLoadFinished(value);
},
);
print("Reinitred GopherLoaderState");
}
LoadedGopherItem _onLoadFinished(LoadedGopherItem data) {
_loadedHistory.add(data);
print("Added to histor: ${_loadedHistory.length}");
return data;
}
void _goToNext(ParsedGopherItem item, {String? why}) {
print("Going to next: $why");
setState(() {
print("requesting view of: ${item.url} ${item.documentType}");
_data = item.load(forceMenu: false).then(
(value) {
print("Pushed beacuse of gotoNext");
return _onLoadFinished(value);
},
);
});
}
void _onBackButton(bool popable, object) {
print("Gonna pop i guess");
// TODO: toast if at the end of history, make time stamp and quit if pressed back button quickly again
// TODO: Setting on whether back button goes up a directory or back in navigation stack -> may be the same but might not be
if (_loadedHistory.length > 1) {
_loadedHistory.removeLast();
setState(() {
_data = Future<LoadedGopherItem>.delayed(
Duration.zero, () => _loadedHistory.last);
});
} else {
// TODO: Toast here
print("At the end of stack");
}
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: _onBackButton,
child: FutureBuilder<LoadedGopherItem>(
future: _data,
builder: (cont, snap) {
print("${snap.connectionState}");
if (snap.connectionState != ConnectionState.done) {
return const CircularProgressIndicator();
} else {
if (snap.hasData) {
print(
"data & doctype: ${snap.data!}, ${snap.data!.parsed.documentType}");
return switch (snap.data!.parsed.documentType) {
DocumentType.TextFile => GopherText(snap.data!),
DocumentType.Directory => GopherDirectory(
parsedDirectory: snap.data!,
onItemClick: _goToNext,
),
_ => Container(color: Colors.yellow),
};
} else {
return Text("Errored ${snap.error}");
}
}
}),
);
}
}
class GopherLoader extends StatefulWidget {
final ParsedGopherItem parsedGopherItem;
final bool? forceMenu;
const GopherLoader(
{super.key, required this.parsedGopherItem, this.forceMenu});
@override
State<GopherLoader> createState() => _GopherLoaderState();
}
class MenuItem extends StatelessWidget {
static const Map<DocumentType, IconData> _iconMap = {
DocumentType.TextFile: Icons.short_text,
DocumentType.Directory: Icons.folder,
DocumentType.CSO: Icons.contact_page,
DocumentType.ErrorCode: Icons.error,
DocumentType.BinHex: Icons.precision_manufacturing_sharp,
DocumentType.DosBinary: Icons.precision_manufacturing_rounded,
DocumentType.Uuencoded: Icons.precision_manufacturing_outlined,
DocumentType.Search: Icons.search_rounded,
DocumentType.Telnet: Icons.smart_screen_sharp,
DocumentType.Telnet3270: Icons.smart_screen_sharp,
DocumentType.Binary: Icons.file_download,
DocumentType.MirrorOrAlternate: Icons.content_copy,
DocumentType.Gif: Icons.animation,
DocumentType.Image: Icons.image,
DocumentType.Bitmap: Icons.picture_as_pdf,
DocumentType.Movie: Icons.local_movies,
DocumentType.Soundfile: Icons.music_note,
DocumentType.Doc: Icons.document_scanner,
DocumentType.Html: Icons.html,
DocumentType.Informational: Icons.info,
DocumentType.PngImage: Icons.hide_image_sharp,
DocumentType.RichTextDocument: Icons.format_color_text,
DocumentType.SoundFile: Icons.library_music,
DocumentType.Pdf: Icons.picture_as_pdf,
DocumentType.Xml: Icons.html_rounded,
DocumentType.Unknown: Icons.question_mark,
};
IconData _getIcon(DocumentType forWhat) {
return _iconMap[forWhat] ?? Icons.question_mark_sharp;
}
final ParsedGopherItem item;
const MenuItem(this.item, {super.key});
void _itemTap(BuildContext cont) {
Navigator.push(cont, MaterialPageRoute(builder: (cont) {
if (item.documentType == DocumentType.Directory) {
return const Text("it was a directory");
} else {
return Container();
}
}));
}
@override
Widget build(BuildContext context) {
// TODO: Something something virtual item here as well
if (item.documentType == DocumentType.Unknown) {
return Text(item.name ?? "?");
}
// Put it into GestureDetector and on click open new route loading given item
return GestureDetector(
onTap: () => _itemTap(context),
child: Row(
children: [
Icon(_getIcon(item.documentType)),
Column(
children: [
Text(item.name ?? ""),
],
),
],
),
);
}
}