Cleanup to get back on track
This commit is contained in:
parent
5c285fc082
commit
2221bd1d04
10 changed files with 658 additions and 140 deletions
|
@ -7,6 +7,9 @@
|
||||||
|
|
||||||
# The following line activates a set of recommended lints for Flutter apps,
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
# packages, and plugins designed to encourage good coding practices.
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
analyzer:
|
||||||
|
errors:
|
||||||
|
avoid_print: ignore
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
|
|
184
lib/gopher_browser.dart
Normal file
184
lib/gopher_browser.dart
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
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
|
||||||
|
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;
|
||||||
|
|
||||||
|
void gotoUrl(String link) {
|
||||||
|
if (link == currentUrl) return;
|
||||||
|
setState(() {
|
||||||
|
print("prev: $currentUrl Submitted: $link");
|
||||||
|
currentUrl = link;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
print("GopherBrowseState: $currentUrl");
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TextField(onSubmitted: gotoUrl),
|
||||||
|
Expanded(
|
||||||
|
child: GopherLoader(
|
||||||
|
key: ValueKey(currentUrl),
|
||||||
|
forceMenu: forceMenu,
|
||||||
|
parsedGopherItem: ParsedGopherItem.parseUrl(currentUrl),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_data = widget.parsedGopherItem.load(forceMenu: widget.forceMenu);
|
||||||
|
print("Reiniterd");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
print("did:change: _data $_data");
|
||||||
|
setState(() {
|
||||||
|
_data = widget.parsedGopherItem.load(forceMenu: widget.forceMenu);
|
||||||
|
});
|
||||||
|
|
||||||
|
print('didChangeDependencies, mounted: ${1} done? ');
|
||||||
|
print("$_data");
|
||||||
|
}
|
||||||
|
|
||||||
|
void goToNext(ParsedGopherItem item) {
|
||||||
|
setState(() {
|
||||||
|
print("requesting view of: ${item.url} ${item.documentType}");
|
||||||
|
_data = item.load(forceMenu: false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return const Text("Errored");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ?? ""),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
66
lib/gopher_widgets.dart
Normal file
66
lib/gopher_widgets.dart
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gophershy/gopherlib.dart';
|
||||||
|
|
||||||
|
/// Basic Gopher plaintext widget
|
||||||
|
/// Should be able to display ASCII ART
|
||||||
|
// TODO: separate ASCIIArtTextWidget and PlainTextWidget
|
||||||
|
// First will scroll horizontlay, preserving formating,
|
||||||
|
// Second will wrap lines to make it more readable on phones
|
||||||
|
// TODO: Somehow add option to re-render plaintext as menu view
|
||||||
|
class GopherText extends StatelessWidget {
|
||||||
|
|
||||||
|
final LoadedGopherItem item;
|
||||||
|
|
||||||
|
const GopherText(this.item, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// TODO: Figure out how to stop line wrap
|
||||||
|
// TODO: figure out how to switch between line wrap and no wrap
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: item.data?.length,
|
||||||
|
itemBuilder: (context, int n) {
|
||||||
|
return Text(
|
||||||
|
utf8.decode(item.data![n], allowMalformed: true),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: "SourceCodePro",
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
height: 1.0,
|
||||||
|
// Might need to be tuned for ASCII art
|
||||||
|
letterSpacing: 1,
|
||||||
|
wordSpacing: 1,
|
||||||
|
overflow: TextOverflow.clip
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: BinaryWidget with option of downloading it
|
||||||
|
//TODO: MediaWidget with option for displaying images, maybe playing WAVs?
|
||||||
|
|
||||||
|
/// Shows listing of gopher directory
|
||||||
|
class GopherDirectory extends StatelessWidget {
|
||||||
|
|
||||||
|
final LoadedGopherItem parsedDirectory;
|
||||||
|
final Function(ParsedGopherItem clickedOn) onItemClick;
|
||||||
|
|
||||||
|
const GopherDirectory({super.key, required this.parsedDirectory, required this.onItemClick});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: parsedDirectory.asMenu!.length,
|
||||||
|
itemBuilder: (context, int n) {
|
||||||
|
final item = parsedDirectory.asMenu![n];
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => item.documentType == DocumentType.Informational ? null : onItemClick(item),
|
||||||
|
child: Text(item.name ?? "err"));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
220
lib/gopherlib.dart
Normal file
220
lib/gopherlib.dart
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
enum DocumentType {
|
||||||
|
/*Canonical types RFC1436 line 548*/
|
||||||
|
/// Item is a file (0)
|
||||||
|
TextFile("0"),
|
||||||
|
|
||||||
|
/// Item is a directory (1)
|
||||||
|
Directory("1"),
|
||||||
|
|
||||||
|
/// Item is a CSO phone-book server (2)
|
||||||
|
CSO("2"),
|
||||||
|
|
||||||
|
/// Error returned by server (3)
|
||||||
|
ErrorCode("3"),
|
||||||
|
|
||||||
|
/// Item is a BinHexed Macintosh file. (4)
|
||||||
|
BinHex("4"),
|
||||||
|
|
||||||
|
/// Item is DOS binary archive of some sort. (5)
|
||||||
|
DosBinary("5"),
|
||||||
|
|
||||||
|
/// Item is a UNIX uuencoded file. (6)
|
||||||
|
Uuencoded("6"),
|
||||||
|
|
||||||
|
/// Item is an Index-Search server. (7)
|
||||||
|
Search("7"),
|
||||||
|
|
||||||
|
/// Item points to a text-based telnet session. (8)
|
||||||
|
Telnet("8"),
|
||||||
|
|
||||||
|
/// Item is a binary file! (9)
|
||||||
|
Binary("9"),
|
||||||
|
|
||||||
|
/// It em is a redundant server (*)
|
||||||
|
MirrorOrAlternate("*"),
|
||||||
|
|
||||||
|
/// Item points to a text-based tn3270 session. (T)
|
||||||
|
Telnet3270("T"),
|
||||||
|
|
||||||
|
/// Item is a GIF format graphics file. (g)
|
||||||
|
Gif("g"),
|
||||||
|
|
||||||
|
/// Item is some kind of image file. Client decides how to display. (I)
|
||||||
|
Image("I"),
|
||||||
|
/**** Gopher+ ****/
|
||||||
|
/// Bitmap image (:)
|
||||||
|
Bitmap(":"),
|
||||||
|
|
||||||
|
/// Movie file (;)
|
||||||
|
Movie(";"),
|
||||||
|
|
||||||
|
/// Sound file (<)
|
||||||
|
Soundfile("<"),
|
||||||
|
/*non-canonical types*/
|
||||||
|
/// Doc. Used for .doc and .pdf (d)
|
||||||
|
Doc("d"),
|
||||||
|
|
||||||
|
/// HTML file (h)
|
||||||
|
Html("h"),
|
||||||
|
|
||||||
|
/// Informational message, widely used (i)
|
||||||
|
Informational("i"),
|
||||||
|
|
||||||
|
/// Usually png image (p)
|
||||||
|
PngImage("p"),
|
||||||
|
|
||||||
|
/// Rich text document (r)
|
||||||
|
RichTextDocument("r"),
|
||||||
|
|
||||||
|
/// usually WAV (s)
|
||||||
|
SoundFile("s"),
|
||||||
|
|
||||||
|
/// Pdf (P)
|
||||||
|
Pdf("P"),
|
||||||
|
|
||||||
|
/// XML file (X)
|
||||||
|
Xml("X"),
|
||||||
|
|
||||||
|
/// Document type isn't any of the known types
|
||||||
|
Unknown("?");
|
||||||
|
|
||||||
|
const DocumentType(this.code);
|
||||||
|
final String code;
|
||||||
|
|
||||||
|
/// Returns the canonical name of the document type
|
||||||
|
String get typeName => name;
|
||||||
|
|
||||||
|
/// Returns character code used for this document type
|
||||||
|
String get typeCode => code;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ParsedGopherItem {
|
||||||
|
static final docMap = {for (var item in DocumentType.values) item.code: item};
|
||||||
|
|
||||||
|
Uri url;
|
||||||
|
String? name;
|
||||||
|
DocumentType documentType;
|
||||||
|
|
||||||
|
ParsedGopherItem._(this.url, this.name, this.documentType);
|
||||||
|
|
||||||
|
/// Parse gopher item from url, from url file type is not known
|
||||||
|
/// so it is guessed by file extension
|
||||||
|
// TODO: Do actuall filetype guessing
|
||||||
|
static ParsedGopherItem parseUrl(String url) {
|
||||||
|
if (!url.startsWith("gopher://")) url = "gopher://$url";
|
||||||
|
var uri = Uri.parse(url);
|
||||||
|
|
||||||
|
// We expect TextFile by default unless we think it's menu
|
||||||
|
final doctype = (uri.path == "" || uri.path.endsWith("/")) ? DocumentType.Directory : DocumentType.TextFile;
|
||||||
|
|
||||||
|
if (uri.port == 0) uri = uri.replace(port: 70);
|
||||||
|
|
||||||
|
return ParsedGopherItem._(uri, "", doctype);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses single gopher item from single menu line
|
||||||
|
static ParsedGopherItem? parseMenuLine(String menuLine) {
|
||||||
|
// If theres not even the item type and name we won't bother
|
||||||
|
if (menuLine.length < 2) return null;
|
||||||
|
|
||||||
|
var parts = menuLine.split("\t");
|
||||||
|
|
||||||
|
if (parts.length != 4) return null;
|
||||||
|
|
||||||
|
// If string is correct, we should have 4 parts
|
||||||
|
// 0Pony Prices Prices/Ponies pserver.bookstore.umn.edu 70
|
||||||
|
//type;name ; selektor ; host ; port
|
||||||
|
// 0;Pony Prices; Prices/Ponies; pserver.bookstore.umn.edu; 70
|
||||||
|
var uri = Uri(
|
||||||
|
scheme: "gopher",
|
||||||
|
path: parts[1],
|
||||||
|
host: parts[2],
|
||||||
|
port: int.tryParse(parts[3]) ?? 70,
|
||||||
|
);
|
||||||
|
|
||||||
|
String type = menuLine[0];
|
||||||
|
String name = parts[0].substring(1);
|
||||||
|
|
||||||
|
final docType = docMap.containsKey(type) ? docMap[type]! : docMap["?"]!;
|
||||||
|
return ParsedGopherItem._(uri, name, docType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads gopher directory/menu from host:port and optionally selector
|
||||||
|
static Future<LoadedGopherItem> loadMenu(String host, int port,
|
||||||
|
{String selector = ""}) async {
|
||||||
|
|
||||||
|
final Socket sock = await Socket.connect(host, port);
|
||||||
|
sock.write("$selector\r\n");
|
||||||
|
await sock.flush();
|
||||||
|
print("Entered load menu");
|
||||||
|
final ret = <ParsedGopherItem>[];
|
||||||
|
final List<Uint8List> rawRet = <Uint8List>[];
|
||||||
|
final subscription = sock.listen(
|
||||||
|
(Uint8List data) {
|
||||||
|
rawRet.add(data);
|
||||||
|
String rcvd = utf8.decode(data, allowMalformed: true);
|
||||||
|
|
||||||
|
ParsedGopherItem? apend;
|
||||||
|
// TODO: This is flawed, it usually works but it's just a matter of time before it breaks
|
||||||
|
// If \n is in the next buffer, the part before \n gets cutoff
|
||||||
|
// resulting in unparsable line
|
||||||
|
// the substrining must begin outside the loop listen call
|
||||||
|
// or it must be rewritten using LineSplitter()
|
||||||
|
int start = 0;
|
||||||
|
for (int i = 0; i < rcvd.length; i++) {
|
||||||
|
if (rcvd[i] == "\n") {
|
||||||
|
var menuLine = rcvd.substring(start, i);
|
||||||
|
// TODO: Gophernicus sends .\r for some reason which doesnt seem acording to spec
|
||||||
|
// Maybe ignore when sent from menu=forced?
|
||||||
|
if (menuLine.startsWith(".")) {
|
||||||
|
print("Received starting dot, this should be the end I guess");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
apend = parseMenuLine(menuLine);
|
||||||
|
if (apend != null) ret.add(apend);
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await subscription.asFuture<void>();
|
||||||
|
sock.close();
|
||||||
|
return LoadedGopherItem(
|
||||||
|
ParsedGopherItem._(Uri.parse("gopher://$host:$port$selector"), selector,
|
||||||
|
DocumentType.Directory),
|
||||||
|
asMenu: ret,
|
||||||
|
data: rawRet,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<LoadedGopherItem> loadItem() async {
|
||||||
|
final Socket sock = await Socket.connect(url.host, url.port);
|
||||||
|
|
||||||
|
print("Sending request to ${url.host}[:${url.port}] => '${url.path}'");
|
||||||
|
sock.write("${url.path}\r\n");
|
||||||
|
var data = await sock.toList();
|
||||||
|
print("Closing: ${sock.close()} as type ${this.documentType}");
|
||||||
|
|
||||||
|
return LoadedGopherItem(this, data: data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<LoadedGopherItem> load({bool? forceMenu}) async {
|
||||||
|
if ((documentType == DocumentType.Directory) || (forceMenu ?? false)) {
|
||||||
|
print("Forcing? ($forceMenu) reload as menu for: $url");
|
||||||
|
return loadMenu(url.host, url.port, selector: url.path);
|
||||||
|
}
|
||||||
|
return loadItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadedGopherItem {
|
||||||
|
final ParsedGopherItem parsed;
|
||||||
|
final List<Uint8List>? data;
|
||||||
|
final List<ParsedGopherItem>? asMenu;
|
||||||
|
|
||||||
|
LoadedGopherItem(this.parsed, {this.data, this.asMenu});
|
||||||
|
}
|
131
lib/main.dart
131
lib/main.dart
|
@ -1,125 +1,40 @@
|
||||||
|
// Warnings ignored for development, we can spit 'const' everywhere
|
||||||
|
// once it will matter
|
||||||
|
// ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:gophershy/gopher_browser.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const GopherShy());
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class GopherShy extends StatelessWidget {
|
||||||
const MyApp({super.key});
|
const GopherShy({super.key});
|
||||||
|
|
||||||
// This widget is the root of your application.
|
// This widget is the root of your application.
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Flutter Demo',
|
title: 'GopherShy',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
// This is the theme of your application.
|
|
||||||
//
|
|
||||||
// TRY THIS: Try running your application with "flutter run". You'll see
|
|
||||||
// the application has a purple toolbar. Then, without quitting the app,
|
|
||||||
// try changing the seedColor in the colorScheme below to Colors.green
|
|
||||||
// and then invoke "hot reload" (save your changes or press the "hot
|
|
||||||
// reload" button in a Flutter-supported IDE, or press "r" if you used
|
|
||||||
// the command line to start the app).
|
|
||||||
//
|
|
||||||
// Notice that the counter didn't reset back to zero; the application
|
|
||||||
// state is not lost during the reload. To reset the state, use hot
|
|
||||||
// restart instead.
|
|
||||||
//
|
|
||||||
// This works for code too, not just values: Most code changes can be
|
|
||||||
// tested with just a hot reload.
|
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
),
|
),
|
||||||
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
home: SafeArea(
|
||||||
);
|
child: Scaffold(
|
||||||
}
|
appBar: PreferredSize(
|
||||||
}
|
preferredSize: Size.fromHeight(50),
|
||||||
|
child: Text(
|
||||||
class MyHomePage extends StatefulWidget {
|
"Whoaaaaa",
|
||||||
const MyHomePage({super.key, required this.title});
|
style: TextStyle(fontSize: 50),
|
||||||
|
),
|
||||||
// This widget is the home page of your application. It is stateful, meaning
|
),
|
||||||
// that it has a State object (defined below) that contains fields that affect
|
// In future replace with bookmark view
|
||||||
// how it looks.
|
body: GopherBrowser(initialUrl: "gopher://treebrary.org"),
|
||||||
|
)),
|
||||||
// This class is the configuration for the state. It holds the values (in this
|
|
||||||
// case the title) provided by the parent (in this case the App widget) and
|
|
||||||
// used by the build method of the State. Fields in a Widget subclass are
|
|
||||||
// always marked "final".
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<MyHomePage> createState() => _MyHomePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
|
||||||
int _counter = 0;
|
|
||||||
|
|
||||||
void _incrementCounter() {
|
|
||||||
setState(() {
|
|
||||||
// This call to setState tells the Flutter framework that something has
|
|
||||||
// changed in this State, which causes it to rerun the build method below
|
|
||||||
// so that the display can reflect the updated values. If we changed
|
|
||||||
// _counter without calling setState(), then the build method would not be
|
|
||||||
// called again, and so nothing would appear to happen.
|
|
||||||
_counter++;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
// This method is rerun every time setState is called, for instance as done
|
|
||||||
// by the _incrementCounter method above.
|
|
||||||
//
|
|
||||||
// The Flutter framework has been optimized to make rerunning build methods
|
|
||||||
// fast, so that you can just rebuild anything that needs updating rather
|
|
||||||
// than having to individually change instances of widgets.
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
// TRY THIS: Try changing the color here to a specific color (to
|
|
||||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
|
||||||
// change color while the other colors stay the same.
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
|
||||||
// Here we take the value from the MyHomePage object that was created by
|
|
||||||
// the App.build method, and use it to set our appbar title.
|
|
||||||
title: Text(widget.title),
|
|
||||||
),
|
|
||||||
body: Center(
|
|
||||||
// Center is a layout widget. It takes a single child and positions it
|
|
||||||
// in the middle of the parent.
|
|
||||||
child: Column(
|
|
||||||
// Column is also a layout widget. It takes a list of children and
|
|
||||||
// arranges them vertically. By default, it sizes itself to fit its
|
|
||||||
// children horizontally, and tries to be as tall as its parent.
|
|
||||||
//
|
|
||||||
// Column has various properties to control how it sizes itself and
|
|
||||||
// how it positions its children. Here we use mainAxisAlignment to
|
|
||||||
// center the children vertically; the main axis here is the vertical
|
|
||||||
// axis because Columns are vertical (the cross axis would be
|
|
||||||
// horizontal).
|
|
||||||
//
|
|
||||||
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
|
|
||||||
// action in the IDE, or press "p" in the console), to see the
|
|
||||||
// wireframe for each widget.
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
const Text(
|
|
||||||
'You have pushed the button this many times:',
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'$_counter',
|
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: _incrementCounter,
|
|
||||||
tooltip: 'Increment',
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
), // This trailing comma makes auto-formatting nicer for build methods.
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
181
pubspec.lock
181
pubspec.lock
|
@ -49,6 +49,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -67,39 +83,35 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
gopherlib:
|
flutter_web_plugins:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description: flutter
|
||||||
path: "."
|
source: sdk
|
||||||
ref: stable
|
version: "0.0.0"
|
||||||
resolved-ref: "2e6bf1902e9f21fdd59f2033196f984460814559"
|
|
||||||
url: "https://git.treebrary.org/Felisp/gopherlib.git"
|
|
||||||
source: git
|
|
||||||
version: "0.0.1"
|
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.0"
|
version: "10.0.5"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "3.0.5"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "3.0.1"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -120,18 +132,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.0"
|
version: "0.11.1"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.0"
|
version: "1.15.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -140,6 +152,102 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.0"
|
||||||
|
path_provider_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_linux
|
||||||
|
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
path_provider_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_platform_interface
|
||||||
|
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
path_provider_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_windows
|
||||||
|
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.4"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
shared_preferences:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shared_preferences
|
||||||
|
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.3"
|
||||||
|
shared_preferences_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_android
|
||||||
|
sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.2"
|
||||||
|
shared_preferences_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_foundation
|
||||||
|
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
|
shared_preferences_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_linux
|
||||||
|
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
shared_preferences_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_platform_interface
|
||||||
|
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
shared_preferences_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_web
|
||||||
|
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
|
shared_preferences_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_windows
|
||||||
|
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -189,10 +297,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.1"
|
version: "0.7.2"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -205,9 +313,34 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "13.0.0"
|
version: "14.2.5"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.1"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.5.4"
|
||||||
|
xdg_directories:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xdg_directories
|
||||||
|
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.3.4 <=3.4.0"
|
dart: ">=3.4.0 <4.0.0"
|
||||||
|
flutter: ">=3.19.0"
|
||||||
|
|
13
pubspec.yaml
13
pubspec.yaml
|
@ -27,10 +27,7 @@ environment:
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
gopherlib:
|
shared_preferences: ^2.2.3
|
||||||
git:
|
|
||||||
url: https://git.treebrary.org/Felisp/gopherlib.git
|
|
||||||
ref: stable # Development will follow alongside
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -70,10 +67,10 @@ flutter:
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
# list giving the asset and other descriptors for the font. For
|
# list giving the asset and other descriptors for the font. For
|
||||||
# example:
|
# example:
|
||||||
# fonts:
|
fonts:
|
||||||
# - family: Schyler
|
- family: SourceCodePro
|
||||||
# fonts:
|
fonts:
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
- asset: res/fonts/SourceCodePro-VariableFont_wght.ttf
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
# style: italic
|
# style: italic
|
||||||
# - family: Trajan Pro
|
# - family: Trajan Pro
|
||||||
|
|
BIN
res/fonts/Domine-VariableFont_wght.ttf
Normal file
BIN
res/fonts/Domine-VariableFont_wght.ttf
Normal file
Binary file not shown.
BIN
res/fonts/SourceCodePro-Medium.ttf
Normal file
BIN
res/fonts/SourceCodePro-Medium.ttf
Normal file
Binary file not shown.
BIN
res/fonts/SourceCodePro-VariableFont_wght.ttf
Normal file
BIN
res/fonts/SourceCodePro-VariableFont_wght.ttf
Normal file
Binary file not shown.
Loading…
Reference in a new issue