It is working now yay ¯\_(ツ)_/¯

This commit is contained in:
Konstantin Ullrich 2021-04-10 22:24:21 +02:00
parent 39aed2715e
commit cab4f6279d
No known key found for this signature in database
GPG Key ID: E9562A013280F5DB
9 changed files with 557 additions and 114 deletions

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lokinet_lib/lokinet_lib.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(MyApp());
@ -11,6 +12,7 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Lokinet App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
// This is the theme of your application.
//
@ -55,6 +57,7 @@ class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
final key = new GlobalKey<ScaffoldState>();
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
@ -62,6 +65,7 @@ class _MyHomePageState extends State<MyHomePage> {
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
key: key,
appBar: AppBar(
// 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.
@ -96,6 +100,9 @@ class _MyHomePageState extends State<MyHomePage> {
child: Text('Bootstrap'),
),
onPressed: () async {
final sharedPreferences = await SharedPreferences.getInstance();
sharedPreferences.setInt(
'test', 30);
await LokinetLib.bootstrapLokinet();
},
),
@ -111,6 +118,24 @@ class _MyHomePageState extends State<MyHomePage> {
},
),
Divider(),
TextButton(
child: Padding(
padding: EdgeInsets.all(10),
child: Text('Is this thing on?'),
),
onPressed: () async {
if (await LokinetLib.isRunning) {
key.currentState.showSnackBar(new SnackBar(
content: new Text('Yes!'),
));
} else {
key.currentState.showSnackBar(new SnackBar(
content: new Text('No!'),
));
}
},
),
Divider(),
TextButton(
child: Padding(
padding: EdgeInsets.all(10),

View File

@ -0,0 +1,101 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:oxen_service_node/src/utils/theme/theme_changer.dart';
import 'package:oxen_service_node/src/utils/theme/themes.dart';
import 'package:provider/provider.dart';
class OxenAppBar extends StatelessWidget
implements ObstructingPreferredSizeWidget {
factory OxenAppBar(
{BuildContext context,
Widget leading,
Widget middle,
Widget trailing,
Color backgroundColor}) {
final _themeChanger = Provider.of<ThemeChanger>(context);
final _isDarkTheme = _themeChanger.theme == Themes.darkTheme;
return OxenAppBar._internal(
leading: leading,
middle: middle,
trailing: trailing,
height: _height,
backgroundColor:
_isDarkTheme ? Theme.of(context).backgroundColor : backgroundColor);
}
factory OxenAppBar.withShadow(
{BuildContext context,
Widget leading,
Widget middle,
Widget trailing,
Color backgroundColor}) {
final _themeChanger = Provider.of<ThemeChanger>(context);
final _isDarkTheme = _themeChanger.theme == Themes.darkTheme;
return OxenAppBar._internal(
leading: leading,
middle: middle,
trailing: trailing,
height: 80,
backgroundColor:
_isDarkTheme ? Theme.of(context).backgroundColor : backgroundColor,
decoration: BoxDecoration(
color: _isDarkTheme
? Theme.of(context).backgroundColor
: backgroundColor,
boxShadow: [
BoxShadow(
color: Color.fromRGBO(132, 141, 198, 0.11),
blurRadius: 8,
offset: Offset(0, 2))
]),
);
}
OxenAppBar._internal(
{this.leading,
this.middle,
this.trailing,
this.backgroundColor,
this.decoration,
this.height = _height});
static const _originalHeight = 44.0; // iOS nav bar height
static const _height = 60.0;
final Widget leading;
final Widget middle;
final Widget trailing;
final Color backgroundColor;
final BoxDecoration decoration;
final double height;
@override
Widget build(BuildContext context) {
final pad = height - _originalHeight;
final paddingTop = pad / 2;
final _paddingBottom = (pad / 2);
return Container(
decoration: decoration ?? BoxDecoration(color: backgroundColor),
padding:
EdgeInsetsDirectional.only(bottom: _paddingBottom, top: paddingTop),
child: CupertinoNavigationBar(
leading: leading,
middle: middle,
trailing: trailing,
backgroundColor: backgroundColor,
border: null,
),
);
}
@override
Size get preferredSize => Size.fromHeight(height);
@override
bool shouldFullyObstruct(BuildContext context) {
return false;
}
}

View File

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:oxen_service_node/src/utils/theme/palette.dart';
class OxenTextField extends StatelessWidget {
OxenTextField(
{this.enabled = true,
this.hintText,
this.keyboardType,
this.controller,
this.validator,
this.inputFormatters,
this.prefixIcon,
this.suffixIcon,
this.focusNode});
final bool enabled;
final String hintText;
final TextInputType keyboardType;
final TextEditingController controller;
final String Function(String) validator;
final List<TextInputFormatter> inputFormatters;
final Widget prefixIcon;
final Widget suffixIcon;
final FocusNode focusNode;
@override
Widget build(BuildContext context) {
return TextFormField(
onFieldSubmitted: (_) => FocusScope.of(context).unfocus(),
enabled: enabled,
controller: controller,
focusNode: focusNode,
style: TextStyle(
fontSize: 18.0,
color: Theme.of(context).accentTextTheme.overline.color),
keyboardType: keyboardType,
inputFormatters: inputFormatters,
decoration: InputDecoration(
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
hintStyle:
TextStyle(fontSize: 18.0, color: Theme.of(context).hintColor),
hintText: hintText,
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: OxenPalette.teal, width: 2.0)),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).focusColor, width: 1.0)),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(color: OxenPalette.red, width: 1.0)),
focusedErrorBorder: OutlineInputBorder(
borderSide: BorderSide(color: OxenPalette.red, width: 1.0)),
errorStyle: TextStyle(color: OxenPalette.red)),
validator: validator);
}
}

View File

@ -0,0 +1,122 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:oxen_service_node/src/utils/theme/theme_changer.dart';
import 'package:oxen_service_node/src/utils/theme/themes.dart';
import 'package:provider/provider.dart';
import 'oxen/oxen_app_bar.dart';
enum AppBarStyle { regular, withShadow }
abstract class OxenBasePage extends StatelessWidget {
String get title => null;
bool get isModalBackButton => false;
Color get backgroundColor => Colors.white;
bool get resizeToAvoidBottomPadding => true;
AppBarStyle get appBarStyle => AppBarStyle.regular;
void onClose(BuildContext context) => Navigator.of(context).pop();
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
Widget leading(BuildContext context) {
if (ModalRoute.of(context).isFirst) {
return null;
}
final _backButton = Icon(Icons.arrow_back_ios_sharp, size: 25);
final _closeButton = Icon(Icons.close_sharp, size: 25);
return SizedBox(
height: 37,
width: isModalBackButton ? 37 : 20,
child: ButtonTheme(
minWidth: double.minPositive,
child: FlatButton(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
padding: EdgeInsets.all(0),
onPressed: () => onClose(context),
child: isModalBackButton ? _closeButton : _backButton),
),
);
}
Widget middle(BuildContext context) {
return title == null
? null
: Text(
title,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w600,
color: Theme.of(context).primaryTextTheme.headline6.color),
);
}
Widget trailing(BuildContext context) => null;
Widget floatingActionButton(BuildContext context) => null;
ObstructingPreferredSizeWidget appBar(BuildContext context) {
final _themeChanger = Provider.of<ThemeChanger>(context);
final _isDarkTheme = _themeChanger.theme == Themes.darkTheme;
switch (appBarStyle) {
case AppBarStyle.regular:
return OxenAppBar(
context: context,
leading: leading(context),
middle: middle(context),
trailing: trailing(context),
backgroundColor: _isDarkTheme
? Theme.of(context).backgroundColor
: backgroundColor);
case AppBarStyle.withShadow:
return OxenAppBar.withShadow(
context: context,
leading: leading(context),
middle: middle(context),
trailing: trailing(context),
backgroundColor: _isDarkTheme
? Theme.of(context).backgroundColor
: backgroundColor);
default:
return OxenAppBar(
context: context,
leading: leading(context),
middle: middle(context),
trailing: trailing(context),
backgroundColor: _isDarkTheme
? Theme.of(context).backgroundColor
: backgroundColor);
}
}
Widget body(BuildContext context);
Widget bottomNavigationBar(BuildContext context) => null;
@override
Widget build(BuildContext context) {
final _themeChanger = Provider.of<ThemeChanger>(context);
final _isDarkTheme = _themeChanger.theme == Themes.darkTheme;
return Scaffold(
key: scaffoldKey,
backgroundColor:
_isDarkTheme ? Theme.of(context).backgroundColor : backgroundColor,
resizeToAvoidBottomPadding: resizeToAvoidBottomPadding,
appBar: appBar(context),
body: SafeArea(child: body(context)),
floatingActionButton: floatingActionButton(context),
bottomNavigationBar: bottomNavigationBar(context),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat);
}
}

View File

@ -2,6 +2,8 @@ package network.loki.lokinet;
import android.content.Intent;
import android.net.VpnService;
import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.util.Log;
@ -11,6 +13,8 @@ public class LokinetDaemon extends VpnService {
public static final String ACTION_CONNECT = "network.loki.lokinet.START";
public static final String ACTION_DISCONNECT = "network.loki.lokinet.STOP";
public static final String LOG_TAG = "LokinetDaemon";
public static final String MESSAGE_CHANNEL = "LOKINET_DAEMON";
static {
System.loadLibrary("lokinet-android");
@ -34,8 +38,6 @@ public class LokinetDaemon extends VpnService {
private static native String DetectFreeRange();
public static final String LOG_TAG = "LokinetDaemon";
ByteBuffer impl = null;
ParcelFileDescriptor iface;
int m_FD = -1;
@ -48,8 +50,8 @@ public class LokinetDaemon extends VpnService {
@Override
public void onDestroy() {
super.onDestroy();
disconnect();
super.onDestroy();
disconnect();
}
@Override
@ -60,96 +62,118 @@ public class LokinetDaemon extends VpnService {
disconnect();
return START_NOT_STICKY;
} else {
if (!IsRunning()) {
if (impl != null) {
Free(impl);
impl = null;
}
impl = Obtain();
if (impl == null) {
Log.e(LOG_TAG, "got nullptr when creating llarp::Context in jni");
return START_NOT_STICKY;
}
String dataDir = getFilesDir().toString();
LokinetConfig config;
try {
config = new LokinetConfig(dataDir);
} catch (RuntimeException ex) {
Log.e(LOG_TAG, ex.toString());
return START_NOT_STICKY;
}
// FIXME: make these configurable
String exitNode = "exit.loki";
String upstreamDNS = "1.1.1.1";
String ourRange = DetectFreeRange();
if (ourRange.isEmpty()) {
Log.e(LOG_TAG, "cannot detect free range");
return START_NOT_STICKY;
}
// set up config values
config.AddDefaultValue("network", "exit-node", exitNode);
config.AddDefaultValue("network", "ifaddr", ourRange);
config.AddDefaultValue("dns", "upstream", upstreamDNS);
if (!config.Load()) {
Log.e(LOG_TAG, "failed to load (or create) config file at: " + dataDir + "/loki.network.loki.lokinet.ini");
return START_NOT_STICKY;
}
VpnService.Builder builder = new VpnService.Builder();
builder.setMtu(1500);
String[] parts = ourRange.split("/");
String ourIP = parts[0];
int ourMask = Integer.parseInt(parts[1]);
builder.addAddress(ourIP, ourMask);
builder.addRoute("0.0.0.0", 0);
builder.addDnsServer(upstreamDNS);
builder.setSession("Lokinet");
builder.setConfigureIntent(null);
iface = builder.establish();
if (iface == null) {
Log.e(LOG_TAG, "VPN Interface from builder.establish() came back null");
return START_NOT_STICKY;
}
m_FD = iface.detachFd();
InjectVPNFD();
new Thread(() -> {
Configure(config);
m_UDPSocket = GetUDPSocket();
protect(m_UDPSocket);
Mainloop();
}).start();
Log.d(LOG_TAG, "started successfully!");
} else {
Log.d(LOG_TAG, "already running");
}
return START_STICKY;
boolean connectedSucessfully = connect("exit.loki");
if (connectedSucessfully)
return START_STICKY;
else
return START_NOT_STICKY;
}
}
private boolean connect(String exitNode) {
if (!IsRunning()) {
if (impl != null) {
Free(impl);
impl = null;
}
impl = Obtain();
if (impl == null) {
Log.e(LOG_TAG, "got nullptr when creating llarp::Context in jni");
return false;
}
String dataDir = getFilesDir().toString();
LokinetConfig config;
try {
config = new LokinetConfig(dataDir);
} catch (RuntimeException ex) {
Log.e(LOG_TAG, ex.toString());
return false;
}
// FIXME: make these configurable
// String exitNode = "exit.loki";
String upstreamDNS = "1.1.1.1";
String ourRange = DetectFreeRange();
if (ourRange.isEmpty()) {
Log.e(LOG_TAG, "cannot detect free range");
return false;
}
// set up config values
config.AddDefaultValue("network", "exit-node", exitNode);
config.AddDefaultValue("network", "ifaddr", ourRange);
config.AddDefaultValue("dns", "upstream", upstreamDNS);
if (!config.Load()) {
Log.e(LOG_TAG, "failed to load (or create) config file at: " + dataDir + "/loki.network.loki.lokinet.ini");
return false;
}
VpnService.Builder builder = new VpnService.Builder();
builder.setMtu(1500);
String[] parts = ourRange.split("/");
String ourIP = parts[0];
int ourMask = Integer.parseInt(parts[1]);
builder.addAddress(ourIP, ourMask);
builder.addRoute("0.0.0.0", 0);
builder.addDnsServer(upstreamDNS);
builder.setSession("Lokinet");
builder.setConfigureIntent(null);
iface = builder.establish();
if (iface == null) {
Log.e(LOG_TAG, "VPN Interface from builder.establish() came back null");
return false;
}
m_FD = iface.detachFd();
InjectVPNFD();
new Thread(() -> {
Configure(config);
m_UDPSocket = GetUDPSocket();
protect(m_UDPSocket);
Mainloop();
}).start();
Log.d(LOG_TAG, "started successfully!");
} else {
Log.d(LOG_TAG, "already running");
}
return true;
}
private void disconnect() {
Log.d(LOG_TAG, "Before STOP");
if (IsRunning()) {
Stop();
}
Log.d(LOG_TAG, "After STOP");
if (impl != null) {
Free(impl);
impl = null;
}
Log.d(LOG_TAG, "After FREE");
// if (impl != null) {
// Free(impl);
// impl = null;
// }
}
/**
* Class for clients to access. Because we know this service always
* runs in the same process as its clients, we don't need to deal with
* IPC.
*/
public class LocalBinder extends Binder {
public LokinetDaemon getService() {
return LokinetDaemon.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private final IBinder mBinder = new LocalBinder();
}

View File

@ -1,8 +1,13 @@
package io.oxen.lokinet_lib
import android.app.Activity.RESULT_OK
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.Intent
import android.content.SharedPreferences
import android.net.VpnService
import android.util.Log
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
@ -12,9 +17,17 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.PluginRegistry
import network.loki.lokinet.LokinetDaemon
import android.content.IntentFilter
import android.widget.Toast
import android.content.ComponentName
import android.os.IBinder
import android.content.ServiceConnection
/** LokinetLibPlugin */
class LokinetLibPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private var mShouldUnbind: Boolean = false
private var mBoundService: LokinetDaemon? = null
private lateinit var activityBinding: ActivityPluginBinding
/// The MethodChannel that will the communication between Flutter and native Android
@ -23,19 +36,19 @@ class LokinetLibPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
/// when the Flutter Engine is detached from the Activity
private lateinit var channel: MethodChannel
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
System.loadLibrary("lokinet-android")
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "lokinet_lib")
channel = MethodChannel(binding.binaryMessenger, "lokinet_lib")
channel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
doUnbindService()
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) {
when (call.method) {
"prepare" -> {
val intent = VpnService.prepare(activityBinding.activity.applicationContext)
@ -47,7 +60,7 @@ class LokinetLibPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
} else {
result.success(false)
}
listener?.let { activityBinding.removeActivityResultListener(it) };
listener?.let { activityBinding.removeActivityResultListener(it) }
true
}
activityBinding.addActivityResultListener(listener)
@ -73,16 +86,25 @@ class LokinetLibPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
lokinetIntent.action = LokinetDaemon.ACTION_CONNECT
activityBinding.activity.applicationContext.startService(lokinetIntent)
doBindService()
result.success(true);
result.success(true)
}
"disconnect" -> {
val lokinetIntent = Intent(activityBinding.activity.applicationContext, LokinetDaemon::class.java)
lokinetIntent.action = LokinetDaemon.ACTION_DISCONNECT
activityBinding.activity.applicationContext.startService(lokinetIntent)
doBindService()
result.success(true);
result.success(true)
}
"isRunning" -> {
if (mBoundService != null) {
result.success(mBoundService!!.IsRunning())
} else {
result.success(false)
}
}
else -> result.notImplemented()
}
@ -99,4 +121,36 @@ class LokinetLibPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
}
override fun onDetachedFromActivityForConfigChanges() {}
private val mConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
mBoundService = (service as LokinetDaemon.LocalBinder).getService()
}
override fun onServiceDisconnected(className: ComponentName) {
mBoundService = null
}
}
fun doBindService() {
if (activityBinding.activity.applicationContext.bindService(
Intent(activityBinding.activity.applicationContext, LokinetDaemon::class.java),
mConnection, Context.BIND_AUTO_CREATE
)
) {
mShouldUnbind = true
} else {
Log.e(
LokinetDaemon.LOG_TAG, "Error: The requested service doesn't exist, or this client isn't allowed access to it."
)
}
}
fun doUnbindService() {
if (mShouldUnbind) {
activityBinding.activity.applicationContext.unbindService(mConnection)
mShouldUnbind = false
}
}
}

View File

@ -36,4 +36,9 @@ class LokinetLib {
final bool disconnect = await _channel.invokeMethod('disconnect');
return disconnect;
}
static Future<bool> get isRunning async {
final bool isRunning = await _channel.invokeMethod('isRunning');
return isRunning;
}
}

View File

@ -21,42 +21,42 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0-nullsafety.1"
version: "2.5.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
version: "2.1.0"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.3"
version: "1.1.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.2.0"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
version: "1.1.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0-nullsafety.3"
version: "1.15.0"
convert:
dependency: transitive
description:
@ -84,7 +84,7 @@ packages:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.2.0"
ffi:
dependency: transitive
description:
@ -116,6 +116,11 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
image:
dependency: transitive
description:
@ -130,6 +135,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
lokinet_lib:
dependency: "direct main"
description:
@ -143,21 +155,21 @@ packages:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10-nullsafety.1"
version: "0.12.10"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
version: "1.3.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.1"
version: "1.8.0"
path_provider:
dependency: transitive
description:
@ -221,6 +233,48 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.13"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.12+4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.2+4"
shared_preferences_macos:
dependency: transitive
description:
name: shared_preferences_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1+11"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2+7"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.2+3"
sky_engine:
dependency: transitive
description: flutter
@ -232,56 +286,56 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.2"
version: "1.8.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0-nullsafety.1"
version: "1.10.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
version: "1.1.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.2.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.19-nullsafety.2"
version: "0.2.19"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
version: "1.3.0"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.3"
version: "2.1.0"
win32:
dependency: transitive
description:
@ -311,5 +365,5 @@ packages:
source: hosted
version: "2.2.1"
sdks:
dart: ">=2.10.0-110 <2.11.0"
flutter: ">=1.20.0 <2.0.0"
dart: ">=2.12.0-0.0 <3.0.0"
flutter: ">=1.20.0"

View File

@ -25,6 +25,7 @@ dependencies:
sdk: flutter
lokinet_lib:
path: ./lokinet_lib
shared_preferences: ^0.5.12+4
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.