diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index a93c9cd..de6968b 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ - - - diff --git a/mobile/android/app/src/main/res/drawable/launch_background.xml b/mobile/android/app/src/main/res/drawable/launch_background.xml index 304732f..0ad9ada 100644 --- a/mobile/android/app/src/main/res/drawable/launch_background.xml +++ b/mobile/android/app/src/main/res/drawable/launch_background.xml @@ -2,11 +2,4 @@ - - - diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4..32c82fb 100644 Binary files a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b7..1896892 100644 Binary files a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391..df4a30c 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d..cfdb79d 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372e..118cbc6 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/mobile/assets/linux.svg b/mobile/assets/linux.svg new file mode 100644 index 0000000..3200fde --- /dev/null +++ b/mobile/assets/linux.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mobile/assets/logo_v2_01.png b/mobile/assets/logo_v2_01.png new file mode 100644 index 0000000..538913e Binary files /dev/null and b/mobile/assets/logo_v2_01.png differ diff --git a/mobile/assets/windows.svg b/mobile/assets/windows.svg new file mode 100644 index 0000000..82d9f74 --- /dev/null +++ b/mobile/assets/windows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index dc9ada4..b8d0df6 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 7353c41..44055a8 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 797d452..3851e58 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 6ed2d93..d247ac2 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cd7b00..e6736c0 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index fe73094..ad2d108 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 321773c..8b4c5b9 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 797d452..3851e58 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 502f463..67d932b 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 0ec3034..dcdcd8a 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 0ec3034..dcdcd8a 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index e9f5fea..3f42acb 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 84ac32a..315f234 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 8953cba..05f73ed 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 0467bf1..e00f81f 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png index 9da19ea..918c0ac 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png index 9da19ea..3867294 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png index 9da19ea..8b37cf2 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and b/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard b/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard index f2e259c..f6d9cd0 100644 --- a/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ b/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -32,6 +32,6 @@ - + diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 07202ea..a15864f 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -7,7 +7,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Mobile + EasyNode CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - mobile + EasyNode CFBundlePackageType APPL CFBundleShortVersionString diff --git a/mobile/lib/app.dart b/mobile/lib/app.dart index 945aba9..b215790 100644 --- a/mobile/lib/app.dart +++ b/mobile/lib/app.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -194,11 +196,11 @@ class _AppRootState extends ConsumerState<_AppRoot> { return MaterialApp( title: 'EasyNode', themeMode: ThemeMode.system, - theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo), + theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.amber), darkTheme: ThemeData( useMaterial3: true, brightness: Brightness.dark, - colorSchemeSeed: Colors.indigo, + colorSchemeSeed: Colors.amber, ), locale: ref.watch(localeProvider), localizationsDelegates: const [ @@ -211,7 +213,166 @@ class _AppRootState extends ConsumerState<_AppRoot> { localeResolutionCallback: (deviceLocale, supported) { return AppLocalizations.resolve(deviceLocale, supported); }, - home: home, + home: _BrandedSplashGate(child: home), + ); + } +} + +class _BrandedSplashGate extends StatefulWidget { + const _BrandedSplashGate({required this.child}); + + final Widget child; + + @override + State<_BrandedSplashGate> createState() => _BrandedSplashGateState(); +} + +class _BrandedSplashGateState extends State<_BrandedSplashGate> + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _logoScale; + late final Animation _logoFade; + late final Animation _textOffset; + bool _showSplash = true; + Timer? _timer; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 850), + ); + _logoScale = CurvedAnimation( + parent: _controller, + curve: Curves.elasticOut, + ); + _logoFade = CurvedAnimation( + parent: _controller, + curve: const Interval(0, 0.45, curve: Curves.easeOut), + ); + _textOffset = Tween( + begin: const Offset(0, 0.35), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.18, 0.7, curve: Curves.easeOutBack), + ), + ); + unawaited(_controller.forward()); + _timer = Timer(const Duration(milliseconds: 1250), () { + if (mounted) setState(() => _showSplash = false); + }); + } + + @override + void dispose() { + _timer?.cancel(); + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 420), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + transitionBuilder: (child, animation) { + final offset = Tween( + begin: const Offset(0, 0.02), + end: Offset.zero, + ).animate(animation); + return FadeTransition( + opacity: animation, + child: SlideTransition(position: offset, child: child), + ); + }, + child: _showSplash + ? _BrandedSplash( + key: const ValueKey('splash'), + controller: _controller, + logoScale: _logoScale, + logoFade: _logoFade, + textOffset: _textOffset, + ) + : KeyedSubtree(key: const ValueKey('home'), child: widget.child), + ); + } +} + +class _BrandedSplash extends StatelessWidget { + const _BrandedSplash({ + super.key, + required this.controller, + required this.logoScale, + required this.logoFade, + required this.textOffset, + }); + + final AnimationController controller; + final Animation logoScale; + final Animation logoFade; + final Animation textOffset; + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colorScheme; + final theme = Theme.of(context); + return Scaffold( + backgroundColor: colors.surfaceContainerLowest, + body: Center( + child: AnimatedBuilder( + animation: controller, + builder: (context, _) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + FadeTransition( + opacity: logoFade, + child: ScaleTransition( + scale: logoScale, + child: Container( + width: 104, + height: 104, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + boxShadow: [ + BoxShadow( + color: colors.primary.withValues(alpha: 0.18), + blurRadius: 28, + offset: const Offset(0, 12), + ), + ], + ), + child: Image.asset( + 'assets/logo_v2_01.png', + fit: BoxFit.cover, + ), + ), + ), + ), + const SizedBox(height: 18), + SlideTransition( + position: textOffset, + child: FadeTransition( + opacity: logoFade, + child: Text( + 'EasyNode', + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w800, + color: colors.onSurface, + ), + ), + ), + ), + ], + ); + }, + ), + ), ); } } diff --git a/mobile/lib/features/auth/login_page.dart b/mobile/lib/features/auth/login_page.dart index 9d48f51..6c1cf30 100644 --- a/mobile/lib/features/auth/login_page.dart +++ b/mobile/lib/features/auth/login_page.dart @@ -37,12 +37,11 @@ class _LoginPageState extends State { final FocusNode _userFocus = FocusNode(); final FocusNode _passwordFocus = FocusNode(); - LoginExpiry _expiry = LoginExpiry.temporary; + LoginExpiry _expiry = LoginExpiry.currentDay; bool _savePassword = false; bool _httpRiskAccepted = false; bool _showHttpRiskBanner = false; bool _submitting = false; - bool _passwordVisible = false; String? _errorMessage; @override @@ -130,117 +129,192 @@ class _LoginPageState extends State { @override Widget build(BuildContext context) { final l = AppLocalizations.of(context); - final spacing = const SizedBox(height: 12); + final theme = Theme.of(context); + final colors = theme.colorScheme; + final spacing = const SizedBox(height: 14); return Scaffold( - appBar: AppBar(title: Text(l.tr('app.title'))), + backgroundColor: colors.surfaceContainerLowest, body: SafeArea( - child: ListView( - padding: const EdgeInsets.all(16), - children: [ - Text(l.tr('app.title'), - style: Theme.of(context).textTheme.headlineMedium), - Text( - l.tr('app.subtitle'), - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 24), - TextField( - key: const Key('field-server'), - controller: _serverCtrl, - focusNode: _serverFocus, - keyboardType: TextInputType.url, - textInputAction: TextInputAction.next, - onSubmitted: (_) => _userFocus.requestFocus(), - decoration: InputDecoration( - labelText: l.tr('login.serverAddress'), - hintText: l.tr('login.serverAddressHint'), - border: const OutlineInputBorder(), - ), - ), - spacing, - TextField( - key: const Key('field-username'), - controller: _userCtrl, - focusNode: _userFocus, - textInputAction: TextInputAction.next, - onSubmitted: (_) => _passwordFocus.requestFocus(), - decoration: InputDecoration( - labelText: l.tr('login.username'), - border: const OutlineInputBorder(), - ), - ), - spacing, - TextField( - key: const Key('field-password'), - controller: _pwdCtrl, - focusNode: _passwordFocus, - obscureText: !_passwordVisible, - textInputAction: TextInputAction.done, - onSubmitted: (_) => _submit(), - decoration: InputDecoration( - labelText: l.tr('login.password'), - border: const OutlineInputBorder(), - suffixIcon: IconButton( - tooltip: _passwordVisible - ? l.tr('login.hidePassword') - : l.tr('login.showPassword'), - icon: Icon( - _passwordVisible ? Icons.visibility_off : Icons.visibility, - ), - onPressed: () => - setState(() => _passwordVisible = !_passwordVisible), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 520), + child: ListView( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 24), + children: [ + _LoginHero( + title: l.tr('app.title'), + subtitle: l.tr('app.subtitle'), ), - ), + const SizedBox(height: 24), + Card( + elevation: 0, + color: colors.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.all(18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + key: const Key('field-server'), + controller: _serverCtrl, + focusNode: _serverFocus, + keyboardType: TextInputType.url, + textInputAction: TextInputAction.next, + onSubmitted: (_) => _userFocus.requestFocus(), + decoration: InputDecoration( + labelText: l.tr('login.serverAddress'), + hintText: l.tr('login.serverAddressHint'), + prefixIcon: const Icon(Icons.dns_outlined), + border: const OutlineInputBorder(), + ), + ), + spacing, + TextField( + key: const Key('field-username'), + controller: _userCtrl, + focusNode: _userFocus, + textInputAction: TextInputAction.next, + onSubmitted: (_) => _passwordFocus.requestFocus(), + decoration: InputDecoration( + labelText: l.tr('login.username'), + prefixIcon: const Icon(Icons.person_outline), + border: const OutlineInputBorder(), + ), + ), + spacing, + TextField( + key: const Key('field-password'), + controller: _pwdCtrl, + focusNode: _passwordFocus, + obscureText: true, + textInputAction: TextInputAction.done, + onSubmitted: (_) => _submit(), + decoration: InputDecoration( + labelText: l.tr('login.password'), + prefixIcon: const Icon(Icons.lock_outline), + border: const OutlineInputBorder(), + ), + ), + spacing, + TextField( + key: const Key('field-mfa'), + controller: _mfaCtrl, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: l.tr('login.mfa'), + prefixIcon: const Icon(Icons.pin_outlined), + border: const OutlineInputBorder(), + ), + ), + const SizedBox(height: 18), + _ExpiryPicker( + value: _expiry, + onChanged: (value) => setState(() => _expiry = value), + ), + const SizedBox(height: 8), + SwitchListTile( + key: const Key('switch-save-password'), + contentPadding: EdgeInsets.zero, + title: Text(l.tr('login.savePassword')), + secondary: const Icon(Icons.enhanced_encryption), + value: _savePassword, + onChanged: (value) => + setState(() => _savePassword = value), + ), + if (_showHttpRiskBanner) ...[ + spacing, + _HttpRiskBanner(onConfirm: _acceptHttpRiskAndSubmit), + ], + if (_errorMessage != null) ...[ + spacing, + _ErrorBox(message: _errorMessage!), + ], + const SizedBox(height: 18), + SizedBox( + height: 48, + child: FilledButton.icon( + key: const Key('btn-login'), + onPressed: _submitting ? null : _submit, + icon: _submitting + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Icons.login), + label: Text(l.tr('login.submit')), + ), + ), + ], + ), + ), + ), + ], ), - spacing, - TextField( - key: const Key('field-mfa'), - controller: _mfaCtrl, - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: l.tr('login.mfa'), - border: const OutlineInputBorder(), - ), - ), - spacing, - _ExpiryPicker( - value: _expiry, - onChanged: (value) => setState(() => _expiry = value), - ), - SwitchListTile( - key: const Key('switch-save-password'), - contentPadding: EdgeInsets.zero, - title: Text(l.tr('login.savePassword')), - value: _savePassword, - onChanged: (value) => setState(() => _savePassword = value), - ), - if (_showHttpRiskBanner) ...[ - spacing, - _HttpRiskBanner(onConfirm: _acceptHttpRiskAndSubmit), - ], - if (_errorMessage != null) ...[ - spacing, - _ErrorBox(message: _errorMessage!), - ], - const SizedBox(height: 16), - FilledButton( - key: const Key('btn-login'), - onPressed: _submitting ? null : _submit, - child: _submitting - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : Text(l.tr('login.submit')), - ), - ], + ), ), ), ); } } +class _LoginHero extends StatelessWidget { + const _LoginHero({required this.title, required this.subtitle}); + + final String title; + final String subtitle; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colorScheme; + return Row( + children: [ + Container( + width: 56, + height: 56, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: colors.surface, + borderRadius: BorderRadius.circular(16), + ), + child: Image.asset( + 'assets/logo_v2_01.png', + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: theme.textTheme.bodyMedium?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ); + } +} + class _HttpRiskBanner extends StatelessWidget { const _HttpRiskBanner({required this.onConfirm}); @@ -331,7 +405,6 @@ class _ExpiryPicker extends StatelessWidget { final ValueChanged onChanged; static const _optionKeys = { - LoginExpiry.temporary: 'login.expiry.temporary', LoginExpiry.currentDay: 'login.expiry.currentDay', LoginExpiry.threeDays: 'login.expiry.threeDays', LoginExpiry.sevenDays: 'login.expiry.sevenDays', @@ -340,24 +413,29 @@ class _ExpiryPicker extends StatelessWidget { @override Widget build(BuildContext context) { final l = AppLocalizations.of(context); - return InputDecorator( - decoration: InputDecoration( - labelText: l.tr('login.sessionDuration'), - border: const OutlineInputBorder(), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: value, - items: [ - for (final entry in _optionKeys.entries) - DropdownMenuItem(value: entry.key, child: Text(l.tr(entry.value))), - ], - onChanged: (v) { - if (v != null) onChanged(v); - }, + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l.tr('login.sessionDuration'), + style: theme.textTheme.labelLarge, ), - ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (final entry in _optionKeys.entries) + ChoiceChip( + label: Text(l.tr(entry.value)), + selected: entry.key == value, + showCheckmark: false, + onSelected: (_) => onChanged(entry.key), + ), + ], + ), + ], ); } } diff --git a/mobile/lib/features/servers/server_model.dart b/mobile/lib/features/servers/server_model.dart index 90ee300..7f125f0 100644 --- a/mobile/lib/features/servers/server_model.dart +++ b/mobile/lib/features/servers/server_model.dart @@ -11,6 +11,7 @@ class ServerModel { required this.port, required this.username, required this.authType, + required this.connectType, required this.group, required this.tag, required this.expired, @@ -23,6 +24,7 @@ class ServerModel { final int port; final String username; final String authType; + final String connectType; final String group; final List tag; final bool expired; @@ -53,6 +55,7 @@ class ServerModel { port: port, username: (json['username'] ?? '').toString(), authType: (json['authType'] ?? '').toString(), + connectType: (json['connectType'] ?? '').toString(), group: (json['group'] ?? '').toString(), tag: tag, expired: json['expired'] == true, @@ -63,6 +66,7 @@ class ServerModel { /// Whether the connect button should be enabled. The server marks hosts /// without auth fields as `isConfig: false`. bool get canConnect => isConfig; + bool get isWindows => connectType == 'rdp'; String get displayName => name.isEmpty ? host : name; String get connectionLabel => '$username@$host:$port'; diff --git a/mobile/lib/features/servers/servers_tab.dart b/mobile/lib/features/servers/servers_tab.dart index 50f844d..5097468 100644 --- a/mobile/lib/features/servers/servers_tab.dart +++ b/mobile/lib/features/servers/servers_tab.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import '../../core/api/api_result.dart'; import '../../l10n/app_localizations.dart'; @@ -9,6 +10,7 @@ import '../../state/host_list_notifier.dart'; import '../../state/terminal_providers.dart'; import '../terminal/ssh_connection_config.dart'; import '../terminal/terminal_shell_page.dart'; +import '../terminal/terminal_session_manager.dart'; import 'server_model.dart'; /// First bottom-nav tab — server list + connect action. Was a top-level @@ -36,15 +38,18 @@ class _ServersTabState extends ConsumerState { Future _refresh() => ref.read(hostListProvider.notifier).refresh(); Future _connect(ServerModel server) async { - if (!server.canConnect) return; - final manager = ref.read(terminalSessionManagerProvider); - - final existing = manager.firstForHost(server.id); - if (existing != null) { - manager.setActive(existing.id); - _openShell(); + if (server.isWindows) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + AppLocalizations.of(context).tr('servers.windowsUnsupported'), + ), + ), + ); return; } + if (!server.canConnect) return; + final manager = ref.read(terminalSessionManagerProvider); setState(() => _connectingIds.add(server.id)); final SshConnectionConfig config; @@ -130,10 +135,28 @@ class _ServersTabState extends ConsumerState { final l = AppLocalizations.of(context); return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surfaceContainerLowest, appBar: AppBar( - centerTitle: true, automaticallyImplyLeading: false, - title: Text(l.tr('servers.title')), + title: Row( + children: [ + Container( + width: 34, + height: 34, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(10), + ), + child: Image.asset( + 'assets/logo_v2_01.png', + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 10), + Text(l.tr('servers.title')), + ], + ), actions: [ IconButton( tooltip: @@ -170,7 +193,7 @@ class _ServersTabState extends ConsumerState { Widget _buildBody( AsyncValue> hostsAsync, - Object manager, + TerminalSessionManager manager, ) { final l = AppLocalizations.of(context); return hostsAsync.when( @@ -199,15 +222,18 @@ class _ServersTabState extends ConsumerState { }, data: (servers) { final filtered = _filteredServers(servers); - final sessions = ref.read(terminalSessionManagerProvider).sessions.length; + final sessions = manager.sessions.length; return ListView( physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.fromLTRB(12, 8, 12, 16), + padding: const EdgeInsets.fromLTRB(16, 10, 16, 18), children: [ - _ActiveTerminalBanner( - count: sessions, - onTap: _openShell, - onCloseAll: _confirmCloseAllTerminals, + _ServersSummary( + total: servers.length, + visible: filtered.length, + activeTerminals: sessions, + onOpenTerminals: sessions > 0 ? _openShell : null, + onCloseAllTerminals: + sessions > 0 ? _confirmCloseAllTerminals : null, ), AnimatedSize( duration: const Duration(milliseconds: 220), @@ -250,7 +276,10 @@ class _ServersTabState extends ConsumerState { _MessageState(message: l.tr('servers.emptyFiltered')) else for (final server in filtered) - _ServerCard(server: server, state: this), + _ServerCard( + server: server, + state: this, + ), ], ); }, @@ -280,59 +309,114 @@ class _ServersTabState extends ConsumerState { } } -class _ActiveTerminalBanner extends StatelessWidget { - const _ActiveTerminalBanner({ - required this.count, - required this.onTap, - required this.onCloseAll, +class _ServersSummary extends StatelessWidget { + const _ServersSummary({ + required this.total, + required this.visible, + required this.activeTerminals, + required this.onOpenTerminals, + required this.onCloseAllTerminals, }); - final int count; - final VoidCallback onTap; - final VoidCallback onCloseAll; + final int total; + final int visible; + final int activeTerminals; + final VoidCallback? onOpenTerminals; + final VoidCallback? onCloseAllTerminals; @override Widget build(BuildContext context) { - if (count == 0) return const SizedBox.shrink(); final colors = Theme.of(context).colorScheme; final l = AppLocalizations.of(context); return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Material( - color: colors.primaryContainer, - borderRadius: BorderRadius.circular(8), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: Row( - children: [ - const Icon(Icons.layers), - const SizedBox(width: 10), - Expanded( - child: Text( - count == 1 - ? l.tr('servers.activeTerminalsOne') - : l.trf('servers.activeTerminalsMany', [count]), - ), - ), - const Icon(Icons.chevron_right), - const SizedBox(width: 4), - InkResponse( - onTap: onCloseAll, - radius: 16, - child: Tooltip( - message: l.tr('servers.closeAllTooltip'), - child: Icon( - Icons.close, - size: 18, - color: colors.onPrimaryContainer, + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + Expanded( + child: _MetricPill( + icon: Icons.dns_outlined, + value: visible == total ? '$total' : '$visible/$total', + color: colors.primaryContainer, + foreground: colors.onPrimaryContainer, + ), + ), + const SizedBox(width: 8), + Expanded( + child: _MetricPill( + icon: Icons.terminal, + value: '$activeTerminals', + color: colors.tertiaryContainer, + foreground: colors.onTertiaryContainer, + onTap: onOpenTerminals, + trailing: activeTerminals > 0 + ? _CloseTerminalsButton( + tooltip: l.tr('servers.closeAllTooltip'), + foreground: colors.onTertiaryContainer, + onPressed: onCloseAllTerminals, + ) + : null, + ), + ), + ], + ), + ); + } +} + +class _MetricPill extends StatelessWidget { + const _MetricPill({ + required this.icon, + required this.value, + required this.color, + required this.foreground, + this.onTap, + this.trailing, + }); + + final IconData icon; + final String value; + final Color color; + final Color foreground; + final VoidCallback? onTap; + final Widget? trailing; + + @override + Widget build(BuildContext context) { + return Material( + color: color, + borderRadius: BorderRadius.circular(12), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: onTap, + child: Container( + height: 42, + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Stack( + alignment: Alignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 18, color: foreground), + const SizedBox(width: 8), + Flexible( + child: Text( + value, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: foreground, + fontWeight: FontWeight.w700, + ), ), ), + ], + ), + if (trailing != null) + Positioned( + right: -6, + child: trailing!, ), - ], - ), + ], ), ), ), @@ -340,8 +424,41 @@ class _ActiveTerminalBanner extends StatelessWidget { } } +class _CloseTerminalsButton extends StatelessWidget { + const _CloseTerminalsButton({ + required this.tooltip, + required this.foreground, + required this.onPressed, + }); + + final String tooltip; + final Color foreground; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Tooltip( + message: tooltip, + child: IconButton( + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor(width: 32, height: 32), + style: IconButton.styleFrom( + backgroundColor: foreground.withValues(alpha: 0.10), + foregroundColor: foreground, + ), + icon: const Icon(Icons.layers_clear, size: 16), + onPressed: onPressed, + ), + ); + } +} + class _ServerCard extends StatelessWidget { - const _ServerCard({required this.server, required this.state}); + const _ServerCard({ + required this.server, + required this.state, + }); final ServerModel server; final _ServersTabState state; @@ -350,42 +467,123 @@ class _ServerCard extends StatelessWidget { Widget build(BuildContext context) { final connecting = state._connectingIds.contains(server.id); final l = AppLocalizations.of(context); + final colors = Theme.of(context).colorScheme; + final statusColor = server.canConnect + ? colors.primary + : colors.onSurfaceVariant; + final iconAsset = server.isWindows + ? 'assets/windows.svg' + : 'assets/linux.svg'; return Card( key: Key('server-${server.id}'), + elevation: 0, margin: const EdgeInsets.symmetric(vertical: 6), - child: ListTile( - title: Text(server.displayName, overflow: TextOverflow.ellipsis), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, + color: colors.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide(color: colors.outlineVariant), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Text(server.connectionLabel, overflow: TextOverflow.ellipsis), - const SizedBox(height: 6), - Wrap( - spacing: 6, - runSpacing: 4, + Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _InfoChip( - label: server.authType.isEmpty - ? l.tr('servers.authFallback') - : server.authType, + Container( + width: 42, + height: 42, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(12), + ), + child: SvgPicture.asset( + iconAsset, + fit: BoxFit.contain, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + server.displayName, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.w700), + ), + const SizedBox(height: 3), + Text( + server.connectionLabel, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: Wrap( + spacing: 6, + runSpacing: 6, + children: [ + _InfoChip( + label: server.authType.isEmpty + ? l.tr('servers.authFallback') + : server.authType, + ), + if (server.group.isNotEmpty) + _InfoChip(label: server.group), + ], + ), + ), + const SizedBox(width: 10), + SizedBox( + height: 40, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 160), + child: connecting + ? const SizedBox( + key: ValueKey('connecting'), + width: 96, + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + ), + ) + : FilledButton.icon( + key: const ValueKey('connect-button'), + onPressed: server.canConnect || server.isWindows + ? () => state._connect(server) + : null, + icon: Icon( + server.canConnect + ? Icons.play_arrow + : Icons.lock_outline, + size: 18, + ), + label: Text(state._actionText(server)), + ), + ), ), - if (server.group.isNotEmpty) _InfoChip(label: server.group), ], ), ], ), - trailing: connecting - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : FilledButton.tonal( - onPressed: server.canConnect - ? () => state._connect(server) - : null, - child: Text(state._actionText(server)), - ), ), ); } diff --git a/mobile/lib/features/terminal/terminal_shell_page.dart b/mobile/lib/features/terminal/terminal_shell_page.dart index 4cf39ab..a3c5615 100644 --- a/mobile/lib/features/terminal/terminal_shell_page.dart +++ b/mobile/lib/features/terminal/terminal_shell_page.dart @@ -147,6 +147,13 @@ class _TerminalShellPageState extends ConsumerState { } } + Future _closeAllAndLeave(TerminalSessionManager manager) async { + await manager.closeAll(); + if (mounted) { + Navigator.of(context).maybePop(); + } + } + @override Widget build(BuildContext context) { final manager = ref.watch(terminalSessionManagerProvider); @@ -173,6 +180,7 @@ class _TerminalShellPageState extends ConsumerState { onSelect: manager.setActive, onNew: _openServerMenu, onClose: _closeActive, + onCloseAll: () => _closeAllAndLeave(manager), onReconnect: active == null ? null : () => manager.reconnect(active.id), @@ -225,6 +233,7 @@ class _TerminalTopBar extends StatelessWidget { required this.onSelect, required this.onNew, required this.onClose, + required this.onCloseAll, required this.onReconnect, }); @@ -234,6 +243,7 @@ class _TerminalTopBar extends StatelessWidget { final ValueChanged onSelect; final ValueChanged onNew; final VoidCallback onClose; + final Future Function() onCloseAll; final VoidCallback? onReconnect; Future _openSessionsMenu(BuildContext context) async { @@ -249,11 +259,27 @@ class _TerminalTopBar extends StatelessWidget { final menuRect = Rect.fromLTWH(0, menuTop, 0, 0); final overlayRect = Offset.zero & overlaySize; final activeSession = active; + final l = AppLocalizations.of(context); final selected = await showMenu( context: context, position: RelativeRect.fromRect(menuRect, overlayRect), constraints: BoxConstraints.tightFor(width: menuWidth), items: [ + if (sessions.length >= 2) ...[ + PopupMenuItem( + value: _kCloseAllTerminalsAction, + child: Row( + children: [ + const Icon(Icons.layers_clear, size: 20), + const SizedBox(width: 10), + Expanded( + child: Text(l.tr('terminal.closeAllTerminals')), + ), + ], + ), + ), + const PopupMenuDivider(), + ], for (final session in sessions) PopupMenuItem( value: session.id, @@ -275,9 +301,31 @@ class _TerminalTopBar extends StatelessWidget { ), ], ); - if (selected != null) { - onSelect(selected); + if (selected == null) return; + if (selected == _kCloseAllTerminalsAction) { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l.tr('terminal.closeAllTitle')), + content: Text(l.trf('terminal.closeAllBodyMany', [sessions.length])), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(l.tr('common.cancel')), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(l.tr('common.closeAll')), + ), + ], + ), + ); + if (confirmed == true) { + await onCloseAll(); + } + return; } + onSelect(selected); } @override @@ -554,3 +602,4 @@ String _statusText(AppLocalizations l, TerminalSessionStatus status) { } const double _kTopBarHeight = 52; +const String _kCloseAllTerminalsAction = '__close_all_terminals__'; diff --git a/mobile/lib/l10n/strings_en.dart b/mobile/lib/l10n/strings_en.dart index 0dc1ebf..0b8630f 100644 --- a/mobile/lib/l10n/strings_en.dart +++ b/mobile/lib/l10n/strings_en.dart @@ -31,7 +31,7 @@ const Map stringsEn = { 'login.failed': 'Login failed', 'login.httpRiskTitle': 'HTTP is not encrypted', 'login.httpRiskBody': - 'Your token and session cookie can be intercepted. Use HTTPS when possible.', + 'All your data may be stolen. It is recommended to use HTTPS.', 'login.errEmptyUsername': 'Please enter a username', 'login.errEmptyPassword': 'Please enter a password', 'login.errInvalidServer': 'Please enter a valid server address', @@ -59,6 +59,7 @@ const Map stringsEn = { 'servers.emptyFiltered': 'No matching servers.', 'servers.notConfigured': 'Not configured', 'servers.fetchSshFailed': 'Failed to get SSH config: {0}', + 'servers.windowsUnsupported': 'Windows connections are not supported yet.', 'servers.activeTerminalsOne': '1 active terminal', 'servers.activeTerminalsMany': '{0} active terminals', 'servers.closeAllTitle': 'Close all terminals?', @@ -85,6 +86,9 @@ const Map stringsEn = { 'terminal.noActive': 'No active terminal', 'terminal.reconnect': 'Reconnect', 'terminal.closeTerminal': 'Close terminal', + 'terminal.closeAllTerminals': 'Close all terminals', + 'terminal.closeAllTitle': 'Close all terminals?', + 'terminal.closeAllBodyMany': 'This will disconnect {0} active terminals.', 'terminal.newTerminal': 'New terminal', 'terminal.noServers': 'No servers available', 'terminal.loadServersFailed': 'Failed to load servers: {0}', diff --git a/mobile/lib/l10n/strings_zh.dart b/mobile/lib/l10n/strings_zh.dart index 7bcab73..d39dfb7 100644 --- a/mobile/lib/l10n/strings_zh.dart +++ b/mobile/lib/l10n/strings_zh.dart @@ -29,14 +29,13 @@ const Map stringsZh = { 'login.submit': '登录', 'login.failed': '登录失败', 'login.httpRiskTitle': 'HTTP 连接未加密', - 'login.httpRiskBody': '您的 token 与 session cookie 可能被截获,建议尽量使用 HTTPS。', + 'login.httpRiskBody': '您的所有数据可能会被盗取,建议使用 HTTPS。', 'login.errEmptyUsername': '请输入用户名', 'login.errEmptyPassword': '请输入密码', 'login.errInvalidServer': '请输入有效的服务端地址', 'login.errSchemeUnsupported': '服务端地址仅支持 http 或 https', 'login.errLoginGeneric': '登录失败', 'login.errMissingFields': '服务端登录响应缺少必要字段', - 'login.expiry.temporary': '临时会话,1 小时', 'login.expiry.currentDay': '今天', 'login.expiry.threeDays': '3 天', @@ -56,6 +55,7 @@ const Map stringsZh = { 'servers.emptyFiltered': '没有匹配的服务器。', 'servers.notConfigured': '未配置', 'servers.fetchSshFailed': '获取 SSH 连接信息失败:{0}', + 'servers.windowsUnsupported': 'Windows 连接暂不支持。', 'servers.activeTerminalsOne': '1 个活动终端', 'servers.activeTerminalsMany': '{0} 个活动终端', 'servers.closeAllTitle': '关闭所有终端?', @@ -82,6 +82,9 @@ const Map stringsZh = { 'terminal.noActive': '没有活动的终端', 'terminal.reconnect': '重新连接', 'terminal.closeTerminal': '关闭终端', + 'terminal.closeAllTerminals': '关闭所有终端', + 'terminal.closeAllTitle': '关闭所有终端?', + 'terminal.closeAllBodyMany': '将断开 {0} 个活动终端。', 'terminal.newTerminal': '新建终端', 'terminal.noServers': '没有可用的服务器', 'terminal.loadServersFailed': '加载服务器失败:{0}', diff --git a/mobile/linux/CMakeLists.txt b/mobile/linux/CMakeLists.txt index 9a39eb1..507c08a 100644 --- a/mobile/linux/CMakeLists.txt +++ b/mobile/linux/CMakeLists.txt @@ -4,7 +4,7 @@ project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "mobile") +set(BINARY_NAME "EasyNode") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "com.example.mobile") diff --git a/mobile/linux/runner/my_application.cc b/mobile/linux/runner/my_application.cc index c03496a..b61044a 100644 --- a/mobile/linux/runner/my_application.cc +++ b/mobile/linux/runner/my_application.cc @@ -45,11 +45,11 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "mobile"); + gtk_header_bar_set_title(header_bar, "EasyNode"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "mobile"); + gtk_window_set_title(window, "EasyNode"); } gtk_window_set_default_size(window, 1280, 720); diff --git a/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 82b6f9d..b8d0df6 100644 Binary files a/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 13b35eb..9794d7e 100644 Binary files a/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 0a3f5fa..d50363d 100644 Binary files a/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bdb5722..0eb47a7 100644 Binary files a/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index f083318..f54f372 100644 Binary files a/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index 326c0e7..78afe0d 100644 Binary files a/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 2f1632c..1e65c27 100644 Binary files a/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/mobile/macos/Runner/Configs/AppInfo.xcconfig b/mobile/macos/Runner/Configs/AppInfo.xcconfig index d378045..44501d0 100644 --- a/mobile/macos/Runner/Configs/AppInfo.xcconfig +++ b/mobile/macos/Runner/Configs/AppInfo.xcconfig @@ -5,7 +5,7 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = mobile +PRODUCT_NAME = EasyNode // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 861ddf3..5fab4c2 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -243,6 +243,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" + url: "https://pub.dev" + source: hosted + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -429,6 +437,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: transitive description: @@ -477,6 +493,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" pinenacl: dependency: transitive description: @@ -682,6 +706,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.3" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: b9b3f391857781aa96acacef96066f2f49b4cd03cf9fce3ca4d8da2ef5ea129e + url: "https://pub.dev" + source: hosted + version: "1.2.3" vector_math: dependency: transitive description: @@ -722,6 +770,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" xterm: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 2a08a1c..a9e1191 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: xterm: ^4.0.0 uuid: ^4.5.1 flutter_riverpod: ^2.5.1 + flutter_svg: ^2.2.4 dev_dependencies: flutter_test: @@ -31,3 +32,7 @@ dev_dependencies: flutter: uses-material-design: true + assets: + - assets/logo_v2_01.png + - assets/linux.svg + - assets/windows.svg diff --git a/mobile/web/favicon.png b/mobile/web/favicon.png index 8aaa46a..f54f372 100644 Binary files a/mobile/web/favicon.png and b/mobile/web/favicon.png differ diff --git a/mobile/web/icons/Icon-192.png b/mobile/web/icons/Icon-192.png index b749bfe..118cbc6 100644 Binary files a/mobile/web/icons/Icon-192.png and b/mobile/web/icons/Icon-192.png differ diff --git a/mobile/web/icons/Icon-512.png b/mobile/web/icons/Icon-512.png index 88cfd48..78afe0d 100644 Binary files a/mobile/web/icons/Icon-512.png and b/mobile/web/icons/Icon-512.png differ diff --git a/mobile/web/icons/Icon-maskable-192.png b/mobile/web/icons/Icon-maskable-192.png index eb9b4d7..118cbc6 100644 Binary files a/mobile/web/icons/Icon-maskable-192.png and b/mobile/web/icons/Icon-maskable-192.png differ diff --git a/mobile/web/icons/Icon-maskable-512.png b/mobile/web/icons/Icon-maskable-512.png index d69c566..78afe0d 100644 Binary files a/mobile/web/icons/Icon-maskable-512.png and b/mobile/web/icons/Icon-maskable-512.png differ diff --git a/mobile/web/manifest.json b/mobile/web/manifest.json index 9dc3ff2..ed12090 100644 --- a/mobile/web/manifest.json +++ b/mobile/web/manifest.json @@ -1,11 +1,11 @@ { - "name": "mobile", - "short_name": "mobile", + "name": "EasyNode", + "short_name": "EasyNode", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", - "description": "A new Flutter project.", + "description": "EasyNode mobile app.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ diff --git a/mobile/windows/CMakeLists.txt b/mobile/windows/CMakeLists.txt index eb19942..8faa77c 100644 --- a/mobile/windows/CMakeLists.txt +++ b/mobile/windows/CMakeLists.txt @@ -1,10 +1,10 @@ # Project-level configuration. cmake_minimum_required(VERSION 3.14) -project(mobile LANGUAGES CXX) +project(EasyNode LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "mobile") +set(BINARY_NAME "EasyNode") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/mobile/windows/runner/Runner.rc b/mobile/windows/runner/Runner.rc index 910c995..28625f6 100644 --- a/mobile/windows/runner/Runner.rc +++ b/mobile/windows/runner/Runner.rc @@ -90,12 +90,12 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "mobile" "\0" + VALUE "FileDescription", "EasyNode" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "mobile" "\0" + VALUE "InternalName", "EasyNode" "\0" VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "mobile.exe" "\0" - VALUE "ProductName", "mobile" "\0" + VALUE "OriginalFilename", "EasyNode.exe" "\0" + VALUE "ProductName", "EasyNode" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/mobile/windows/runner/main.cpp b/mobile/windows/runner/main.cpp index dbb4717..6dd78fa 100644 --- a/mobile/windows/runner/main.cpp +++ b/mobile/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"mobile", origin, size)) { + if (!window.Create(L"EasyNode", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); diff --git a/mobile/windows/runner/resources/app_icon.ico b/mobile/windows/runner/resources/app_icon.ico index c04e20c..341fb38 100644 Binary files a/mobile/windows/runner/resources/app_icon.ico and b/mobile/windows/runner/resources/app_icon.ico differ