Compare commits
4 Commits
a9cc4f55b8
...
19de68e4f0
Author | SHA1 | Date | |
---|---|---|---|
19de68e4f0 | |||
dc17d7d304 | |||
2372dbf6b3 | |||
3382e35447 |
@ -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;
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
38
lib/ui/pages/util/qrcode.dart
Normal file
38
lib/ui/pages/util/qrcode.dart
Normal 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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
56
pubspec.lock
56
pubspec.lock
@ -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:
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user