import 'package:flutter/material.dart'; import 'package:gophershy/gopherlib.dart'; import 'package:gophershy/gopher_widgets.dart'; class _GopherBrowserState extends State { // 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 _urlStack = List.empty(growable: true); @override void initState() { super.initState(); _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 createState() => _GopherBrowserState(); } class _GopherLoaderState extends State { Future? _data; final List _loadedHistory = List.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.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( 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 createState() => _GopherLoaderState(); } class MenuItem extends StatelessWidget { static const Map _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 ?? ""), ], ), ], ), ); } }