Merge branch 'main' into 'error-logging-update'

# Conflicts:
#   lib/screens/sign_in.dart
main
HankG 2023-11-29 18:26:51 +00:00
commit bc86189549
5 zmienionych plików z 228 dodań i 115 usunięć

Wyświetl plik

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import '../models/timeline_entry.dart';
import '../services/auth_service.dart';
import '../utils/clipboard_utils.dart';
import '../utils/url_opening_utils.dart';
import 'html_text_viewer_control.dart';
@ -127,6 +129,25 @@ class _SearchResultStatusControlState extends State<SearchResultStatusControl> {
if (items.isEmpty) {
return const SizedBox();
}
// A Link Preview with only one media attachment will have a duplicate image
// even though it points to different resources server side. So we don't
// want to render it twice.
if (widget.status.linkPreviewData != null && items.length == 1) {
return const SizedBox();
}
// A Diaspora reshare will have an HTML-built card with a link preview image
// to the same image as what would be in the single attachment but at a
// different link. So we don't want it to render twice.
final linkPhotoBaseUrl = Uri.https(
context.read<AccountsService>().currentProfile.serverName,
'photo/link',
).toString();
if (widget.status.body.contains(linkPhotoBaseUrl) && items.length == 1) {
return const SizedBox();
}
return SizedBox(
height: 250.0,
child: ListView.separated(

Wyświetl plik

@ -10,6 +10,7 @@ import '../../globals.dart';
import '../../models/filters/timeline_entry_filter.dart';
import '../../models/flattened_tree_item.dart';
import '../../models/timeline_entry.dart';
import '../../services/auth_service.dart';
import '../../services/timeline_entry_filter_service.dart';
import '../../services/timeline_manager.dart';
import '../../utils/active_profile_selector.dart';
@ -228,6 +229,25 @@ class _StatusControlState extends State<FlattenedTreeEntryControl> {
if (items.isEmpty) {
return const SizedBox();
}
// A Link Preview with only one media attachment will have a duplicate image
// even though it points to different resources server side. So we don't
// want to render it twice.
if (entry.linkPreviewData != null && items.length == 1) {
return const SizedBox();
}
// A Diaspora reshare will have an HTML-built card with a link preview image
// to the same image as what would be in the single attachment but at a
// different link. So we don't want it to render twice.
final linkPhotoBaseUrl = Uri.https(
context.read<AccountsService>().currentProfile.serverName,
'photo/link',
).toString();
if (entry.body.contains(linkPhotoBaseUrl) && items.length == 1) {
return const SizedBox();
}
return SizedBox(
height: ResponsiveSizesCalculator(context).maxThumbnailHeight,
child: ListView.separated(

Wyświetl plik

@ -11,9 +11,13 @@ final getIt = GetIt.instance;
String randomId() => const Uuid().v4().toString();
final platformHasCamera = Platform.isIOS || Platform.isAndroid;
final platformIsMobile = Platform.isIOS || Platform.isAndroid;
final useImagePicker = kIsWeb || Platform.isAndroid || Platform.isIOS;
final platformHasCamera = platformIsMobile;
final platformIsDesktop = !platformIsMobile;
final useImagePicker = kIsWeb || platformIsMobile;
const usePhpDebugging = false;

Wyświetl plik

@ -3,7 +3,9 @@ import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import '../controls/app_bottom_nav_bar.dart';
import '../controls/linear_status_indicator.dart';
import '../controls/notifications_control.dart';
import '../controls/padding.dart';
import '../controls/responsive_max_width.dart';
import '../controls/standard_app_drawer.dart';
import '../controls/standard_appbar.dart';
@ -36,10 +38,11 @@ class NotificationsScreen extends StatelessWidget {
managerResult.match(onSuccess: (manager) {
final notifications = manager.notifications;
actions = [
StatusAndRefreshButton(
valueListenable: nss.notificationsUpdateStatus,
refreshFunction: () async => update(manager),
),
if (platformIsDesktop)
StatusAndRefreshButton(
valueListenable: nss.notificationsUpdateStatus,
refreshFunction: () async => update(manager),
),
IconButton(
onPressed: () async => _clearAllNotifications(context, manager),
icon: const Icon(Icons.cleaning_services),
@ -52,11 +55,21 @@ class NotificationsScreen extends StatelessWidget {
update(manager);
return;
},
child: const Center(
child: Center(
child: Column(
children: [
Center(child: Text('No notifications')),
],
mainAxisAlignment: MainAxisAlignment.center,
children: nss.notificationsUpdateStatus.value
? [
const Center(child: Text('Loading Notifications')),
]
: [
const Center(child: Text('No notifications')),
const VerticalPadding(),
ElevatedButton(
onPressed: () => update(manager),
child: const Text('Load Notifications'),
)
],
)),
);
} else {
@ -131,7 +144,16 @@ class NotificationsScreen extends StatelessWidget {
actions: actions,
),
drawer: const StandardAppDrawer(),
body: body,
body: Center(
child: Column(
children: [
StandardLinearProgressIndicator(nss.notificationsUpdateStatus),
Expanded(
child: ResponsiveMaxWidth(child: body),
),
],
),
),
bottomNavigationBar: const AppBottomNavBar(
currentButton: NavBarButtons.notifications,
),

Wyświetl plik

@ -47,6 +47,28 @@ class _SignInScreenState extends State<SignInScreen> {
} else {
newProfile();
}
usernameController.addListener(() {
_updateSignInButtonStatus();
});
passwordController.addListener(() {
_updateSignInButtonStatus();
});
serverNameController.addListener(() {
_updateSignInButtonStatus();
});
}
void _updateSignInButtonStatus() {
setState(() {
signInButtonEnabled = switch (oauthType) {
usernamePasswordType =>
serverNameController.text.isNotEmpty &&
usernameController.text.isNotEmpty &&
passwordController.text.isNotEmpty,
oauthType => serverNameController.text.isNotEmpty,
_ => false,
};
});
}
void newProfile() {
@ -54,7 +76,7 @@ class _SignInScreenState extends State<SignInScreen> {
passwordController.clear();
serverNameController.clear();
authType = oauthType;
signInButtonEnabled = true;
signInButtonEnabled = false;
existingProfile = null;
}
@ -131,14 +153,14 @@ class _SignInScreenState extends State<SignInScreen> {
onChanged: existingAccount
? null
: (value) {
if (existingAccount) {
buildSnackbar(context,
"Can't change the type on an existing account");
return;
}
authType = value!;
setState(() {});
}),
if (existingAccount) {
buildSnackbar(context,
"Can't change the type on an existing account");
return;
}
authType = value!;
setState(() {});
}),
),
const VerticalPadding(),
TextFormField(
@ -147,12 +169,15 @@ class _SignInScreenState extends State<SignInScreen> {
autovalidateMode: AutovalidateMode.onUserInteraction,
controller: serverNameController,
validator: (value) =>
isFQDN(value ?? '') ? null : 'Not a valid server name',
isFQDN(value ?? '') ? null : 'Not a valid server name',
decoration: InputDecoration(
hintText: 'Server Name (friendica.example.com)',
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.background,
color: Theme
.of(context)
.colorScheme
.background,
),
borderRadius: BorderRadius.circular(5.0),
),
@ -163,7 +188,8 @@ class _SignInScreenState extends State<SignInScreen> {
if (!showUsernameAndPasswordFields) ...[
Text(
existingAccount
? 'Configured to sign in as user ${existingProfile?.handle}'
? 'Configured to sign in as user ${existingProfile
?.handle}'
: 'Relatica will open the requested Friendica site in a web browser where you will be asked to authorize this client.',
softWrap: true,
),
@ -195,7 +221,10 @@ class _SignInScreenState extends State<SignInScreen> {
hintText: 'Username (user@example.com)',
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.background,
color: Theme
.of(context)
.colorScheme
.background,
),
borderRadius: BorderRadius.circular(5.0),
),
@ -229,7 +258,10 @@ class _SignInScreenState extends State<SignInScreen> {
hintText: 'Password',
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.background,
color: Theme
.of(context)
.colorScheme
.background,
),
borderRadius: BorderRadius.circular(5.0),
),
@ -240,111 +272,117 @@ class _SignInScreenState extends State<SignInScreen> {
],
signInButtonEnabled
? ElevatedButton(
onPressed: () => _signIn(context),
child: const Text('Signin'),
)
onPressed: () async => await _signIn(context),
child: const Text('Signin'),
)
: SizedBox(),
const VerticalPadding(),
Text(
'Logged out:',
style: Theme.of(context).textTheme.headlineSmall,
style: Theme
.of(context)
.textTheme
.headlineSmall,
),
loggedOutProfiles.isEmpty
? const Text(
'No logged out profiles',
textAlign: TextAlign.center,
)
'No logged out profiles',
textAlign: TextAlign.center,
)
: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(
width: 0.5,
)),
child: Column(
children: loggedOutProfiles.map((p) {
return ListTile(
onTap: () {
setCredentials(context, p);
setState(() {});
},
title: Text(p.handle),
subtitle: Text(p.credentials is BasicCredentials
? usernamePasswordType
: oauthType),
trailing: ElevatedButton(
onPressed: () async {
final confirm = await showYesNoDialog(context,
'Remove login information from app?');
if (confirm ?? false) {
await service.removeProfile(p);
}
setState(() {});
},
child: const Text('Remove'),
),
);
}).toList(),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(
width: 0.5,
)),
child: Column(
children: loggedOutProfiles.map((p) {
return ListTile(
onTap: () {
setCredentials(context, p);
setState(() {});
},
title: Text(p.handle),
subtitle: Text(p.credentials is BasicCredentials
? usernamePasswordType
: oauthType),
trailing: ElevatedButton(
onPressed: () async {
final confirm = await showYesNoDialog(context,
'Remove login information from app?');
if (confirm ?? false) {
await service.removeProfile(p);
}
setState(() {});
},
child: const Text('Remove'),
),
),
);
}).toList(),
),
),
const VerticalPadding(),
Text(
'Logged in:',
style: Theme.of(context).textTheme.headlineSmall,
style: Theme
.of(context)
.textTheme
.headlineSmall,
),
loggedInProfiles.isEmpty
? const Text(
'No logged in profiles',
textAlign: TextAlign.center,
)
'No logged in profiles',
textAlign: TextAlign.center,
)
: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(
width: 0.5,
)),
child: Column(
children: loggedInProfiles.map((p) {
final active = service.loggedIn
? p.id == service.currentProfile.id
: false;
return ListTile(
onTap: () async {
setCredentials(context, p);
setState(() {});
},
title: Text(
p.handle,
style: active
? const TextStyle(
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic)
: null,
),
subtitle: Text(
p.credentials is BasicCredentials
? usernamePasswordType
: oauthType,
style: active
? const TextStyle(
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic)
: null,
),
trailing: ElevatedButton(
onPressed: () async {
final confirm = await showYesNoDialog(
context, 'Log out account?');
if (confirm == true) {
await getIt<AccountsService>().signOut(p);
setState(() {});
}
},
child: const Text('Sign out'),
),
);
}).toList(),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(
width: 0.5,
)),
child: Column(
children: loggedInProfiles.map((p) {
final active = service.loggedIn
? p.id == service.currentProfile.id
: false;
return ListTile(
onTap: () async {
setCredentials(context, p);
setState(() {});
},
title: Text(
p.handle,
style: active
? const TextStyle(
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic)
: null,
),
),
subtitle: Text(
p.credentials is BasicCredentials
? usernamePasswordType
: oauthType,
style: active
? const TextStyle(
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic)
: null,
),
trailing: ElevatedButton(
onPressed: () async {
final confirm = await showYesNoDialog(
context, 'Log out account?');
if (confirm == true) {
await getIt<AccountsService>().signOut(p);
setState(() {});
}
},
child: const Text('Sign out'),
),
);
}).toList(),
),
),
],
),
),
@ -353,7 +391,7 @@ class _SignInScreenState extends State<SignInScreen> {
);
}
void _signIn(BuildContext context) async {
Future<void> _signIn(BuildContext context) async {
final valid = formKey.currentState?.validate() ?? false;
if (!valid) {
buildSnackbar(
@ -385,11 +423,19 @@ class _SignInScreenState extends State<SignInScreen> {
return;
}
final result = await getIt<AccountsService>().signIn(creds);
buildSnackbar(context, 'Attempting to sign in account...');
final result = await getIt<AccountsService>().signIn(
creds,
withNotification: false,
);
if (mounted && result.isFailure) {
buildSnackbar(context, 'Error signing in: ${result.error}');
return;
}
if (mounted) {
buildSnackbar(context, 'Account signed in...');
}
await getIt<AccountsService>().setActiveProfile(result.value);
if (mounted) {
context.goNamed(ScreenPaths.timelines);