fix: updated login screen glitches
All checks were successful
deploy / deploy (push) Successful in 4m2s

This commit is contained in:
Aswin B. S 2026-11-05 01:39:06 +05:30
parent 0b86be6796
commit 78c77f708c
7 changed files with 94 additions and 74 deletions

View File

@ -1 +0,0 @@
API_BASE_URL=http://127.0.0.1:8080

5
.vscode/launch.json vendored
View File

@ -2,12 +2,11 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "SRS (local API)", "name": "SRS",
"request": "launch", "request": "launch",
"type": "dart", "type": "dart",
"program": "lib/main.dart", "program": "lib/main.dart",
"deviceId": "chrome", "deviceId": "chrome"
"args": ["--web-port", "5000", "--dart-define-from-file=.env"]
} }
] ]
} }

View File

@ -1,11 +1,5 @@
import 'package:flutter/foundation.dart';
class AppConfig { class AppConfig {
const AppConfig._(); const AppConfig._();
static const apiBaseUrl = String.fromEnvironment('API_BASE_URL');
static bool get unconfigured => kDebugMode && apiBaseUrl.isEmpty;
static const setupHint = static const apiBaseUrl = String.fromEnvironment('API_BASE_URL');
'No API_BASE_URL set. For local development run with '
'--dart-define-from-file=.env (copy .env.example to .env first).';
} }

View File

@ -5,7 +5,6 @@ import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'api.dart'; import 'api.dart';
import 'config.dart';
import 'format.dart'; import 'format.dart';
import 'models.dart'; import 'models.dart';
@ -20,9 +19,8 @@ class HttpApi implements Api {
_client = client ?? http.Client(); _client = client ?? http.Client();
static const _tokenKey = 'token'; static const _tokenKey = 'token';
static String get _unreachable => AppConfig.unconfigured static const _unreachable =
? 'Unable to reach the server. ${AppConfig.setupHint}' 'Unable to reach the server. Check your connection and try again.';
: 'Unable to reach the server. Check your connection and try again.';
final String _base; final String _base;
final SharedPreferences _prefs; final SharedPreferences _prefs;

View File

@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/api.dart'; import '../../core/api.dart';
@ -16,16 +16,34 @@ class LoginScreen extends ConsumerStatefulWidget {
class _LoginScreenState extends ConsumerState<LoginScreen> { class _LoginScreenState extends ConsumerState<LoginScreen> {
final _user = TextEditingController(); final _user = TextEditingController();
final _pass = TextEditingController(); final _pass = TextEditingController();
final _scroll = ScrollController();
bool _busy = false; bool _busy = false;
bool _keyboard = false;
String? _error; String? _error;
@override @override
void dispose() { void dispose() {
_user.dispose(); _user.dispose();
_pass.dispose(); _pass.dispose();
_scroll.dispose();
super.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<void> _submit() async { Future<void> _submit() async {
if (_busy) return; if (_busy) return;
if (_user.text.trim().isEmpty || _pass.text.isEmpty) { if (_user.text.trim().isEmpty || _pass.text.isEmpty) {
@ -51,74 +69,82 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screen = MediaQuery.sizeOf(context);
final landscape = screen.width > screen.height && screen.width >= 600;
if (screen.width >= 900 || landscape) {
return Scaffold( return Scaffold(
backgroundColor: C.navy, backgroundColor: C.navy,
body: LayoutBuilder( body: Row(
builder: (context, c) {
final landscape = c.maxWidth > c.maxHeight && c.maxWidth >= 600;
if (c.maxWidth >= 900 || landscape) {
return Row(
children: [ children: [
Expanded(child: _BrandPanel(large: c.maxHeight >= 560)), Expanded(child: _BrandPanel(large: screen.height >= 560)),
Container( Container(
width: c.maxWidth >= 1280 width: screen.width >= 1280
? 560 ? 560
: (c.maxWidth >= 900 ? 480 : 420), : (screen.width >= 900 ? 480 : 420),
color: C.bg, color: C.bg,
child: Center( child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(S.s36), padding: const EdgeInsets.all(S.s36),
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 380), constraints: const BoxConstraints(maxWidth: 380),
child: _form(), child: _form(autofocus: true),
), ),
), ),
), ),
), ),
], ],
),
); );
} }
final hero = (c.maxHeight * 0.36).clamp(230.0, 340.0);
return Column( final hero = (screen.height * 0.36).clamp(230.0, 340.0);
children: [ _followKeyboard(context);
SizedBox( return Scaffold(
backgroundColor: C.bg,
body: CustomScrollView(
controller: _scroll,
physics: const ClampingScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: ColoredBox(
color: C.navy,
child: SizedBox(
height: hero, height: hero,
width: double.infinity,
child: const _BrandPanel(large: false), child: const _BrandPanel(large: false),
), ),
Expanded( ),
),
SliverFillRemaining(
hasScrollBody: false,
child: ColoredBox(
color: C.navy,
child: Container( child: Container(
width: double.infinity,
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: C.bg, color: C.bg,
borderRadius: BorderRadius.vertical( borderRadius: BorderRadius.vertical(
top: Radius.circular(R.hero), top: Radius.circular(R.hero),
), ),
), ),
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
S.s24, S.s24,
S.s28, S.s28,
S.s24, S.s24,
S.s24, S.s24,
), ),
child: Center( alignment: Alignment.topCenter,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420), constraints: const BoxConstraints(maxWidth: 420),
child: _form(), child: _form(autofocus: false),
),
), ),
), ),
), ),
), ),
], ],
);
},
), ),
); );
} }
Widget _form() => AutofillGroup( Widget _form({required bool autofocus}) => AutofillGroup(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -132,7 +158,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
hint: 'Enter username', hint: 'Enter username',
error: _error != null, error: _error != null,
enabled: !_busy, enabled: !_busy,
autofocus: true, autofocus: autofocus,
autofill: const [AutofillHints.username], autofill: const [AutofillHints.username],
onChanged: (_) => setState(() => _error = null), onChanged: (_) => setState(() => _error = null),
), ),

View File

@ -11,7 +11,6 @@ import 'core/providers.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
if (AppConfig.unconfigured) debugPrint('SRS: ${AppConfig.setupHint}');
final Api api = HttpApi( final Api api = HttpApi(
baseUrl: AppConfig.apiBaseUrl, baseUrl: AppConfig.apiBaseUrl,
prefs: await SharedPreferences.getInstance(), prefs: await SharedPreferences.getInstance(),

5
web_dev_config.yaml Normal file
View File

@ -0,0 +1,5 @@
server:
port: 5000
proxy:
- target: "http://127.0.0.1:8080/"
prefix: "/api/"