refactor and move stuff

This commit is contained in:
John Smith 2023-01-10 21:04:18 -05:00
parent 8c22bf8cc0
commit b54868cc55
28 changed files with 500 additions and 94 deletions

3
.gitignore vendored
View File

@ -42,3 +42,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# WASM
/web/wasm/

13
_script_common Normal file
View File

@ -0,0 +1,13 @@
set -eo pipefail
get_abs_filename() {
# $1 : relative filename
echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
}
# Veilid location
VEILIDDIR=$(get_abs_filename "$SCRIPTDIR/../veilid")
if [ ! -d "$VEILIDDIR" ]; then
echo 'Veilid git clone needs to be at $VEILIDDIR'
exit 1
fi

20
lib/entities/profile.dart Normal file
View File

@ -0,0 +1,20 @@
class Profile {
String name;
String publicKey;
bool invisible;
Profile(this.name, this.publicKey) : invisible = false;
Profile.fromJson(Map<String, dynamic> json)
: name = json['name'],
publicKey = json['public_key'],
invisible = json['invisible'];
Map<String, dynamic> toJson() {
return {
'name': name,
'public_key': publicKey,
'invisible': invisible,
};
}
}

2
lib/log/log.dart Normal file
View File

@ -0,0 +1,2 @@
export 'loggy.dart';
export 'state_logger.dart';

View File

@ -15,12 +15,12 @@ class ChatPage extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text("Home Page"),
ElevatedButton(
onPressed: () {
ref.watch(authNotifierProvider.notifier).logout();
},
child: const Text("Logout"),
),
// ElevatedButton(
// onPressed: () {
// ref.watch(authNotifierProvider.notifier).logout();
// },
// child: const Text("Logout"),
// ),
],
),
),

View File

@ -0,0 +1 @@

View File

@ -15,12 +15,12 @@ class HomePage extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text("Home Page"),
ElevatedButton(
onPressed: () {
ref.watch(authNotifierProvider.notifier).logout();
},
child: const Text("Logout"),
),
// ElevatedButton(
// onPressed: () {
// ref.watch(authNotifierProvider.notifier).logout();
// },
// child: const Text("Logout"),
// ),
],
),
),

View File

@ -15,15 +15,15 @@ class LoginPage extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text("Login Page"),
ElevatedButton(
onPressed: () async {
ref.watch(authNotifierProvider.notifier).login(
"myEmail",
"myPassword",
);
},
child: const Text("Login"),
),
// ElevatedButton(
// onPressed: () async {
// ref.watch(authNotifierProvider.notifier).login(
// "myEmail",
// "myPassword",
// );
// },
// child: const Text("Login"),
// ),
],
),
),

View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class NewAccountPage extends ConsumerWidget {
const NewAccountPage({super.key});
static const path = '/new_account';
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: null,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text("New Account Page"),
// ElevatedButton(
// onPressed: () async {
// ref.watch(authNotifierProvider.notifier).login(
// "myEmail",
// "myPassword",
// );
// },
// child: const Text("Login"),
// ),
],
),
),
);
}
}

View File

@ -1,2 +1,4 @@
export 'home.dart';
export 'splash.dart';
export 'index.dart';
export 'login.dart';
export 'new_account.dart';

View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class LoginPage extends ConsumerWidget {
const LoginPage({super.key});
static const path = '/settings';
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: null,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text("Settings Page"),
// ElevatedButton(
// onPressed: () async {
// ref.watch(authNotifierProvider.notifier).login(
// "myEmail",
// "myPassword",
// );
// },
// child: const Text("Login"),
// ),
],
),
),
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../pages/pages.dart';
import 'router_notifier.dart';
final _key = GlobalKey<NavigatorState>(debugLabel: 'routerKey');
@ -14,7 +15,7 @@ final routerProvider = Provider.autoDispose<GoRouter>((ref) {
navigatorKey: _key,
refreshListenable: notifier,
debugLogDiagnostics: true,
initialLocation: SplashPage.path,
initialLocation: IndexPage.path,
routes: notifier.routes,
redirect: notifier.redirect,
);

View File

@ -1,12 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../entities/user_role.dart';
import '../pages/pages.dart';
import '../state/auth.dart';
import '../state/permissions.dart';
/// This notifier is meant to implement the [Listenable] our [GoRouter] needs.
///
@ -45,9 +42,9 @@ class RouterNotifier extends AutoDisposeAsyncNotifier<void>
String? redirect(BuildContext context, GoRouterState state) {
if (this.state.isLoading || this.state.hasError) return null;
final isSplash = state.location == IndexPage.path;
final isIndex = state.location == IndexPage.path;
if (isSplash) {
if (isIndex) {
return isAuth ? HomePage.path : LoginPage.path;
}
@ -57,7 +54,7 @@ class RouterNotifier extends AutoDisposeAsyncNotifier<void>
return isAuth ? null : IndexPage.path;
}
/// Our application routes. Obtained through code generation
/// Our application routes
List<GoRoute> get routes => [
GoRoute(
path: IndexPage.path,
@ -66,38 +63,43 @@ class RouterNotifier extends AutoDisposeAsyncNotifier<void>
GoRoute(
path: HomePage.path,
builder: (context, state) => const HomePage(),
redirect: (context, state) async {
if (state.location == HomePage.path) return null;
// redirect: (context, state) async {
// if (state.location == HomePage.path) return null;
final roleListener = ProviderScope.containerOf(context).listen(
permissionsProvider.select((value) => value.valueOrNull),
(previous, next) {},
);
// // final roleListener = ProviderScope.containerOf(context).listen(
// // permissionsProvider.select((value) => value.valueOrNull),
// // (previous, next) {},
// // );
final userRole = roleListener.read();
final redirectTo = userRole?.redirectBasedOn(state.location);
// // final userRole = roleListener.read();
// // final redirectTo = userRole?.redirectBasedOn(state.location);
roleListener.close();
return redirectTo;
},
routes: [
GoRoute(
path: AdminPage.path,
builder: (context, state) => const AdminPage(),
// // roleListener.close();
// // return redirectTo;
// },
// routes: [
// GoRoute(
// path: AdminPage.path,
// builder: (context, state) => const AdminPage(),
// ),
// GoRoute(
// path: UserPage.path,
// builder: (context, state) => const UserPage(),
// ),
// GoRoute(
// path: GuestPage.path,
// builder: (context, state) => const GuestPage(),
// )
// ]
),
GoRoute(
path: UserPage.path,
builder: (context, state) => const UserPage(),
),
GoRoute(
path: GuestPage.path,
builder: (context, state) => const GuestPage(),
)
]),
GoRoute(
path: LoginPage.path,
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: NewAccountPage.path,
builder: (context, state) => const NewAccountPage(),
),
];
/// Adds [GoRouter]'s listener as specified by its [Listenable].
@ -127,20 +129,20 @@ final routerNotifierProvider =
});
/// A simple extension to determine wherever should we redirect our users
extension RedirecttionBasedOnRole on UserRole {
/// Redirects the users based on [this] and its current [location]
String? redirectBasedOn(String location) {
switch (this) {
case UserRole.admin:
return null;
case UserRole.verifiedUser:
case UserRole.unverifiedUser:
if (location == AdminPage.path) return HomePage.path;
return null;
case UserRole.guest:
case UserRole.none:
if (location != HomePage.path) return HomePage.path;
return null;
}
}
}
// extension RedirecttionBasedOnRole on UserRole {
// /// Redirects the users based on [this] and its current [location]
// String? redirectBasedOn(String location) {
// switch (this) {
// case UserRole.admin:
// return null;
// case UserRole.verifiedUser:
// case UserRole.unverifiedUser:
// if (location == AdminPage.path) return HomePage.path;
// return null;
// case UserRole.guest:
// case UserRole.none:
// if (location != HomePage.path) return HomePage.path;
// return null;
// }
// }
// }

110
lib/state/auth.dart Normal file
View File

@ -0,0 +1,110 @@
import 'dart:async';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
class User {
final String publicKey;
final String secretKey;
const User(this.publicKey, this.secretKey);
}
/// A mock of an Authenticated User
const _dummyUser = User("", "");
/// XXXX THIS IS TOTALLY BOGUS FOR NOW
/// This notifier holds and handles the authentication state of the application
class AuthNotifier extends AutoDisposeAsyncNotifier<User?> {
late SharedPreferences sharedPreferences;
static const _sharedPrefsKey = 'token';
/// Mock of the duration of a network request
@override
FutureOr<User?> build() async {
sharedPreferences = await SharedPreferences.getInstance();
_persistenceRefreshLogic();
return await _loginRecoveryAttempt();
}
/// Tries to perform a login with the saved token on the persistant storage.
/// If _anything_ goes wrong, deletes the internal token and returns a [User.signedOut].
Future<User?> _loginRecoveryAttempt() async {
try {
final savedToken = sharedPreferences.getString(_sharedPrefsKey);
if (savedToken == null) {
throw const UnauthorizedException(
"Couldn't find the authentication token");
}
return await _loginWithToken(savedToken);
} catch (_, __) {
await sharedPreferences.remove(_sharedPrefsKey);
return null;
}
}
/// Mock of a request performed on logout (might be common, or not, whatevs).
Future<void> logout() async {
await Future.delayed(networkRoundTripTime);
state = const AsyncValue.data(null);
}
/// Mock of a successful login attempt, which results come from the network.
Future<void> login(String publicKey, String password) async {
state = await AsyncValue.guard<User>(() async {
return Future.delayed(
networkRoundTripTime,
() => _dummyUser,
);
});
}
/// Mock of a login request performed with a saved token.
/// If such request fails, this method will throw an [UnauthorizedException].
Future<User> _loginWithToken(String token) async {
final logInAttempt = await Future.delayed(
networkRoundTripTime,
() => true,
);
if (logInAttempt) return _dummyUser;
throw const UnauthorizedException('401 Unauthorized or something');
}
/// Internal method used to listen authentication state changes.
/// When the auth object is in a loading state, nothing happens.
/// When the auth object is in a error state, we choose to remove the token
/// Otherwise, we expect the current auth value to be reflected in our persistence API
void _persistenceRefreshLogic() {
ref.listenSelf((_, next) {
if (next.isLoading) return;
if (next.hasError) {
sharedPreferences.remove(_sharedPrefsKey);
return;
}
final val = next.requireValue;
final isAuthenticated = val == null;
isAuthenticated
? sharedPreferences.remove(_sharedPrefsKey)
: sharedPreferences.setString(_sharedPrefsKey, val.publicKey);
});
}
}
final authNotifierProvider =
AutoDisposeAsyncNotifierProvider<AuthNotifier, User?>(() {
return AuthNotifier();
});
class UnauthorizedException implements Exception {
final String message;
const UnauthorizedException(this.message);
}
/// Mock of the duration of a network request
const networkRoundTripTime = Duration(milliseconds: 750);

View File

@ -0,0 +1,16 @@
import '../tools/tools.dart';
enum ConnectionState {
detached,
detaching,
attaching,
attachedWeak,
attachedGood,
attachedStrong,
fullyAttached,
overAttached,
}
ExternalStreamState<ConnectionState> globalConnectionState =
ExternalStreamState<ConnectionState>(ConnectionState.detached);
var globalConnectionStateProvider = globalConnectionState.provider();

2
lib/state/state.dart Normal file
View File

@ -0,0 +1,2 @@
export 'connection_state.dart';
export 'auth.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'themes.dart';
import 'themes/themes.dart';
class ThemeService {
ThemeService._();
@ -18,9 +18,6 @@ class ThemeService {
final allThemes = <String, ThemeData>{
'dark': darkTheme,
'light': lightTheme,
'pink': pinkTheme,
'darkBlue': darkBlueTheme,
'halloween': halloweenTheme,
};
String get previousThemeName {

View File

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
ThemeData darkTheme = ThemeData.dark();
// late Color primaryColor = const Color(0xFF343455);
// late Color secondaryColor = const Color(0xFF3D3E77);
// late Color tertiaryColor = const Color(0xFFBB108E);
// late Color alternate = const Color(0xFF5086DF);
// late Color primaryBackground = const Color(0xFF252534);
// late Color secondaryBackground = const Color(0xFF292D44);
// late Color primaryText = const Color(0xFFD0D0E0);
// late Color secondaryText = const Color(0xFFB0B0D0);
// late Color disabledText = Color(0x808F8F8F);
// late Color primaryEdge = Color(0xFF555594);
// late Color header = Color(0xFF8A8AD8);
// late Color textBackground = Color(0xFF181820);
// late Color active = Color(0xFF463BAD);
// late Color inactive = Color(0xFF2E2E3C);

View File

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
ThemeData lightTheme = ThemeData.light();
// late Color primaryColor = const Color(0xFF6667AB);
// late Color secondaryColor = const Color(0xFF7C7ED0);
// late Color tertiaryColor = const Color(0xFFF259C9);
// late Color alternate = const Color(0xFF77ABFF);
// late Color primaryBackground = const Color(0xFFA0A0D0);
// late Color secondaryBackground = const Color(0xFFB0B0D0);
// late Color primaryText = const Color(0xFF101010);
// late Color secondaryText = const Color(0xFF282840);
// late Color disabledText = Color(0x8047464F);
// late Color primaryEdge = Color(0xFF44447F);
// late Color header = Color(0xFFE0E0F0);
// late Color textBackground = Color(0xFFE0E0F0);
// late Color active = Color(0xFF5B4FFA);
// late Color inactive = Color(0xFF3E3E72);

View File

@ -0,0 +1,2 @@
export 'light.dart';
export 'dark.dart';

View File

@ -0,0 +1,27 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Caches a state value which can be changed from anywhere
// Creates a provider interface that notices when the value changes
class ExternalStreamState<T> {
T currentState;
StreamController<T> streamController;
ExternalStreamState(T initialState)
: currentState = initialState,
streamController = StreamController<T>.broadcast();
void add(T newState) {
currentState = newState;
streamController.add(newState);
}
AutoDisposeStreamProvider<T> provider() {
return AutoDisposeStreamProvider<T>((ref) async* {
if (await streamController.stream.isEmpty) {
yield currentState;
}
await for (final value in streamController.stream) {
yield value;
}
});
}
}

1
lib/tools/tools.dart Normal file
View File

@ -0,0 +1 @@
export 'external_stream_state.dart';

View File

@ -14,7 +14,7 @@ Future<String> getVeilidVersion() async {
// Initialize Veilid
// Call only once.
void _init() {
void _initVeilid() {
if (kIsWeb) {
var platformConfig = VeilidWASMConfig(
logging: VeilidWASMConfigLogging(
@ -44,7 +44,6 @@ void _init() {
}
}
// Called from FlutterFlow stub initialize() function upon Main page load
bool initialized = false;
Processor processor = Processor();
@ -54,7 +53,7 @@ Future<void> initializeVeilid() async {
}
// Init Veilid
_init();
_initVeilid();
// Startup Veilid
await processor.startup();

View File

@ -2,7 +2,8 @@ import 'dart:async';
import 'package:veilid/veilid.dart';
import 'config.dart';
import 'veilid_log.dart';
import '../log/loggy.dart';
import '../log/log.dart';
import '../state/state.dart';
class Processor {
String _veilidVersion = "";
@ -57,48 +58,46 @@ class Processor {
//loggy.info("Attachment: ${updateAttachment.json}");
// Set connection meter and ui state for connection state
var connectionState = "";
var cs = ConnectionState.detached;
var checkPublicInternet = false;
switch (updateAttachment.state.state) {
case AttachmentState.detached:
connectionState = "detached";
cs = ConnectionState.detached;
break;
case AttachmentState.detaching:
connectionState = "detaching";
cs = ConnectionState.detaching;
break;
case AttachmentState.attaching:
connectionState = "attaching";
cs = ConnectionState.attaching;
break;
case AttachmentState.attachedWeak:
checkPublicInternet = true;
connectionState = "weak";
cs = ConnectionState.attachedWeak;
break;
case AttachmentState.attachedGood:
checkPublicInternet = true;
connectionState = "good";
cs = ConnectionState.attachedGood;
break;
case AttachmentState.attachedStrong:
checkPublicInternet = true;
connectionState = "strong";
cs = ConnectionState.attachedStrong;
break;
case AttachmentState.fullyAttached:
checkPublicInternet = true;
connectionState = "full";
cs = ConnectionState.fullyAttached;
break;
case AttachmentState.overAttached:
checkPublicInternet = true;
connectionState = "over";
cs = ConnectionState.overAttached;
break;
}
if (checkPublicInternet) {
if (!updateAttachment.state.publicInternetReady) {
connectionState = "attaching";
cs = ConnectionState.attaching;
}
}
FFAppState().update(() {
FFAppState().ConnectionState = connectionState;
});
globalConnectionState.add(cs);
}
Future<void> processUpdateConfig(VeilidUpdateConfig updateConfig) async {

View File

@ -0,0 +1,5 @@
export 'config.dart';
export 'processor.dart';
export 'tools.dart';
export 'veilid_log.dart';
export 'init.dart';

41
setup_linux.sh Executable file
View File

@ -0,0 +1,41 @@
#!/bin/bash
SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
source $SCRIPTDIR/_script_common
if [[ "$(uname)" != "Linux" ]]; then
echo Not running Linux
exit 1
fi
if [ "$(lsb_release -d | grep -qEi 'debian|buntu|mint')" ]; then
echo Not a supported Linux
exit 1
fi
# # ensure unzip is installed
# if command -v unzip &> /dev/null; then
# echo '[X] unzip is available in the path'
# else
# echo 'unzip is not available in the path'
# exit 1
# fi
# # ensure rsync is installed
# if command -v rsync &> /dev/null; then
# echo '[X] rsync is available in the path'
# else
# echo 'rsync is not available in the path'
# exit 1
# fi
# # ensure sed is installed
# if command -v sed &> /dev/null; then
# echo '[X] sed is available in the path'
# else
# echo 'sed is not available in the path'
# exit 1
# fi
# run setup for veilid
$VEILIDDIR/setup_linux.sh

36
setup_macos.sh Executable file
View File

@ -0,0 +1,36 @@
#!/bin/bash
SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
source $SCRIPTDIR/_script_common
if [[ "$(uname)" != "Darwin" ]]; then
echo Not running MacOS
exit 1
fi
# # ensure unzip is installed
# if command -v unzip &> /dev/null; then
# echo '[X] unzip is available in the path'
# else
# echo 'unzip is not available in the path'
# exit 1
# fi
# # ensure rsync is installed
# if command -v rsync &> /dev/null; then
# echo '[X] rsync is available in the path'
# else
# echo 'rsync is not available in the path'
# exit 1
# fi
# # ensure sed is installed
# if command -v sed &> /dev/null; then
# echo '[X] sed is available in the path'
# else
# echo 'sed is not available in the path'
# exit 1
# fi
# run setup for veilid
$VEILIDDIR/setup_macos.sh

25
wasm_update.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
source $SCRIPTDIR/_script_common
pushd $SCRIPTDIR >/dev/null
# WASM output dir
WASMDIR=$SCRIPTDIR/web/wasm
# Build veilid-wasm, passing any arguments here to the build script
pushd $VEILIDDIR/veilid-wasm >/dev/null
PKGDIR=$(./wasm_build.sh $@ | grep SUCCESS:OUTPUTDIR | cut -d= -f2)
popd >/dev/null
# Copy wasm blob into place
echo Updating WASM from $PKGDIR to $WASMDIR
if [ -d $WASMDIR ]; then
rm -f $WASMDIR/*
fi
mkdir -p $WASMDIR
cp -f $PKGDIR/* $WASMDIR/
#### Done
popd >/dev/null