2024-11-05 01:30:46 +01:00
|
|
|
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
|
2024-11-07 01:51:58 +01:00
|
|
|
// This may not be the most common use case so maybe hide it in some hayburger menu
|
2024-11-05 01:30:46 +01:00
|
|
|
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;
|
|
|
|
|
2024-11-07 01:51:58 +01:00
|
|
|
/// 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();
|
|
|
|
_currentItem = ParsedGopherItem.parseUrl(widget.initialUrl);
|
|
|
|
}
|
|
|
|
|
|
|
|
late ParsedGopherItem _currentItem;
|
2024-11-05 01:30:46 +01:00
|
|
|
void gotoUrl(String link) {
|
2024-11-07 01:51:58 +01:00
|
|
|
print("GoTo: $link from: $currentUrl");
|
2024-11-05 01:30:46 +01:00
|
|
|
if (link == currentUrl) return;
|
|
|
|
setState(() {
|
2024-11-07 01:51:58 +01:00
|
|
|
print("prev: Submitted: $link");
|
|
|
|
_currentItem = ParsedGopherItem.parseUrl(link);
|
|
|
|
_urlStack.add(_currentItem);
|
2024-11-05 01:30:46 +01:00
|
|
|
currentUrl = link;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
print("GopherBrowseState: $currentUrl");
|
2024-11-07 01:51:58 +01:00
|
|
|
return Builder(builder: (context) {
|
|
|
|
return Column(
|
|
|
|
children: [
|
|
|
|
TextField(
|
|
|
|
onSubmitted: gotoUrl,
|
|
|
|
),
|
|
|
|
Expanded(
|
2024-11-05 01:30:46 +01:00
|
|
|
child: GopherLoader(
|
2024-11-07 01:51:58 +01:00
|
|
|
key: ValueKey(currentUrl),
|
|
|
|
forceMenu: forceMenu,
|
|
|
|
parsedGopherItem: _currentItem,
|
|
|
|
)),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
});
|
2024-11-05 01:30:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
2024-11-07 01:51:58 +01:00
|
|
|
final List<LoadedGopherItem> _loadedHistory =
|
|
|
|
List<LoadedGopherItem>.empty(growable: true);
|
|
|
|
|
2024-11-05 01:30:46 +01:00
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
2024-11-07 01:51:58 +01:00
|
|
|
_data = widget.parsedGopherItem.load(forceMenu: widget.forceMenu).then(
|
|
|
|
(value) {
|
|
|
|
print("Pushed beacuse of initState");
|
|
|
|
return _onLoadFinished(value);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
print("Reinitred GopherLoaderState");
|
2024-11-05 01:30:46 +01:00
|
|
|
}
|
|
|
|
|
2024-11-07 01:51:58 +01:00
|
|
|
LoadedGopherItem _onLoadFinished(LoadedGopherItem data) {
|
|
|
|
_loadedHistory.add(data);
|
|
|
|
print("Added to histor: ${_loadedHistory.length}");
|
|
|
|
return data;
|
2024-11-05 01:30:46 +01:00
|
|
|
}
|
|
|
|
|
2024-11-07 01:51:58 +01:00
|
|
|
void _goToNext(ParsedGopherItem item, {String? why}) {
|
|
|
|
print("Going to next: $why");
|
2024-11-05 01:30:46 +01:00
|
|
|
setState(() {
|
|
|
|
print("requesting view of: ${item.url} ${item.documentType}");
|
2024-11-07 01:51:58 +01:00
|
|
|
_data = item.load(forceMenu: false).then(
|
|
|
|
(value) {
|
|
|
|
print("Pushed beacuse of gotoNext");
|
|
|
|
return _onLoadFinished(value);
|
|
|
|
},
|
|
|
|
);
|
2024-11-05 01:30:46 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-11-07 01:51:58 +01:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-05 01:30:46 +01:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2024-11-07 01:51:58 +01:00
|
|
|
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),
|
|
|
|
};
|
2024-11-05 01:30:46 +01:00
|
|
|
} else {
|
2024-11-07 01:51:58 +01:00
|
|
|
return Text("Errored ${snap.error}");
|
2024-11-05 01:30:46 +01:00
|
|
|
}
|
2024-11-07 01:51:58 +01:00
|
|
|
}
|
|
|
|
}),
|
|
|
|
);
|
2024-11-05 01:30:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 ?? ""),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|