Browse Source

Merge pull request #231 from provokateurin/refactor/dev-container

tool, neon: Refactor dev container
pull/232/head
Kate 2 years ago committed by GitHub
parent
commit
4e7e28597c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      packages/neon/assets/.gitignore
  2. 3
      packages/neon/integration_test/screenshot_test.dart
  3. 19
      packages/neon/lib/main.dart
  4. 2
      packages/neon/lib/src/app.dart
  5. 1
      packages/neon/lib/src/neon.dart
  6. 261
      packages/neon/lib/src/pages/login.dart
  7. 19
      packages/neon/lib/src/utils/env.dart
  8. 8
      packages/neon/pubspec.lock
  9. 1
      packages/neon/pubspec.yaml
  10. 0
      tool/build-dev-container.sh
  11. 8
      tool/dev.sh
  12. 2
      tool/generate-screenshots.sh
  13. 28
      tool/run-dev-instance.sh

1
packages/neon/assets/.gitignore vendored

@ -1 +0,0 @@
.env

3
packages/neon/integration_test/screenshot_test.dart

@ -147,9 +147,6 @@ Future pumpAppPage(
Provider<SharedPreferences>( Provider<SharedPreferences>(
create: (final _) => sharedPreferences, create: (final _) => sharedPreferences,
), ),
Provider<Env?>(
create: (final _) => null,
),
Provider<NeonPlatform>( Provider<NeonPlatform>(
create: (final _) => platform, create: (final _) => platform,
), ),

19
packages/neon/lib/main.dart

@ -1,6 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:neon/src/neon.dart'; import 'package:neon/src/neon.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@ -8,19 +6,6 @@ import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
Future main() async { Future main() async {
Env? env;
try {
await dotenv.load(fileName: 'assets/.env');
if (dotenv.env.keys.isNotEmpty) {
if (kReleaseMode) {
throw Exception('A release build can not contain a .env file');
}
env = Env.fromMap(dotenv.env);
}
} catch (e) {
debugPrint('Failed to load env: $e');
}
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: WidgetsBinding.instance); FlutterNativeSplash.preserve(widgetsBinding: WidgetsBinding.instance);
@ -63,9 +48,6 @@ Future main() async {
Provider<SharedPreferences>( Provider<SharedPreferences>(
create: (final _) => sharedPreferences, create: (final _) => sharedPreferences,
), ),
Provider<Env?>(
create: (final _) => env,
),
Provider<NeonPlatform>( Provider<NeonPlatform>(
create: (final _) => platform, create: (final _) => platform,
), ),
@ -97,7 +79,6 @@ Future main() async {
child: NeonApp( child: NeonApp(
accountsBloc: accountsBloc, accountsBloc: accountsBloc,
sharedPreferences: sharedPreferences, sharedPreferences: sharedPreferences,
env: env,
platform: platform, platform: platform,
globalOptions: globalOptions, globalOptions: globalOptions,
), ),

2
packages/neon/lib/src/app.dart

@ -4,7 +4,6 @@ class NeonApp extends StatefulWidget {
const NeonApp({ const NeonApp({
required this.accountsBloc, required this.accountsBloc,
required this.sharedPreferences, required this.sharedPreferences,
required this.env,
required this.platform, required this.platform,
required this.globalOptions, required this.globalOptions,
super.key, super.key,
@ -12,7 +11,6 @@ class NeonApp extends StatefulWidget {
final AccountsBloc accountsBloc; final AccountsBloc accountsBloc;
final SharedPreferences sharedPreferences; final SharedPreferences sharedPreferences;
final Env? env;
final NeonPlatform platform; final NeonPlatform platform;
final GlobalOptions globalOptions; final GlobalOptions globalOptions;

1
packages/neon/lib/src/neon.dart

@ -69,7 +69,6 @@ part 'utils/account_options.dart';
part 'utils/app_implementation.dart'; part 'utils/app_implementation.dart';
part 'utils/bloc.dart'; part 'utils/bloc.dart';
part 'utils/confirmation_dialog.dart'; part 'utils/confirmation_dialog.dart';
part 'utils/env.dart';
part 'utils/global.dart'; part 'utils/global.dart';
part 'utils/global_options.dart'; part 'utils/global_options.dart';
part 'utils/global_popups.dart'; part 'utils/global_popups.dart';

261
packages/neon/lib/src/pages/login.dart

@ -38,9 +38,6 @@ class _LoginPageState extends State<LoginPage> {
await launchUrlString( await launchUrlString(
init.login, init.login,
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
webViewConfiguration: WebViewConfiguration(
headers: _buildHeaders(context, Provider.of<Env?>(context, listen: false)),
),
); );
}); });
} }
@ -85,18 +82,6 @@ class _LoginPageState extends State<LoginPage> {
}); });
} }
Map<String, String> _buildHeaders(
final BuildContext context,
final Env? env,
) =>
{
HttpHeaders.userAgentHeader: userAgent(_packageInfo),
if (env != null) ...{
HttpHeaders.authorizationHeader:
'Basic ${base64.encode(utf8.encode('${env.testUsername}:${env.testPassword}'))}',
},
};
@override @override
void dispose() { void dispose() {
_loginBloc.dispose(); _loginBloc.dispose();
@ -104,151 +89,143 @@ class _LoginPageState extends State<LoginPage> {
} }
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) => StreamBuilder<List<Account>>(
final env = Provider.of<Env?>(context); stream: _accountsBloc.accounts,
builder: (final context, final accountsSnapshot) => WillPopScope(
return StreamBuilder<List<Account>>( onWillPop: () async {
stream: _accountsBloc.accounts, if (accountsSnapshot.data?.isNotEmpty ?? false) {
builder: (final context, final accountsSnapshot) => WillPopScope( return true;
onWillPop: () async { }
if (accountsSnapshot.data?.isNotEmpty ?? false) {
return true;
}
if ((await _loginBloc.serverURL.first) == null) { if ((await _loginBloc.serverURL.first) == null) {
return true; return true;
} }
_loginBloc.setServerURL(null); _loginBloc.setServerURL(null);
return false; return false;
}, },
child: StreamBuilder<String?>( child: StreamBuilder<String?>(
stream: _loginBloc.serverURL, stream: _loginBloc.serverURL,
builder: (final context, final serverURLSnapshot) => StreamBuilder<ServerConnectionState?>( builder: (final context, final serverURLSnapshot) => StreamBuilder<ServerConnectionState?>(
stream: _loginBloc.serverConnectionState, stream: _loginBloc.serverConnectionState,
builder: (final context, final serverConnectionStateSnapshot) => Scaffold( builder: (final context, final serverConnectionStateSnapshot) => Scaffold(
appBar: serverConnectionStateSnapshot.data == ServerConnectionState.success || appBar: serverConnectionStateSnapshot.data == ServerConnectionState.success ||
(accountsSnapshot.data?.isNotEmpty ?? false) (accountsSnapshot.data?.isNotEmpty ?? false)
? AppBar( ? AppBar(
leading: IconButton( leading: IconButton(
onPressed: () { onPressed: () {
if (accountsSnapshot.data?.isNotEmpty ?? false) { if (accountsSnapshot.data?.isNotEmpty ?? false) {
Navigator.of(context).pop(); Navigator.of(context).pop();
} else { } else {
_loginBloc.setServerURL(null); _loginBloc.setServerURL(null);
}
},
icon: const Icon(Icons.arrow_back),
),
actions: [
if (serverConnectionStateSnapshot.data != null) ...[
IconButton(
onPressed: _loginBloc.refresh,
icon: const Icon(Icons.refresh),
),
],
],
)
: null,
body: serverConnectionStateSnapshot.data == ServerConnectionState.success
? Provider.of<NeonPlatform>(context).canUseWebView
? WebView(
javascriptMode: JavascriptMode.unrestricted,
zoomEnabled: false,
userAgent: userAgent(_packageInfo),
onWebViewCreated: (final controller) async {
_webViewController = controller;
final url =
(await _loginBloc.loginFlowInit.firstWhere((final init) => init != null))!.login;
if (mounted) {
await _webViewController!.loadUrl(
url,
headers: _buildHeaders(context, env),
);
} }
}, },
) icon: const Icon(Icons.arrow_back),
: Center( ),
child: Column( actions: [
mainAxisAlignment: MainAxisAlignment.center, if (serverConnectionStateSnapshot.data != null) ...[
children: [ IconButton(
Text(AppLocalizations.of(context).loginSwitchToBrowserWindow), onPressed: _loginBloc.refresh,
const SizedBox( icon: const Icon(Icons.refresh),
height: 10, ),
), ],
ElevatedButton( ],
onPressed: _loginBloc.refresh, )
child: Text(AppLocalizations.of(context).loginOpenAgain), : null,
), body: serverConnectionStateSnapshot.data == ServerConnectionState.success
], ? Provider.of<NeonPlatform>(context).canUseWebView
), ? WebView(
) javascriptMode: JavascriptMode.unrestricted,
: Center( zoomEnabled: false,
child: ListView( userAgent: userAgent(_packageInfo),
shrinkWrap: true, onWebViewCreated: (final controller) async {
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20), _webViewController = controller;
children: [ final url =
SizedBox( (await _loginBloc.loginFlowInit.firstWhere((final init) => init != null))!.login;
height: MediaQuery.of(context).size.height / 2, if (mounted) {
await _webViewController!.loadUrl(url);
}
},
)
: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const NeonLogo(), Text(AppLocalizations.of(context).loginSwitchToBrowserWindow),
const SizedBox( const SizedBox(
height: 30, height: 10,
), ),
Text(AppLocalizations.of(context).loginWorksWith), ElevatedButton(
const SizedBox( onPressed: _loginBloc.refresh,
height: 20, child: Text(AppLocalizations.of(context).loginOpenAgain),
), ),
const NextcloudLogo(),
], ],
), ),
), )
Form( : Center(
key: _formKey, child: ListView(
child: TextFormField( shrinkWrap: true,
focusNode: _focusNode, padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20),
decoration: const InputDecoration( children: [
hintText: 'https://...', SizedBox(
height: MediaQuery.of(context).size.height / 2,
child: Column(
children: [
const NeonLogo(),
const SizedBox(
height: 30,
),
Text(AppLocalizations.of(context).loginWorksWith),
const SizedBox(
height: 20,
),
const NextcloudLogo(),
],
), ),
initialValue:
widget.serverURL ?? (env?.testHost != null ? 'http://${env!.testHost}' : null),
validator: (final input) => validateHttpUrl(context, input),
onFieldSubmitted: (final input) {
if (_formKey.currentState!.validate()) {
_loginBloc.setServerURL(input);
} else {
_focusNode.requestFocus();
}
},
), ),
), Form(
Column( key: _formKey,
children: [ child: TextFormField(
CustomLinearProgressIndicator( focusNode: _focusNode,
visible: serverConnectionStateSnapshot.data == ServerConnectionState.loading, decoration: const InputDecoration(
), hintText: 'https://...',
if (serverConnectionStateSnapshot.data == ServerConnectionState.unreachable) ...[
ExceptionWidget(
AppLocalizations.of(context).errorUnableToReachServer,
onRetry: _loginBloc.refresh,
), ),
], initialValue: widget.serverURL,
if (serverConnectionStateSnapshot.data == ServerConnectionState.maintenanceMode) ...[ validator: (final input) => validateHttpUrl(context, input),
ExceptionWidget( onFieldSubmitted: (final input) {
AppLocalizations.of(context).errorServerInMaintenanceMode, if (_formKey.currentState!.validate()) {
onRetry: _loginBloc.refresh, _loginBloc.setServerURL(input);
} else {
_focusNode.requestFocus();
}
},
),
),
Column(
children: [
CustomLinearProgressIndicator(
visible: serverConnectionStateSnapshot.data == ServerConnectionState.loading,
), ),
if (serverConnectionStateSnapshot.data == ServerConnectionState.unreachable) ...[
ExceptionWidget(
AppLocalizations.of(context).errorUnableToReachServer,
onRetry: _loginBloc.refresh,
),
],
if (serverConnectionStateSnapshot.data == ServerConnectionState.maintenanceMode) ...[
ExceptionWidget(
AppLocalizations.of(context).errorServerInMaintenanceMode,
onRetry: _loginBloc.refresh,
),
],
], ],
], ),
), ],
], ),
), ),
), ),
), ),
), ),
), ),
), );
);
}
} }

19
packages/neon/lib/src/utils/env.dart

@ -1,19 +0,0 @@
part of '../neon.dart';
class Env {
Env({
this.testHost,
this.testUsername,
this.testPassword,
});
factory Env.fromMap(final Map<String, String> data) => Env(
testHost: data['TEST_HOST'],
testUsername: data['TEST_USER'],
testPassword: data['TEST_PASSWORD'],
);
final String? testHost;
final String? testUsername;
final String? testPassword;
}

8
packages/neon/pubspec.lock

@ -293,14 +293,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.0" version: "3.3.0"
flutter_dotenv:
dependency: "direct main"
description:
name: flutter_dotenv
sha256: d9283d92059a22e9834bc0a31336658ffba77089fb6f3cc36751f1fc7c6661a3
url: "https://pub.dev"
source: hosted
version: "5.0.2"
flutter_driver: flutter_driver:
dependency: transitive dependency: transitive
description: flutter description: flutter

1
packages/neon/pubspec.yaml

@ -16,7 +16,6 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_cache_manager: ^3.3.0 flutter_cache_manager: ^3.3.0
flutter_dotenv: ^5.0.2
flutter_file_dialog: ^2.3.0 flutter_file_dialog: ^2.3.0
flutter_html: ^3.0.0-alpha.3 flutter_html: ^3.0.0-alpha.3
flutter_local_notifications: ^12.0.2 flutter_local_notifications: ^12.0.2

0
tool/build-dev-container-image.sh → tool/build-dev-container.sh

8
tool/dev.sh

@ -0,0 +1,8 @@
#!/bin/bash
set -euxo pipefail
cd "$(dirname "$0")/.."
./tool/build-dev-container.sh
echo "Running development instance on http://localhost. To access it in an Android Emulator use http://10.0.2.2"
docker run --rm -v nextcloud-neon-dev:/usr/src/nextcloud -v nextcloud-neon-dev:/var/www/html -p "80:80" --add-host host.docker.internal:host-gateway nextcloud-neon-dev

2
tool/generate-screenshots.sh

@ -2,7 +2,7 @@
set -euxo pipefail set -euxo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
./tool/build-dev-container-image.sh ./tool/build-dev-container.sh
container_id="$(docker run --rm -d -p "80:80" nextcloud-neon-dev)" container_id="$(docker run --rm -d -p "80:80" nextcloud-neon-dev)"
function cleanup() { function cleanup() {
docker kill "$container_id" docker kill "$container_id"

28
tool/run-dev-instance.sh

@ -1,28 +0,0 @@
#!/bin/bash
set -euxo pipefail
cd "$(dirname "$0")/.."
ip=""
if [ "$#" -ne 1 ]; then
echo "You need to give the platform type: localhost, android-emulator"
exit 1
elif [[ "$1" == "android-emulator" ]]; then
ip="10.0.2.2"
elif [[ "$1" == "localhost" ]]; then
ip="localhost"
else
echo "Unknown platform type: $1"
exit 1
fi
./tool/build-dev-container-image.sh
echo "TEST_HOST=$ip
TEST_USER=user1
TEST_PASSWORD=user1" > packages/neon/assets/.env
function cleanup() {
rm packages/neon/assets/.env
}
trap cleanup EXIT
docker run --rm -v nextcloud-neon-dev:/usr/src/nextcloud -v nextcloud-neon-dev:/var/www/html -p "80:80" --add-host host.docker.internal:host-gateway nextcloud-neon-dev
Loading…
Cancel
Save