@ -32,48 +32,78 @@ class LoginForm extends HookConsumerWidget {
final serverEndpointController =
useTextEditingController . fromValue ( TextEditingValue . empty ) ;
final apiService = ref . watch ( apiServiceProvider ) ;
final emailFocusNode = useFocusNode ( ) ;
final passwordFocusNode = useFocusNode ( ) ;
final serverEndpointFocusNode = useFocusNode ( ) ;
final isLoading = useState < bool > ( false ) ;
final isLoadingServer = useState < bool > ( false ) ;
final isOauthEnable = useState < bool > ( false ) ;
final oAuthButtonLabel = useState < String > ( ' OAuth ' ) ;
final logoAnimationController = useAnimationController (
duration: const Duration ( seconds: 60 ) ,
) . . repeat ( ) ;
getServeLoginConfig ( ) async {
if ( ! serverEndpointFocusNode . hasFocus ) {
var serverUrl = serverEndpointController . text . trim ( ) ;
final ValueNotifier < String ? > serverEndpoint = useState < String ? > ( null ) ;
try {
if ( serverUrl . isNotEmpty ) {
isLoading . value = true ;
final serverEndpoint =
await apiService . resolveAndSetEndpoint ( serverUrl . toString ( ) ) ;
/ / / Fetch the server login credential and enables oAuth login if necessary
/ / / Returns true if successful , false otherwise
Future < bool > getServerLoginCredential ( ) async {
final serverUrl = serverEndpointController . text . trim ( ) ;
var loginConfig = await apiService . oAuthApi . generateConfig (
OAuthConfigDto ( redirectUri: serverEndpoint ) ,
) ;
/ / Guard empty URL
if ( serverUrl . isEmpty ) {
ImmichToast . show (
context: context ,
msg: " login_form_server_empty " . tr ( ) ,
toastType: ToastType . error ,
) ;
return false ;
}
if ( loginConfig ! = null ) {
isOauthEnable . value = loginConfig . enabled ;
oAuthButtonLabel . value = loginConfig . buttonText ? ? ' OAuth ' ;
} else {
isOauthEnable . value = false ;
}
try {
isLoadingServer . value = true ;
final endpoint =
await apiService . resolveAndSetEndpoint ( serverUrl ) ;
isLoading . value = false ;
}
} catch ( _ ) {
isLoading . value = false ;
final loginConfig = await apiService . oAuthApi . generateConfig (
OAuthConfigDto ( redirectUri: serverUrl ) ,
) ;
if ( loginConfig ! = null ) {
isOauthEnable . value = loginConfig . enabled ;
oAuthButtonLabel . value = loginConfig . buttonText ? ? ' OAuth ' ;
} else {
isOauthEnable . value = false ;
}
}
serverEndpoint . value = endpoint ;
} on ApiException catch ( e ) {
ImmichToast . show (
context: context ,
msg: e . message ? ? ' login_form_api_exception ' . tr ( ) ,
toastType: ToastType . error ,
) ;
isOauthEnable . value = false ;
isLoadingServer . value = false ;
return false ;
} catch ( e ) {
ImmichToast . show (
context: context ,
msg: ' login_form_server_error ' . tr ( ) ,
toastType: ToastType . error ,
) ;
isOauthEnable . value = false ;
isLoadingServer . value = false ;
return false ;
}
isLoadingServer . value = false ;
return true ;
}
useEffect (
( ) {
serverEndpointFocusNode . addListener ( getServeLoginConfig ) ;
var loginInfo = Hive . box < HiveSavedLoginInfo > ( hiveLoginInfoBox )
. get ( savedLoginInfoKey ) ;
@ -83,7 +113,6 @@ class LoginForm extends HookConsumerWidget {
serverEndpointController . text = loginInfo . serverUrl ;
}
getServeLoginConfig ( ) ;
return null ;
} ,
[ ] ,
@ -95,86 +124,260 @@ class LoginForm extends HookConsumerWidget {
serverEndpointController . text = ' http://10.1.15.216:2283/api ' ;
}
return Center (
child: ConstrainedBox (
login ( ) async {
/ / Start loading
isLoading . value = true ;
/ / This will remove current cache asset state of previous user login .
ref . read ( assetProvider . notifier ) . clearAllAsset ( ) ;
try {
final isAuthenticated =
await ref . read ( authenticationProvider . notifier ) . login (
usernameController . text ,
passwordController . text ,
serverEndpointController . text . trim ( ) ,
) ;
if ( isAuthenticated ) {
/ / Resume backup ( if enable ) then navigate
if ( ref . read ( authenticationProvider ) . shouldChangePassword & &
! ref . read ( authenticationProvider ) . isAdmin ) {
AutoRouter . of ( context ) . push ( const ChangePasswordRoute ( ) ) ;
} else {
final hasPermission = await ref
. read ( galleryPermissionNotifier . notifier )
. hasPermission ;
if ( hasPermission ) {
/ / Don ' t resume the backup until we have gallery permission
ref . read ( backupProvider . notifier ) . resumeBackup ( ) ;
}
AutoRouter . of ( context ) . replace ( const TabControllerRoute ( ) ) ;
}
} else {
ImmichToast . show (
context: context ,
msg: " login_form_failed_login " . tr ( ) ,
toastType: ToastType . error ,
) ;
}
} finally {
/ / Make sure we stop loading
isLoading . value = false ;
}
}
oAuthLogin ( ) async {
var oAuthService = ref . watch ( oAuthServiceProvider ) ;
ref . watch ( assetProvider . notifier ) . clearAllAsset ( ) ;
OAuthConfigResponseDto ? oAuthServerConfig ;
try {
oAuthServerConfig = await oAuthService
. getOAuthServerConfig ( serverEndpointController . text ) ;
isLoading . value = true ;
} catch ( e ) {
ImmichToast . show (
context: context ,
msg: " login_form_failed_get_oauth_server_config " . tr ( ) ,
toastType: ToastType . error ,
) ;
isLoading . value = false ;
return ;
}
if ( oAuthServerConfig ! = null & & oAuthServerConfig . enabled ) {
var loginResponseDto =
await oAuthService . oAuthLogin ( oAuthServerConfig . url ! ) ;
if ( loginResponseDto ! = null ) {
var isSuccess = await ref
. watch ( authenticationProvider . notifier )
. setSuccessLoginInfo (
accessToken: loginResponseDto . accessToken ,
serverUrl: serverEndpointController . text ,
) ;
if ( isSuccess ) {
isLoading . value = false ;
final permission = ref . watch ( galleryPermissionNotifier ) ;
if ( permission . isGranted | | permission . isLimited ) {
ref . watch ( backupProvider . notifier ) . resumeBackup ( ) ;
}
AutoRouter . of ( context ) . replace (
const TabControllerRoute ( ) ,
) ;
} else {
ImmichToast . show (
context: context ,
msg: " login_form_failed_login " . tr ( ) ,
toastType: ToastType . error ,
) ;
}
}
isLoading . value = false ;
} else {
ImmichToast . show (
context: context ,
msg: " login_form_failed_get_oauth_server_disable " . tr ( ) ,
toastType: ToastType . info ,
) ;
isLoading . value = false ;
return ;
}
}
buildSelectServer ( ) {
return ConstrainedBox (
key: const ValueKey ( ' server ' ) ,
constraints: const BoxConstraints ( maxWidth: 300 ) ,
child: SingleChildScrollView (
child: AutofillGroup (
child: Wrap (
spacing: 16 ,
runSpacing: 16 ,
alignment: WrapAlignment . center ,
children: [
GestureDetector (
onDoubleTap: ( ) = > populateTestLoginInfo ( ) ,
child: RotationTransition (
turns: logoAnimationController ,
child: const ImmichLogo (
heroTag: ' logo ' ,
) ,
) ,
) ,
const ImmichTitleText ( ) ,
EmailInput ( controller: usernameController ) ,
PasswordInput ( controller: passwordController ) ,
ServerEndpointInput (
controller: serverEndpointController ,
focusNode: serverEndpointFocusNode ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
ServerEndpointInput (
controller: serverEndpointController ,
focusNode: serverEndpointFocusNode ,
onSubmit: getServerLoginCredential ,
) ,
const SizedBox ( height: 18 ) ,
ElevatedButton . icon (
style: ElevatedButton . styleFrom (
padding: const EdgeInsets . symmetric ( vertical: 12 ) ,
) ,
onPressed: isLoadingServer . value ? null : getServerLoginCredential ,
icon: const Icon ( Icons . arrow_forward_rounded ) ,
label: const Text (
' Next ' ,
style: TextStyle ( fontSize: 14 , fontWeight: FontWeight . bold ) ,
) . tr ( ) ,
) ,
if ( isLoadingServer . value )
const Padding (
padding: EdgeInsets . only ( top: 18.0 ) ,
child: Center (
child: CircularProgressIndicator ( ) ,
) ,
if ( isLoading . value )
const SizedBox (
width: 24 ,
height: 24 ,
child: CircularProgressIndicator (
strokeWidth: 2 ,
) ,
) ,
if ( ! isLoading . value )
Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
mainAxisAlignment: MainAxisAlignment . center ,
children: [
const SizedBox ( height: 18 ) ,
LoginButton (
emailController: usernameController ,
passwordController: passwordController ,
serverEndpointController: serverEndpointController ,
) ,
] ,
) ,
) ;
}
buildLogin ( ) {
return ConstrainedBox (
key: const ValueKey ( ' login ' ) ,
constraints: const BoxConstraints ( maxWidth: 300 ) ,
child: AutofillGroup (
child: Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
Text (
serverEndpointController . text ,
style: Theme . of ( context ) . textTheme . displaySmall ,
textAlign: TextAlign . center ,
) ,
const SizedBox ( height: 18 ) ,
EmailInput (
controller: usernameController ,
focusNode: emailFocusNode ,
onSubmit: passwordFocusNode . requestFocus ,
) ,
const SizedBox ( height: 8 ) ,
PasswordInput (
controller: passwordController ,
focusNode: passwordFocusNode ,
onSubmit: login ,
) ,
AnimatedSwitcher (
duration: const Duration ( milliseconds: 500 ) ,
child: isLoading . value
? const SizedBox (
width: 24 ,
height: 24 ,
child: CircularProgressIndicator (
strokeWidth: 2 ,
) ,
if ( isOauthEnable . value ) . . . [
Padding (
padding: const EdgeInsets . symmetric (
horizontal: 16.0 ,
)
: Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
mainAxisAlignment: MainAxisAlignment . center ,
children: [
const SizedBox ( height: 18 ) ,
LoginButton ( onPressed: login ) ,
if ( isOauthEnable . value ) . . . [
Padding (
padding: const EdgeInsets . symmetric (
horizontal: 16.0 ,
) ,
child: Divider (
color:
Brightness . dark = = Theme . of ( context ) . brightness
? Colors . white
: Colors . black ,
) ,
) ,
child: Divider (
color:
Brightness . dark = = Theme . of ( context ) . brightness
? Colors . white
: Colors . black ,
OAuthLoginButton (
serverEndpointController: serverEndpointController ,
buttonLabel: oAuthButtonLabel . value ,
isLoading: isLoading ,
onPressed: oAuthLogin ,
) ,
) ,
OAuthLoginButton (
serverEndpointController: serverEndpointController ,
buttonLabel: oAuthButtonLabel . value ,
isLoading: isLoading ,
onLoginSuccess: ( ) {
isLoading . value = false ;
final permission = ref . watch ( galleryPermissionNotifier ) ;
if ( permission . isGranted | | permission . isLimited ) {
ref . watch ( backupProvider . notifier ) . resumeBackup ( ) ;
}
AutoRouter . of ( context ) . replace (
const TabControllerRoute ( ) ,
) ;
} ,
) ,
] ,
] ,
] ,
)
] ,
) ,
) ,
) ,
const SizedBox ( height: 12 ) ,
TextButton . icon (
icon: const Icon ( Icons . arrow_back ) ,
onPressed: ( ) = > serverEndpoint . value = null ,
label: const Text ( ' Back ' ) ,
) ,
] ,
) ,
) ,
) ,
) ;
}
final child = serverEndpoint . value = = null
? buildSelectServer ( )
: buildLogin ( ) ;
return LayoutBuilder (
builder: ( context , constraints ) {
return SingleChildScrollView (
child: Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
mainAxisAlignment: MainAxisAlignment . center ,
children: [
SizedBox (
height: constraints . maxHeight / 5 ,
) ,
Column (
crossAxisAlignment: CrossAxisAlignment . center ,
mainAxisAlignment: MainAxisAlignment . end ,
children: [
GestureDetector (
onDoubleTap: ( ) = > populateTestLoginInfo ( ) ,
child: RotationTransition (
turns: logoAnimationController ,
child: const ImmichLogo (
heroTag: ' logo ' ,
) ,
) ,
) ,
const ImmichTitleText ( ) ,
] ,
) ,
const SizedBox ( height: 18 ) ,
AnimatedSwitcher (
duration: const Duration ( milliseconds: 500 ) ,
child: child ,
) ,
] ,
) ,
) ;
} ,
) ;
}
}
@ -182,10 +385,13 @@ class LoginForm extends HookConsumerWidget {
class ServerEndpointInput extends StatelessWidget {
final TextEditingController controller ;
final FocusNode focusNode ;
final Function ( ) ? onSubmit ;
const ServerEndpointInput ( {
Key ? key ,
required this . controller ,
required this . focusNode ,
this . onSubmit ,
} ) : super ( key: key ) ;
String ? _validateInput ( String ? url ) {
@ -218,14 +424,23 @@ class ServerEndpointInput extends StatelessWidget {
autofillHints: const [ AutofillHints . url ] ,
keyboardType: TextInputType . url ,
autocorrect: false ,
onFieldSubmitted: ( _ ) = > onSubmit ? . call ( ) ,
textInputAction: TextInputAction . go ,
) ;
}
}
class EmailInput extends StatelessWidget {
final TextEditingController controller ;
final FocusNode ? focusNode ;
final Function ( ) ? onSubmit ;
const EmailInput ( { Key ? key , required this . controller } ) : super ( key: key ) ;
const EmailInput ( {
Key ? key ,
required this . controller ,
this . focusNode ,
this . onSubmit ,
} ) : super ( key: key ) ;
String ? _validateInput ( String ? email ) {
if ( email = = null | | email = = ' ' ) return null ;
@ -240,6 +455,7 @@ class EmailInput extends StatelessWidget {
@ override
Widget build ( BuildContext context ) {
return TextFormField (
autofocus: true ,
controller: controller ,
decoration: InputDecoration (
labelText: ' login_form_label_email ' . tr ( ) ,
@ -250,14 +466,24 @@ class EmailInput extends StatelessWidget {
autovalidateMode: AutovalidateMode . always ,
autofillHints: const [ AutofillHints . email ] ,
keyboardType: TextInputType . emailAddress ,
onFieldSubmitted: ( _ ) = > onSubmit ? . call ( ) ,
focusNode: focusNode ,
textInputAction: TextInputAction . next ,
) ;
}
}
class PasswordInput extends StatelessWidget {
final TextEditingController controller ;
final FocusNode ? focusNode ;
final Function ( ) ? onSubmit ;
const PasswordInput ( { Key ? key , required this . controller } ) : super ( key: key ) ;
const PasswordInput ( {
Key ? key ,
required this . controller ,
this . focusNode ,
this . onSubmit ,
} ) : super ( key: key ) ;
@ override
Widget build ( BuildContext context ) {
@ -271,20 +497,19 @@ class PasswordInput extends StatelessWidget {
) ,
autofillHints: const [ AutofillHints . password ] ,
keyboardType: TextInputType . text ,
onFieldSubmitted: ( _ ) = > onSubmit ? . call ( ) ,
focusNode: focusNode ,
textInputAction: TextInputAction . go ,
) ;
}
}
class LoginButton extends ConsumerWidget {
final TextEditingController emailController ;
final TextEditingController passwordController ;
final TextEditingController serverEndpointController ;
final Function ( ) onPressed ;
const LoginButton ( {
Key ? key ,
required this . emailController ,
required this . passwordController ,
required this . serverEndpointController ,
required this . onPressed ,
} ) : super ( key: key ) ;
@ override
@ -293,40 +518,7 @@ class LoginButton extends ConsumerWidget {
style: ElevatedButton . styleFrom (
padding: const EdgeInsets . symmetric ( vertical: 12 ) ,
) ,
onPressed: ( ) async {
/ / This will remove current cache asset state of previous user login .
ref . read ( assetProvider . notifier ) . clearAllAsset ( ) ;
var isAuthenticated =
await ref . read ( authenticationProvider . notifier ) . login (
emailController . text ,
passwordController . text ,
serverEndpointController . text ,
) ;
if ( isAuthenticated ) {
/ / Resume backup ( if enable ) then navigate
if ( ref . read ( authenticationProvider ) . shouldChangePassword & &
! ref . read ( authenticationProvider ) . isAdmin ) {
AutoRouter . of ( context ) . push ( const ChangePasswordRoute ( ) ) ;
} else {
final hasPermission = await ref
. read ( galleryPermissionNotifier . notifier )
. hasPermission ;
if ( hasPermission ) {
/ / Don ' t resume the backup until we have gallery permission
ref . read ( backupProvider . notifier ) . resumeBackup ( ) ;
}
AutoRouter . of ( context ) . replace ( const TabControllerRoute ( ) ) ;
}
} else {
ImmichToast . show (
context: context ,
msg: " login_form_failed_login " . tr ( ) ,
toastType: ToastType . error ,
) ;
}
} ,
onPressed: onPressed ,
icon: const Icon ( Icons . login_rounded ) ,
label: const Text (
" login_form_button_text " ,
@ -339,82 +531,26 @@ class LoginButton extends ConsumerWidget {
class OAuthLoginButton extends ConsumerWidget {
final TextEditingController serverEndpointController ;
final ValueNotifier < bool > isLoading ;
final VoidCallback onLoginSuccess ;
final String buttonLabel ;
final Function ( ) onPressed ;
const OAuthLoginButton ( {
Key ? key ,
required this . serverEndpointController ,
required this . isLoading ,
required this . onLoginSuccess ,
required this . buttonLabel ,
required this . onPressed ,
} ) : super ( key: key ) ;
@ override
Widget build ( BuildContext context , WidgetRef ref ) {
var oAuthService = ref . watch ( oAuthServiceProvider ) ;
void performOAuthLogin ( ) async {
ref . watch ( assetProvider . notifier ) . clearAllAsset ( ) ;
OAuthConfigResponseDto ? oAuthServerConfig ;
try {
oAuthServerConfig = await oAuthService
. getOAuthServerConfig ( serverEndpointController . text ) ;
isLoading . value = true ;
} catch ( e ) {
ImmichToast . show (
context: context ,
msg: " login_form_failed_get_oauth_server_config " . tr ( ) ,
toastType: ToastType . error ,
) ;
isLoading . value = false ;
return ;
}
if ( oAuthServerConfig ! = null & & oAuthServerConfig . enabled ) {
var loginResponseDto =
await oAuthService . oAuthLogin ( oAuthServerConfig . url ! ) ;
if ( loginResponseDto ! = null ) {
var isSuccess = await ref
. watch ( authenticationProvider . notifier )
. setSuccessLoginInfo (
accessToken: loginResponseDto . accessToken ,
serverUrl: serverEndpointController . text ,
) ;
if ( isSuccess ) {
isLoading . value = false ;
onLoginSuccess ( ) ;
} else {
ImmichToast . show (
context: context ,
msg: " login_form_failed_login " . tr ( ) ,
toastType: ToastType . error ,
) ;
}
}
isLoading . value = false ;
} else {
ImmichToast . show (
context: context ,
msg: " login_form_failed_get_oauth_server_disable " . tr ( ) ,
toastType: ToastType . info ,
) ;
isLoading . value = false ;
return ;
}
}
return ElevatedButton . icon (
style: ElevatedButton . styleFrom (
backgroundColor: Theme . of ( context ) . primaryColor . withAlpha ( 230 ) ,
padding: const EdgeInsets . symmetric ( vertical: 12 ) ,
) ,
onPressed: perf ormOAuthLogi n,
onPressed: onPressed ,
icon: const Icon ( Icons . pin_rounded ) ,
label: Text (
buttonLabel ,