Compare commits

...

7 Commits

Author SHA1 Message Date
Valentin Anger f1fa4310e7 Support a few more gopherholes 2018-12-08 22:09:31 +01:00
Valentin Anger 8ad21d54c6 Add a share button to pages 2018-12-08 22:09:20 +01:00
Valentin Anger bb1d4c844b Pop the page from navigation on a network error 2018-12-08 22:08:33 +01:00
Valentin Anger ff98e9a285 Actually initialize state in initState
This removes the parameter to the state constructors.

Also remove the outdated comment.
2018-12-08 22:06:36 +01:00
Valentin Anger 248786ca59 Add settings and a setting for font size 2018-12-08 21:53:24 +01:00
Valentin Anger 5cddec2c7c Generate Widgets for one page once 2018-12-08 16:47:31 +01:00
Valentin Anger a87261ed4b Fix autoreplace gone slightly wrong 2018-12-08 16:14:51 +01:00
7 changed files with 275 additions and 118 deletions

1
lib/constants.dart Normal file
View File

@ -0,0 +1 @@
const double DEFAULT_FONT_SIZE = 12.0;

View File

@ -13,7 +13,7 @@ class GopherPath {
GopherPath(String path) {
if (path.substring(0,1) != "/")
throw "Path does not start with /<_type>";
throw "Path does not start with /<type>";
switch (path.substring(1,2)) {
case "0":
_type = GopherType.Text;

View File

@ -1,11 +1,25 @@
import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';
import 'package:shared_preferences/shared_preferences.dart';
import './page/gopher.dart';
import './page/settings.dart';
import './constants.dart';
import './gopher.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
MyApp() {
SharedPreferences.getInstance().then((prefs) {
var init = prefs.getBool("initial_settings");
if (init == null || !init) {
prefs.setDouble("font_size", DEFAULT_FONT_SIZE);
prefs.setBool("initial_settings", true);
}
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
@ -85,7 +99,18 @@ class _MyHomePageState extends State<MyHomePage> {
"Creators favorites:",
),
_createEntry("Bitreich", "gopher://bitreich.org/1"),
_createEntry("Floodgap", "gopher://gopher.floodgap.com/1"),
_createEntry("Gopherpedia", "gopher://gopherpedia.com/1"),
_createEntry("HackerNews mirror", "gopher://hngopher.com/1"),
const Divider(),
ListTile(
title: Text("Settings"),
onTap: () { Navigator.push(context, MaterialPageRoute<void>(
builder: (BuildContext context) {
return SettingsPage();
},
));},
),
],
),
);

View File

@ -1,17 +1,19 @@
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';
class GopherPage extends StatefulWidget {
final Uri uri;
final TextStyle tstyle = const TextStyle(fontFamily: 'monospace', fontSize: 12.0);
const GopherPage({Key key, this.uri}) : super(key: key);
createState() => new GopherPageState(uri: uri);
createState() => new GopherPageState();
static void openURI(BuildContext context, Uri uri) {
if (!uri.hasScheme || uri.scheme == "gopher") {
@ -21,128 +23,51 @@ class GopherPage extends StatefulWidget {
},
));
} else {
Scaffold.of(context).showSnackBar(const SnackBar(content: Text("URL does not have the gopher scheme")));
Scaffold.of(context).showSnackBar(
const SnackBar(content: Text("URL does not have the gopher scheme")));
}
}
}
class GopherPageState extends State<GopherPage> {
var content = <String>[];
var content = <Widget>[];
var done = false;
TextStyle tstyle = const TextStyle(fontFamily: 'monospace', fontSize: DEFAULT_FONT_SIZE);
GopherPageState({Uri uri}) {
_fetchContent(uri);
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)),
),
body: ListView.builder(
appBar: AppBar(
title: Text(generateGopherTitle(widget.uri)),
actions: <Widget>[
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) {
// TODO Create is at insertion time, this way we don't need to recreate them on scrolling
if (index < content.length) {
var line = content[index];
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":
return ListTile(
title: Text(fields[0], style: widget.tstyle),
trailing: Icon(Icons.short_text),
onTap: () { TextPage.openURI(context, buildUri("0")); }
);
case "1":
return ListTile(
title: Text(fields[0], style: widget.tstyle),
trailing: Icon(Icons.arrow_forward),
onTap: () { GopherPage.openURI(context, buildUri("1")); }
);
case "3":
return Padding(
child: ListTile(title: Text(fields[0], style: widget.tstyle), trailing: Icon(Icons.error, color: Colors.red)),
padding: EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 2.0),
);
case "7":
return ListTile(title: Text(fields[0], style: widget.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]),
));
},
),
],
);
},
);
}
);
case "i":
return Padding(
child: Text(
fields[0],
style: widget.tstyle,
softWrap: true,
),
padding: EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 2.0),
);
case "h":
const PREFIX = "URL:";
return ListTile(
title: Text(fields[0], style: widget.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")));
}
});
// case "g":
// case "I":
// TODO ImagePage
case ".":
return Divider(color: Colors.green);
default:
return ListTile(title: Text(fields[0], style: widget.tstyle),
trailing: Stack(
alignment: AlignmentDirectional.center,
children: <Widget>[
Text(type),
Icon(Icons.block, color: Colors.pink.withAlpha(150)),
],
),
);
}
} else if (content.length == index && (content.length == 0 || content.last != ".")) {
return content[index];
} else if (!done && content.length == index) {
return Divider(color: Colors.red);
} else {
return null;
}
}
),
}),
);
}
@ -150,7 +75,8 @@ class GopherPageState extends State<GopherPage> {
if (await canLaunch(url)) {
await launch(url);
} else {
Scaffold.of(context).showSnackBar(const SnackBar(content: Text("Unable to open URL")));
Scaffold.of(context)
.showSnackBar(const SnackBar(content: Text("Unable to open URL")));
}
}
@ -160,13 +86,133 @@ class GopherPageState extends State<GopherPage> {
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.transform(utf8.decoder).transform(const LineSplitter()).forEach((line) {
socket
.transform(utf8.decoder)
.transform(const LineSplitter())
.forEach((line) {
if (mounted) {
setState(() {
if (!line.isEmpty) content.add(line);
if (!line.isEmpty) {
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 "I":
// TODO ImagePage
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); });
}
}

66
lib/page/settings.dart Normal file
View File

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({Key key}) : super(key: key);
@override
_SettingsPageState createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
SharedPreferences prefs;
// The controllers are stored in the state so that,
// when the Widget is rebuild, the cursors do not jump to the beginning of the input field.
TextEditingController font_size_contr = TextEditingController();
_SettingsPageState() {
SharedPreferences.getInstance().then((prefs_a) {
setState(() {
prefs = prefs_a;
});
});
}
@override
Widget build(BuildContext context) {
if (prefs == null)
return Scaffold(
appBar: AppBar(
title: const Text("Settings"),
),
body: const Text("Loading..."),
);
var f_s = prefs.getDouble('font_size').toString();
if (font_size_contr.text != f_s)
font_size_contr.text = f_s;
return Scaffold(
appBar: AppBar(
title: const Text("Settings"),
),
body: Center(
child: ListView(
padding: EdgeInsets.all(8.0),
children: <Widget>[
const Text(
"Font size:",
),
TextField(
enabled: true,
enableInteractiveSelection: true,
keyboardType: TextInputType.number,
controller: font_size_contr,
onSubmitted: (s) {
try {
var f_s = double.parse(s);
prefs.setDouble('font_size', f_s);
} on FormatException catch (e) {}
},
),
const Divider(),
],
),
),
);
}
}

View File

@ -1,15 +1,17 @@
import 'package:flutter/material.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';
class TextPage extends StatefulWidget {
final Uri uri;
final TextStyle tstyle = const TextStyle(fontFamily: 'monospace');
const TextPage({Key key, this.uri}) : super(key: key);
createState() => new TextPageState(uri: uri);
createState() => new TextPageState();
static void openURI(BuildContext context, Uri uri) {
if (!uri.hasScheme || uri.scheme == "gopher") {
@ -25,9 +27,17 @@ class TextPage extends StatefulWidget {
}
class TextPageState extends State<TextPage> {
String content = "";
TextStyle tstyle = const TextStyle(fontFamily: 'monospace', fontSize: DEFAULT_FONT_SIZE);
TextPageState({Uri uri}) {
_fetchContent(uri);
TextPageState() {
SharedPreferences.getInstance().then((prefs) {
setState(() { this.tstyle = TextStyle(fontFamily: 'monospace', fontSize: prefs.getDouble('font_size')); });
});
}
void initState() {
super.initState();
_fetchContent(widget.uri);
}
@override
@ -35,9 +45,16 @@ class TextPageState extends State<TextPage> {
return Scaffold(
appBar: AppBar(
title: Text(generateGopherTitle(widget.uri)),
actions: <Widget>[
IconButton(
icon: Icon(Icons.share),
tooltip: "Share URL",
onPressed: () => Share.share(widget.uri.toString()),
),
],
),
body: SingleChildScrollView( // TODO Extend to width of context
child: Text(content, style: widget.tstyle, softWrap: true),
child: Text(content, style: tstyle, softWrap: true),
padding: EdgeInsets.all(8.0),
scrollDirection: Axis.vertical,
),
@ -57,6 +74,6 @@ class TextPageState extends State<TextPage> {
});
}
});
});
}).catchError((e) { Navigator.pop(context); });
}
}

View File

@ -21,6 +21,8 @@ dependencies:
cupertino_icons: ^0.1.2
url_launcher: ^4.0.2
uni_links: ^0.1.4
shared_preferences: ^0.4.3
share: ^0.5.3
dev_dependencies:
flutter_test: