Compare commits
7 Commits
fe745d2ff1
...
f1fa4310e7
Author | SHA1 | Date |
---|---|---|
Valentin Anger | f1fa4310e7 | |
Valentin Anger | 8ad21d54c6 | |
Valentin Anger | bb1d4c844b | |
Valentin Anger | ff98e9a285 | |
Valentin Anger | 248786ca59 | |
Valentin Anger | 5cddec2c7c | |
Valentin Anger | a87261ed4b |
|
@ -0,0 +1 @@
|
|||
const double DEFAULT_FONT_SIZE = 12.0;
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
));},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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); });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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); });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
Reference in New Issue