diff --git a/.env.example b/.env.example deleted file mode 100644 index b6c18e9..0000000 --- a/.env.example +++ /dev/null @@ -1 +0,0 @@ -API_BASE_URL=http://127.0.0.1:8080 diff --git a/.vscode/launch.json b/.vscode/launch.json index b614b09..85b830e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,12 +2,11 @@ "version": "0.2.0", "configurations": [ { - "name": "SRS (local API)", + "name": "SRS", "request": "launch", "type": "dart", "program": "lib/main.dart", - "deviceId": "chrome", - "args": ["--web-port", "5000", "--dart-define-from-file=.env"] + "deviceId": "chrome" } ] } diff --git a/lib/core/config.dart b/lib/core/config.dart index 9a2effa..d1cf9a8 100644 --- a/lib/core/config.dart +++ b/lib/core/config.dart @@ -1,11 +1,5 @@ -import 'package:flutter/foundation.dart'; - class AppConfig { const AppConfig._(); - static const apiBaseUrl = String.fromEnvironment('API_BASE_URL'); - static bool get unconfigured => kDebugMode && apiBaseUrl.isEmpty; - static const setupHint = - 'No API_BASE_URL set. For local development run with ' - '--dart-define-from-file=.env (copy .env.example to .env first).'; + static const apiBaseUrl = String.fromEnvironment('API_BASE_URL'); } diff --git a/lib/core/http_api.dart b/lib/core/http_api.dart index 8fd2001..a59e49d 100644 --- a/lib/core/http_api.dart +++ b/lib/core/http_api.dart @@ -5,7 +5,6 @@ import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import 'api.dart'; -import 'config.dart'; import 'format.dart'; import 'models.dart'; @@ -20,9 +19,8 @@ class HttpApi implements Api { _client = client ?? http.Client(); static const _tokenKey = 'token'; - static String get _unreachable => AppConfig.unconfigured - ? 'Unable to reach the server. ${AppConfig.setupHint}' - : 'Unable to reach the server. Check your connection and try again.'; + static const _unreachable = + 'Unable to reach the server. Check your connection and try again.'; final String _base; final SharedPreferences _prefs; diff --git a/lib/features/auth/login_screen.dart b/lib/features/auth/login_screen.dart index 298b7a2..f38a57a 100644 --- a/lib/features/auth/login_screen.dart +++ b/lib/features/auth/login_screen.dart @@ -1,4 +1,4 @@ - import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/api.dart'; @@ -16,16 +16,34 @@ class LoginScreen extends ConsumerStatefulWidget { class _LoginScreenState extends ConsumerState { final _user = TextEditingController(); final _pass = TextEditingController(); + final _scroll = ScrollController(); bool _busy = false; + bool _keyboard = false; String? _error; @override void dispose() { _user.dispose(); _pass.dispose(); + _scroll.dispose(); super.dispose(); } + void _followKeyboard(BuildContext context) { + final open = MediaQuery.viewInsetsOf(context).bottom > 0; + if (open && !_keyboard) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!_scroll.hasClients) return; + _scroll.animateTo( + _scroll.position.maxScrollExtent, + duration: const Duration(milliseconds: 250), + curve: Curves.easeOutCubic, + ); + }); + } + _keyboard = open; + } + Future _submit() async { if (_busy) return; if (_user.text.trim().isEmpty || _pass.text.isEmpty) { @@ -51,74 +69,82 @@ class _LoginScreenState extends ConsumerState { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: C.navy, - body: LayoutBuilder( - builder: (context, c) { - final landscape = c.maxWidth > c.maxHeight && c.maxWidth >= 600; - if (c.maxWidth >= 900 || landscape) { - return Row( - children: [ - Expanded(child: _BrandPanel(large: c.maxHeight >= 560)), - Container( - width: c.maxWidth >= 1280 - ? 560 - : (c.maxWidth >= 900 ? 480 : 420), - color: C.bg, - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(S.s36), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 380), - child: _form(), - ), - ), + final screen = MediaQuery.sizeOf(context); + final landscape = screen.width > screen.height && screen.width >= 600; + if (screen.width >= 900 || landscape) { + return Scaffold( + backgroundColor: C.navy, + body: Row( + children: [ + Expanded(child: _BrandPanel(large: screen.height >= 560)), + Container( + width: screen.width >= 1280 + ? 560 + : (screen.width >= 900 ? 480 : 420), + color: C.bg, + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(S.s36), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 380), + child: _form(autofocus: true), ), ), - ], - ); - } - final hero = (c.maxHeight * 0.36).clamp(230.0, 340.0); - return Column( - children: [ - SizedBox( + ), + ), + ], + ), + ); + } + + final hero = (screen.height * 0.36).clamp(230.0, 340.0); + _followKeyboard(context); + return Scaffold( + backgroundColor: C.bg, + body: CustomScrollView( + controller: _scroll, + physics: const ClampingScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: ColoredBox( + color: C.navy, + child: SizedBox( height: hero, - width: double.infinity, child: const _BrandPanel(large: false), ), - Expanded( - child: Container( - width: double.infinity, - decoration: const BoxDecoration( - color: C.bg, - borderRadius: BorderRadius.vertical( - top: Radius.circular(R.hero), - ), - ), - child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB( - S.s24, - S.s28, - S.s24, - S.s24, - ), - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 420), - child: _form(), - ), - ), + ), + ), + SliverFillRemaining( + hasScrollBody: false, + child: ColoredBox( + color: C.navy, + child: Container( + decoration: const BoxDecoration( + color: C.bg, + borderRadius: BorderRadius.vertical( + top: Radius.circular(R.hero), ), ), + padding: const EdgeInsets.fromLTRB( + S.s24, + S.s28, + S.s24, + S.s24, + ), + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: _form(autofocus: false), + ), ), - ], - ); - }, + ), + ), + ], ), ); } - Widget _form() => AutofillGroup( + Widget _form({required bool autofocus}) => AutofillGroup( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -132,7 +158,7 @@ class _LoginScreenState extends ConsumerState { hint: 'Enter username', error: _error != null, enabled: !_busy, - autofocus: true, + autofocus: autofocus, autofill: const [AutofillHints.username], onChanged: (_) => setState(() => _error = null), ), diff --git a/lib/main.dart b/lib/main.dart index 2f554c2..3def650 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,7 +11,6 @@ import 'core/providers.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - if (AppConfig.unconfigured) debugPrint('SRS: ${AppConfig.setupHint}'); final Api api = HttpApi( baseUrl: AppConfig.apiBaseUrl, prefs: await SharedPreferences.getInstance(), diff --git a/web_dev_config.yaml b/web_dev_config.yaml new file mode 100644 index 0000000..cfa17cb --- /dev/null +++ b/web_dev_config.yaml @@ -0,0 +1,5 @@ +server: + port: 5000 + proxy: + - target: "http://127.0.0.1:8080/" + prefix: "/api/"