236 lines
8.2 KiB
Dart
236 lines
8.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:share/share.dart';
|
|
import 'dart:io';
|
|
import 'dart:convert';
|
|
import '../constants.dart';
|
|
import '../gopher.dart';
|
|
import './text.dart';
|
|
import './image.dart';
|
|
|
|
class GopherPage extends StatefulWidget {
|
|
final Uri uri;
|
|
|
|
const GopherPage({Key key, this.uri}) : super(key: key);
|
|
|
|
createState() => new GopherPageState();
|
|
|
|
static void openURI(BuildContext context, Uri uri) {
|
|
if (!uri.hasScheme || uri.scheme == "gopher") {
|
|
Navigator.push(context, MaterialPageRoute<void>(
|
|
builder: (BuildContext context) {
|
|
return GopherPage(uri: uri);
|
|
},
|
|
));
|
|
} else {
|
|
Scaffold.of(context).showSnackBar(
|
|
const SnackBar(content: Text("URL does not have the gopher scheme")));
|
|
}
|
|
}
|
|
}
|
|
class GopherPageState extends State<GopherPage> {
|
|
var content = <Widget>[];
|
|
var done = false;
|
|
TextStyle tstyle = const TextStyle(fontFamily: 'monospace', fontSize: DEFAULT_FONT_SIZE);
|
|
|
|
GopherPageState() {
|
|
SharedPreferences.getInstance().then((prefs) {
|
|
setState(() { this.tstyle = TextStyle(fontFamily: 'monospace', fontSize: prefs.getDouble('font_size')); });
|
|
});
|
|
}
|
|
|
|
void initState() {
|
|
super.initState();
|
|
_fetchContent(widget.uri);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(generateGopherTitle(widget.uri)),
|
|
actions: <Widget>[
|
|
IconButton(
|
|
icon: Icon(Icons.home),
|
|
tooltip: "go back",
|
|
onPressed: () => Navigator.popUntil(context, ModalRoute.withName('/'))
|
|
),
|
|
IconButton(
|
|
icon: Icon(Icons.share),
|
|
tooltip: "Share URL",
|
|
onPressed: () => Share.share(widget.uri.toString()),
|
|
),
|
|
],
|
|
),
|
|
body: ListView.builder(
|
|
padding: EdgeInsets.all(4.0),
|
|
itemBuilder: (BuildContext context, int index) {
|
|
if (index < content.length) {
|
|
return content[index];
|
|
} else if (!done && content.length == index) {
|
|
return Divider(color: Colors.red);
|
|
} else {
|
|
return null;
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
void _launchURL(BuildContext context, String url) async {
|
|
if (await canLaunch(url)) {
|
|
await launch(url);
|
|
} else {
|
|
Scaffold.of(context)
|
|
.showSnackBar(const SnackBar(content: Text("Unable to open URL")));
|
|
}
|
|
}
|
|
|
|
void _fetchContent(Uri uri) {
|
|
var path = GopherPath(uri.path);
|
|
if (path.type != GopherType.Directory && path.type != GopherType.Search)
|
|
throw "Invalid gopher type, expected 1 or 7";
|
|
Socket.connect(uri.host, uri.port != 0 ? uri.port : 70).then((socket) {
|
|
socket.write(Uri.decodeComponent(path.selector) + "\r\n");
|
|
socket
|
|
.cast<List<int>>()
|
|
.transform(utf8.decoder)
|
|
.transform(const LineSplitter())
|
|
.forEach((line) {
|
|
if (mounted) {
|
|
setState(() {
|
|
if (line.isNotEmpty) {
|
|
var type = line.substring(0, 1);
|
|
var fields = line.substring(1).split("\t");
|
|
|
|
Uri buildUri(String type) {
|
|
return Uri(
|
|
scheme: "gopher",
|
|
host: fields[2],
|
|
path: "/" + type + fields[1],
|
|
port: int.parse(fields[3]));
|
|
}
|
|
|
|
switch (type) {
|
|
case "0":
|
|
content.add(ListTile(
|
|
title: Text(fields[0], style: tstyle),
|
|
trailing: Icon(Icons.short_text),
|
|
onTap: () {
|
|
TextPage.openURI(context, buildUri("0"));
|
|
}));
|
|
break;
|
|
case "1":
|
|
content.add(ListTile(
|
|
title: Text(fields[0], style: tstyle),
|
|
trailing: Icon(Icons.arrow_forward),
|
|
onTap: () {
|
|
GopherPage.openURI(context, buildUri("1"));
|
|
}));
|
|
break;
|
|
case "3":
|
|
content.add(Padding(
|
|
child: ListTile(
|
|
title: Text(fields[0], style: tstyle),
|
|
trailing: Icon(Icons.error, color: Colors.red)),
|
|
padding: EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 2.0),
|
|
));
|
|
break;
|
|
case "7":
|
|
content.add(ListTile(
|
|
title: Text(fields[0], style: tstyle),
|
|
trailing: Icon(Icons.search),
|
|
onTap: () {
|
|
showDialog<void>(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return SimpleDialog(
|
|
title: const Text("Search"),
|
|
contentPadding: const EdgeInsets.fromLTRB(
|
|
12.0, 12.0, 12.0, 16.0),
|
|
children: <Widget>[
|
|
TextField(
|
|
autofocus: true,
|
|
enabled: true,
|
|
enableInteractiveSelection: true,
|
|
onSubmitted: (s) {
|
|
Navigator.of(context).pop(true);
|
|
GopherPage.openURI(
|
|
context,
|
|
Uri(
|
|
host: fields[2],
|
|
path: "/7" + fields[1] + "\t" + s,
|
|
port: int.parse(fields[3]),
|
|
));
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}));
|
|
break;
|
|
case "i":
|
|
content.add(Padding(
|
|
child: Text(
|
|
fields[0],
|
|
style: tstyle,
|
|
softWrap: true,
|
|
),
|
|
padding: EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 2.0),
|
|
));
|
|
break;
|
|
case "h":
|
|
const PREFIX = "URL:";
|
|
content.add(ListTile(
|
|
title: Text(fields[0], style: tstyle),
|
|
trailing: Icon(fields[1].startsWith(PREFIX)
|
|
? Icons.launch
|
|
: Icons.block),
|
|
onTap: () {
|
|
if (fields[1].startsWith(PREFIX)) {
|
|
_launchURL(context, fields[1].substring(4));
|
|
} else {
|
|
Scaffold.of(context).showSnackBar(const SnackBar(
|
|
content: Text("Malformed gopher entry")));
|
|
}
|
|
}));
|
|
break;
|
|
case "g":
|
|
case "G":
|
|
case "i":
|
|
case "I":
|
|
case "p":
|
|
case "P":
|
|
content.add(ListTile(
|
|
title: Text(fields[0], style: tstyle),
|
|
trailing: Icon(Icons.arrow_forward),
|
|
onTap: () {
|
|
ImagePage.openURI(context, buildUri(type));
|
|
}));
|
|
break;
|
|
case ".":
|
|
content.add(Divider(color: Colors.green));
|
|
done = true;
|
|
break;
|
|
default:
|
|
content.add(ListTile(
|
|
title: Text(fields[0], style: tstyle),
|
|
trailing: Stack(
|
|
alignment: AlignmentDirectional.center,
|
|
children: <Widget>[
|
|
Text(type),
|
|
Icon(Icons.block, color: Colors.pink.withAlpha(150)),
|
|
],
|
|
),
|
|
));
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}).catchError((e) { Navigator.pop(context); });
|
|
}
|
|
}
|