diff --git a/Dockerfile b/Dockerfile index 1a1479f98..65fc6a341 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ ENV BASE_URL="" ENV SEERR_BASE_URL="" ENV SEERR_HEADER="null" ENV FLADDER_WEBPATH="/" +ENV HIDE_PASSWORD_LOGIN="" COPY build/web /usr/share/nginx/html COPY docker-entrypoint.sh /docker-entrypoint.sh diff --git a/Dockerfile-rootless b/Dockerfile-rootless index 1ed773351..952c23476 100644 --- a/Dockerfile-rootless +++ b/Dockerfile-rootless @@ -6,6 +6,7 @@ ENV BASE_URL="" ENV SEERR_BASE_URL="" ENV SEERR_HEADER="null" ENV FLADDER_WEBPATH="/" +ENV HIDE_PASSWORD_LOGIN="" USER root COPY build/web /usr/share/nginx/html diff --git a/docker-compose.yml b/docker-compose.yml index 8625a2cf9..4c0a9f94e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,3 +9,4 @@ services: - SEERR_BASE_URL=https://seerr-url #OPTIONAL: Presets Seerr base URL - SEERR_HEADER={"key":"value"} #OPTIONAL: JSON object string of Seerr headers - FLADDER_WEBPATH=/ #OPTIONAL: Configures a subpath to run Fladder at + - HIDE_PASSWORD_LOGIN= #OPTIONAL: Set to "true" to hide password fields and only show QuickConnect diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index a2c39328c..a8196f1d2 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,11 +1,14 @@ #!/bin/sh set -e +HIDE_PW_VAL=$([ "$HIDE_PASSWORD_LOGIN" = "true" ] && echo true || echo null) + # Generate config.json from environment variables cat > /usr/share/nginx/html/assets/config/config.json < { bool hasBaseUrl, bool loading, String? tempSeerrUrl, - String? tempSeerrSessionCookie}); + String? tempSeerrSessionCookie, + bool hidePasswordLogin}); $ServerLoginModelCopyWith<$Res>? get serverLoginModel; } @@ -77,6 +79,7 @@ class _$LoginScreenModelCopyWithImpl<$Res> Object? loading = null, Object? tempSeerrUrl = freezed, Object? tempSeerrSessionCookie = freezed, + Object? hidePasswordLogin = null, }) { return _then(_self.copyWith( accounts: null == accounts @@ -111,6 +114,10 @@ class _$LoginScreenModelCopyWithImpl<$Res> ? _self.tempSeerrSessionCookie : tempSeerrSessionCookie // ignore: cast_nullable_to_non_nullable as String?, + hidePasswordLogin: null == hidePasswordLogin + ? _self.hidePasswordLogin + : hidePasswordLogin // ignore: cast_nullable_to_non_nullable + as bool, )); } @@ -230,7 +237,8 @@ extension LoginScreenModelPatterns on LoginScreenModel { bool hasBaseUrl, bool loading, String? tempSeerrUrl, - String? tempSeerrSessionCookie)? + String? tempSeerrSessionCookie, + bool hidePasswordLogin)? $default, { required TResult orElse(), }) { @@ -245,7 +253,8 @@ extension LoginScreenModelPatterns on LoginScreenModel { _that.hasBaseUrl, _that.loading, _that.tempSeerrUrl, - _that.tempSeerrSessionCookie); + _that.tempSeerrSessionCookie, + _that.hidePasswordLogin); case _: return orElse(); } @@ -274,7 +283,8 @@ extension LoginScreenModelPatterns on LoginScreenModel { bool hasBaseUrl, bool loading, String? tempSeerrUrl, - String? tempSeerrSessionCookie) + String? tempSeerrSessionCookie, + bool hidePasswordLogin) $default, ) { final _that = this; @@ -288,7 +298,8 @@ extension LoginScreenModelPatterns on LoginScreenModel { _that.hasBaseUrl, _that.loading, _that.tempSeerrUrl, - _that.tempSeerrSessionCookie); + _that.tempSeerrSessionCookie, + _that.hidePasswordLogin); case _: throw StateError('Unexpected subclass'); } @@ -316,7 +327,8 @@ extension LoginScreenModelPatterns on LoginScreenModel { bool hasBaseUrl, bool loading, String? tempSeerrUrl, - String? tempSeerrSessionCookie)? + String? tempSeerrSessionCookie, + bool hidePasswordLogin)? $default, ) { final _that = this; @@ -330,7 +342,8 @@ extension LoginScreenModelPatterns on LoginScreenModel { _that.hasBaseUrl, _that.loading, _that.tempSeerrUrl, - _that.tempSeerrSessionCookie); + _that.tempSeerrSessionCookie, + _that.hidePasswordLogin); case _: return null; } @@ -348,7 +361,8 @@ class _LoginScreenModel implements LoginScreenModel { this.hasBaseUrl = false, this.loading = false, this.tempSeerrUrl, - this.tempSeerrSessionCookie}) + this.tempSeerrSessionCookie, + this.hidePasswordLogin = false}) : _accounts = accounts; final List _accounts; @@ -377,6 +391,9 @@ class _LoginScreenModel implements LoginScreenModel { final String? tempSeerrUrl; @override final String? tempSeerrSessionCookie; + @override + @JsonKey() + final bool hidePasswordLogin; /// Create a copy of LoginScreenModel /// with the given fields replaced by the non-null parameter values. @@ -388,7 +405,7 @@ class _LoginScreenModel implements LoginScreenModel { @override String toString() { - return 'LoginScreenModel(accounts: $accounts, screen: $screen, serverLoginModel: $serverLoginModel, errorMessage: $errorMessage, hasBaseUrl: $hasBaseUrl, loading: $loading, tempSeerrUrl: $tempSeerrUrl, tempSeerrSessionCookie: $tempSeerrSessionCookie)'; + return 'LoginScreenModel(accounts: $accounts, screen: $screen, serverLoginModel: $serverLoginModel, errorMessage: $errorMessage, hasBaseUrl: $hasBaseUrl, loading: $loading, tempSeerrUrl: $tempSeerrUrl, tempSeerrSessionCookie: $tempSeerrSessionCookie, hidePasswordLogin: $hidePasswordLogin)'; } } @@ -408,7 +425,8 @@ abstract mixin class _$LoginScreenModelCopyWith<$Res> bool hasBaseUrl, bool loading, String? tempSeerrUrl, - String? tempSeerrSessionCookie}); + String? tempSeerrSessionCookie, + bool hidePasswordLogin}); @override $ServerLoginModelCopyWith<$Res>? get serverLoginModel; @@ -435,6 +453,7 @@ class __$LoginScreenModelCopyWithImpl<$Res> Object? loading = null, Object? tempSeerrUrl = freezed, Object? tempSeerrSessionCookie = freezed, + Object? hidePasswordLogin = null, }) { return _then(_LoginScreenModel( accounts: null == accounts @@ -469,6 +488,10 @@ class __$LoginScreenModelCopyWithImpl<$Res> ? _self.tempSeerrSessionCookie : tempSeerrSessionCookie // ignore: cast_nullable_to_non_nullable as String?, + hidePasswordLogin: null == hidePasswordLogin + ? _self.hidePasswordLogin + : hidePasswordLogin // ignore: cast_nullable_to_non_nullable + as bool, )); } diff --git a/lib/models/settings/client_settings_model.dart b/lib/models/settings/client_settings_model.dart index 688ceecc4..4a4ce5f92 100644 --- a/lib/models/settings/client_settings_model.dart +++ b/lib/models/settings/client_settings_model.dart @@ -91,6 +91,7 @@ abstract class ClientSettingsModel with _$ClientSettingsModel { @Default(false) bool usePosterForLibrary, @Default(false) bool useSystemIME, @Default(false) bool useTVExpandedLayout, + @Default(false) bool hidePasswordLogin, String? lastViewedUpdate, int? libraryPageSize, @Default({}) Map shortcuts, diff --git a/lib/models/settings/client_settings_model.freezed.dart b/lib/models/settings/client_settings_model.freezed.dart index a7af8642f..0dc53f58d 100644 --- a/lib/models/settings/client_settings_model.freezed.dart +++ b/lib/models/settings/client_settings_model.freezed.dart @@ -44,6 +44,7 @@ mixin _$ClientSettingsModel implements DiagnosticableTreeMixin { bool get usePosterForLibrary; bool get useSystemIME; bool get useTVExpandedLayout; + bool get hidePasswordLogin; String? get lastViewedUpdate; int? get libraryPageSize; Map get shortcuts; @@ -96,6 +97,7 @@ mixin _$ClientSettingsModel implements DiagnosticableTreeMixin { ..add(DiagnosticsProperty('usePosterForLibrary', usePosterForLibrary)) ..add(DiagnosticsProperty('useSystemIME', useSystemIME)) ..add(DiagnosticsProperty('useTVExpandedLayout', useTVExpandedLayout)) + ..add(DiagnosticsProperty('hidePasswordLogin', hidePasswordLogin)) ..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate)) ..add(DiagnosticsProperty('libraryPageSize', libraryPageSize)) ..add(DiagnosticsProperty('shortcuts', shortcuts)); @@ -103,7 +105,7 @@ mixin _$ClientSettingsModel implements DiagnosticableTreeMixin { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'ClientSettingsModel(syncPath: $syncPath, transcodeDownloadModel: $transcodeDownloadModel, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, updateNotificationsInterval: $updateNotificationsInterval, themeMode: $themeMode, themeColor: $themeColor, deriveColorsFromItem: $deriveColorsFromItem, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, expandSideBar: $expandSideBar, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundImage: $backgroundImage, enableBlurEffects: $enableBlurEffects, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, useSystemIME: $useSystemIME, useTVExpandedLayout: $useTVExpandedLayout, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize, shortcuts: $shortcuts)'; + return 'ClientSettingsModel(syncPath: $syncPath, transcodeDownloadModel: $transcodeDownloadModel, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, updateNotificationsInterval: $updateNotificationsInterval, themeMode: $themeMode, themeColor: $themeColor, deriveColorsFromItem: $deriveColorsFromItem, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, expandSideBar: $expandSideBar, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundImage: $backgroundImage, enableBlurEffects: $enableBlurEffects, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, useSystemIME: $useSystemIME, useTVExpandedLayout: $useTVExpandedLayout, hidePasswordLogin: $hidePasswordLogin, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize, shortcuts: $shortcuts)'; } } @@ -143,6 +145,7 @@ abstract mixin class $ClientSettingsModelCopyWith<$Res> { bool usePosterForLibrary, bool useSystemIME, bool useTVExpandedLayout, + bool hidePasswordLogin, String? lastViewedUpdate, int? libraryPageSize, Map shortcuts}); @@ -192,6 +195,7 @@ class _$ClientSettingsModelCopyWithImpl<$Res> Object? usePosterForLibrary = null, Object? useSystemIME = null, Object? useTVExpandedLayout = null, + Object? hidePasswordLogin = null, Object? lastViewedUpdate = freezed, Object? libraryPageSize = freezed, Object? shortcuts = null, @@ -313,6 +317,10 @@ class _$ClientSettingsModelCopyWithImpl<$Res> ? _self.useTVExpandedLayout : useTVExpandedLayout // ignore: cast_nullable_to_non_nullable as bool, + hidePasswordLogin: null == hidePasswordLogin + ? _self.hidePasswordLogin + : hidePasswordLogin // ignore: cast_nullable_to_non_nullable + as bool, lastViewedUpdate: freezed == lastViewedUpdate ? _self.lastViewedUpdate : lastViewedUpdate // ignore: cast_nullable_to_non_nullable @@ -463,6 +471,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel { bool usePosterForLibrary, bool useSystemIME, bool useTVExpandedLayout, + bool hidePasswordLogin, String? lastViewedUpdate, int? libraryPageSize, Map shortcuts)? @@ -502,6 +511,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel { _that.usePosterForLibrary, _that.useSystemIME, _that.useTVExpandedLayout, + _that.hidePasswordLogin, _that.lastViewedUpdate, _that.libraryPageSize, _that.shortcuts); @@ -555,6 +565,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel { bool usePosterForLibrary, bool useSystemIME, bool useTVExpandedLayout, + bool hidePasswordLogin, String? lastViewedUpdate, int? libraryPageSize, Map shortcuts) @@ -593,6 +604,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel { _that.usePosterForLibrary, _that.useSystemIME, _that.useTVExpandedLayout, + _that.hidePasswordLogin, _that.lastViewedUpdate, _that.libraryPageSize, _that.shortcuts); @@ -645,6 +657,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel { bool usePosterForLibrary, bool useSystemIME, bool useTVExpandedLayout, + bool hidePasswordLogin, String? lastViewedUpdate, int? libraryPageSize, Map shortcuts)? @@ -683,6 +696,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel { _that.usePosterForLibrary, _that.useSystemIME, _that.useTVExpandedLayout, + _that.hidePasswordLogin, _that.lastViewedUpdate, _that.libraryPageSize, _that.shortcuts); @@ -726,6 +740,7 @@ class _ClientSettingsModel extends ClientSettingsModel this.usePosterForLibrary = false, this.useSystemIME = false, this.useTVExpandedLayout = false, + this.hidePasswordLogin = false, this.lastViewedUpdate, this.libraryPageSize, final Map shortcuts = const {}}) @@ -818,6 +833,9 @@ class _ClientSettingsModel extends ClientSettingsModel @JsonKey() final bool useTVExpandedLayout; @override + @JsonKey() + final bool hidePasswordLogin; + @override final String? lastViewedUpdate; @override final int? libraryPageSize; @@ -883,6 +901,7 @@ class _ClientSettingsModel extends ClientSettingsModel ..add(DiagnosticsProperty('usePosterForLibrary', usePosterForLibrary)) ..add(DiagnosticsProperty('useSystemIME', useSystemIME)) ..add(DiagnosticsProperty('useTVExpandedLayout', useTVExpandedLayout)) + ..add(DiagnosticsProperty('hidePasswordLogin', hidePasswordLogin)) ..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate)) ..add(DiagnosticsProperty('libraryPageSize', libraryPageSize)) ..add(DiagnosticsProperty('shortcuts', shortcuts)); @@ -890,7 +909,7 @@ class _ClientSettingsModel extends ClientSettingsModel @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'ClientSettingsModel.internal(syncPath: $syncPath, transcodeDownloadModel: $transcodeDownloadModel, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, updateNotificationsInterval: $updateNotificationsInterval, themeMode: $themeMode, themeColor: $themeColor, deriveColorsFromItem: $deriveColorsFromItem, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, expandSideBar: $expandSideBar, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundImage: $backgroundImage, enableBlurEffects: $enableBlurEffects, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, useSystemIME: $useSystemIME, useTVExpandedLayout: $useTVExpandedLayout, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize, shortcuts: $shortcuts)'; + return 'ClientSettingsModel.internal(syncPath: $syncPath, transcodeDownloadModel: $transcodeDownloadModel, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, updateNotificationsInterval: $updateNotificationsInterval, themeMode: $themeMode, themeColor: $themeColor, deriveColorsFromItem: $deriveColorsFromItem, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, expandSideBar: $expandSideBar, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundImage: $backgroundImage, enableBlurEffects: $enableBlurEffects, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, useSystemIME: $useSystemIME, useTVExpandedLayout: $useTVExpandedLayout, hidePasswordLogin: $hidePasswordLogin, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize, shortcuts: $shortcuts)'; } } @@ -932,6 +951,7 @@ abstract mixin class _$ClientSettingsModelCopyWith<$Res> bool usePosterForLibrary, bool useSystemIME, bool useTVExpandedLayout, + bool hidePasswordLogin, String? lastViewedUpdate, int? libraryPageSize, Map shortcuts}); @@ -982,6 +1002,7 @@ class __$ClientSettingsModelCopyWithImpl<$Res> Object? usePosterForLibrary = null, Object? useSystemIME = null, Object? useTVExpandedLayout = null, + Object? hidePasswordLogin = null, Object? lastViewedUpdate = freezed, Object? libraryPageSize = freezed, Object? shortcuts = null, @@ -1103,6 +1124,10 @@ class __$ClientSettingsModelCopyWithImpl<$Res> ? _self.useTVExpandedLayout : useTVExpandedLayout // ignore: cast_nullable_to_non_nullable as bool, + hidePasswordLogin: null == hidePasswordLogin + ? _self.hidePasswordLogin + : hidePasswordLogin // ignore: cast_nullable_to_non_nullable + as bool, lastViewedUpdate: freezed == lastViewedUpdate ? _self.lastViewedUpdate : lastViewedUpdate // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/settings/client_settings_model.g.dart b/lib/models/settings/client_settings_model.g.dart index 613f3c3db..bb5a81e3e 100644 --- a/lib/models/settings/client_settings_model.g.dart +++ b/lib/models/settings/client_settings_model.g.dart @@ -57,6 +57,7 @@ _ClientSettingsModel _$ClientSettingsModelFromJson(Map json) => usePosterForLibrary: json['usePosterForLibrary'] as bool? ?? false, useSystemIME: json['useSystemIME'] as bool? ?? false, useTVExpandedLayout: json['useTVExpandedLayout'] as bool? ?? false, + hidePasswordLogin: json['hidePasswordLogin'] as bool? ?? false, lastViewedUpdate: json['lastViewedUpdate'] as String?, libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(), shortcuts: (json['shortcuts'] as Map?)?.map( @@ -99,6 +100,7 @@ Map _$ClientSettingsModelToJson( 'usePosterForLibrary': instance.usePosterForLibrary, 'useSystemIME': instance.useSystemIME, 'useTVExpandedLayout': instance.useTVExpandedLayout, + 'hidePasswordLogin': instance.hidePasswordLogin, 'lastViewedUpdate': instance.lastViewedUpdate, 'libraryPageSize': instance.libraryPageSize, 'shortcuts': instance.shortcuts diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index 9b479c8f1..dd9c3329c 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -50,6 +50,9 @@ class AuthNotifier extends StateNotifier { await setServer(url); } } + if (FladderConfig.hidePasswordLogin == true) { + state = state.copyWith(hidePasswordLogin: true); + } state = state.copyWith( accounts: currentAccounts, screen: currentAccounts.isEmpty ? LoginScreenType.login : LoginScreenType.users, diff --git a/lib/screens/login/login_screen_credentials.dart b/lib/screens/login/login_screen_credentials.dart index 1349a34a2..885494dd6 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -11,6 +11,7 @@ import 'package:fladder/models/account_model.dart'; import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/auth_provider.dart'; import 'package:fladder/providers/seerr_api_provider.dart'; +import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/shared_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; @@ -103,6 +104,38 @@ class _LoginScreenCredentialsState extends ConsumerState final hasBaseUrl = ref.watch(authProvider.select((value) => value.hasBaseUrl)); final urlError = ref.watch(authProvider.select((value) => value.errorMessage)); final hasQuickConnect = ref.watch(authProvider.select((value) => value.serverLoginModel?.hasQuickConnect ?? false)); + // Note: hidePasswordLogin is a UI preference, not a security control. + // It hides the password fields but does not disable password-based authentication on the server. + final hidePasswordLogin = ref.watch(authProvider.select((value) => value.hidePasswordLogin)) || + ref.watch(clientSettingsProvider.select((value) => value.hidePasswordLogin)); + + final advancedOptionsButton = IconButton.filledTonal( + onPressed: () async { + final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); + final result = await showAdvancedLoginOptionsDialog( + context, + initialSeerrUrl: tempSeerrUrl, + ); + if (result != null) { + ref.read(authProvider.notifier).setTempSeerrUrl(result); + } + }, + icon: const Icon(IconsaxPlusLinear.setting_3), + ); + + final backButton = IconButton.filledTonal( + onPressed: () => provider.goUserSelect(), + icon: const Icon(IconsaxPlusLinear.arrow_left_2), + ); + + final refreshButton = Tooltip( + message: context.localized.retrievePublicListOfUsers, + waitDuration: const Duration(seconds: 1), + child: IconButton.filled( + onPressed: () => provider.setServer(serverTextController.text), + icon: const Icon(IconsaxPlusLinear.refresh), + ), + ); ref.listen( authProvider.select((value) => value.serverLoginModel), @@ -118,50 +151,31 @@ class _LoginScreenCredentialsState extends ConsumerState crossAxisAlignment: CrossAxisAlignment.center, spacing: 16, children: [ - IntrinsicHeight( - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - spacing: 8, - children: [ - AspectRatio( - aspectRatio: 1, - child: IconButton.filledTonal( - onPressed: () => provider.goUserSelect(), - icon: const Icon( - IconsaxPlusLinear.arrow_left_2, - ), - ), - ), - if (!hasBaseUrl) - Expanded( - child: OutlinedTextField( - controller: serverTextController, - onSubmitted: (value) => provider.setServer(value), - autoFillHints: const [AutofillHints.url], - keyboardType: TextInputType.url, - autocorrect: false, - textInputAction: TextInputAction.go, - label: context.localized.server, - errorText: urlError, - ), - ), - AspectRatio( - aspectRatio: 1, - child: Tooltip( - message: context.localized.retrievePublicListOfUsers, - waitDuration: const Duration(seconds: 1), - child: IconButton.filled( - onPressed: () => provider.setServer(serverTextController.text), - icon: const Icon( - IconsaxPlusLinear.refresh, + if (!(hasBaseUrl && hidePasswordLogin)) + IntrinsicHeight( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 8, + children: [ + if (existingUsers.isNotEmpty) AspectRatio(aspectRatio: 1, child: backButton), + if (!hasBaseUrl) + Expanded( + child: OutlinedTextField( + controller: serverTextController, + onSubmitted: (value) => provider.setServer(value), + autoFillHints: const [AutofillHints.url], + keyboardType: TextInputType.url, + autocorrect: false, + textInputAction: TextInputAction.go, + label: context.localized.server, + errorText: urlError, ), ), - ), - ), - ], + AspectRatio(aspectRatio: 1, child: refreshButton), + ], + ), ), - ), if (serverCredentials == null) Column( mainAxisSize: MainAxisSize.max, @@ -214,83 +228,89 @@ class _LoginScreenCredentialsState extends ConsumerState crossAxisAlignment: CrossAxisAlignment.stretch, spacing: 8, children: [ - Flexible( - child: AutofillGroup( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - spacing: 8, - children: [ - Flexible( - child: OutlinedTextField( - controller: usernameController, - autoFillHints: const [AutofillHints.username], - textInputAction: TextInputAction.next, - autocorrect: false, - onChanged: (value) => setState(() {}), - label: context.localized.userName, + if (!hidePasswordLogin) + Flexible( + child: AutofillGroup( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 8, + children: [ + Flexible( + child: OutlinedTextField( + controller: usernameController, + autoFillHints: const [AutofillHints.username], + textInputAction: TextInputAction.next, + autocorrect: false, + onChanged: (value) => setState(() {}), + label: context.localized.userName, + ), ), - ), - Flexible( - child: OutlinedTextField( - controller: passwordController, - autoFillHints: const [AutofillHints.password], - keyboardType: TextInputType.visiblePassword, - focusNode: focusNode, - autocorrect: false, - textInputAction: TextInputAction.send, - onSubmitted: (value) => enterCredentialsTryLogin?.call(), - onChanged: (value) => setState(() {}), - label: context.localized.password, + Flexible( + child: OutlinedTextField( + controller: passwordController, + autoFillHints: const [AutofillHints.password], + keyboardType: TextInputType.visiblePassword, + focusNode: focusNode, + autocorrect: false, + textInputAction: TextInputAction.send, + onSubmitted: (value) => enterCredentialsTryLogin?.call(), + onChanged: (value) => setState(() {}), + label: context.localized.password, + ), ), - ), - ], - ), - ), - ), - const Divider( - indent: 32, - endIndent: 32, - ), - Row( - spacing: 8, - children: [ - Expanded( - child: FilledButton( - onPressed: enterCredentialsTryLogin, - child: loggingIn - ? SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - color: Theme.of(context).colorScheme.inversePrimary, strokeCap: StrokeCap.round), - ) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(context.localized.login), - const SizedBox(width: 8), - const Icon(IconsaxPlusBold.send_1), - ], - ), + ], ), ), - if (FladderConfig.seerrBaseUrl?.isNotEmpty != true) - IconButton.filledTonal( - onPressed: () async { - final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); - final result = await showAdvancedLoginOptionsDialog( - context, - initialSeerrUrl: tempSeerrUrl, - ); - if (result != null) { - ref.read(authProvider.notifier).setTempSeerrUrl(result); - } - }, - icon: const Icon(IconsaxPlusLinear.setting_3), + ), + if (!hidePasswordLogin) ...[ + const Divider( + indent: 32, + endIndent: 32, + ), + Row( + spacing: 8, + children: [ + Expanded( + child: FilledButton( + onPressed: enterCredentialsTryLogin, + child: loggingIn + ? SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + color: Theme.of(context).colorScheme.inversePrimary, + strokeCap: StrokeCap.round), + ) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(context.localized.login), + const SizedBox(width: 8), + const Icon(IconsaxPlusBold.send_1), + ], + ), + ), ), - ], - ), + if (FladderConfig.seerrBaseUrl?.isNotEmpty != true) advancedOptionsButton, + ], + ), + ], + if (hasBaseUrl && hidePasswordLogin) + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + if (existingUsers.isNotEmpty) backButton, + refreshButton, + if (FladderConfig.seerrBaseUrl?.isNotEmpty != true) advancedOptionsButton, + ], + ), + if (!hasBaseUrl && hidePasswordLogin && FladderConfig.seerrBaseUrl?.isNotEmpty != true) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [advancedOptionsButton], + ), if (hasQuickConnect) FilledButton( onPressed: () async { diff --git a/lib/screens/settings/client_sections/client_settings_advanced.dart b/lib/screens/settings/client_sections/client_settings_advanced.dart index 8faee1482..d1c7b8646 100644 --- a/lib/screens/settings/client_sections/client_settings_advanced.dart +++ b/lib/screens/settings/client_sections/client_settings_advanced.dart @@ -5,6 +5,7 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/home_settings_provider.dart'; +import 'package:fladder/util/fladder_config.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; @@ -115,6 +116,24 @@ List buildClientSettingsAdvanced(BuildContext context, WidgetRef ref) { onChanged: (value) => ref.read(clientSettingsProvider.notifier).useSystemIME(value), ), ), + SettingsListTile( + label: Text(context.localized.hidePasswordLogin), + subLabel: Text(context.localized.hidePasswordLoginDescription), + onTap: FladderConfig.hidePasswordLogin != null + ? null + : () => ref + .read(clientSettingsProvider.notifier) + .update((current) => current.copyWith(hidePasswordLogin: !current.hidePasswordLogin)), + trailing: Switch( + value: FladderConfig.hidePasswordLogin ?? + ref.watch(clientSettingsProvider.select((value) => value.hidePasswordLogin)), + onChanged: FladderConfig.hidePasswordLogin != null + ? null + : (value) => ref + .read(clientSettingsProvider.notifier) + .update((current) => current.copyWith(hidePasswordLogin: value)), + ), + ), ], ); } diff --git a/lib/util/fladder_config.dart b/lib/util/fladder_config.dart index 0289ef89d..9451b6f3a 100644 --- a/lib/util/fladder_config.dart +++ b/lib/util/fladder_config.dart @@ -10,6 +10,10 @@ class FladderConfig { static set seerrBaseUrl(String? value) => _instance._seerrBaseUrl = value; String? _seerrBaseUrl; + static bool? get hidePasswordLogin => _instance._hidePasswordLogin; + static set hidePasswordLogin(bool? value) => _instance._hidePasswordLogin = value; + bool? _hidePasswordLogin; + static void fromJson(Map json) => _instance = FladderConfig._fromJson(json); factory FladderConfig._fromJson(Map json) { @@ -19,6 +23,7 @@ class FladderConfig { config._baseUrl = newUrl?.isEmpty == true ? null : newUrl; config._seerrBaseUrl = newSeerrUrl?.isEmpty == true ? null : newSeerrUrl; + config._hidePasswordLogin = json['hidePasswordLogin'] as bool?; return config; }