Compare commits

...

4 Commits

11 changed files with 255 additions and 77 deletions

View File

@ -58,6 +58,7 @@ import 'package:moxxyv2/ui/pages/settings/settings.dart';
import 'package:moxxyv2/ui/pages/share_selection.dart';
import 'package:moxxyv2/ui/pages/sharedmedia.dart';
import 'package:moxxyv2/ui/pages/splashscreen/splashscreen.dart';
import 'package:moxxyv2/ui/pages/util/qrcode.dart';
import 'package:moxxyv2/ui/service/data.dart';
import 'package:moxxyv2/ui/service/progress.dart';
import 'package:moxxyv2/ui/theme.dart';
@ -299,6 +300,9 @@ class MyAppState extends State<MyApp> with WidgetsBindingObserver {
case devicesRoute: return DevicesPage.route;
case ownDevicesRoute: return OwnDevicesPage.route;
case appearanceRoute: return AppearanceSettingsPage.route;
case qrCodeScannerRoute: return QrCodeScanningPage.getRoute(
settings.arguments! as QrCodeScanningArguments,
);
}
return null;

View File

@ -16,12 +16,10 @@ class AddContactBloc extends Bloc<AddContactEvent, AddContactState> {
AddContactBloc() : super(AddContactState()) {
on<AddedContactEvent>(_onContactAdded);
on<JidChangedEvent>(_onJidChanged);
on<PageResetEvent>(_onPageReset);
}
Future<void> _onContactAdded(AddedContactEvent event, Emitter<AddContactState> emit) async {
// TODO(Unknown): Remove once we can disable the custom buttom
if (state.working) return;
final validation = validateJidString(state.jid);
if (validation != null) {
emit(state.copyWith(jidError: validation));
@ -30,7 +28,7 @@ class AddContactBloc extends Bloc<AddContactEvent, AddContactState> {
emit(
state.copyWith(
working: true,
isWorking: true,
jidError: null,
),
);
@ -42,11 +40,17 @@ class AddContactBloc extends Bloc<AddContactEvent, AddContactState> {
),
) as AddContactResultEvent;
await _onPageReset(PageResetEvent(), emit);
if (result.conversation != null) {
if (result.added) {
GetIt.I.get<ConversationsBloc>().add(ConversationsAddedEvent(result.conversation!));
GetIt.I.get<ConversationsBloc>().add(
ConversationsAddedEvent(result.conversation!),
);
} else {
GetIt.I.get<ConversationsBloc>().add(ConversationsUpdatedEvent(result.conversation!));
GetIt.I.get<ConversationsBloc>().add(
ConversationsUpdatedEvent(result.conversation!),
);
}
}
@ -61,6 +65,20 @@ class AddContactBloc extends Bloc<AddContactEvent, AddContactState> {
}
Future<void> _onJidChanged(JidChangedEvent event, Emitter<AddContactState> emit) async {
emit(state.copyWith(jid: event.jid));
emit(
state.copyWith(
jid: event.jid,
),
);
}
Future<void> _onPageReset(PageResetEvent event, Emitter<AddContactState> emit) async {
emit(
state.copyWith(
jidError: null,
jid: '',
isWorking: false,
),
);
}
}

View File

@ -7,7 +7,9 @@ class AddedContactEvent extends AddContactEvent {}
/// Triggered by the UI when the JID input field is changed
class JidChangedEvent extends AddContactEvent {
JidChangedEvent(this.jid);
final String jid;
}
/// Triggered when the UI wants to reset its state
class PageResetEvent extends AddContactEvent {}

View File

@ -5,6 +5,6 @@ class AddContactState with _$AddContactState {
factory AddContactState({
@Default('') String jid,
@Default(null) String? jidError,
@Default(false) bool working,
@Default(false) bool isWorking,
}) = _AddContactState;
}

View File

@ -66,3 +66,4 @@ const String shareSelectionRoute = '/share_selection';
const String serverInfoRoute = '$profileRoute/server_info';
const String devicesRoute = '$profileRoute/devices';
const String ownDevicesRoute = '$profileRoute/own_devices';
const String qrCodeScannerRoute = '/util/qr_code_scanner';

View File

@ -10,6 +10,7 @@ import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/shared/avatar.dart';
import 'package:moxxyv2/ui/bloc/crop_bloc.dart';
import 'package:moxxyv2/ui/constants.dart';
import 'package:moxxyv2/ui/pages/util/qrcode.dart';
/// Shows a dialog asking the user if they are sure that they want to proceed with an
/// action. Resolves to true if the user pressed the confirm button. Returns false if
@ -179,3 +180,31 @@ String localeCodeToLanguageName(String localeCode) {
assert(false, 'Language code $localeCode has no name');
return '';
}
/// Scans QR Codes for an URI with a scheme of xmpp:. Returns the URI when found.
/// Returns null if not.
Future<Uri?> scanXmppUriQrCode(BuildContext context) async {
final value = await Navigator.of(context).pushNamed<String>(
qrCodeScannerRoute,
arguments: QrCodeScanningArguments(
(value) {
if (value == null) return false;
final uri = Uri.tryParse(value);
if (uri == null) return false;
if (uri.scheme == 'xmpp') {
return true;
}
return false;
},
),
);
if (value != null) {
return Uri.parse(value);
}
return null;
}

View File

@ -8,70 +8,97 @@ import 'package:moxxyv2/ui/widgets/button.dart';
import 'package:moxxyv2/ui/widgets/textfield.dart';
import 'package:moxxyv2/ui/widgets/topbar.dart';
class AddContactPage extends StatelessWidget {
class AddContactPage extends StatefulWidget {
const AddContactPage({ super.key });
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
builder: (_) => const AddContactPage(),
settings: const RouteSettings(
name: addContactRoute,
),
);
@override
AddContactPageState createState() => AddContactPageState();
}
class AddContactPageState extends State<AddContactPage> {
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return BlocBuilder<AddContactBloc, AddContactState>(
builder: (context, state) => Scaffold(
appBar: BorderlessTopbar.simple(t.pages.addcontact.title),
body: Column(
children: [
Visibility(
visible: state.working,
child: const LinearProgressIndicator(),
),
builder: (context, state) => WillPopScope(
onWillPop: () async {
if (state.isWorking) {
return false;
}
Padding(
padding: const EdgeInsets.symmetric(horizontal: paddingVeryLarge).add(const EdgeInsets.only(top: 8)),
child: CustomTextField(
labelText: t.pages.addcontact.xmppAddress,
onChanged: (value) => context.read<AddContactBloc>().add(
JidChangedEvent(value),
),
enabled: !state.working,
cornerRadius: textfieldRadiusRegular,
borderColor: primaryColor,
borderWidth: 1,
errorText: state.jidError,
suffixIcon: IconButton(
icon: const Icon(Icons.qr_code),
onPressed: () {
showNotImplementedDialog('QR-code scanning', context);
},
context.read<AddContactBloc>().add(
PageResetEvent(),
);
return true;
},
child: Scaffold(
appBar: BorderlessTopbar.simple(t.pages.addcontact.title),
body: Column(
children: [
Visibility(
visible: state.isWorking,
child: const LinearProgressIndicator(),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: paddingVeryLarge).add(const EdgeInsets.only(top: 8)),
child: CustomTextField(
labelText: t.pages.addcontact.xmppAddress,
onChanged: (value) => context.read<AddContactBloc>().add(
JidChangedEvent(value),
),
controller: _controller,
enabled: !state.isWorking,
cornerRadius: textfieldRadiusRegular,
borderColor: primaryColor,
borderWidth: 1,
errorText: state.jidError,
suffixIcon: IconButton(
icon: const Icon(Icons.qr_code),
onPressed: () async {
final jid = await scanXmppUriQrCode(context);
if (jid == null) return;
_controller.text = jid.path;
// ignore: use_build_context_synchronously
context.read<AddContactBloc>().add(
JidChangedEvent(jid.path),
);
},
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: paddingVeryLarge).add(const EdgeInsets.only(top: 8)),
child: Text(t.pages.addcontact.subtitle),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: paddingVeryLarge).add(const EdgeInsets.only(top: 32)),
child: Row(
children: [
Expanded(
child: RoundedButton(
cornerRadius: 32,
onTap: () => context.read<AddContactBloc>().add(AddedContactEvent()),
enabled: !state.working,
child: Text(t.pages.addcontact.buttonAddToContact),
),
)
],
Padding(
padding: const EdgeInsets.symmetric(horizontal: paddingVeryLarge).add(const EdgeInsets.only(top: 8)),
child: Text(t.pages.addcontact.subtitle),
),
)
],
Padding(
padding: const EdgeInsets.symmetric(horizontal: paddingVeryLarge).add(const EdgeInsets.only(top: 32)),
child: Row(
children: [
Expanded(
child: RoundedButton(
cornerRadius: 32,
onTap: () => context.read<AddContactBloc>().add(AddedContactEvent()),
enabled: !state.isWorking,
child: Text(t.pages.addcontact.buttonAddToContact),
),
)
],
),
)
],
),
),
),
);

View File

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_vibrate/flutter_vibrate.dart';
import 'package:flutter_zxing/flutter_zxing.dart';
import 'package:moxxyv2/ui/constants.dart';
typedef QrCodeScanningValidatorCallback = bool Function(String? value);
class QrCodeScanningArguments {
const QrCodeScanningArguments(this.validator);
final QrCodeScanningValidatorCallback validator;
}
class QrCodeScanningPage extends StatelessWidget {
const QrCodeScanningPage(this.args, { super.key });
final QrCodeScanningArguments args;
static MaterialPageRoute<String> getRoute(QrCodeScanningArguments args) => MaterialPageRoute<String>(
builder: (_) => QrCodeScanningPage(args),
settings: const RouteSettings(
name: qrCodeScannerRoute,
),
);
@override
Widget build(BuildContext context) {
return Scaffold(
body: ReaderWidget(
onScan: (value) {
final content = value.textString;
if (args.validator(content)) {
Vibrate.feedback(FeedbackType.heavy);
Navigator.of(context).pop(content);
}
},
),
);
}
}

View File

@ -15,25 +15,28 @@ class RoundedButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Ink(
decoration: BoxDecoration(
color: Theme.of(context).elevatedButtonTheme.style!.backgroundColor!.resolve(
// ignore: prefer_collection_literals
Set.from([
// ignore: prefer_if_elements_to_conditional_expressions
enabled ? MaterialState.selected : MaterialState.disabled,
]),
return ClipRRect(
borderRadius: BorderRadius.circular(cornerRadius),
child: Material(
type: MaterialType.transparency,
child: Ink(
decoration: BoxDecoration(
color: Theme.of(context).elevatedButtonTheme.style!.backgroundColor!.resolve(
// ignore: prefer_collection_literals
Set.from([
// ignore: prefer_if_elements_to_conditional_expressions
enabled ? MaterialState.selected : MaterialState.disabled,
]),
),
borderRadius: BorderRadius.circular(cornerRadius),
),
borderRadius: BorderRadius.circular(cornerRadius),
),
child: InkWell(
onTap: enabled ? onTap : null,
child: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: child,
child: InkWell(
onTap: enabled ? onTap : null,
child: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: child,
),
),
),
),

View File

@ -134,6 +134,41 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "8.4.0"
camera:
dependency: transitive
description:
name: camera
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.0+4"
camera_android:
dependency: transitive
description:
name: camera_android
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.0+4"
camera_avfoundation:
dependency: transitive
description:
name: camera_avfoundation
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.8+6"
camera_platform_interface:
dependency: transitive
description:
name: camera_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.1"
camera_web:
dependency: transitive
description:
name: camera_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0+1"
characters:
dependency: transitive
description:
@ -232,6 +267,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
cross_file:
dependency: transitive
description:
name: cross_file
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3+2"
crypto:
dependency: transitive
description:
@ -483,6 +525,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_zxing:
dependency: "direct main"
description:
name: flutter_zxing
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.4"
fluttertoast:
dependency: "direct main"
description:
@ -980,6 +1029,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
random_string:
dependency: "direct main"
description:

View File

@ -38,6 +38,7 @@ dependencies:
flutter_secure_storage: 5.0.2
flutter_speed_dial: 6.0.0
flutter_vibrate: 1.3.0
flutter_zxing: 0.8.4
fluttertoast: 8.1.1
freezed_annotation: 2.1.0
get_it: 7.2.0
@ -77,7 +78,6 @@ dependencies:
path_provider: 2.0.11
permission_handler: 10.0.0
phosphor_flutter: 1.4.0
#qr_code_scanner: 0.6.1
qr_flutter: 4.0.0
random_string: 2.3.1
record: 4.4.3