diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d29021..ace0476 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,6 +7,9 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. +analyzer: + errors: + avoid_print: ignore include: package:flutter_lints/flutter.yaml linter: diff --git a/lib/gopher_browser.dart b/lib/gopher_browser.dart new file mode 100644 index 0000000..7272a82 --- /dev/null +++ b/lib/gopher_browser.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:gophershy/gopherlib.dart'; +import 'package:gophershy/gopher_widgets.dart'; + +class _GopherBrowserState extends State { + // 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 createState() => _GopherBrowserState(); +} + +class _GopherLoaderState extends State { + Future? _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( + 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 createState() => _GopherLoaderState(); +} + +class MenuItem extends StatelessWidget { + static const Map _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 ?? ""), + ], + ), + ], + ), + ); + } +} diff --git a/lib/gopher_widgets.dart b/lib/gopher_widgets.dart new file mode 100644 index 0000000..0c73dca --- /dev/null +++ b/lib/gopher_widgets.dart @@ -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")); + }, + ); + } +} + + diff --git a/lib/gopherlib.dart b/lib/gopherlib.dart new file mode 100644 index 0000000..9ce1f3a --- /dev/null +++ b/lib/gopherlib.dart @@ -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 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 = []; + final List rawRet = []; + 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(); + sock.close(); + return LoadedGopherItem( + ParsedGopherItem._(Uri.parse("gopher://$host:$port$selector"), selector, + DocumentType.Directory), + asMenu: ret, + data: rawRet, + ); + } + + Future 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 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? data; + final List? asMenu; + + LoadedGopherItem(this.parsed, {this.data, this.asMenu}); +} diff --git a/lib/main.dart b/lib/main.dart index 8e94089..f5e2f53 100644 --- a/lib/main.dart +++ b/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 'dart:io'; + +import 'package:gophershy/gopher_browser.dart'; void main() { - runApp(const MyApp()); + runApp(const GopherShy()); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class GopherShy extends StatelessWidget { + const GopherShy({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'GopherShy', 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), useMaterial3: true, ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // 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 - // how it looks. - - // 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 createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - 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: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], + home: SafeArea( + child: Scaffold( + appBar: PreferredSize( + preferredSize: Size.fromHeight(50), + child: Text( + "Whoaaaaa", + style: TextStyle(fontSize: 50), + ), ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. + // In future replace with bookmark view + body: GopherBrowser(initialUrl: "gopher://treebrary.org"), + )), ); } } diff --git a/pubspec.lock b/pubspec.lock index c11ed4a..e8aa334 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,22 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: flutter @@ -67,39 +83,35 @@ packages: description: flutter source: sdk version: "0.0.0" - gopherlib: - dependency: "direct main" - description: - path: "." - ref: stable - resolved-ref: "2e6bf1902e9f21fdd59f2033196f984460814559" - url: "https://git.treebrary.org/Felisp/gopherlib.git" - source: git - version: "0.0.1" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -120,18 +132,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" path: dependency: transitive description: @@ -140,6 +152,102 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: flutter @@ -189,10 +297,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" vector_math: dependency: transitive description: @@ -205,9 +313,34 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" 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: - dart: ">=3.3.4 <=3.4.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index 02df6d8..be65b97 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,10 +27,7 @@ environment: dependencies: flutter: sdk: flutter - gopherlib: - git: - url: https://git.treebrary.org/Felisp/gopherlib.git - ref: stable # Development will follow alongside + shared_preferences: ^2.2.3 dev_dependencies: flutter_test: @@ -70,11 +67,11 @@ flutter: # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf + fonts: + - family: SourceCodePro + fonts: + - asset: res/fonts/SourceCodePro-VariableFont_wght.ttf + # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: diff --git a/res/fonts/Domine-VariableFont_wght.ttf b/res/fonts/Domine-VariableFont_wght.ttf new file mode 100644 index 0000000..2fe5de9 Binary files /dev/null and b/res/fonts/Domine-VariableFont_wght.ttf differ diff --git a/res/fonts/SourceCodePro-Medium.ttf b/res/fonts/SourceCodePro-Medium.ttf new file mode 100644 index 0000000..af3f57c Binary files /dev/null and b/res/fonts/SourceCodePro-Medium.ttf differ diff --git a/res/fonts/SourceCodePro-VariableFont_wght.ttf b/res/fonts/SourceCodePro-VariableFont_wght.ttf new file mode 100644 index 0000000..19bb671 Binary files /dev/null and b/res/fonts/SourceCodePro-VariableFont_wght.ttf differ