diff --git a/analysis_options.yaml b/analysis_options.yaml index 3820af2..daaf9c6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,4 +9,5 @@ analyzer: errors: todo: ignore exclude: - - lib/generated_plugin_registrant.dart \ No newline at end of file + - lib/generated_plugin_registrant.dart + - lib/l10n/*.dart \ No newline at end of file diff --git a/lib/components/audio_player.dart b/lib/components/audio_player.dart index b77da54..0ffb4bf 100644 --- a/lib/components/audio_player.dart +++ b/lib/components/audio_player.dart @@ -32,7 +32,7 @@ class _AudioPlayerState extends State { StreamSubscription soundSubscription; Uint8List audioFile; - String statusText = "00:00"; + String statusText = '00:00'; double currentPosition = 0; double maxPosition = 0; @@ -45,7 +45,7 @@ class _AudioPlayerState extends State { super.dispose(); } - _downloadAction() async { + Future _downloadAction() async { if (status != AudioPlayerStatus.NOT_DOWNLOADED) return; setState(() => status = AudioPlayerStatus.DOWNLOADING); final matrixFile = await SimpleDialogs(context) @@ -57,7 +57,7 @@ class _AudioPlayerState extends State { _playAction(); } - _playAction() async { + void _playAction() async { if (AudioPlayer.currentId != widget.event.eventId) { if (AudioPlayer.currentId != null) { if (flutterSound.audioState != t_AUDIO_STATE.IS_STOPPED) { @@ -84,16 +84,16 @@ class _AudioPlayerState extends State { soundSubscription ??= flutterSound.onPlayerStateChanged.listen((e) { if (AudioPlayer.currentId != widget.event.eventId) { soundSubscription?.cancel()?.then((f) => soundSubscription = null); - this.setState(() { + setState(() { currentPosition = 0; - statusText = "00:00"; + statusText = '00:00'; }); AudioPlayer.currentId = null; } else if (e != null) { - DateTime date = + var date = DateTime.fromMillisecondsSinceEpoch(e.currentPosition.toInt()); - String txt = DateFormat('mm:ss', 'en_US').format(date); - this.setState(() { + var txt = DateFormat('mm:ss', 'en_US').format(date); + setState(() { maxPosition = e.duration; currentPosition = e.currentPosition; statusText = txt; diff --git a/lib/components/avatar.dart b/lib/components/avatar.dart index 11987cf..d6c7008 100644 --- a/lib/components/avatar.dart +++ b/lib/components/avatar.dart @@ -23,13 +23,14 @@ class Avatar extends StatelessWidget { @override Widget build(BuildContext context) { - final String src = mxContent?.getThumbnail( + var thumbnail = mxContent?.getThumbnail( Matrix.of(context).client, width: size * MediaQuery.of(context).devicePixelRatio, height: size * MediaQuery.of(context).devicePixelRatio, method: ThumbnailMethod.scale, ); - String fallbackLetters = "@"; + final src = thumbnail; + var fallbackLetters = '@'; if ((name?.length ?? 0) >= 2) { fallbackLetters = name.substring(0, 2); } else if ((name?.length ?? 0) == 1) { diff --git a/lib/components/chat_settings_popup_menu.dart b/lib/components/chat_settings_popup_menu.dart index 9551a56..1c3f8ee 100644 --- a/lib/components/chat_settings_popup_menu.dart +++ b/lib/components/chat_settings_popup_menu.dart @@ -48,26 +48,26 @@ class _ChatSettingsPopupMenuState extends State { .client .onUserEvent .stream - .where((u) => u.type == 'account_data' && u.eventType == "m.push_rules") + .where((u) => u.type == 'account_data' && u.eventType == 'm.push_rules') .listen( (u) => setState(() => null), ); - List> items = >[ + var items = >[ widget.room.pushRuleState == PushRuleState.notify ? PopupMenuItem( - value: "mute", + value: 'mute', child: Text(L10n.of(context).muteChat), ) : PopupMenuItem( - value: "unmute", + value: 'unmute', child: Text(L10n.of(context).unmuteChat), ), PopupMenuItem( - value: "call", + value: 'call', child: Text(L10n.of(context).videoCall), ), PopupMenuItem( - value: "leave", + value: 'leave', child: Text(L10n.of(context).leave), ), ]; @@ -75,7 +75,7 @@ class _ChatSettingsPopupMenuState extends State { items.insert( 0, PopupMenuItem( - value: "details", + value: 'details', child: Text(L10n.of(context).chatDetails), ), ); @@ -83,8 +83,8 @@ class _ChatSettingsPopupMenuState extends State { return PopupMenuButton( onSelected: (String choice) async { switch (choice) { - case "leave": - bool confirmed = await SimpleDialogs(context).askConfirmation(); + case 'leave': + var confirmed = await SimpleDialogs(context).askConfirmation(); if (confirmed) { final success = await SimpleDialogs(context) .tryRequestWithLoadingDialog(widget.room.leave()); @@ -95,18 +95,18 @@ class _ChatSettingsPopupMenuState extends State { } } break; - case "mute": + case 'mute': await SimpleDialogs(context).tryRequestWithLoadingDialog( widget.room.setPushRuleState(PushRuleState.mentions_only)); break; - case "unmute": + case 'unmute': await SimpleDialogs(context).tryRequestWithLoadingDialog( widget.room.setPushRuleState(PushRuleState.notify)); break; - case "call": + case 'call': startCallAction(context); break; - case "details": + case 'details': await Navigator.of(context).push( AppRoute.defaultRoute( context, diff --git a/lib/components/content_banner.dart b/lib/components/content_banner.dart index 55453db..4342534 100644 --- a/lib/components/content_banner.dart +++ b/lib/components/content_banner.dart @@ -23,9 +23,9 @@ class ContentBanner extends StatelessWidget { @override Widget build(BuildContext context) { final mediaQuery = MediaQuery.of(context); - final int bannerSize = + final bannerSize = (mediaQuery.size.width * mediaQuery.devicePixelRatio).toInt(); - final String src = mxContent?.getThumbnail( + final src = mxContent?.getThumbnail( Matrix.of(context).client, width: bannerSize, height: bannerSize, @@ -60,7 +60,7 @@ class ContentBanner extends StatelessWidget { : Icon(defaultIcon, size: 300), ), ), - if (this.onEdit != null) + if (onEdit != null) Container( margin: EdgeInsets.all(8), alignment: Alignment.bottomRight, diff --git a/lib/components/dialogs/recording_dialog.dart b/lib/components/dialogs/recording_dialog.dart index 97ebc33..e79861f 100644 --- a/lib/components/dialogs/recording_dialog.dart +++ b/lib/components/dialogs/recording_dialog.dart @@ -16,7 +16,7 @@ class RecordingDialog extends StatefulWidget { class _RecordingDialogState extends State { FlutterSound flutterSound = FlutterSound(); - String time = "00:00:00"; + String time = '00:00:00'; StreamSubscription _recorderSubscription; @@ -28,7 +28,7 @@ class _RecordingDialogState extends State { codec: t_CODEC.CODEC_AAC, ); _recorderSubscription = flutterSound.onRecorderStateChanged.listen((e) { - DateTime date = + var date = DateTime.fromMillisecondsSinceEpoch(e.currentPosition.toInt()); setState(() => time = DateFormat('mm:ss:SS', 'en_US').format(date)); }); @@ -67,7 +67,7 @@ class _RecordingDialogState extends State { SizedBox(width: 8), Expanded( child: Text( - "${L10n.of(context).recording}: $time", + '${L10n.of(context).recording}: $time', style: TextStyle( fontSize: 18, ), @@ -95,7 +95,7 @@ class _RecordingDialogState extends State { ), onPressed: () async { await _recorderSubscription?.cancel(); - final String result = await flutterSound.stopRecorder(); + final result = await flutterSound.stopRecorder(); if (widget.onFinished != null) { widget.onFinished(result); } diff --git a/lib/components/dialogs/simple_dialogs.dart b/lib/components/dialogs/simple_dialogs.dart index aecbdf1..4d2c816 100644 --- a/lib/components/dialogs/simple_dialogs.dart +++ b/lib/components/dialogs/simple_dialogs.dart @@ -19,7 +19,8 @@ class SimpleDialogs { bool password = false, bool multiLine = false, }) async { - final TextEditingController controller = TextEditingController(); + var textEditingController = TextEditingController(); + final controller = textEditingController; String input; await showDialog( context: context, @@ -77,7 +78,7 @@ class SimpleDialogs { String confirmText, String cancelText, }) async { - bool confirmed = false; + var confirmed = false; await showDialog( context: context, builder: (c) => AlertDialog( @@ -157,8 +158,8 @@ class SimpleDialogs { } } - showLoadingDialog(BuildContext context) { - showDialog( + void showLoadingDialog(BuildContext context) async { + await showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) => AlertDialog( diff --git a/lib/components/encryption_button.dart b/lib/components/encryption_button.dart index 3cd89de..e152a3c 100644 --- a/lib/components/encryption_button.dart +++ b/lib/components/encryption_button.dart @@ -67,7 +67,8 @@ class _EncryptionButtonState extends State { builder: (BuildContext context, snapshot) { Color color; if (widget.room.encrypted && snapshot.hasData) { - final List deviceKeysList = snapshot.data; + var data = snapshot.data; + final deviceKeysList = data; color = Colors.orange; if (deviceKeysList.indexWhere((DeviceKeys deviceKeys) => deviceKeys.verified == false && diff --git a/lib/components/image_bubble.dart b/lib/components/image_bubble.dart index b1a2eda..6886395 100644 --- a/lib/components/image_bubble.dart +++ b/lib/components/image_bubble.dart @@ -15,7 +15,7 @@ class ImageBubble extends StatefulWidget { } class _ImageBubbleState extends State { - static Map _matrixFileMap = {}; + static final Map _matrixFileMap = {}; MatrixFile get _file => _matrixFileMap[widget.event.eventId]; set _file(MatrixFile file) { _matrixFileMap[widget.event.eventId] = file; @@ -65,7 +65,7 @@ class _ImageBubbleState extends State { } _getFile().then((MatrixFile file) { setState(() => _file = file); - }, onError: (error) { + }, onError: (error, stacktrace) { setState(() => _error = error); }); return Center( diff --git a/lib/components/list_items/chat_list_item.dart b/lib/components/list_items/chat_list_item.dart index d900508..0218e38 100644 --- a/lib/components/list_items/chat_list_item.dart +++ b/lib/components/list_items/chat_list_item.dart @@ -71,11 +71,11 @@ class ChatListItem extends StatelessWidget { if (room.membership == Membership.join) { if (Matrix.of(context).shareContent != null) { - if (Matrix.of(context).shareContent["msgtype"] == - "chat.fluffy.shared_file") { + if (Matrix.of(context).shareContent['msgtype'] == + 'chat.fluffy.shared_file') { await SimpleDialogs(context).tryRequestWithErrorToast( room.sendFileEvent( - Matrix.of(context).shareContent["file"], + Matrix.of(context).shareContent['file'], ), ); } else { @@ -98,11 +98,11 @@ class ChatListItem extends StatelessWidget { final success = await SimpleDialogs(context) .tryRequestWithLoadingDialog(room.forget()); if (success != false) { - if (this.onForget != null) this.onForget(); + if (onForget != null) onForget(); } return success; } - final bool confirmed = await SimpleDialogs(context).askConfirmation(); + final confirmed = await SimpleDialogs(context).askConfirmation(); if (!confirmed) { return false; } @@ -217,7 +217,7 @@ class ChatListItem extends StatelessWidget { ), ), ) - : Text(" "), + : Text(' '), ], ), onTap: () => clickAction(context), diff --git a/lib/components/list_items/message.dart b/lib/components/list_items/message.dart index 350caae..2d6e255 100644 --- a/lib/components/list_items/message.dart +++ b/lib/components/list_items/message.dart @@ -37,23 +37,23 @@ class Message extends StatelessWidget { return StateMessage(event); } - Client client = Matrix.of(context).client; - final bool ownMessage = event.senderId == client.userID; - Alignment alignment = ownMessage ? Alignment.topRight : Alignment.topLeft; - Color color = Theme.of(context).secondaryHeaderColor; - final bool sameSender = nextEvent != null && + var client = Matrix.of(context).client; + final ownMessage = event.senderId == client.userID; + var alignment = ownMessage ? Alignment.topRight : Alignment.topLeft; + var color = Theme.of(context).secondaryHeaderColor; + final sameSender = nextEvent != null && [EventTypes.Message, EventTypes.Sticker].contains(nextEvent.type) ? nextEvent.sender.id == event.sender.id : false; - BubbleNip nip = sameSender + var nip = sameSender ? BubbleNip.no : ownMessage ? BubbleNip.rightBottom : BubbleNip.leftBottom; - Color textColor = ownMessage + var textColor = ownMessage ? Colors.white : Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black; - MainAxisAlignment rowMainAxisAlignment = + var rowMainAxisAlignment = ownMessage ? MainAxisAlignment.end : MainAxisAlignment.start; if (event.showThumbnail) { @@ -65,7 +65,7 @@ class Message extends StatelessWidget { : Theme.of(context).primaryColor; } - List rowChildren = [ + var rowChildren = [ Expanded( child: Bubble( elevation: 0, @@ -84,14 +84,14 @@ class Message extends StatelessWidget { FutureBuilder( future: event.getReplyEvent(timeline), builder: (BuildContext context, snapshot) { - final Event replyEvent = snapshot.hasData + final replyEvent = snapshot.hasData ? snapshot.data : Event( eventId: event.content['m.relates_to'] ['m.in_reply_to']['event_id'], - content: {"msgtype": "m.text", "body": "..."}, + content: {'msgtype': 'm.text', 'body': '...'}, senderId: event.senderId, - typeKey: "m.room.message", + typeKey: 'm.room.message', room: event.room, roomId: event.roomId, status: 1, @@ -110,7 +110,7 @@ class Message extends StatelessWidget { ), if (event.type == EventTypes.Encrypted && event.messageType == MessageTypes.BadEncrypted && - event.content["body"] == DecryptError.UNKNOWN_SESSION) + event.content['body'] == DecryptError.UNKNOWN_SESSION) RaisedButton( color: color.withAlpha(100), child: Text( @@ -146,7 +146,7 @@ class Message extends StatelessWidget { ), ), ]; - final Widget avatarOrSizedBox = sameSender + final avatarOrSizedBox = sameSender ? SizedBox(width: Avatar.defaultSize) : Avatar( event.sender.avatarUrl, @@ -193,8 +193,8 @@ class _MetaRow extends StatelessWidget { @override Widget build(BuildContext context) { - final String displayname = event.sender.calcDisplayname(); - final bool showDisplayname = + final displayname = event.sender.calcDisplayname(); + final showDisplayname = !ownMessage && event.senderId != event.room.directChatMatrixID; return Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/components/list_items/participant_list_item.dart b/lib/components/list_items/participant_list_item.dart index 7a58735..6ce9023 100644 --- a/lib/components/list_items/participant_list_item.dart +++ b/lib/components/list_items/participant_list_item.dart @@ -13,44 +13,44 @@ class ParticipantListItem extends StatelessWidget { const ParticipantListItem(this.user); - participantAction(BuildContext context, String action) async { + void participantAction(BuildContext context, String action) async { switch (action) { - case "ban": + case 'ban': if (await SimpleDialogs(context).askConfirmation()) { await SimpleDialogs(context).tryRequestWithLoadingDialog(user.ban()); } break; - case "unban": + case 'unban': if (await SimpleDialogs(context).askConfirmation()) { await SimpleDialogs(context) .tryRequestWithLoadingDialog(user.unban()); } break; - case "kick": + case 'kick': if (await SimpleDialogs(context).askConfirmation()) { await SimpleDialogs(context).tryRequestWithLoadingDialog(user.kick()); } break; - case "admin": + case 'admin': if (await SimpleDialogs(context).askConfirmation()) { await SimpleDialogs(context) .tryRequestWithLoadingDialog(user.setPower(100)); } break; - case "moderator": + case 'moderator': if (await SimpleDialogs(context).askConfirmation()) { await SimpleDialogs(context) .tryRequestWithLoadingDialog(user.setPower(50)); } break; - case "user": + case 'user': if (await SimpleDialogs(context).askConfirmation()) { await SimpleDialogs(context) .tryRequestWithLoadingDialog(user.setPower(0)); } break; - case "message": - final String roomId = await user.startDirectChat(); + case 'message': + final roomId = await user.startDirectChat(); await Navigator.of(context).pushAndRemoveUntil( AppRoute.defaultRoute( context, @@ -63,21 +63,21 @@ class ParticipantListItem extends StatelessWidget { @override Widget build(BuildContext context) { - Map membershipBatch = { - Membership.join: "", + var membershipBatch = { + Membership.join: '', Membership.ban: L10n.of(context).banned, Membership.invite: L10n.of(context).invited, Membership.leave: L10n.of(context).leftTheChat, }; - final String permissionBatch = user.powerLevel == 100 + final permissionBatch = user.powerLevel == 100 ? L10n.of(context).admin - : user.powerLevel >= 50 ? L10n.of(context).moderator : ""; - List> items = >[]; + : user.powerLevel >= 50 ? L10n.of(context).moderator : ''; + var items = >[]; if (user.id != Matrix.of(context).client.userID) { items.add( PopupMenuItem( - child: Text(L10n.of(context).sendAMessage), value: "message"), + child: Text(L10n.of(context).sendAMessage), value: 'message'), ); } if (user.canChangePowerLevel && @@ -85,7 +85,7 @@ class ParticipantListItem extends StatelessWidget { user.powerLevel != 100) { items.add( PopupMenuItem( - child: Text(L10n.of(context).makeAnAdmin), value: "admin"), + child: Text(L10n.of(context).makeAnAdmin), value: 'admin'), ); } if (user.canChangePowerLevel && @@ -93,29 +93,29 @@ class ParticipantListItem extends StatelessWidget { user.powerLevel != 50) { items.add( PopupMenuItem( - child: Text(L10n.of(context).makeAModerator), value: "moderator"), + child: Text(L10n.of(context).makeAModerator), value: 'moderator'), ); } if (user.canChangePowerLevel && user.powerLevel != 0) { items.add( PopupMenuItem( - child: Text(L10n.of(context).revokeAllPermissions), value: "user"), + child: Text(L10n.of(context).revokeAllPermissions), value: 'user'), ); } if (user.canKick) { items.add( PopupMenuItem( - child: Text(L10n.of(context).kickFromChat), value: "kick"), + child: Text(L10n.of(context).kickFromChat), value: 'kick'), ); } if (user.canBan && user.membership != Membership.ban) { items.add( - PopupMenuItem(child: Text(L10n.of(context).banFromChat), value: "ban"), + PopupMenuItem(child: Text(L10n.of(context).banFromChat), value: 'ban'), ); } else if (user.canBan && user.membership == Membership.ban) { items.add( PopupMenuItem( - child: Text(L10n.of(context).removeExile), value: "unban"), + child: Text(L10n.of(context).removeExile), value: 'unban'), ); } return PopupMenuButton( diff --git a/lib/components/list_items/presence_list_item.dart b/lib/components/list_items/presence_list_item.dart index 3699026..1082734 100644 --- a/lib/components/list_items/presence_list_item.dart +++ b/lib/components/list_items/presence_list_item.dart @@ -13,7 +13,7 @@ class PresenceListItem extends StatelessWidget { const PresenceListItem(this.presence); - static Map _presences = {}; + static final Map _presences = {}; Future _requestProfile(BuildContext context) async { _presences[presence.sender] ??= @@ -28,7 +28,7 @@ class PresenceListItem extends StatelessWidget { builder: (context, snapshot) { if (!snapshot.hasData) return Container(); Uri avatarUrl; - String displayname = presence.sender.localpart; + var displayname = presence.sender.localpart; if (snapshot.hasData) { avatarUrl = snapshot.data.avatarUrl; displayname = snapshot.data.displayname; @@ -64,7 +64,7 @@ class PresenceListItem extends StatelessWidget { FlatButton( child: Text(L10n.of(context).sendAMessage), onPressed: () async { - final String roomId = await User( + final roomId = await User( presence.sender, room: Room(id: '', client: Matrix.of(context).client), ).startDirectChat(); diff --git a/lib/components/list_items/public_room_list_item.dart b/lib/components/list_items/public_room_list_item.dart index 84268af..71ff959 100644 --- a/lib/components/list_items/public_room_list_item.dart +++ b/lib/components/list_items/public_room_list_item.dart @@ -27,7 +27,7 @@ class PublicRoomListItem extends StatelessWidget { @override Widget build(BuildContext context) { - final bool hasTopic = + final hasTopic = publicRoomEntry.topic != null && publicRoomEntry.topic.isNotEmpty; return ListTile( leading: Avatar( @@ -36,13 +36,13 @@ class PublicRoomListItem extends StatelessWidget { : Uri.parse(publicRoomEntry.avatarUrl), publicRoomEntry.name), title: Text(hasTopic - ? "${publicRoomEntry.name} (${publicRoomEntry.numJoinedMembers})" + ? '${publicRoomEntry.name} (${publicRoomEntry.numJoinedMembers})' : publicRoomEntry.name), subtitle: Text( hasTopic ? publicRoomEntry.topic : L10n.of(context).countParticipants( - publicRoomEntry.numJoinedMembers?.toString() ?? "0"), + publicRoomEntry.numJoinedMembers?.toString() ?? '0'), maxLines: 1, ), onTap: () => joinAction(context), diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index e032daa..d6e6b90 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -24,14 +24,17 @@ class Matrix extends StatefulWidget { final Client client; - Matrix({this.child, this.clientName, this.client, Key key}) : super(key: key); + final Store store; + + Matrix({this.child, this.clientName, this.client, this.store, Key key}) + : super(key: key); @override MatrixState createState() => MatrixState(); /// Returns the (nearest) Client instance of your application. static MatrixState of(BuildContext context) { - MatrixState newState = + var newState = (context.dependOnInheritedWidgetOfExactType<_InheritedMatrix>()).data; newState.context = FirebaseController.context = context; return newState; @@ -40,6 +43,8 @@ class Matrix extends StatefulWidget { class MatrixState extends State { Client client; + Store store; + @override BuildContext context; Map get shareContent => _shareContent; @@ -62,16 +67,15 @@ class MatrixState extends State { void clean() async { if (!kIsWeb) return; - final LocalStorage storage = LocalStorage('LocalStorage'); + final storage = LocalStorage('LocalStorage'); await storage.ready; await storage.deleteItem(widget.clientName); } void _initWithStore() async { - Future initLoginState = client.onLoginStateChanged.stream.first; - client.storeAPI = kIsWeb ? Store(client) : ExtendedStore(client); - debugPrint( - "[Store] Store is extended: ${client.storeAPI.extended.toString()}"); + var initLoginState = client.onLoginStateChanged.stream.first; + client.database = await getDatabase(client, store); + client.connect(); if (await initLoginState == LoginState.logged && !kIsWeb) { await FirebaseController.setupFirebase( client, @@ -81,14 +85,14 @@ class MatrixState extends State { } Map getAuthByPassword(String password, String session) => { - "type": "m.login.password", - "identifier": { - "type": "m.id.user", - "user": client.userID, + 'type': 'm.login.password', + 'identifier': { + 'type': 'm.id.user', + 'user': client.userID, }, - "user": client.userID, - "password": password, - "session": session, + 'user': client.userID, + 'password': password, + 'session': session, }; StreamSubscription onRoomKeyRequestSub; @@ -151,8 +155,9 @@ class MatrixState extends State { @override void initState() { + store = widget.store ?? Store(); if (widget.client == null) { - debugPrint("[Matrix] Init matrix client"); + debugPrint('[Matrix] Init matrix client'); client = Client(widget.clientName, debug: false); onJitsiCallSub ??= client.onEvent.stream .where((e) => @@ -163,12 +168,12 @@ class MatrixState extends State { .listen(onJitsiCall); onRoomKeyRequestSub ??= client.onRoomKeyRequest.stream.listen((RoomKeyRequest request) async { - final Room room = request.room; - final User sender = room.getUserByMXIDSync(request.sender); + final room = request.room; + final sender = room.getUserByMXIDSync(request.sender); if (await SimpleDialogs(context).askConfirmation( titleText: L10n.of(context).requestToReadOlderMessages, contentText: - "${sender.id}\n\n${L10n.of(context).device}:\n${request.requestingDevice.deviceId}\n\n${L10n.of(context).identity}:\n${request.requestingDevice.curve25519Key.beautified}", + '${sender.id}\n\n${L10n.of(context).device}:\n${request.requestingDevice.deviceId}\n\n${L10n.of(context).identity}:\n${request.requestingDevice.curve25519Key.beautified}', confirmText: L10n.of(context).verify, cancelText: L10n.of(context).deny, )) { @@ -178,20 +183,21 @@ class MatrixState extends State { _initWithStore(); } else { client = widget.client; + client.connect(); } - if (client.storeAPI != null) { - client.storeAPI - .getItem("chat.fluffy.jitsi_instance") + if (store != null) { + store + .getItem('chat.fluffy.jitsi_instance') .then((final instance) => jitsiInstance = instance ?? jitsiInstance); - client.storeAPI.getItem("chat.fluffy.wallpaper").then((final path) async { + store.getItem('chat.fluffy.wallpaper').then((final path) async { if (path == null) return; final file = File(path); if (await file.exists()) { wallpaper = file; } }); - client.storeAPI.getItem("chat.fluffy.renderHtml").then((final render) async { - renderHtml = render == "1"; + store.getItem('chat.fluffy.renderHtml').then((final render) async { + renderHtml = render == '1'; }); } super.initState(); @@ -221,12 +227,11 @@ class _InheritedMatrix extends InheritedWidget { @override bool updateShouldNotify(_InheritedMatrix old) { - bool update = old.data.client.accessToken != this.data.client.accessToken || - old.data.client.userID != this.data.client.userID || - old.data.client.matrixVersions != this.data.client.matrixVersions || - old.data.client.deviceID != this.data.client.deviceID || - old.data.client.deviceName != this.data.client.deviceName || - old.data.client.homeserver != this.data.client.homeserver; + var update = old.data.client.accessToken != data.client.accessToken || + old.data.client.userID != data.client.userID || + old.data.client.deviceID != data.client.deviceID || + old.data.client.deviceName != data.client.deviceName || + old.data.client.homeserver != data.client.homeserver; return update; } } diff --git a/lib/components/message_content.dart b/lib/components/message_content.dart index cf60e73..6d23ba3 100644 --- a/lib/components/message_content.dart +++ b/lib/components/message_content.dart @@ -40,14 +40,13 @@ class MessageContent extends StatelessWidget { case MessageTypes.Text: case MessageTypes.Notice: case MessageTypes.Emote: - if ( - Matrix.of(context).renderHtml && !event.redacted && - event.content['format'] == 'org.matrix.custom.html' && - event.content['formatted_body'] is String - ) { + if (Matrix.of(context).renderHtml && + !event.redacted && + event.content['format'] == 'org.matrix.custom.html' && + event.content['formatted_body'] is String) { String html = event.content['formatted_body']; if (event.messageType == MessageTypes.Emote) { - html = "* $html"; + html = '* $html'; } return HtmlMessage( html: html, diff --git a/lib/components/message_download_content.dart b/lib/components/message_download_content.dart index 86b0fe0..29529bd 100644 --- a/lib/components/message_download_content.dart +++ b/lib/components/message_download_content.dart @@ -36,9 +36,9 @@ class MessageDownloadContent extends StatelessWidget { matrixFile.open(); }), Text( - "- " + - (event.content.containsKey("filename") - ? event.content["filename"] + '- ' + + (event.content.containsKey('filename') + ? event.content['filename'] : event.body), style: TextStyle( color: textColor, @@ -47,7 +47,7 @@ class MessageDownloadContent extends StatelessWidget { ), if (event.sizeString != null) Text( - "- " + event.sizeString, + '- ' + event.sizeString, style: TextStyle( color: textColor, fontWeight: FontWeight.bold, diff --git a/lib/components/reply_content.dart b/lib/components/reply_content.dart index 1cc56aa..4822ccc 100644 --- a/lib/components/reply_content.dart +++ b/lib/components/reply_content.dart @@ -15,15 +15,17 @@ class ReplyContent extends StatelessWidget { @override Widget build(BuildContext context) { Widget replyBody; - if ( - replyEvent != null && Matrix.of(context).renderHtml && - [EventTypes.Message, EventTypes.Encrypted].contains(replyEvent.type) && - [MessageTypes.Text, MessageTypes.Notice, MessageTypes.Emote].contains(replyEvent.messageType) && - !replyEvent.redacted && replyEvent.content['format'] == 'org.matrix.custom.html' && replyEvent.content['formatted_body'] is String - ) { + if (replyEvent != null && + Matrix.of(context).renderHtml && + [EventTypes.Message, EventTypes.Encrypted].contains(replyEvent.type) && + [MessageTypes.Text, MessageTypes.Notice, MessageTypes.Emote] + .contains(replyEvent.messageType) && + !replyEvent.redacted && + replyEvent.content['format'] == 'org.matrix.custom.html' && + replyEvent.content['formatted_body'] is String) { String html = replyEvent.content['formatted_body']; if (replyEvent.messageType == MessageTypes.Emote) { - html = "* $html"; + html = '* $html'; } replyBody = HtmlMessage( html: html, @@ -39,7 +41,7 @@ class ReplyContent extends StatelessWidget { withSenderNamePrefix: false, hideReply: true, ) ?? - "", + '', overflow: TextOverflow.ellipsis, maxLines: 1, style: TextStyle( @@ -62,7 +64,7 @@ class ReplyContent extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - (replyEvent?.sender?.calcDisplayname() ?? "") + ":", + (replyEvent?.sender?.calcDisplayname() ?? '') + ':', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( diff --git a/lib/components/settings_themes.dart b/lib/components/settings_themes.dart index f024337..c0832da 100644 --- a/lib/components/settings_themes.dart +++ b/lib/components/settings_themes.dart @@ -15,9 +15,8 @@ class ThemesSettingsState extends State { @override Widget build(BuildContext context) { - final MatrixState matrix = Matrix.of(context); - final ThemeSwitcherWidgetState themeEngine = - ThemeSwitcherWidget.of(context); + final matrix = Matrix.of(context); + final themeEngine = ThemeSwitcherWidget.of(context); _selectedTheme = themeEngine.selectedTheme; _amoledEnabled = themeEngine.amoledEnabled; diff --git a/lib/components/theme_switcher.dart b/lib/components/theme_switcher.dart index 04afdc7..4226653 100644 --- a/lib/components/theme_switcher.dart +++ b/lib/components/theme_switcher.dart @@ -147,7 +147,7 @@ class ThemeSwitcher extends InheritedWidget { class ThemeSwitcherWidget extends StatefulWidget { final Widget child; - ThemeSwitcherWidget({Key key, this.child}) + ThemeSwitcherWidget({Key key, @required this.child}) : assert(child != null), super(key: key); @@ -156,7 +156,7 @@ class ThemeSwitcherWidget extends StatefulWidget { /// Returns the (nearest) Client instance of your application. static ThemeSwitcherWidgetState of(BuildContext context) { - ThemeSwitcherWidgetState newState = + var newState = (context.dependOnInheritedWidgetOfExactType()).data; newState.context = context; return newState; @@ -167,17 +167,17 @@ class ThemeSwitcherWidgetState extends State { ThemeData themeData; Themes selectedTheme; bool amoledEnabled; + @override BuildContext context; Future loadSelection(MatrixState matrix) async { - String item = await matrix.client.storeAPI.getItem("theme") ?? "light"; + String item = await matrix.store.getItem('theme') ?? 'light'; selectedTheme = Themes.values.firstWhere((e) => e.toString() == 'Themes.' + item); - amoledEnabled = - (await matrix.client.storeAPI.getItem("amoled_enabled") ?? "false") - .toLowerCase() == - 'true'; + amoledEnabled = (await matrix.store.getItem('amoled_enabled') ?? 'false') + .toLowerCase() == + 'true'; switchTheme(matrix, selectedTheme, amoledEnabled); return; @@ -199,7 +199,7 @@ class ThemeSwitcherWidgetState extends State { break; case Themes.system: // This needs to be a low level call as we don't have a MaterialApp yet - Brightness brightness = + var brightness = MediaQueryData.fromWindow(WidgetsBinding.instance.window) .platformBrightness; if (brightness == Brightness.dark) { @@ -224,16 +224,15 @@ class ThemeSwitcherWidgetState extends State { } Future saveThemeValue(MatrixState matrix, Themes value) async { - await matrix.client.storeAPI - .setItem("theme", value.toString().split('.').last); + await matrix.store.setItem('theme', value.toString().split('.').last); } Future saveAmoledEnabledValue(MatrixState matrix, bool value) async { - await matrix.client.storeAPI.setItem("amoled_enabled", value.toString()); + await matrix.store.setItem('amoled_enabled', value.toString()); } void setup() async { - final MatrixState matrix = Matrix.of(context); + final matrix = Matrix.of(context); await loadSelection(matrix); if (selectedTheme == null) { @@ -271,9 +270,8 @@ class ThemeSwitcherWidgetState extends State { Widget build(BuildContext context) { if (themeData == null) { // This needs to be a low level call as we don't have a MaterialApp yet - Brightness brightness = - MediaQueryData.fromWindow(WidgetsBinding.instance.window) - .platformBrightness; + var brightness = MediaQueryData.fromWindow(WidgetsBinding.instance.window) + .platformBrightness; if (brightness == Brightness.dark) { themeData = darkTheme; } else { diff --git a/lib/main.dart b/lib/main.dart index 3ab73fc..db162fb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,11 +21,11 @@ void main() { } class App extends StatelessWidget { - final String platform = kIsWeb ? "Web" : Platform.operatingSystem; + final String platform = kIsWeb ? 'Web' : Platform.operatingSystem; @override Widget build(BuildContext context) { return Matrix( - clientName: "FluffyChat $platform", + clientName: 'FluffyChat $platform', child: Builder( builder: (BuildContext context) => ThemeSwitcherWidget( child: Builder( @@ -47,7 +47,7 @@ class App extends StatelessWidget { const Locale('pl'), // Polish ], locale: kIsWeb - ? Locale(html.window.navigator.language.split("-").first) + ? Locale(html.window.navigator.language.split('-').first) : null, home: FutureBuilder( future: diff --git a/lib/utils/beautify_string_extension.dart b/lib/utils/beautify_string_extension.dart index b3cefa0..b555d6d 100644 --- a/lib/utils/beautify_string_extension.dart +++ b/lib/utils/beautify_string_extension.dart @@ -1,13 +1,13 @@ extension BeautifyStringExtension on String { String get beautified { - String beautifiedStr = ""; - for (int i = 0; i < this.length; i++) { - beautifiedStr += this.substring(i, i + 1); + var beautifiedStr = ''; + for (var i = 0; i < length; i++) { + beautifiedStr += substring(i, i + 1); if (i % 4 == 3) { - beautifiedStr += " "; + beautifiedStr += ' '; } if (i % 16 == 15) { - beautifiedStr += "\n"; + beautifiedStr += '\n'; } } return beautifiedStr; diff --git a/lib/utils/database/mobile.dart b/lib/utils/database/mobile.dart new file mode 100644 index 0000000..999ef1c --- /dev/null +++ b/lib/utils/database/mobile.dart @@ -0,0 +1,12 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:encrypted_moor/encrypted_moor.dart'; +import 'package:flutter/material.dart'; + +Database constructDb({bool logStatements = false, String filename = 'database.sqlite', String password = ''}) { + debugPrint('[Moor] using encrypted moor'); + return Database(EncryptedExecutor(path: filename, password: password, logStatements: logStatements)); +} + +Future getLocalstorage(String key) async { + return null; +} diff --git a/lib/utils/database/shared.dart b/lib/utils/database/shared.dart new file mode 100644 index 0000000..f0e9e1c --- /dev/null +++ b/lib/utils/database/shared.dart @@ -0,0 +1,3 @@ +export 'unsupported.dart' + if (dart.library.html) 'web.dart' + if (dart.library.io) 'mobile.dart'; diff --git a/lib/utils/database/unsupported.dart b/lib/utils/database/unsupported.dart new file mode 100644 index 0000000..c706336 --- /dev/null +++ b/lib/utils/database/unsupported.dart @@ -0,0 +1,9 @@ +import 'package:famedlysdk/famedlysdk.dart'; + +Database constructDb({bool logStatements = false, String filename = 'database.sqlite', String password = ''}) { + throw 'Platform not supported'; +} + +Future getLocalstorage(String key) async { + return null; +} diff --git a/lib/utils/database/web.dart b/lib/utils/database/web.dart new file mode 100644 index 0000000..af1e220 --- /dev/null +++ b/lib/utils/database/web.dart @@ -0,0 +1,13 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:moor/moor_web.dart'; +import 'package:flutter/material.dart'; +import 'dart:html'; + +Database constructDb({bool logStatements = false, String filename = 'database.sqlite', String password = ''}) { + debugPrint('[Moor] Using moor web'); + return Database(WebDatabase.withStorage(MoorWebStorage.indexedDbIfSupported(filename), logStatements: logStatements)); +} + +Future getLocalstorage(String key) async { + return await window.localStorage[key]; +} diff --git a/lib/utils/date_time_extension.dart b/lib/utils/date_time_extension.dart index e9a0d48..60a1983 100644 --- a/lib/utils/date_time_extension.dart +++ b/lib/utils/date_time_extension.dart @@ -3,20 +3,20 @@ import 'package:flutter/material.dart'; /// Provides extra functionality for formatting the time. extension DateTimeExtension on DateTime { - operator <(DateTime other) { - return this.millisecondsSinceEpoch < other.millisecondsSinceEpoch; + bool operator <(DateTime other) { + return millisecondsSinceEpoch < other.millisecondsSinceEpoch; } - operator >(DateTime other) { - return this.millisecondsSinceEpoch > other.millisecondsSinceEpoch; + bool operator >(DateTime other) { + return millisecondsSinceEpoch > other.millisecondsSinceEpoch; } - operator >=(DateTime other) { - return this.millisecondsSinceEpoch >= other.millisecondsSinceEpoch; + bool operator >=(DateTime other) { + return millisecondsSinceEpoch >= other.millisecondsSinceEpoch; } - operator <=(DateTime other) { - return this.millisecondsSinceEpoch <= other.millisecondsSinceEpoch; + bool operator <=(DateTime other) { + return millisecondsSinceEpoch <= other.millisecondsSinceEpoch; } /// Two message events can belong to the same environment. That means that they @@ -34,28 +34,28 @@ extension DateTimeExtension on DateTime { /// Returns a simple time String. /// TODO: Add localization String localizedTimeOfDay(BuildContext context) { - return L10n.of(context).timeOfDay(_z(this.hour % 12), _z(this.hour), - _z(this.minute), this.hour > 11 ? 'pm' : 'am'); + return L10n.of(context).timeOfDay( + _z(hour % 12), _z(hour), _z(minute), hour > 11 ? 'pm' : 'am'); } /// Returns [localizedTimeOfDay()] if the ChatTime is today, the name of the week /// day if the ChatTime is this week and a date string else. String localizedTimeShort(BuildContext context) { - DateTime now = DateTime.now(); + var now = DateTime.now(); - bool sameYear = now.year == this.year; + var sameYear = now.year == year; - bool sameDay = sameYear && now.month == this.month && now.day == this.day; + var sameDay = sameYear && now.month == month && now.day == day; - bool sameWeek = sameYear && + var sameWeek = sameYear && !sameDay && - now.millisecondsSinceEpoch - this.millisecondsSinceEpoch < + now.millisecondsSinceEpoch - millisecondsSinceEpoch < 1000 * 60 * 60 * 24 * 7; if (sameDay) { return localizedTimeOfDay(context); } else if (sameWeek) { - switch (this.weekday) { + switch (weekday) { case 1: return L10n.of(context).monday; case 2: @@ -73,29 +73,26 @@ extension DateTimeExtension on DateTime { } } else if (sameYear) { return L10n.of(context).dateWithoutYear( - this.month.toString().padLeft(2, '0'), - this.day.toString().padLeft(2, '0')); + month.toString().padLeft(2, '0'), day.toString().padLeft(2, '0')); } - return L10n.of(context).dateWithYear( - this.year.toString(), - this.month.toString().padLeft(2, '0'), - this.day.toString().padLeft(2, '0')); + return L10n.of(context).dateWithYear(year.toString(), + month.toString().padLeft(2, '0'), day.toString().padLeft(2, '0')); } /// If the DateTime is today, this returns [localizedTimeOfDay()], if not it also /// shows the date. /// TODO: Add localization String localizedTime(BuildContext context) { - DateTime now = DateTime.now(); + var now = DateTime.now(); - bool sameYear = now.year == this.year; + var sameYear = now.year == year; - bool sameDay = sameYear && now.month == this.month && now.day == this.day; + var sameDay = sameYear && now.month == month && now.day == day; if (sameDay) return localizedTimeOfDay(context); return L10n.of(context).dateAndTimeOfDay( localizedTimeShort(context), localizedTimeOfDay(context)); } - static String _z(int i) => i < 10 ? "0${i.toString()}" : i.toString(); + static String _z(int i) => i < 10 ? '0${i.toString()}' : i.toString(); } diff --git a/lib/utils/event_extension.dart b/lib/utils/event_extension.dart index e3cd5fb..0a8be3e 100644 --- a/lib/utils/event_extension.dart +++ b/lib/utils/event_extension.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; extension LocalizedBody on Event { IconData get statusIcon { - switch (this.status) { + switch (status) { case -1: return Icons.error_outline; case 0: @@ -22,21 +22,21 @@ extension LocalizedBody on Event { [MessageTypes.Image, MessageTypes.Sticker].contains(messageType) && (kIsWeb || (content['info'] is Map && - content['info']['size'] < room.client.store.maxFileSize)); + content['info']['size'] < room.client.database.maxFileSize)); String get sizeString { - if (content["info"] is Map && - content["info"].containsKey("size")) { - num size = content["info"]["size"]; + if (content['info'] is Map && + content['info'].containsKey('size')) { + num size = content['info']['size']; if (size < 1000000) { size = size / 1000; - return "${size.toString()}kb"; + return '${size.toString()}kb'; } else if (size < 1000000000) { size = size / 1000000; - return "${size.toString()}mb"; + return '${size.toString()}mb'; } else { size = size / 1000000000; - return "${size.toString()}gb"; + return '${size.toString()}gb'; } } else { return null; diff --git a/lib/utils/famedlysdk_store.dart b/lib/utils/famedlysdk_store.dart index 6f912b6..8b0a9c9 100644 --- a/lib/utils/famedlysdk_store.dart +++ b/lib/utils/famedlysdk_store.dart @@ -1,25 +1,186 @@ import 'dart:convert'; -import 'dart:typed_data'; import 'package:famedlysdk/famedlysdk.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:localstorage/localstorage.dart'; import 'dart:async'; import 'dart:core'; -import 'package:path/path.dart' as p; -import 'package:sqflite/sqflite.dart'; +import './database/shared.dart'; +import 'package:olm/olm.dart' as olm; // needed for migration +import 'package:random_string/random_string.dart'; -class Store extends StoreAPI { - final Client client; +Future getDatabase(Client client, Store store) async { + var password = await store.getItem('database-password'); + var needMigration = false; + if (password == null || password.isEmpty) { + needMigration = true; + password = randomString(255); + } + final db = constructDb( + logStatements: false, + filename: 'moor.sqlite', + password: password, + ); + if (needMigration) { + await migrate(client.clientName, db, store); + await store.setItem('database-password', password); + } + return db; +} + +Future migrate(String clientName, Database db, Store store) async { + debugPrint('[Store] attempting old migration to moor...'); + final oldKeys = await store.getAllItems(); + if (oldKeys == null || oldKeys.isEmpty) { + debugPrint('[Store] empty store!'); + return; // we are done! + } + final credentialsStr = oldKeys[clientName]; + if (credentialsStr == null || credentialsStr.isEmpty) { + debugPrint('[Store] no credentials found!'); + return; // no credentials + } + final Map credentials = json.decode(credentialsStr); + if (!credentials.containsKey('homeserver') || + !credentials.containsKey('token') || + !credentials.containsKey('userID')) { + debugPrint('[Store] invalid credentials!'); + return; // invalid old store, we are done, too! + } + var clientId = 0; + final oldClient = await db.getClient(clientName); + if (oldClient == null) { + clientId = await db.insertClient( + clientName, + credentials['homeserver'], + credentials['token'], + credentials['userID'], + credentials['deviceID'], + credentials['deviceName'], + null, + credentials['olmAccount'], + ); + } else { + clientId = oldClient.clientId; + await db.updateClient( + credentials['homeserver'], + credentials['token'], + credentials['userID'], + credentials['deviceID'], + credentials['deviceName'], + null, + credentials['olmAccount'], + clientId, + ); + } + await db.clearCache(clientId); + debugPrint('[Store] Inserted/updated client, clientId = ${clientId}'); + await db.transaction(() async { + // alright, we stored / updated the client and have the account ID, time to import everything else! + // user_device_keys and user_device_keys_key + debugPrint('[Store] Migrating user device keys...'); + final deviceKeysListString = oldKeys['${clientName}.user_device_keys']; + if (deviceKeysListString != null && deviceKeysListString.isNotEmpty) { + Map rawUserDeviceKeys = + json.decode(deviceKeysListString); + for (final entry in rawUserDeviceKeys.entries) { + final map = entry.value; + await db.storeUserDeviceKeysInfo( + clientId, map['user_id'], map['outdated']); + for (final rawKey in map['device_keys'].entries) { + final jsonVaue = rawKey.value; + await db.storeUserDeviceKey( + clientId, + jsonVaue['user_id'], + jsonVaue['device_id'], + json.encode(jsonVaue), + jsonVaue['verified'], + jsonVaue['blocked']); + } + } + } + for (final entry in oldKeys.entries) { + final key = entry.key; + final value = entry.value; + if (value == null || value.isEmpty) { + continue; + } + // olm_sessions + final olmSessionsMatch = + RegExp(r'^\/clients\/([^\/]+)\/olm-sessions$').firstMatch(key); + if (olmSessionsMatch != null) { + if (olmSessionsMatch[1] != credentials['deviceID']) { + continue; + } + debugPrint('[Store] migrating olm sessions...'); + final identityKey = json.decode(value); + for (final olmKey in identityKey.entries) { + final identKey = olmKey.key; + final sessions = olmKey.value; + for (final pickle in sessions) { + var sess = olm.Session(); + sess.unpickle(credentials['userID'], pickle); + await db.storeOlmSession( + clientId, identKey, sess.session_id(), pickle); + sess?.free(); + } + } + } + // outbound_group_sessions + final outboundGroupSessionsMatch = RegExp( + r'^\/clients\/([^\/]+)\/rooms\/([^\/]+)\/outbound_group_session$') + .firstMatch(key); + if (outboundGroupSessionsMatch != null) { + if (outboundGroupSessionsMatch[1] != credentials['deviceID']) { + continue; + } + final pickle = value; + final roomId = outboundGroupSessionsMatch[2]; + debugPrint( + '[Store] Migrating outbound group sessions for room ${roomId}...'); + final devicesString = oldKeys[ + '/clients/${outboundGroupSessionsMatch[1]}/rooms/${roomId}/outbound_group_session_devices']; + var devices = []; + if (devicesString != null) { + devices = List.from(json.decode(devicesString)); + } + await db.storeOutboundGroupSession( + clientId, roomId, pickle, json.encode(devices)); + } + // session_keys + final sessionKeysMatch = + RegExp(r'^\/clients\/([^\/]+)\/rooms\/([^\/]+)\/session_keys$') + .firstMatch(key); + if (sessionKeysMatch != null) { + if (sessionKeysMatch[1] != credentials['deviceID']) { + continue; + } + final roomId = sessionKeysMatch[2]; + debugPrint('[Store] Migrating session keys for room ${roomId}...'); + final map = json.decode(value); + for (final entry in map.entries) { + await db.storeInboundGroupSession( + clientId, + roomId, + entry.key, + entry.value['inboundGroupSession'], + json.encode(entry.value['content']), + json.encode(entry.value['indexes'])); + } + } + } + }); +} + +class Store { final LocalStorage storage; final FlutterSecureStorage secureStorage; - Store(this.client) + Store() : storage = LocalStorage('LocalStorage'), - secureStorage = kIsWeb ? null : FlutterSecureStorage() { - _init(); - } + secureStorage = kIsWeb ? null : FlutterSecureStorage(); Future getItem(String key) async { if (kIsWeb) { @@ -49,587 +210,19 @@ class Store extends StoreAPI { } } - Future> getUserDeviceKeys() async { - final deviceKeysListString = await getItem(_UserDeviceKeysKey); - if (deviceKeysListString == null) return {}; - Map rawUserDeviceKeys = json.decode(deviceKeysListString); - Map userDeviceKeys = {}; - for (final entry in rawUserDeviceKeys.entries) { - userDeviceKeys[entry.key] = DeviceKeysList.fromJson(entry.value); + Future> getAllItems() async { + if (kIsWeb) { + try { + final rawStorage = await getLocalstorage('LocalStorage'); + return json.decode(rawStorage); + } catch (_) { + return {}; + } } - return userDeviceKeys; - } - - Future storeUserDeviceKeys( - Map userDeviceKeys) async { - await setItem(_UserDeviceKeysKey, json.encode(userDeviceKeys)); - } - - String get _UserDeviceKeysKey => "${client.clientName}.user_device_keys"; - - _init() async { - final credentialsStr = await getItem(client.clientName); - - if (credentialsStr == null || credentialsStr.isEmpty) { - client.onLoginStateChanged.add(LoginState.loggedOut); - return; + try { + return await secureStorage.readAll(); + } catch (_) { + return {}; } - debugPrint("[Matrix] Restoring account credentials"); - final Map credentials = json.decode(credentialsStr); - if (credentials["homeserver"] == null || - credentials["token"] == null || - credentials["userID"] == null) { - client.onLoginStateChanged.add(LoginState.loggedOut); - return; - } - client.connect( - newDeviceID: credentials["deviceID"], - newDeviceName: credentials["deviceName"], - newHomeserver: credentials["homeserver"], - newMatrixVersions: List.from(credentials["matrixVersions"] ?? []), - newToken: credentials["token"], - newUserID: credentials["userID"], - newPrevBatch: kIsWeb - ? null - : (credentials["prev_batch"]?.isEmpty ?? true) - ? null - : credentials["prev_batch"], - newOlmAccount: credentials["olmAccount"], - ); } - - Future storeClient() async { - final Map credentials = { - "deviceID": client.deviceID, - "deviceName": client.deviceName, - "homeserver": client.homeserver, - "matrixVersions": client.matrixVersions, - "token": client.accessToken, - "userID": client.userID, - "olmAccount": client.pickledOlmAccount, - }; - await setItem(client.clientName, json.encode(credentials)); - return; - } - - Future clear() => kIsWeb ? storage.clear() : secureStorage.deleteAll(); -} - -/// Responsible to store all data persistent and to query objects from the -/// database. -class ExtendedStore extends Store implements ExtendedStoreAPI { - /// The maximum time that files are allowed to stay in the - /// store. By default this is are 30 days. - static const int MAX_FILE_STORING_TIME = 1 * 30 * 24 * 60 * 60 * 1000; - - @override - final bool extended = true; - - ExtendedStore(Client client) : super(client); - - Database _db; - var txn; - - /// SQLite database for all persistent data. It is recommended to extend this - /// SDK instead of writing direct queries to the database. - //Database get db => _db; - - @override - _init() async { - // Open the database and migrate if necessary. - var databasePath = await getDatabasesPath(); - String path = p.join(databasePath, "FluffyMatrix.db"); - _db = await openDatabase(path, version: 20, - onCreate: (Database db, int version) async { - await createTables(db); - }, onUpgrade: (Database db, int oldVersion, int newVersion) async { - debugPrint( - "[Store] Migrate database from version $oldVersion to $newVersion"); - if (oldVersion >= 18 && newVersion <= 20) { - await createTables(db); - } else if (oldVersion != newVersion) { - // Look for an old entry in an old clients library - List list = []; - try { - list = await db.rawQuery( - "SELECT * FROM Clients WHERE client=?", [client.clientName]); - } catch (_) { - list = []; - } - client.prevBatch = null; - await this.storePrevBatch(null); - schemes.forEach((String name, String scheme) async { - await db.execute("DROP TABLE IF EXISTS $name"); - }); - await createTables(db); - - if (list.length == 1) { - debugPrint("[Store] Found old client from deprecated store"); - var clientList = list[0]; - _db = db; - client.connect( - newToken: clientList["token"], - newHomeserver: clientList["homeserver"], - newUserID: clientList["matrix_id"], - newDeviceID: clientList["device_id"], - newDeviceName: clientList["device_name"], - newMatrixVersions: - clientList["matrix_versions"].toString().split(","), - newPrevBatch: null, - ); - await db.execute("DROP TABLE IF EXISTS Clients"); - debugPrint( - "[Store] Restore client credentials from deprecated database of ${client.userID}"); - } - } else { - client.onLoginStateChanged.add(LoginState.loggedOut); - } - return; - }); - - // Mark all pending events as failed. - await _db.rawUpdate("UPDATE Events SET status=-1 WHERE status=0"); - - // Delete all stored files which are older than [MAX_FILE_STORING_TIME] - final int currentDeadline = DateTime.now().millisecondsSinceEpoch - - ExtendedStore.MAX_FILE_STORING_TIME; - await _db.rawDelete( - "DELETE From Files WHERE saved_at setRoomPrevBatch(String roomId, String prevBatch) async { - await txn.rawUpdate( - "UPDATE Rooms SET prev_batch=? WHERE room_id=?", [roomId, prevBatch]); - return; - } - - Future createTables(Database db) async { - schemes.forEach((String name, String scheme) async { - await db.execute(scheme); - }); - } - - /// Clears all tables from the database. - Future clear() async { - schemes.forEach((String name, String scheme) async { - await _db.rawDelete("DELETE FROM $name"); - }); - await super.clear(); - return; - } - - Future transaction(Function queries) async { - return _db.transaction((txnObj) async { - txn = txnObj.batch(); - queries(); - await txn.commit(noResult: true); - }); - } - - /// Will be automatically called on every synchronisation. - Future storePrevBatch(String prevBatch) async { - final credentialsStr = await getItem(client.clientName); - if (credentialsStr == null) return; - final Map credentials = json.decode(credentialsStr); - credentials["prev_batch"] = prevBatch; - await setItem(client.clientName, json.encode(credentials)); - } - - Future storeRoomPrevBatch(Room room) async { - await _db.rawUpdate("UPDATE Rooms SET prev_batch=? WHERE room_id=?", - [room.prev_batch, room.id]); - return null; - } - - /// Stores a RoomUpdate object in the database. Must be called inside of - /// [transaction]. - Future storeRoomUpdate(RoomUpdate roomUpdate) { - if (txn == null) return null; - // Insert the chat into the database if not exists - if (roomUpdate.membership != Membership.leave) { - txn.rawInsert( - "INSERT OR IGNORE INTO Rooms " + "VALUES(?, ?, 0, 0, '', 0, 0, '') ", - [roomUpdate.id, roomUpdate.membership.toString().split('.').last]); - } else { - txn.rawDelete("DELETE FROM Rooms WHERE room_id=? ", [roomUpdate.id]); - return null; - } - - // Update the notification counts and the limited timeline boolean and the summary - String updateQuery = - "UPDATE Rooms SET highlight_count=?, notification_count=?, membership=?"; - List updateArgs = [ - roomUpdate.highlight_count, - roomUpdate.notification_count, - roomUpdate.membership.toString().split('.').last - ]; - if (roomUpdate.summary?.mJoinedMemberCount != null) { - updateQuery += ", joined_member_count=?"; - updateArgs.add(roomUpdate.summary.mJoinedMemberCount); - } - if (roomUpdate.summary?.mInvitedMemberCount != null) { - updateQuery += ", invited_member_count=?"; - updateArgs.add(roomUpdate.summary.mInvitedMemberCount); - } - if (roomUpdate.summary?.mHeroes != null) { - updateQuery += ", heroes=?"; - updateArgs.add(roomUpdate.summary.mHeroes.join(",")); - } - updateQuery += " WHERE room_id=?"; - updateArgs.add(roomUpdate.id); - txn.rawUpdate(updateQuery, updateArgs); - - // Is the timeline limited? Then all previous messages should be - // removed from the database! - if (roomUpdate.limitedTimeline) { - txn.rawDelete("DELETE FROM Events WHERE room_id=?", [roomUpdate.id]); - txn.rawUpdate("UPDATE Rooms SET prev_batch=? WHERE room_id=?", - [roomUpdate.prev_batch, roomUpdate.id]); - } - return null; - } - - /// Stores an UserUpdate object in the database. Must be called inside of - /// [transaction]. - Future storeUserEventUpdate(UserUpdate userUpdate) { - if (txn == null) return null; - if (userUpdate.type == "account_data") { - txn.rawInsert("INSERT OR REPLACE INTO AccountData VALUES(?, ?)", [ - userUpdate.eventType, - json.encode(userUpdate.content["content"]), - ]); - } else if (userUpdate.type == "presence") { - txn.rawInsert("INSERT OR REPLACE INTO Presences VALUES(?, ?, ?)", [ - userUpdate.eventType, - userUpdate.content["sender"], - json.encode(userUpdate.content["content"]), - ]); - } - return null; - } - - Future redactMessage(EventUpdate eventUpdate) async { - List> res = await _db.rawQuery( - "SELECT * FROM Events WHERE event_id=?", - [eventUpdate.content["redacts"]]); - if (res.length == 1) { - Event event = Event.fromJson(res[0], null); - event.setRedactionEvent(Event.fromJson(eventUpdate.content, null)); - final int changes1 = await _db.rawUpdate( - "UPDATE Events SET unsigned=?, content=?, prev_content=? WHERE event_id=?", - [ - json.encode(event.unsigned ?? ""), - json.encode(event.content ?? ""), - json.encode(event.prevContent ?? ""), - event.eventId, - ], - ); - final int changes2 = await _db.rawUpdate( - "UPDATE RoomStates SET unsigned=?, content=?, prev_content=? WHERE event_id=?", - [ - json.encode(event.unsigned ?? ""), - json.encode(event.content ?? ""), - json.encode(event.prevContent ?? ""), - event.eventId, - ], - ); - if (changes1 == 1 && changes2 == 1) return true; - } - return false; - } - - /// Stores an EventUpdate object in the database. Must be called inside of - /// [transaction]. - Future storeEventUpdate(EventUpdate eventUpdate) { - if (txn == null || eventUpdate.type == "ephemeral") return null; - Map eventContent = eventUpdate.content; - String type = eventUpdate.type; - String chatId = eventUpdate.roomID; - - // Get the state_key for m.room.member events - String stateKey = ""; - if (eventContent["state_key"] is String) { - stateKey = eventContent["state_key"]; - } - - if (eventUpdate.eventType == "m.room.redaction") { - redactMessage(eventUpdate); - } - - if (type == "timeline" || type == "history") { - // calculate the status - num status = 2; - if (eventContent["status"] is num) status = eventContent["status"]; - - // Save the event in the database - if ((status == 1 || status == -1) && - eventContent["unsigned"] is Map && - eventContent["unsigned"]["transaction_id"] is String) { - txn.rawUpdate( - "UPDATE Events SET status=?, event_id=? WHERE event_id=?", [ - status, - eventContent["event_id"], - eventContent["unsigned"]["transaction_id"] - ]); - } else { - txn.rawInsert( - "INSERT OR REPLACE INTO Events VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - [ - eventContent["event_id"], - chatId, - eventContent["origin_server_ts"], - eventContent["sender"], - eventContent["type"], - json.encode(eventContent["unsigned"] ?? ""), - json.encode(eventContent["content"]), - json.encode(eventContent["prevContent"]), - eventContent["state_key"], - status - ]); - } - - // Is there a transaction id? Then delete the event with this id. - if (status != -1 && - eventUpdate.content.containsKey("unsigned") && - eventUpdate.content["unsigned"]["transaction_id"] is String) { - txn.rawDelete("DELETE FROM Events WHERE event_id=?", - [eventUpdate.content["unsigned"]["transaction_id"]]); - } - } - - if (type == "history") return null; - - if (type != "account_data") { - final String now = DateTime.now().millisecondsSinceEpoch.toString(); - txn.rawInsert( - "INSERT OR REPLACE INTO RoomStates VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", - [ - eventContent["event_id"] ?? now, - chatId, - eventContent["origin_server_ts"] ?? now, - eventContent["sender"], - stateKey, - json.encode(eventContent["unsigned"] ?? ""), - json.encode(eventContent["prev_content"] ?? ""), - eventContent["type"], - json.encode(eventContent["content"]), - ]); - } else if (type == "account_data") { - txn.rawInsert("INSERT OR REPLACE INTO RoomAccountData VALUES(?, ?, ?)", [ - eventContent["type"], - chatId, - json.encode(eventContent["content"]), - ]); - } - - return null; - } - - /// Returns a User object by a given Matrix ID and a Room. - Future getUser({String matrixID, Room room}) async { - List> res = await _db.rawQuery( - "SELECT * FROM RoomStates WHERE state_key=? AND room_id=?", - [matrixID, room.id]); - if (res.length != 1) return null; - return Event.fromJson(res[0], room).asUser; - } - - /// Returns a list of events for the given room and sets all participants. - Future> getEventList(Room room) async { - List> eventRes = await _db.rawQuery( - "SELECT * " + - " FROM Events " + - " WHERE room_id=?" + - " GROUP BY event_id " + - " ORDER BY origin_server_ts DESC", - [room.id]); - - List eventList = []; - - for (num i = 0; i < eventRes.length; i++) { - eventList.add(Event.fromJson(eventRes[i], room)); - } - - return eventList; - } - - /// Returns all rooms, the client is participating. Excludes left rooms. - Future> getRoomList({bool onlyLeft = false}) async { - List> res = await _db.rawQuery("SELECT * " + - " FROM Rooms" + - " WHERE membership" + - (onlyLeft ? "=" : "!=") + - "'leave' " + - " GROUP BY room_id "); - List> resStates = await _db.rawQuery("SELECT * FROM RoomStates WHERE type IS NOT NULL"); - List> resAccountData = await _db.rawQuery("SELECT * FROM RoomAccountData"); - List roomList = []; - for (num i = 0; i < res.length; i++) { - Room room = await Room.getRoomFromTableRow( - res[i], - client, - states: Future.value(resStates.where((r) => r["room_id"] == res[i]["room_id"]).toList()), - roomAccountData: Future.value(resAccountData.where((r) => r["room_id"] == res[i]["room_id"]).toList()), - ); - roomList.add(room); - } - return roomList; - } - - Future>> getStatesFromRoomId(String id) async { - return _db.rawQuery( - "SELECT * FROM RoomStates WHERE room_id=? AND type IS NOT NULL", [id]); - } - - Future>> getAccountDataFromRoomId(String id) async { - return _db.rawQuery("SELECT * FROM RoomAccountData WHERE room_id=?", [id]); - } - - Future resetNotificationCount(String roomID) async { - await _db.rawDelete( - "UPDATE Rooms SET notification_count=0, highlight_count=0 WHERE room_id=?", - [roomID]); - return; - } - - Future forgetRoom(String roomID) async { - await _db.rawDelete("DELETE FROM Rooms WHERE room_id=?", [roomID]); - await _db.rawDelete("DELETE FROM Events WHERE room_id=?", [roomID]); - await _db.rawDelete("DELETE FROM RoomStates WHERE room_id=?", [roomID]); - await _db - .rawDelete("DELETE FROM RoomAccountData WHERE room_id=?", [roomID]); - return; - } - - /// Searches for the event in the store. - Future getEventById(String eventID, Room room) async { - List> res = await _db.rawQuery( - "SELECT * FROM Events WHERE event_id=? AND room_id=?", - [eventID, room.id]); - if (res.isEmpty) return null; - return Event.fromJson(res[0], room); - } - - Future> getAccountData() async { - Map newAccountData = {}; - List> rawAccountData = - await _db.rawQuery("SELECT * FROM AccountData"); - for (int i = 0; i < rawAccountData.length; i++) { - newAccountData[rawAccountData[i]["type"]] = - AccountData.fromJson(rawAccountData[i]); - } - return newAccountData; - } - - Future> getPresences() async { - Map newPresences = {}; - List> rawPresences = - await _db.rawQuery("SELECT * FROM Presences"); - for (int i = 0; i < rawPresences.length; i++) { - Map rawPresence = { - "sender": rawPresences[i]["sender"], - "content": json.decode(rawPresences[i]["content"]), - }; - newPresences[rawPresences[i]["sender"]] = Presence.fromJson(rawPresence); - } - return newPresences; - } - - Future removeEvent(String eventId) async { - assert(eventId != ""); - await _db.rawDelete("DELETE FROM Events WHERE event_id=?", [eventId]); - return; - } - - Future storeFile(Uint8List bytes, String mxcUri) async { - await _db.rawInsert( - "INSERT OR REPLACE INTO Files VALUES(?, ?, ?)", - [mxcUri, bytes, DateTime.now().millisecondsSinceEpoch], - ); - return; - } - - Future getFile(String mxcUri) async { - List> res = await _db.rawQuery( - "SELECT * FROM Files WHERE mxc_uri=?", - [mxcUri], - ); - if (res.isEmpty) return null; - return res.first["bytes"]; - } - - static final Map schemes = { - /// The database scheme for the Room class. - 'Rooms': 'CREATE TABLE IF NOT EXISTS Rooms(' + - 'room_id TEXT PRIMARY KEY, ' + - 'membership TEXT, ' + - 'highlight_count INTEGER, ' + - 'notification_count INTEGER, ' + - 'prev_batch TEXT, ' + - 'joined_member_count INTEGER, ' + - 'invited_member_count INTEGER, ' + - 'heroes TEXT, ' + - 'UNIQUE(room_id))', - - /// The database scheme for the TimelineEvent class. - 'Events': 'CREATE TABLE IF NOT EXISTS Events(' + - 'event_id TEXT PRIMARY KEY, ' + - 'room_id TEXT, ' + - 'origin_server_ts INTEGER, ' + - 'sender TEXT, ' + - 'type TEXT, ' + - 'unsigned TEXT, ' + - 'content TEXT, ' + - 'prev_content TEXT, ' + - 'state_key TEXT, ' + - "status INTEGER, " + - 'UNIQUE(event_id))', - - /// The database scheme for room states. - 'RoomStates': 'CREATE TABLE IF NOT EXISTS RoomStates(' + - 'event_id TEXT PRIMARY KEY, ' + - 'room_id TEXT, ' + - 'origin_server_ts INTEGER, ' + - 'sender TEXT, ' + - 'state_key TEXT, ' + - 'unsigned TEXT, ' + - 'prev_content TEXT, ' + - 'type TEXT, ' + - 'content TEXT, ' + - 'UNIQUE(room_id,state_key,type))', - - /// The database scheme for room states. - 'AccountData': 'CREATE TABLE IF NOT EXISTS AccountData(' + - 'type TEXT PRIMARY KEY, ' + - 'content TEXT, ' + - 'UNIQUE(type))', - - /// The database scheme for room states. - 'RoomAccountData': 'CREATE TABLE IF NOT EXISTS RoomAccountData(' + - 'type TEXT, ' + - 'room_id TEXT, ' + - 'content TEXT, ' + - 'UNIQUE(type,room_id))', - - /// The database scheme for room states. - 'Presences': 'CREATE TABLE IF NOT EXISTS Presences(' + - 'type TEXT PRIMARY KEY, ' + - 'sender TEXT, ' + - 'content TEXT, ' + - 'UNIQUE(sender))', - - /// The database scheme for room states. - 'Files': 'CREATE TABLE IF NOT EXISTS Files(' + - 'mxc_uri TEXT PRIMARY KEY, ' + - 'bytes BLOB, ' + - 'saved_at INTEGER, ' + - 'UNIQUE(mxc_uri))', - }; - - @override - int get maxFileSize => 1 * 1024 * 1024; } diff --git a/lib/utils/firebase_controller.dart b/lib/utils/firebase_controller.dart index 8799913..41e127f 100644 --- a/lib/utils/firebase_controller.dart +++ b/lib/utils/firebase_controller.dart @@ -15,9 +15,9 @@ import 'package:famedlysdk/famedlysdk.dart'; import 'famedlysdk_store.dart'; abstract class FirebaseController { - static FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); - static FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); + static final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); + static final FlutterLocalNotificationsPlugin + _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); static BuildContext context; static const String CHANNEL_ID = 'fluffychat_push'; static const String CHANNEL_NAME = 'FluffyChat push channel'; @@ -52,7 +52,7 @@ abstract class FirebaseController { currentPushers.first.lang == 'en' && currentPushers.first.data.url == GATEWAY_URL && currentPushers.first.data.format == PUSHER_FORMAT) { - debugPrint("[Push] Pusher already set"); + debugPrint('[Push] Pusher already set'); } else { if (currentPushers.isNotEmpty) { for (final currentPusher in currentPushers) { @@ -66,16 +66,16 @@ abstract class FirebaseController { currentPusher.data.url, append: true, ); - debugPrint("[Push] Remove legacy pusher for this device"); + debugPrint('[Push] Remove legacy pusher for this device'); } } await client.setPushers( token, - "http", + 'http', APP_ID, clientName, client.deviceName, - "en", + 'en', GATEWAY_URL, append: false, format: PUSHER_FORMAT, @@ -88,9 +88,9 @@ abstract class FirebaseController { if (message is String) { roomId = message; } else if (message is Map) { - roomId = (message["data"] ?? message)["room_id"]; + roomId = (message['data'] ?? message)['room_id']; } - if (roomId?.isEmpty ?? true) throw ("Bad roomId"); + if (roomId?.isEmpty ?? true) throw ('Bad roomId'); await Navigator.of(context).pushAndRemoveUntil( AppRoute.defaultRoute( context, @@ -98,7 +98,7 @@ abstract class FirebaseController { ), (r) => r.isFirst); } catch (_) { - BotToast.showText(text: "Failed to open chat..."); + BotToast.showText(text: 'Failed to open chat...'); debugPrint(_); } }; @@ -121,16 +121,16 @@ abstract class FirebaseController { onResume: goToRoom, onLaunch: goToRoom, ); - debugPrint("[Push] Firebase initialized"); + debugPrint('[Push] Firebase initialized'); return; } static Future _onMessage(Map message) async { try { final data = message['data'] ?? message; - final String roomId = data["room_id"]; - final String eventId = data["event_id"]; - final int unread = json.decode(data["counts"])["unread"]; + final String roomId = data['room_id']; + final String eventId = data['event_id']; + final int unread = json.decode(data['counts'])['unread']; if ((roomId?.isEmpty ?? true) || (eventId?.isEmpty ?? true) || unread == 0) { @@ -148,10 +148,12 @@ abstract class FirebaseController { if (context != null) { client = Matrix.of(context).client; } else { - final platform = kIsWeb ? "Web" : Platform.operatingSystem; - final clientName = "FluffyChat $platform"; + final platform = kIsWeb ? 'Web' : Platform.operatingSystem; + final clientName = 'FluffyChat $platform'; client = Client(clientName, debug: false); - client.storeAPI = ExtendedStore(client); + final store = Store(); + client.database = await getDatabase(client, store); + client.connect(); await client.onLoginStateChanged.stream .firstWhere((l) => l == LoginState.logged) .timeout( @@ -160,7 +162,7 @@ abstract class FirebaseController { } // Get the room - Room room = client.getRoomById(roomId); + var room = client.getRoomById(roomId); if (room == null) { await client.onRoomUpdate.stream .where((u) => u.id == roomId) @@ -171,10 +173,10 @@ abstract class FirebaseController { } // Get the event - Event event = await client.store.getEventById(eventId, room); + var event = await client.database.getEventById(client.id, eventId, room); if (event == null) { - final EventUpdate eventUpdate = await client.onEvent.stream - .where((u) => u.content["event_id"] == eventId) + final eventUpdate = await client.onEvent.stream + .where((u) => u.content['event_id'] == eventId) .first .timeout(Duration(seconds: 5)); event = Event.fromJson(eventUpdate.content, room); @@ -182,18 +184,18 @@ abstract class FirebaseController { } // Count all unread events - int unreadEvents = 0; + var unreadEvents = 0; client.rooms .forEach((Room room) => unreadEvents += room.notificationCount); // Calculate title - final String title = unread > 1 + final title = unread > 1 ? i18n.unreadMessagesInChats( unreadEvents.toString(), unread.toString()) : i18n.unreadMessages(unreadEvents.toString()); // Calculate the body - final String body = event.getLocalizedBody( + final body = event.getLocalizedBody( i18n, withSenderNamePrefix: true, hideReply: true, @@ -238,7 +240,7 @@ abstract class FirebaseController { 0, room.getLocalizedDisplayname(i18n), body, platformChannelSpecifics, payload: roomId); } catch (exception) { - debugPrint("[Push] Error while processing notification: " + + debugPrint('[Push] Error while processing notification: ' + exception.toString()); await _showDefaultNotification(message); } @@ -248,8 +250,7 @@ abstract class FirebaseController { static Future _showDefaultNotification( Map message) async { try { - FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); + var flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); // Init notifications framework var initializationSettingsAndroid = AndroidInitializationSettings('notifications_icon'); @@ -261,10 +262,10 @@ abstract class FirebaseController { // Notification data and matrix data Map data = message['data'] ?? message; - String eventID = data["event_id"]; - String roomID = data["room_id"]; - final int unread = data.containsKey("counts") - ? json.decode(data["counts"])["unread"] + String eventID = data['event_id']; + String roomID = data['room_id']; + final int unread = data.containsKey('counts') + ? json.decode(data['counts'])['unread'] : 1; await flutterLocalNotificationsPlugin.cancelAll(); if (unread == 0 || roomID == null || eventID == null) { @@ -278,12 +279,12 @@ abstract class FirebaseController { var iOSPlatformChannelSpecifics = IOSNotificationDetails(); var platformChannelSpecifics = NotificationDetails( androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics); - final String title = l10n.unreadChats(unread.toString()); + final title = l10n.unreadChats(unread.toString()); await flutterLocalNotificationsPlugin.show( 1, title, l10n.openAppToReadMessages, platformChannelSpecifics, payload: roomID); } catch (exception) { - debugPrint("[Push] Error while processing background notification: " + + debugPrint('[Push] Error while processing background notification: ' + exception.toString()); } return Future.value(); @@ -291,10 +292,10 @@ abstract class FirebaseController { static Future downloadAndSaveAvatar(Uri content, Client client, {int width, int height}) async { - final bool thumbnail = width == null && height == null ? false : true; - final String tempDirectory = (await getTemporaryDirectory()).path; - final String prefix = thumbnail ? "thumbnail" : ""; - File file = + final thumbnail = width == null && height == null ? false : true; + final tempDirectory = (await getTemporaryDirectory()).path; + final prefix = thumbnail ? 'thumbnail' : ''; + var file = File('$tempDirectory/${prefix}_${content.toString().split("/").last}'); if (!file.existsSync()) { @@ -315,7 +316,7 @@ abstract class FirebaseController { IosNotificationSettings(sound: true, badge: true, alert: true)); _firebaseMessaging.onIosSettingsRegistered .listen((IosNotificationSettings settings) { - debugPrint("Settings registered: $settings"); + debugPrint('Settings registered: $settings'); }); } } diff --git a/lib/utils/matrix_file_extension.dart b/lib/utils/matrix_file_extension.dart index 1723f11..04cb7e4 100644 --- a/lib/utils/matrix_file_extension.dart +++ b/lib/utils/matrix_file_extension.dart @@ -15,8 +15,8 @@ extension MatrixFileExtension on MatrixFile { var element = html.document.createElement('a'); element.setAttribute( 'href', html.Url.createObjectUrlFromBlob(html.Blob([bytes]))); - element.setAttribute('target', "_blank"); - element.setAttribute('rel', "noopener"); + element.setAttribute('target', '_blank'); + element.setAttribute('rel', 'noopener'); element.setAttribute('download', fileName); element.setAttribute('type', mimeType); element.style.display = 'none'; @@ -24,8 +24,8 @@ extension MatrixFileExtension on MatrixFile { element.click(); element.remove(); } else { - Directory tempDir = await getTemporaryDirectory(); - final file = File(tempDir.path + "/" + path.split("/").last); + var tempDir = await getTemporaryDirectory(); + final file = File(tempDir.path + '/' + path.split('/').last); file.writeAsBytesSync(bytes); await OpenFile.open(file.path); } diff --git a/lib/utils/string_color.dart b/lib/utils/string_color.dart index 7579809..983dcef 100644 --- a/lib/utils/string_color.dart +++ b/lib/utils/string_color.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; extension StringColor on String { Color get color { - double number = 0.0; - for (var i = 0; i < this.length; i++) { - number += this.codeUnitAt(i); + var number = 0.0; + for (var i = 0; i < length; i++) { + number += codeUnitAt(i); } number = (number % 10) * 25.5; return HSLColor.fromAHSL(1, number, 1, 0.35).toColor(); diff --git a/lib/utils/url_launcher.dart b/lib/utils/url_launcher.dart index f90d481..224d767 100644 --- a/lib/utils/url_launcher.dart +++ b/lib/utils/url_launcher.dart @@ -12,7 +12,7 @@ class UrlLauncher { const UrlLauncher(this.context, this.url); void launchUrl() { - if (url.startsWith("https://matrix.to/#/")) { + if (url.startsWith('https://matrix.to/#/')) { return openMatrixToUrl(); } launch(url); @@ -20,8 +20,8 @@ class UrlLauncher { void openMatrixToUrl() async { final matrix = Matrix.of(context); - final String identifier = url.replaceAll("https://matrix.to/#/", ""); - if (identifier.substring(0, 1) == "#") { + final identifier = url.replaceAll('https://matrix.to/#/', ''); + if (identifier.substring(0, 1) == '#') { final response = await SimpleDialogs(context).tryRequestWithLoadingDialog( matrix.client.joinRoomById( Uri.encodeComponent(identifier), @@ -30,13 +30,13 @@ class UrlLauncher { if (response == false) return; await Navigator.pushAndRemoveUntil( context, - AppRoute.defaultRoute(context, ChatView(response["room_id"])), + AppRoute.defaultRoute(context, ChatView(response['room_id'])), (r) => r.isFirst, ); - } else if (identifier.substring(0, 1) == "@") { - final User user = User( + } else if (identifier.substring(0, 1) == '@') { + final user = User( identifier, - room: Room(id: "", client: matrix.client), + room: Room(id: '', client: matrix.client), ); final String roomID = await SimpleDialogs(context) .tryRequestWithLoadingDialog(user.startDirectChat()); diff --git a/lib/views/app_info.dart b/lib/views/app_info.dart index e96cab4..579c9ef 100644 --- a/lib/views/app_info.dart +++ b/lib/views/app_info.dart @@ -1,4 +1,3 @@ -import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/components/adaptive_page_layout.dart'; import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -21,7 +20,7 @@ class AppInfoView extends StatelessWidget { class AppInfo extends StatelessWidget { @override Widget build(BuildContext context) { - Client client = Matrix.of(context).client; + var client = Matrix.of(context).client; return Scaffold( appBar: AppBar( title: Text(L10n.of(context).accountInformations), @@ -29,43 +28,39 @@ class AppInfo extends StatelessWidget { body: ListView( children: [ ListTile( - title: Text(L10n.of(context).yourOwnUsername + ":"), + title: Text(L10n.of(context).yourOwnUsername + ':'), subtitle: Text(client.userID), ), ListTile( - title: Text("Homeserver:"), + title: Text('Homeserver:'), subtitle: Text(client.homeserver), ), ListTile( - title: Text("Supported versions:"), - subtitle: Text(client.matrixVersions.toString()), - ), - ListTile( - title: Text("Device name:"), + title: Text('Device name:'), subtitle: Text(client.deviceName), ), ListTile( - title: Text("Device ID:"), + title: Text('Device ID:'), subtitle: Text(client.deviceID), ), ListTile( - title: Text("Encryption enabled:"), + title: Text('Encryption enabled:'), subtitle: Text(client.encryptionEnabled.toString()), ), if (client.encryptionEnabled) Column( children: [ ListTile( - title: Text("Your public fingerprint key:"), + title: Text('Your public fingerprint key:'), subtitle: Text(client.fingerprintKey.beautified), ), ListTile( - title: Text("Your public identity key:"), + title: Text('Your public identity key:'), subtitle: Text(client.identityKey.beautified), ), ListTile( - title: Text("LibOlm version:"), - subtitle: Text(olm.get_library_version().join(".")), + title: Text('LibOlm version:'), + subtitle: Text(olm.get_library_version().join('.')), ), ], ), diff --git a/lib/views/archive.dart b/lib/views/archive.dart index 74686cf..03a1fd5 100644 --- a/lib/views/archive.dart +++ b/lib/views/archive.dart @@ -44,7 +44,7 @@ class _ArchiveState extends State { ), secondScaffold: Scaffold( body: Center( - child: Image.asset("assets/logo.png", width: 100, height: 100), + child: Image.asset('assets/logo.png', width: 100, height: 100), ), ), primaryPage: FocusPage.FIRST, diff --git a/lib/views/auth_web_view.dart b/lib/views/auth_web_view.dart index 29b10a1..56132c8 100644 --- a/lib/views/auth_web_view.dart +++ b/lib/views/auth_web_view.dart @@ -14,8 +14,9 @@ class AuthWebView extends StatelessWidget { @override Widget build(BuildContext context) { - final String url = Matrix.of(context).client.homeserver + - "/_matrix/client/r0/auth/$authType/fallback/web?session=$session"; + final url = + '/_matrix/client/r0/auth/$authType/fallback/web?session=$session' + + Matrix.of(context).client.homeserver; if (kIsWeb) launch(url); return Scaffold( appBar: AppBar( diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 9f81649..11bc6cc 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -55,7 +55,7 @@ class _ChatState extends State<_Chat> { MatrixState matrix; - String seenByText = ""; + String seenByText = ''; final ScrollController _scrollController = ScrollController(); @@ -77,15 +77,19 @@ class _ChatState extends State<_Chat> { final int _loadHistoryCount = 100; - String inputText = ""; + String inputText = ''; bool get _canLoadMore => timeline.events.last.type != EventTypes.RoomCreate; void requestHistory() async { if (_canLoadMore) { - setState(() => this._loadingHistory = true); - await timeline.requestHistory(historyCount: _loadHistoryCount); - if (mounted) setState(() => this._loadingHistory = false); + setState(() => _loadingHistory = true); + try { + await timeline.requestHistory(historyCount: _loadHistoryCount); + } catch (e) { + debugPrint('Error loading history: ' + e.toString()); + } + if (mounted) setState(() => _loadingHistory = false); } } @@ -114,9 +118,9 @@ class _ChatState extends State<_Chat> { void updateView() { if (!mounted) return; - String seenByText = ""; + var seenByText = ''; if (timeline.events.isNotEmpty) { - List lastReceipts = List.from(timeline.events.first.receipts); + var lastReceipts = List.from(timeline.events.first.receipts); lastReceipts.removeWhere((r) => r.user.id == room.client.userID || r.user.id == timeline.events.first.senderId); @@ -147,7 +151,7 @@ class _ChatState extends State<_Chat> { unawaited(room.sendReadReceipt(timeline.events.first.eventId)); } if (timeline.events.length < _loadHistoryCount) { - this.requestHistory(); + requestHistory(); } } updateView(); @@ -158,7 +162,7 @@ class _ChatState extends State<_Chat> { void dispose() { timeline?.cancelSubscriptions(); timeline = null; - matrix.activeRoomId = ""; + matrix.activeRoomId = ''; super.dispose(); } @@ -167,12 +171,12 @@ class _ChatState extends State<_Chat> { void send() { if (sendController.text.isEmpty) return; room.sendTextEvent(sendController.text, inReplyTo: replyEvent); - sendController.text = ""; + sendController.text = ''; if (replyEvent != null) { setState(() => replyEvent = null); } - setState(() => inputText = ""); + setState(() => inputText = ''); } void sendFileAction(BuildContext context) async { @@ -180,7 +184,7 @@ class _ChatState extends State<_Chat> { BotToast.showText(text: L10n.of(context).notSupportedInWeb); return; } - File file = await FilePicker.getFile(); + var file = await FilePicker.getFile(); if (file == null) return; await SimpleDialogs(context).tryRequestWithLoadingDialog( room.sendFileEvent( @@ -194,7 +198,7 @@ class _ChatState extends State<_Chat> { BotToast.showText(text: L10n.of(context).notSupportedInWeb); return; } - File file = await ImagePicker.pickImage( + var file = await ImagePicker.pickImage( source: ImageSource.gallery, imageQuality: 50, maxWidth: 1600, @@ -212,7 +216,7 @@ class _ChatState extends State<_Chat> { BotToast.showText(text: L10n.of(context).notSupportedInWeb); return; } - File file = await ImagePicker.pickImage( + var file = await ImagePicker.pickImage( source: ImageSource.camera, imageQuality: 50, maxWidth: 1600, @@ -233,7 +237,7 @@ class _ChatState extends State<_Chat> { onFinished: (r) => result = r, )); if (result == null) return; - final File audioFile = File(result); + final audioFile = File(result); await SimpleDialogs(context).tryRequestWithLoadingDialog( room.sendAudioEvent( MatrixFile(bytes: audioFile.readAsBytesSync(), path: audioFile.path), @@ -242,12 +246,12 @@ class _ChatState extends State<_Chat> { } String _getSelectedEventString(BuildContext context) { - String copyString = ""; + var copyString = ''; if (selectedEvents.length == 1) { return selectedEvents.first.getLocalizedBody(L10n.of(context)); } - for (Event event in selectedEvents) { - if (copyString.isNotEmpty) copyString += "\n\n"; + for (var event in selectedEvents) { + if (copyString.isNotEmpty) copyString += '\n\n'; copyString += event.getLocalizedBody(L10n.of(context), withSenderNamePrefix: true); } @@ -260,12 +264,12 @@ class _ChatState extends State<_Chat> { } void redactEventsAction(BuildContext context) async { - bool confirmed = await SimpleDialogs(context).askConfirmation( + var confirmed = await SimpleDialogs(context).askConfirmation( titleText: L10n.of(context).messageWillBeRemovedWarning, confirmText: L10n.of(context).remove, ); if (!confirmed) return; - for (Event event in selectedEvents) { + for (var event in selectedEvents) { await SimpleDialogs(context).tryRequestWithLoadingDialog( event.status > 0 ? event.redact() : event.remove()); } @@ -273,7 +277,7 @@ class _ChatState extends State<_Chat> { } bool get canRedactSelectedEvents { - for (Event event in selectedEvents) { + for (var event in selectedEvents) { if (event.canRedact == false) return false; } return true; @@ -284,8 +288,8 @@ class _ChatState extends State<_Chat> { Matrix.of(context).shareContent = selectedEvents.first.content; } else { Matrix.of(context).shareContent = { - "msgtype": "m.text", - "body": _getSelectedEventString(context), + 'msgtype': 'm.text', + 'body': _getSelectedEventString(context), }; } setState(() => selectedEvents.clear()); @@ -308,7 +312,7 @@ class _ChatState extends State<_Chat> { @override Widget build(BuildContext context) { matrix = Matrix.of(context); - Client client = matrix.client; + var client = matrix.client; room ??= client.getRoomById(widget.id); if (room == null) { return Scaffold( @@ -326,8 +330,8 @@ class _ChatState extends State<_Chat> { SimpleDialogs(context).tryRequestWithLoadingDialog(room.join()); } - String typingText = ""; - List typingUsers = room.typingUsers; + var typingText = ''; + var typingUsers = room.typingUsers; typingUsers.removeWhere((User u) => u.id == client.userID); if (typingUsers.length == 1) { @@ -616,22 +620,22 @@ class _ChatState extends State<_Chat> { PopupMenuButton( icon: Icon(Icons.add), onSelected: (String choice) async { - if (choice == "file") { + if (choice == 'file') { sendFileAction(context); - } else if (choice == "image") { + } else if (choice == 'image') { sendImageAction(context); } - if (choice == "camera") { + if (choice == 'camera') { openCameraAction(context); } - if (choice == "voice") { + if (choice == 'voice') { voiceMessageAction(context); } }, itemBuilder: (BuildContext context) => >[ PopupMenuItem( - value: "file", + value: 'file', child: ListTile( leading: CircleAvatar( backgroundColor: Colors.green, @@ -644,7 +648,7 @@ class _ChatState extends State<_Chat> { ), ), PopupMenuItem( - value: "image", + value: 'image', child: ListTile( leading: CircleAvatar( backgroundColor: Colors.blue, @@ -657,7 +661,7 @@ class _ChatState extends State<_Chat> { ), ), PopupMenuItem( - value: "camera", + value: 'camera', child: ListTile( leading: CircleAvatar( backgroundColor: Colors.purple, @@ -670,7 +674,7 @@ class _ChatState extends State<_Chat> { ), ), PopupMenuItem( - value: "voice", + value: 'voice', child: ListTile( leading: CircleAvatar( backgroundColor: Colors.red, @@ -708,20 +712,20 @@ class _ChatState extends State<_Chat> { border: InputBorder.none, ), onChanged: (String text) { - this.typingCoolDown?.cancel(); - this.typingCoolDown = + typingCoolDown?.cancel(); + typingCoolDown = Timer(Duration(seconds: 2), () { - this.typingCoolDown = null; - this.currentlyTyping = false; + typingCoolDown = null; + currentlyTyping = false; room.sendTypingInfo(false); }); - this.typingTimeout ??= + typingTimeout ??= Timer(Duration(seconds: 30), () { - this.typingTimeout = null; - this.currentlyTyping = false; + typingTimeout = null; + currentlyTyping = false; }); - if (!this.currentlyTyping) { - this.currentlyTyping = true; + if (!currentlyTyping) { + currentlyTyping = true; room.sendTypingInfo(true, timeout: Duration(seconds: 30) .inMilliseconds); diff --git a/lib/views/chat_details.dart b/lib/views/chat_details.dart index c43fddb..b02a995 100644 --- a/lib/views/chat_details.dart +++ b/lib/views/chat_details.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/components/adaptive_page_layout.dart'; import 'package:fluffychat/components/chat_settings_popup_menu.dart'; @@ -30,11 +28,12 @@ class ChatDetails extends StatefulWidget { class _ChatDetailsState extends State { List members; void setDisplaynameAction(BuildContext context) async { - final String displayname = await SimpleDialogs(context).enterText( + var enterText = SimpleDialogs(context).enterText( titleText: L10n.of(context).changeTheNameOfTheGroup, labelText: L10n.of(context).changeTheNameOfTheGroup, hintText: widget.room.getLocalizedDisplayname(L10n.of(context)), ); + final displayname = await enterText; if (displayname == null) return; final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( widget.room.setName(displayname), @@ -45,26 +44,26 @@ class _ChatDetailsState extends State { } void setCanonicalAliasAction(context) async { - final String s = await SimpleDialogs(context).enterText( + final s = await SimpleDialogs(context).enterText( titleText: L10n.of(context).setInvitationLink, labelText: L10n.of(context).setInvitationLink, hintText: L10n.of(context).alias.toLowerCase(), - prefixText: "#", - suffixText: ":" + widget.room.client.userID.domain, + prefixText: '#', + suffixText: ':' + widget.room.client.userID.domain, ); if (s == null) return; - final String domain = widget.room.client.userID.domain; - final String canonicalAlias = "%23" + s + "%3A" + domain; - final Event aliasEvent = widget.room.getState("m.room.aliases", domain); - final List aliases = - aliasEvent != null ? aliasEvent.content["aliases"] ?? [] : []; + final domain = widget.room.client.userID.domain; + final canonicalAlias = '%23' + s + '%3A' + domain; + final aliasEvent = widget.room.getState('m.room.aliases', domain); + final aliases = + aliasEvent != null ? aliasEvent.content['aliases'] ?? [] : []; if (aliases.indexWhere((s) => s == canonicalAlias) == -1) { - List newAliases = List.from(aliases); + var newAliases = List.from(aliases); newAliases.add(canonicalAlias); final response = await SimpleDialogs(context).tryRequestWithLoadingDialog( widget.room.client.jsonRequest( type: HTTPType.GET, - action: "/client/r0/directory/room/$canonicalAlias", + action: '/client/r0/directory/room/$canonicalAlias', ), ); if (response == false) { @@ -72,8 +71,8 @@ class _ChatDetailsState extends State { await SimpleDialogs(context).tryRequestWithLoadingDialog( widget.room.client.jsonRequest( type: HTTPType.PUT, - action: "/client/r0/directory/room/$canonicalAlias", - data: {"room_id": widget.room.id}), + action: '/client/r0/directory/room/$canonicalAlias', + data: {'room_id': widget.room.id}), ); if (success == false) return; } @@ -82,13 +81,13 @@ class _ChatDetailsState extends State { widget.room.client.jsonRequest( type: HTTPType.PUT, action: - "/client/r0/rooms/${widget.room.id}/state/m.room.canonical_alias", - data: {"alias": "#$s:$domain"}), + '/client/r0/rooms/${widget.room.id}/state/m.room.canonical_alias', + data: {'alias': '#$s:$domain'}), ); } void setTopicAction(BuildContext context) async { - final String displayname = await SimpleDialogs(context).enterText( + final displayname = await SimpleDialogs(context).enterText( titleText: L10n.of(context).setGroupDescription, labelText: L10n.of(context).setGroupDescription, hintText: (widget.room.topic?.isNotEmpty ?? false) @@ -106,7 +105,7 @@ class _ChatDetailsState extends State { } void setAvatarAction(BuildContext context) async { - final File tempFile = await ImagePicker.pickImage( + final tempFile = await ImagePicker.pickImage( source: ImageSource.gallery, imageQuality: 50, maxWidth: 1600, @@ -145,9 +144,9 @@ class _ChatDetailsState extends State { } members ??= widget.room.getParticipants(); members.removeWhere((u) => u.membership == Membership.leave); - final int actualMembersCount = + final actualMembersCount = widget.room.mInvitedMemberCount + widget.room.mJoinedMemberCount; - final bool canRequestMoreMembers = members.length < actualMembersCount; + final canRequestMoreMembers = members.length < actualMembersCount; return AdaptivePageLayout( primaryPage: FocusPage.SECOND, firstScaffold: ChatList( @@ -189,7 +188,7 @@ class _ChatDetailsState extends State { backgroundColor: Theme.of(context).appBarTheme.color, flexibleSpace: FlexibleSpaceBar( background: ContentBanner(widget.room.avatar, - onEdit: widget.room.canSendEvent("m.room.avatar") && + onEdit: widget.room.canSendEvent('m.room.avatar') && !kIsWeb ? () => setAvatarAction(context) : null), @@ -204,7 +203,7 @@ class _ChatDetailsState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ListTile( - leading: widget.room.canSendEvent("m.room.topic") + leading: widget.room.canSendEvent('m.room.topic') ? CircleAvatar( backgroundColor: Theme.of(context) .scaffoldBackgroundColor, @@ -213,7 +212,7 @@ class _ChatDetailsState extends State { ) : null, title: Text( - "${L10n.of(context).groupDescription}:", + '${L10n.of(context).groupDescription}:', style: TextStyle( color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold)), @@ -230,7 +229,7 @@ class _ChatDetailsState extends State { .color, ), ), - onTap: widget.room.canSendEvent("m.room.topic") + onTap: widget.room.canSendEvent('m.room.topic') ? () => setTopicAction(context) : null, ), @@ -244,7 +243,7 @@ class _ChatDetailsState extends State { ), ), ), - if (widget.room.canSendEvent("m.room.name")) + if (widget.room.canSendEvent('m.room.name')) ListTile( leading: CircleAvatar( backgroundColor: @@ -259,7 +258,7 @@ class _ChatDetailsState extends State { onTap: () => setDisplaynameAction(context), ), if (widget.room - .canSendEvent("m.room.canonical_alias") && + .canSendEvent('m.room.canonical_alias') && widget.room.joinRules == JoinRules.public) ListTile( leading: CircleAvatar( diff --git a/lib/views/chat_encryption_settings.dart b/lib/views/chat_encryption_settings.dart index 8c9b76e..366d4ec 100644 --- a/lib/views/chat_encryption_settings.dart +++ b/lib/views/chat_encryption_settings.dart @@ -50,14 +50,14 @@ class _ChatEncryptionSettingsState extends State { if (snapshot.hasError) { return Center( child: Text(L10n.of(context).oopsSomethingWentWrong + - ": " + + ': ' + snapshot.error.toString()), ); } if (!snapshot.hasData) { return Center(child: CircularProgressIndicator()); } - final List deviceKeys = snapshot.data; + final deviceKeys = snapshot.data; return ListView.separated( separatorBuilder: (BuildContext context, int i) => Divider(height: 1), @@ -96,7 +96,7 @@ class _ChatEncryptionSettingsState extends State { ), subtitle: Text( deviceKeys[i] - .keys["ed25519:${deviceKeys[i].deviceId}"] + .keys['ed25519:${deviceKeys[i].deviceId}'] .beautified, style: TextStyle( color: diff --git a/lib/views/chat_list.dart b/lib/views/chat_list.dart index d623079..fcbddfb 100644 --- a/lib/views/chat_list.dart +++ b/lib/views/chat_list.dart @@ -35,7 +35,7 @@ class ChatListView extends StatelessWidget { firstScaffold: ChatList(), secondScaffold: Scaffold( body: Center( - child: Image.asset("assets/logo.png", width: 100, height: 100), + child: Image.asset('assets/logo.png', width: 100, height: 100), ), ), ); @@ -62,7 +62,7 @@ class _ChatListState extends State { final ScrollController _scrollController = ScrollController(); Future waitForFirstSync(BuildContext context) async { - Client client = Matrix.of(context).client; + var client = Matrix.of(context).client; if (client.prevBatch?.isEmpty ?? true) { await client.onFirstSync.stream.first; } @@ -106,7 +106,7 @@ class _ChatListState extends State { publicRoomsResponse = newPublicRoomsResponse; if (searchController.text.isNotEmpty && searchController.text.isValidMatrixId && - searchController.text.sigil == "#") { + searchController.text.sigil == '#') { publicRoomsResponse.publicRooms.add( PublicRoomEntry( aliases: [searchController.text], @@ -134,11 +134,11 @@ class _ChatListState extends State { if (Navigator.of(context).canPop()) { Navigator.of(context).popUntil((r) => r.isFirst); } - final File file = File(files.first.path); + final file = File(files.first.path); Matrix.of(context).shareContent = { - "msgtype": "chat.fluffy.shared_file", - "file": MatrixFile( + 'msgtype': 'chat.fluffy.shared_file', + 'file': MatrixFile( bytes: file.readAsBytesSync(), path: file.path, ), @@ -150,13 +150,13 @@ class _ChatListState extends State { if (Navigator.of(context).canPop()) { Navigator.of(context).popUntil((r) => r.isFirst); } - if (text.startsWith("https://matrix.to/#/")) { + if (text.startsWith('https://matrix.to/#/')) { UrlLauncher(context, text).openMatrixToUrl(); return; } Matrix.of(context).shareContent = { - "msgtype": "m.text", - "body": text, + 'msgtype': 'm.text', + 'body': text, }; } @@ -204,8 +204,8 @@ class _ChatListState extends State { action: '/client/r0/presence/${Matrix.of(context).client.userID}/status', data: { - "presence": "online", - "status_msg": status, + 'presence': 'online', + 'status_msg': status, }, ), ); @@ -288,7 +288,7 @@ class _ChatListState extends State { Navigator.of(context).pop(); Share.share(L10n.of(context).inviteText( Matrix.of(context).client.userID, - "https://matrix.to/#/${Matrix.of(context).client.userID}")); + 'https://matrix.to/#/${Matrix.of(context).client.userID}')); }, ), ], @@ -381,13 +381,13 @@ class _ChatListState extends State { future: waitForFirstSync(context), builder: (BuildContext context, snapshot) { if (snapshot.hasData) { - List rooms = List.from( + var rooms = List.from( Matrix.of(context).client.rooms); rooms.removeWhere((Room room) => searchMode && !room.displayname.toLowerCase().contains( searchController.text.toLowerCase() ?? - "")); + '')); if (rooms.isEmpty && (!searchMode || publicRoomsResponse == null)) { @@ -410,10 +410,10 @@ class _ChatListState extends State { ), ); } - final int publicRoomsCount = + final publicRoomsCount = (publicRoomsResponse?.publicRooms?.length ?? 0); - final int totalCount = + final totalCount = rooms.length + publicRoomsCount; return ListView.separated( controller: _scrollController, diff --git a/lib/views/homeserver_picker.dart b/lib/views/homeserver_picker.dart index 1fe645a..8f6dbec 100644 --- a/lib/views/homeserver_picker.dart +++ b/lib/views/homeserver_picker.dart @@ -8,7 +8,7 @@ import 'package:fluffychat/views/sign_up.dart'; import 'package:flutter/material.dart'; class HomeserverPicker extends StatelessWidget { - _setHomeserverAction(BuildContext context) async { + Future _setHomeserverAction(BuildContext context) async { final homeserver = await SimpleDialogs(context).enterText( titleText: L10n.of(context).enterYourHomeserver, hintText: Matrix.defaultHomeserver, @@ -17,7 +17,7 @@ class HomeserverPicker extends StatelessWidget { _checkHomeserverAction(homeserver, context); } - _checkHomeserverAction(String homeserver, BuildContext context) async { + void _checkHomeserverAction(String homeserver, BuildContext context) async { if (!homeserver.startsWith('https://')) { homeserver = 'https://$homeserver'; } @@ -40,7 +40,7 @@ class HomeserverPicker extends StatelessWidget { children: [ Hero( tag: 'loginBanner', - child: Image.asset("assets/fluffychat-banner.png"), + child: Image.asset('assets/fluffychat-banner.png'), ), Padding( padding: const EdgeInsets.all(16.0), diff --git a/lib/views/invitation_selection.dart b/lib/views/invitation_selection.dart index 4e52924..ab893bf 100644 --- a/lib/views/invitation_selection.dart +++ b/lib/views/invitation_selection.dart @@ -27,17 +27,18 @@ class _InvitationSelectionState extends State { Timer coolDown; Future> getContacts(BuildContext context) async { - final Client client = Matrix.of(context).client; - List participants = await widget.room.requestParticipants(); + var client2 = Matrix.of(context).client; + final client = client2; + var participants = await widget.room.requestParticipants(); participants.removeWhere( (u) => ![Membership.join, Membership.invite].contains(u.membership), ); - List contacts = []; - Map userMap = {}; - for (int i = 0; i < client.rooms.length; i++) { - List roomUsers = client.rooms[i].getParticipants(); + var contacts = []; + var userMap = {}; + for (var i = 0; i < client.rooms.length; i++) { + var roomUsers = client.rooms[i].getParticipants(); - for (int j = 0; j < roomUsers.length; j++) { + for (var j = 0; j < roomUsers.length; j++) { if (userMap[roomUsers[j].id] != true && participants.indexWhere((u) => u.id == roomUsers[j].id) == -1) { contacts.add(roomUsers[j]); @@ -81,41 +82,41 @@ class _InvitationSelectionState extends State { if (currentSearchTerm.isEmpty) return; if (loading) return; setState(() => loading = true); - final MatrixState matrix = Matrix.of(context); + final matrix = Matrix.of(context); final response = await SimpleDialogs(context).tryRequestWithErrorToast( matrix.client.jsonRequest( type: HTTPType.POST, - action: "/client/r0/user_directory/search", + action: '/client/r0/user_directory/search', data: { - "search_term": text, - "limit": 10, + 'search_term': text, + 'limit': 10, }), ); setState(() => loading = false); if (response == false || !(response is Map) || - (response["results"] == null)) return; + (response['results'] == null)) return; setState(() { - foundProfiles = List>.from(response["results"]); - if ("@$text".isValidMatrixId && + foundProfiles = List>.from(response['results']); + if ('@$text'.isValidMatrixId && foundProfiles - .indexWhere((profile) => "@$text" == profile["user_id"]) == + .indexWhere((profile) => '@$text' == profile['user_id']) == -1) { setState(() => foundProfiles = [ - {"user_id": "@$text"} + {'user_id': '@$text'} ]); } foundProfiles.removeWhere((profile) => widget.room .getParticipants() - .indexWhere((u) => u.id == profile["user_id"]) != + .indexWhere((u) => u.id == profile['user_id']) != -1); }); } @override Widget build(BuildContext context) { - final String groupName = widget.room.name?.isEmpty ?? false + final groupName = widget.room.name?.isEmpty ?? false ? L10n.of(context).group : widget.room.name; return AdaptivePageLayout( @@ -138,7 +139,7 @@ class _InvitationSelectionState extends State { onSubmitted: (String text) => searchUser(context, text), decoration: InputDecoration( border: OutlineInputBorder(), - prefixText: "@", + prefixText: '@', hintText: L10n.of(context).username, labelText: L10n.of(context).inviteContactToGroup(groupName), suffixIcon: loading @@ -159,19 +160,19 @@ class _InvitationSelectionState extends State { itemCount: foundProfiles.length, itemBuilder: (BuildContext context, int i) => ListTile( leading: Avatar( - foundProfiles[i]["avatar_url"] == null + foundProfiles[i]['avatar_url'] == null ? null - : Uri.parse(foundProfiles[i]["avatar_url"]), - foundProfiles[i]["display_name"] ?? - foundProfiles[i]["user_id"], + : Uri.parse(foundProfiles[i]['avatar_url']), + foundProfiles[i]['display_name'] ?? + foundProfiles[i]['user_id'], ), title: Text( - foundProfiles[i]["display_name"] ?? - (foundProfiles[i]["user_id"] as String).localpart, + foundProfiles[i]['display_name'] ?? + (foundProfiles[i]['user_id'] as String).localpart, ), - subtitle: Text(foundProfiles[i]["user_id"]), + subtitle: Text(foundProfiles[i]['user_id']), onTap: () => - inviteAction(context, foundProfiles[i]["user_id"]), + inviteAction(context, foundProfiles[i]['user_id']), ), ) : FutureBuilder>( @@ -182,7 +183,7 @@ class _InvitationSelectionState extends State { child: CircularProgressIndicator(), ); } - List contacts = snapshot.data; + var contacts = snapshot.data; return ListView.builder( itemCount: contacts.length, itemBuilder: (BuildContext context, int i) => ListTile( diff --git a/lib/views/login.dart b/lib/views/login.dart index 79152c8..8b49e7e 100644 --- a/lib/views/login.dart +++ b/lib/views/login.dart @@ -24,7 +24,7 @@ class _LoginState extends State { bool showPassword = false; void login(BuildContext context) async { - MatrixState matrix = Matrix.of(context); + var matrix = Matrix.of(context); if (usernameController.text.isEmpty) { setState(() => usernameError = L10n.of(context).pleaseEnterYourUsername); } else { @@ -101,7 +101,7 @@ class _LoginState extends State { controller: usernameController, decoration: InputDecoration( hintText: - "@${L10n.of(context).username.toLowerCase()}:domain", + '@${L10n.of(context).username.toLowerCase()}:domain', errorText: usernameError, labelText: L10n.of(context).username), ), @@ -120,7 +120,7 @@ class _LoginState extends State { obscureText: !showPassword, onSubmitted: (t) => login(context), decoration: InputDecoration( - hintText: "****", + hintText: '****', errorText: passwordError, suffixIcon: IconButton( icon: Icon(showPassword diff --git a/lib/views/new_group.dart b/lib/views/new_group.dart index 47c4f18..b367e39 100644 --- a/lib/views/new_group.dart +++ b/lib/views/new_group.dart @@ -31,18 +31,18 @@ class _NewGroupState extends State<_NewGroup> { bool publicGroup = false; void submitAction(BuildContext context) async { - final MatrixState matrix = Matrix.of(context); - Map params = {}; + final matrix = Matrix.of(context); + var params = {}; if (publicGroup) { - params["preset"] = "public_chat"; - params["visibility"] = "public"; + params['preset'] = 'public_chat'; + params['visibility'] = 'public'; if (controller.text.isNotEmpty) { - params["room_alias_name"] = controller.text; + params['room_alias_name'] = controller.text; } } else { - params["preset"] = "private_chat"; + params['preset'] = 'private_chat'; } - if (controller.text.isNotEmpty) params["name"] = controller.text; + if (controller.text.isNotEmpty) params['name'] = controller.text; final String roomID = await SimpleDialogs(context).tryRequestWithLoadingDialog( matrix.client.createRoom(params: params), @@ -99,7 +99,7 @@ class _NewGroupState extends State<_NewGroup> { onChanged: (bool b) => setState(() => publicGroup = b), ), Expanded( - child: Image.asset("assets/new_group_wallpaper.png"), + child: Image.asset('assets/new_group_wallpaper.png'), ), ], ), diff --git a/lib/views/new_private_chat.dart b/lib/views/new_private_chat.dart index 7aada65..46d5a08 100644 --- a/lib/views/new_private_chat.dart +++ b/lib/views/new_private_chat.dart @@ -37,23 +37,23 @@ class _NewPrivateChatState extends State<_NewPrivateChat> { List> foundProfiles = []; Timer coolDown; Map get foundProfile => foundProfiles.firstWhere( - (user) => user["user_id"] == "@$currentSearchTerm", + (user) => user['user_id'] == '@$currentSearchTerm', orElse: () => null); bool get correctMxId => foundProfiles - .indexWhere((user) => user["user_id"] == "@$currentSearchTerm") != + .indexWhere((user) => user['user_id'] == '@$currentSearchTerm') != -1; void submitAction(BuildContext context) async { if (controller.text.isEmpty) return; if (!_formKey.currentState.validate()) return; - final MatrixState matrix = Matrix.of(context); + final matrix = Matrix.of(context); - if ("@" + controller.text.trim() == matrix.client.userID) return; + if ('@' + controller.text.trim() == matrix.client.userID) return; - final User user = User( - "@" + controller.text.trim(), - room: Room(id: "", client: matrix.client), + final user = User( + '@' + controller.text.trim(), + room: Room(id: '', client: matrix.client), ); final String roomID = await SimpleDialogs(context) .tryRequestWithLoadingDialog(user.startDirectChat()); @@ -87,22 +87,22 @@ class _NewPrivateChatState extends State<_NewPrivateChat> { if (currentSearchTerm.isEmpty) return; if (loading) return; setState(() => loading = true); - final MatrixState matrix = Matrix.of(context); + final matrix = Matrix.of(context); final response = await SimpleDialogs(context).tryRequestWithErrorToast( matrix.client.jsonRequest( type: HTTPType.POST, - action: "/client/r0/user_directory/search", + action: '/client/r0/user_directory/search', data: { - "search_term": text, - "limit": 10, + 'search_term': text, + 'limit': 10, }), ); setState(() => loading = false); if (response == false || !(response is Map) || - (response["results"]?.isEmpty ?? true)) return; + (response['results']?.isEmpty ?? true)) return; setState(() { - foundProfiles = List>.from(response["results"]); + foundProfiles = List>.from(response['results']); }); } @@ -131,15 +131,15 @@ class _NewPrivateChatState extends State<_NewPrivateChat> { if (value.isEmpty) { return L10n.of(context).pleaseEnterAMatrixIdentifier; } - final MatrixState matrix = Matrix.of(context); - String mxid = "@" + controller.text.trim(); + final matrix = Matrix.of(context); + var mxid = '@' + controller.text.trim(); if (mxid == matrix.client.userID) { return L10n.of(context).youCannotInviteYourself; } - if (!mxid.contains("@")) { + if (!mxid.contains('@')) { return L10n.of(context).makeSureTheIdentifierIsValid; } - if (!mxid.contains(":")) { + if (!mxid.contains(':')) { return L10n.of(context).makeSureTheIdentifierIsValid; } return null; @@ -158,17 +158,17 @@ class _NewPrivateChatState extends State<_NewPrivateChat> { ? Padding( padding: const EdgeInsets.all(8.0), child: Avatar( - foundProfile["avatar_url"] == null + foundProfile['avatar_url'] == null ? null - : Uri.parse(foundProfile["avatar_url"]), - foundProfile["display_name"] ?? - foundProfile["user_id"], + : Uri.parse(foundProfile['avatar_url']), + foundProfile['display_name'] ?? + foundProfile['user_id'], size: 12, ), ) : Icon(Icons.account_circle), - prefixText: "@", - hintText: "${L10n.of(context).username.toLowerCase()}", + prefixText: '@', + hintText: '${L10n.of(context).username.toLowerCase()}', ), ), ), @@ -179,29 +179,29 @@ class _NewPrivateChatState extends State<_NewPrivateChat> { child: ListView.builder( itemCount: foundProfiles.length, itemBuilder: (BuildContext context, int i) { - Map foundProfile = foundProfiles[i]; + var foundProfile = foundProfiles[i]; return ListTile( onTap: () { setState(() { controller.text = currentSearchTerm = - foundProfile["user_id"].substring(1); + foundProfile['user_id'].substring(1); }); }, leading: Avatar( - foundProfile["avatar_url"] == null + foundProfile['avatar_url'] == null ? null - : Uri.parse(foundProfile["avatar_url"]), - foundProfile["display_name"] ?? foundProfile["user_id"], + : Uri.parse(foundProfile['avatar_url']), + foundProfile['display_name'] ?? foundProfile['user_id'], //size: 24, ), title: Text( - foundProfile["display_name"] ?? - (foundProfile["user_id"] as String).localpart, + foundProfile['display_name'] ?? + (foundProfile['user_id'] as String).localpart, style: TextStyle(), maxLines: 1, ), subtitle: Text( - foundProfile["user_id"], + foundProfile['user_id'], maxLines: 1, style: TextStyle( fontSize: 12, @@ -219,9 +219,9 @@ class _NewPrivateChatState extends State<_NewPrivateChat> { ), onTap: () => Share.share(L10n.of(context).inviteText( Matrix.of(context).client.userID, - "https://matrix.to/#/${Matrix.of(context).client.userID}")), + 'https://matrix.to/#/${Matrix.of(context).client.userID}')), title: Text( - "${L10n.of(context).yourOwnUsername}:", + '${L10n.of(context).yourOwnUsername}:', style: TextStyle( fontStyle: FontStyle.italic, ), @@ -237,7 +237,7 @@ class _NewPrivateChatState extends State<_NewPrivateChat> { Divider(height: 1), if (foundProfiles.isEmpty || correctMxId) Expanded( - child: Image.asset("assets/private_chat_wallpaper.png"), + child: Image.asset('assets/private_chat_wallpaper.png'), ), ], ), diff --git a/lib/views/settings.dart b/lib/views/settings.dart index fcba257..4ba98ad 100644 --- a/lib/views/settings.dart +++ b/lib/views/settings.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/components/settings_themes.dart'; import 'package:fluffychat/views/settings_devices.dart'; @@ -42,7 +40,7 @@ class _SettingsState extends State { if (await SimpleDialogs(context).askConfirmation() == false) { return; } - MatrixState matrix = Matrix.of(context); + var matrix = Matrix.of(context); await SimpleDialogs(context) .tryRequestWithLoadingDialog(matrix.client.logout()); } @@ -57,20 +55,20 @@ class _SettingsState extends State { if (!jitsi.endsWith('/')) { jitsi += '/'; } - final MatrixState matrix = Matrix.of(context); - await matrix.client.storeAPI.setItem('chat.fluffy.jitsi_instance', jitsi); + final matrix = Matrix.of(context); + await matrix.store.setItem('chat.fluffy.jitsi_instance', jitsi); matrix.jitsiInstance = jitsi; } void setDisplaynameAction(BuildContext context) async { - final String displayname = await SimpleDialogs(context).enterText( + final displayname = await SimpleDialogs(context).enterText( titleText: L10n.of(context).editDisplayname, hintText: profile?.displayname ?? Matrix.of(context).client.userID.localpart, labelText: L10n.of(context).enterAUsername, ); if (displayname == null) return; - final MatrixState matrix = Matrix.of(context); + final matrix = Matrix.of(context); final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( matrix.client.setDisplayname(displayname), ); @@ -83,13 +81,13 @@ class _SettingsState extends State { } void setAvatarAction(BuildContext context) async { - final File tempFile = await ImagePicker.pickImage( + final tempFile = await ImagePicker.pickImage( source: ImageSource.gallery, imageQuality: 50, maxWidth: 1600, maxHeight: 1600); if (tempFile == null) return; - final MatrixState matrix = Matrix.of(context); + final matrix = Matrix.of(context); final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( matrix.client.setAvatar( MatrixFile( @@ -111,24 +109,20 @@ class _SettingsState extends State { if (wallpaper == null) return; Matrix.of(context).wallpaper = wallpaper; await Matrix.of(context) - .client - .storeAPI - .setItem("chat.fluffy.wallpaper", wallpaper.path); + .store + .setItem('chat.fluffy.wallpaper', wallpaper.path); setState(() => null); } void deleteWallpaperAction(BuildContext context) async { Matrix.of(context).wallpaper = null; - await Matrix.of(context) - .client - .storeAPI - .setItem("chat.fluffy.wallpaper", null); + await Matrix.of(context).store.setItem('chat.fluffy.wallpaper', null); setState(() => null); } @override Widget build(BuildContext context) { - final Client client = Matrix.of(context).client; + final client = Matrix.of(context).client; profileFuture ??= client.ownProfile; profileFuture.then((p) { if (mounted) setState(() => profile = p); @@ -174,8 +168,9 @@ class _SettingsState extends State { ), ), ThemesSettings(), - if (!kIsWeb && client.storeAPI != null) Divider(thickness: 1), - if (!kIsWeb && client.storeAPI != null) + if (!kIsWeb && Matrix.of(context).store != null) + Divider(thickness: 1), + if (!kIsWeb && Matrix.of(context).store != null) ListTile( title: Text( L10n.of(context).wallpaper, @@ -198,7 +193,7 @@ class _SettingsState extends State { ), onTap: () => deleteWallpaperAction(context), ), - if (!kIsWeb && client.storeAPI != null) + if (!kIsWeb && Matrix.of(context).store != null) Builder(builder: (context) { return ListTile( title: Text(L10n.of(context).changeWallpaper), @@ -223,8 +218,9 @@ class _SettingsState extends State { activeColor: Theme.of(context).primaryColor, onChanged: (bool newValue) async { Matrix.of(context).renderHtml = newValue; - await client.storeAPI - .setItem("chat.fluffy.renderHtml", newValue ? "1" : "0"); + await Matrix.of(context) + .store + .setItem('chat.fluffy.renderHtml', newValue ? '1' : '0'); setState(() => null); }, ), @@ -300,19 +296,19 @@ class _SettingsState extends State { trailing: Icon(Icons.help), title: Text(L10n.of(context).help), onTap: () => launch( - "https://gitlab.com/ChristianPauly/fluffychat-flutter/issues"), + 'https://gitlab.com/ChristianPauly/fluffychat-flutter/issues'), ), ListTile( trailing: Icon(Icons.link), title: Text(L10n.of(context).license), onTap: () => launch( - "https://gitlab.com/ChristianPauly/fluffychat-flutter/raw/master/LICENSE"), + 'https://gitlab.com/ChristianPauly/fluffychat-flutter/raw/master/LICENSE'), ), ListTile( trailing: Icon(Icons.code), title: Text(L10n.of(context).sourceCode), onTap: () => launch( - "https://gitlab.com/ChristianPauly/fluffychat-flutter"), + 'https://gitlab.com/ChristianPauly/fluffychat-flutter'), ), ], ), diff --git a/lib/views/settings_devices.dart b/lib/views/settings_devices.dart index 3046260..da38173 100644 --- a/lib/views/settings_devices.dart +++ b/lib/views/settings_devices.dart @@ -37,18 +37,18 @@ class DevicesSettingsState extends State { void _removeDevicesAction( BuildContext context, List devices) async { if (await SimpleDialogs(context).askConfirmation() == false) return; - MatrixState matrix = Matrix.of(context); - List deviceIds = []; - for (UserDevice userDevice in devices) { + var matrix = Matrix.of(context); + var deviceIds = []; + for (var userDevice in devices) { deviceIds.add(userDevice.deviceId); } final success = await SimpleDialogs(context) .tryRequestWithLoadingDialog(matrix.client.deleteDevices(deviceIds), onAdditionalAuth: (MatrixException exception) async { - final String password = await SimpleDialogs(context).enterText( + final password = await SimpleDialogs(context).enterText( titleText: L10n.of(context).pleaseEnterYourPassword, labelText: L10n.of(context).pleaseEnterYourPassword, - hintText: "******", + hintText: '******', password: true); if (password == null) return; await matrix.client.deleteDevices(deviceIds, @@ -83,9 +83,8 @@ class DevicesSettingsState extends State { } Function isOwnDevice = (UserDevice userDevice) => userDevice.deviceId == Matrix.of(context).client.deviceID; - final List devices = List.from(this.devices); - UserDevice thisDevice = - devices.firstWhere(isOwnDevice, orElse: () => null); + final devices = List.from(this.devices); + var thisDevice = devices.firstWhere(isOwnDevice, orElse: () => null); devices.removeWhere(isOwnDevice); devices.sort((a, b) => b.lastSeenTs.compareTo(a.lastSeenTs)); return Column( @@ -145,13 +144,13 @@ class UserDeviceListItem extends StatelessWidget { Widget build(BuildContext context) { return PopupMenuButton( onSelected: (String action) { - if (action == "remove" && this.remove != null) { + if (action == 'remove' && remove != null) { remove(userDevice); } }, itemBuilder: (BuildContext context) => [ PopupMenuItem( - value: "remove", + value: 'remove', child: Text(L10n.of(context).removeDevice, style: TextStyle(color: Colors.red)), ), @@ -175,8 +174,8 @@ class UserDeviceListItem extends StatelessWidget { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("${L10n.of(context).id}: ${userDevice.deviceId}"), - Text("${L10n.of(context).lastSeenIp}: ${userDevice.lastSeenIp}"), + Text('${L10n.of(context).id}: ${userDevice.deviceId}'), + Text('${L10n.of(context).lastSeenIp}: ${userDevice.lastSeenIp}'), ], ), ), diff --git a/lib/views/settings_emotes.dart b/lib/views/settings_emotes.dart index 6b28909..ff590e5 100644 --- a/lib/views/settings_emotes.dart +++ b/lib/views/settings_emotes.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_advanced_networkimage/provider.dart'; @@ -55,7 +53,7 @@ class _EmotesSettingsState extends State { if (readonly) { return; } - debugPrint("Saving...."); + debugPrint('Saving....'); final client = Matrix.of(context).client; // be sure to preserve any data not in "short" Map content; @@ -95,7 +93,7 @@ class _EmotesSettingsState extends State { @override Widget build(BuildContext context) { - Client client = Matrix.of(context).client; + var client = Matrix.of(context).client; if (emotes == null) { emotes = <_EmoteEntry>[]; Map emoteSource; @@ -173,7 +171,7 @@ class _EmotesSettingsState extends State { size: 32.0, ), onTap: () async { - debugPrint("blah"); + debugPrint('blah'); if (newEmoteController.text == null || newEmoteController.text.isEmpty || newMxcController.text == null || @@ -374,7 +372,7 @@ class _EmoteImagePickerState extends State<_EmoteImagePicker> { BotToast.showText(text: L10n.of(context).notSupportedInWeb); return; } - File file = await ImagePicker.pickImage( + var file = await ImagePicker.pickImage( source: ImageSource.gallery, imageQuality: 50, maxWidth: 128, diff --git a/lib/views/sign_up.dart b/lib/views/sign_up.dart index 22299a1..54a3b4b 100644 --- a/lib/views/sign_up.dart +++ b/lib/views/sign_up.dart @@ -23,7 +23,7 @@ class _SignUpState extends State { File avatar; void setAvatarAction() async { - File file = await ImagePicker.pickImage( + var file = await ImagePicker.pickImage( source: ImageSource.gallery, maxHeight: 512, maxWidth: 512, @@ -33,7 +33,7 @@ class _SignUpState extends State { } void signUpAction(BuildContext context) async { - MatrixState matrix = Matrix.of(context); + var matrix = Matrix.of(context); if (usernameController.text.isEmpty) { setState(() => usernameError = L10n.of(context).pleaseChooseAUsername); } else { @@ -45,8 +45,8 @@ class _SignUpState extends State { } setState(() => loading = true); - final String preferredUsername = - usernameController.text.toLowerCase().replaceAll(" ", "-"); + final preferredUsername = + usernameController.text.toLowerCase().replaceAll(' ', '-'); try { await matrix.client.usernameAvailable(preferredUsername); @@ -83,7 +83,7 @@ class _SignUpState extends State { children: [ Hero( tag: 'loginBanner', - child: Image.asset("assets/fluffychat-banner.png"), + child: Image.asset('assets/fluffychat-banner.png'), ), ListTile( leading: CircleAvatar( diff --git a/lib/views/sign_up_password.dart b/lib/views/sign_up_password.dart index 1f16fbc..16fc5c8 100644 --- a/lib/views/sign_up_password.dart +++ b/lib/views/sign_up_password.dart @@ -27,7 +27,7 @@ class _SignUpPasswordState extends State { bool showPassword = true; void _signUpAction(BuildContext context, {Map auth}) async { - MatrixState matrix = Matrix.of(context); + var matrix = Matrix.of(context); if (passwordController.text.isEmpty) { setState(() => passwordError = L10n.of(context).pleaseEnterYourPassword); } else { @@ -40,8 +40,7 @@ class _SignUpPasswordState extends State { try { setState(() => loading = true); - Future waitForLogin = - matrix.client.onLoginStateChanged.stream.first; + var waitForLogin = matrix.client.onLoginStateChanged.stream.first; await matrix.client.register( username: widget.username, password: passwordController.text, @@ -51,21 +50,20 @@ class _SignUpPasswordState extends State { await waitForLogin; } on MatrixException catch (exception) { if (exception.requireAdditionalAuthentication) { - final List stages = exception.authenticationFlows - .firstWhere((a) => !a.stages.contains("m.login.email.identity")) + final stages = exception.authenticationFlows + .firstWhere((a) => !a.stages.contains('m.login.email.identity')) .stages; - final String currentStage = - exception.completedAuthenticationFlows == null - ? stages.first - : stages.firstWhere((stage) => - !exception.completedAuthenticationFlows.contains(stage) ?? - true); + final currentStage = exception.completedAuthenticationFlows == null + ? stages.first + : stages.firstWhere((stage) => + !exception.completedAuthenticationFlows.contains(stage) ?? + true); - if (currentStage == "m.login.dummy") { + if (currentStage == 'm.login.dummy') { _signUpAction(context, auth: { - "type": currentStage, - "session": exception.session, + 'type': currentStage, + 'session': exception.session, }); } else { await Navigator.of(context).push( @@ -75,7 +73,7 @@ class _SignUpPasswordState extends State { currentStage, exception.session, () => _signUpAction(context, auth: { - "session": exception.session, + 'session': exception.session, }), ), ), @@ -141,7 +139,7 @@ class _SignUpPasswordState extends State { autocorrect: false, onSubmitted: (t) => _signUpAction(context), decoration: InputDecoration( - hintText: "****", + hintText: '****', errorText: passwordError, suffixIcon: IconButton( icon: Icon( diff --git a/pubspec.lock b/pubspec.lock index 232d8f1..60278b5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "3.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.39.4" + version: "0.39.8" archive: dependency: transitive description: @@ -91,7 +91,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "0.13.6" + version: "0.13.9" crypto: dependency: transitive description: @@ -119,13 +119,22 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "1.3.3" + version: "1.3.6" + encrypted_moor: + dependency: "direct main" + description: + path: "extras/encryption" + ref: HEAD + resolved-ref: "6f930b011577e5bc8a5e5511691c8fcc43869a1c" + url: "https://github.com/simolus3/moor.git" + source: git + version: "1.0.0" famedlysdk: dependency: "direct main" description: path: "." - ref: "2525b3d9f156fa303ca9283a96fd8cf8db154dd9" - resolved-ref: "2525b3d9f156fa303ca9283a96fd8cf8db154dd9" + ref: "2455bac3bf8dab846ba453a6393f0be2c0b61001" + resolved-ref: "2455bac3bf8dab846ba453a6393f0be2c0b61001" url: "https://gitlab.com/famedly/famedlysdk.git" source: git version: "0.0.1" @@ -142,14 +151,14 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "1.4.3+2" + version: "1.9.0+1" firebase_messaging: dependency: "direct main" description: name: firebase_messaging url: "https://pub.dartlang.org" source: hosted - version: "6.0.13" + version: "6.0.15" flutter: dependency: "direct main" description: flutter @@ -161,14 +170,14 @@ packages: name: flutter_advanced_networkimage url: "https://pub.dartlang.org" source: hosted - version: "0.6.4" + version: "0.7.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons url: "https://pub.dartlang.org" source: hosted - version: "0.7.4" + version: "0.7.5" flutter_local_notifications: dependency: "direct main" description: @@ -195,13 +204,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.5" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.7" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage url: "https://pub.dartlang.org" source: hosted - version: "3.3.1+1" + version: "3.3.3" flutter_slidable: dependency: "direct main" description: @@ -281,7 +297,7 @@ packages: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.3" + version: "3.1.4" image: dependency: transitive description: @@ -295,7 +311,14 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.6.2+3" + version: "0.6.6+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" intl: dependency: "direct main" description: @@ -316,7 +339,7 @@ packages: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.3" + version: "0.3.4" js: dependency: transitive description: @@ -330,14 +353,14 @@ packages: name: link_text url: "https://pub.dartlang.org" source: hosted - version: "0.1.1" + version: "0.1.2" localstorage: dependency: "direct main" description: name: localstorage url: "https://pub.dartlang.org" source: hosted - version: "3.0.1+4" + version: "3.0.2+5" logging: dependency: transitive description: @@ -389,6 +412,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.3.0" + moor: + dependency: "direct main" + description: + name: moor + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" multi_server_socket: dependency: transitive description: @@ -402,14 +432,14 @@ packages: name: node_interop url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.1.1" node_io: dependency: transitive description: name: node_io url: "https://pub.dartlang.org" source: hosted - version: "1.0.1+2" + version: "1.1.1" node_preamble: dependency: transitive description: @@ -439,14 +469,7 @@ packages: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" - package_resolver: - dependency: transitive - description: - name: package_resolver - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.10" + version: "1.9.3" path: dependency: transitive description: @@ -474,14 +497,28 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.5.1" + version: "1.6.8" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" pedantic: dependency: "direct dev" description: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.8.0+1" + version: "1.9.0" petitparser: dependency: transitive description: @@ -509,7 +546,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" pointycastle: dependency: transitive description: @@ -530,7 +567,7 @@ packages: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "1.4.2" + version: "1.4.4" quiver: dependency: transitive description: @@ -538,20 +575,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" + random_string: + dependency: "direct main" + description: + name: random_string + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" receive_sharing_intent: dependency: "direct main" description: name: receive_sharing_intent url: "https://pub.dartlang.org" source: hosted - version: "1.3.3" + version: "1.4.0+2" share: dependency: "direct main" description: name: share url: "https://pub.dartlang.org" source: hosted - version: "0.6.3+5" + version: "0.6.4+2" shelf: dependency: transitive description: @@ -565,7 +609,7 @@ packages: name: shelf_packages_handler url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.0" shelf_static: dependency: transitive description: @@ -591,7 +635,7 @@ packages: name: source_map_stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.1.5" + version: "2.0.0" source_maps: dependency: transitive description: @@ -612,7 +656,21 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + sqflite_sqlcipher: + dependency: transitive + description: + name: sqflite_sqlcipher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0+6" stack_trace: dependency: transitive description: @@ -640,7 +698,7 @@ packages: name: synchronized url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.2.0" term_glyph: dependency: transitive description: @@ -654,7 +712,7 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.13.0" + version: "1.14.3" test_api: dependency: transitive description: @@ -668,7 +726,7 @@ packages: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.3.1" + version: "0.3.4" typed_data: dependency: transitive description: @@ -682,14 +740,14 @@ packages: name: universal_html url: "https://pub.dartlang.org" source: hosted - version: "1.1.12" + version: "1.2.2" universal_io: dependency: transitive description: name: universal_io url: "https://pub.dartlang.org" source: hosted - version: "0.8.6" + version: "1.0.1" unorm_dart: dependency: transitive description: @@ -703,28 +761,28 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.4.1" + version: "5.4.7" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+2" + version: "0.0.1+5" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.0.7" url_launcher_web: dependency: "direct main" description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.0+2" + version: "0.1.1+5" vector_math: dependency: transitive description: @@ -738,14 +796,14 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "2.3.1" + version: "4.0.4" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+13" + version: "0.9.7+15" web_socket_channel: dependency: transitive description: @@ -766,7 +824,7 @@ packages: name: webview_flutter url: "https://pub.dartlang.org" source: hosted - version: "0.3.19+9" + version: "0.3.21" xml: dependency: transitive description: @@ -780,7 +838,7 @@ packages: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.2.1" zone_local: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d06f67c..976a854 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: famedlysdk: git: url: https://gitlab.com/famedly/famedlysdk.git - ref: 2525b3d9f156fa303ca9283a96fd8cf8db154dd9 + ref: 2455bac3bf8dab846ba453a6393f0be2c0b61001 localstorage: ^3.0.1+4 bubble: ^1.1.9+1 @@ -55,18 +55,25 @@ dependencies: mime_type: ^0.3.0 bot_toast: ^3.0.0 flutter_matrix_html: ^0.0.5 + moor: ^3.0.2 + random_string: ^2.0.1 intl: ^0.16.0 intl_translation: ^0.17.9 flutter_localizations: sdk: flutter + encrypted_moor: + git: + url: https://github.com/simolus3/moor.git + path: extras/encryption + dev_dependencies: flutter_test: sdk: flutter flutter_launcher_icons: "^0.7.4" - pedantic: ^1.5.0 + pedantic: ^1.9.0 flutter_icons: android: "launcher_icon" diff --git a/web/index.html b/web/index.html index 7f6e5b6..01e44df 100644 --- a/web/index.html +++ b/web/index.html @@ -11,6 +11,7 @@ + diff --git a/web/sql-wasm.js b/web/sql-wasm.js new file mode 100644 index 0000000..f75abd4 --- /dev/null +++ b/web/sql-wasm.js @@ -0,0 +1,209 @@ + +// We are modularizing this manually because the current modularize setting in Emscripten has some issues: +// https://github.com/kripken/emscripten/issues/5820 +// In addition, When you use emcc's modularization, it still expects to export a global object called `Module`, +// which is able to be used/called before the WASM is loaded. +// The modularization below exports a promise that loads and resolves to the actual sql.js module. +// That way, this module can't be used before the WASM is finished loading. + +// We are going to define a function that a user will call to start loading initializing our Sql.js library +// However, that function might be called multiple times, and on subsequent calls, we don't actually want it to instantiate a new instance of the Module +// Instead, we want to return the previously loaded module + +// TODO: Make this not declare a global if used in the browser +var initSqlJsPromise = undefined; + +var initSqlJs = function (moduleConfig) { + + if (initSqlJsPromise){ + return initSqlJsPromise; + } + // If we're here, we've never called this function before + initSqlJsPromise = new Promise((resolveModule, reject) => { + + // We are modularizing this manually because the current modularize setting in Emscripten has some issues: + // https://github.com/kripken/emscripten/issues/5820 + + // The way to affect the loading of emcc compiled modules is to create a variable called `Module` and add + // properties to it, like `preRun`, `postRun`, etc + // We are using that to get notified when the WASM has finished loading. + // Only then will we return our promise + + // If they passed in a moduleConfig object, use that + // Otherwise, initialize Module to the empty object + var Module = typeof moduleConfig !== 'undefined' ? moduleConfig : {}; + + // EMCC only allows for a single onAbort function (not an array of functions) + // So if the user defined their own onAbort function, we remember it and call it + var originalOnAbortFunction = Module['onAbort']; + Module['onAbort'] = function (errorThatCausedAbort) { + reject(new Error(errorThatCausedAbort)); + if (originalOnAbortFunction){ + originalOnAbortFunction(errorThatCausedAbort); + } + }; + + Module['postRun'] = Module['postRun'] || []; + Module['postRun'].push(function () { + // When Emscripted calls postRun, this promise resolves with the built Module + resolveModule(Module); + }); + + // There is a section of code in the emcc-generated code below that looks like this: + // (Note that this is lowercase `module`) + // if (typeof module !== 'undefined') { + // module['exports'] = Module; + // } + // When that runs, it's going to overwrite our own modularization export efforts in shell-post.js! + // The only way to tell emcc not to emit it is to pass the MODULARIZE=1 or MODULARIZE_INSTANCE=1 flags, + // but that carries with it additional unnecessary baggage/bugs we don't want either. + // So, we have three options: + // 1) We undefine `module` + // 2) We remember what `module['exports']` was at the beginning of this function and we restore it later + // 3) We write a script to remove those lines of code as part of the Make process. + // + // Since those are the only lines of code that care about module, we will undefine it. It's the most straightforward + // of the options, and has the side effect of reducing emcc's efforts to modify the module if its output were to change in the future. + // That's a nice side effect since we're handling the modularization efforts ourselves + module = undefined; + + // The emcc-generated code and shell-post.js code goes below, + // meaning that all of it runs inside of this promise. If anything throws an exception, our promise will abort +var aa;var f;f||(f=typeof Module !== 'undefined' ? Module : {}); +var va=function(){var a;var b=h(4);var c={};var d=function(){function a(a,b){this.fb=a;this.db=b;this.nb=1;this.Eb=[]}a.prototype.bind=function(a){if(!this.fb)throw"Statement closed";this.reset();return Array.isArray(a)?this.lc(a):this.mc(a)};a.prototype.step=function(){var a;if(!this.fb)throw"Statement closed";this.nb=1;switch(a=Tb(this.fb)){case c.hc:return!0;case c.DONE:return!1;default:return this.db.handleError(a)}};a.prototype.sc=function(a){null==a&&(a=this.nb++);return Ub(this.fb,a)};a.prototype.tc= +function(a){null==a&&(a=this.nb++);return Vb(this.fb,a)};a.prototype.getBlob=function(a){var b;null==a&&(a=this.nb++);var c=Wb(this.fb,a);var d=Xb(this.fb,a);var e=new Uint8Array(c);for(a=b=0;0<=c?bc;a=0<=c?++b:--b)e[a]=l[d+a];return e};a.prototype.get=function(a){var b,d;null!=a&&this.bind(a)&&this.step();var e=[];a=b=0;for(d=ib(this.fb);0<=d?bd;a=0<=d?++b:--b)switch(Yb(this.fb,a)){case c.fc:case c.FLOAT:e.push(this.sc(a));break;case c.ic:e.push(this.tc(a));break;case c.Zb:e.push(this.getBlob(a)); +break;default:e.push(null)}return e};a.prototype.getColumnNames=function(){var a,b;var c=[];var d=a=0;for(b=ib(this.fb);0<=b?ab;d=0<=b?++a:--a)c.push(Zb(this.fb,d));return c};a.prototype.getAsObject=function(a){var b,c;var d=this.get(a);var e=this.getColumnNames();var g={};a=b=0;for(c=e.length;b>>0);if(null!=a){var c=this.filename,d=c?n("/",c):"/";c=ia(!0,!0);d=ja(d,(void 0!==c?c:438)&4095|32768,0);if(a){if("string"===typeof a){for(var e=Array(a.length),k=0,m=a.length;kc;e=0<=c?++g:--g){var m=q(d+4*e,"i32");var z=jc(m);e=function(){switch(!1){case 1!==z:return kc; +case 2!==z:return lc;case 3!==z:return mc;case 4!==z:return function(a){var b,c;var d=nc(a);var e=oc(a);a=new Uint8Array(d);for(b=c=0;0<=d?cd;b=0<=d?++c:--c)a[b]=l[e+b];return a};default:return function(){return null}}}();e=e(m);k.push(e)}if(c=b.apply(null,k))switch(typeof c){case "number":return pc(a,c);case "string":return qc(a,c,-1,-1)}else return rc(a)});this.handleError(sc(this.db,a,b.length,c.jc,0,d,0,0,0));return this};return a}();var g=f.cwrap("sqlite3_open","number",["string","number"]); +var k=f.cwrap("sqlite3_close_v2","number",["number"]);var m=f.cwrap("sqlite3_exec","number",["number","string","number","number","number"]);f.cwrap("sqlite3_free","",["number"]);var y=f.cwrap("sqlite3_changes","number",["number"]);var z=f.cwrap("sqlite3_prepare_v2","number",["number","string","number","number","number"]);var fa=f.cwrap("sqlite3_prepare_v2","number",["number","number","number","number","number"]);var ca=f.cwrap("sqlite3_bind_text","number",["number","number","number","number","number"]); +var Ia=f.cwrap("sqlite3_bind_blob","number",["number","number","number","number","number"]);var ac=f.cwrap("sqlite3_bind_double","number",["number","number","number"]);var $b=f.cwrap("sqlite3_bind_int","number",["number","number","number"]);var bc=f.cwrap("sqlite3_bind_parameter_index","number",["number","string"]);var Tb=f.cwrap("sqlite3_step","number",["number"]);var hc=f.cwrap("sqlite3_errmsg","string",["number"]);var ib=f.cwrap("sqlite3_data_count","number",["number"]);var Ub=f.cwrap("sqlite3_column_double", +"number",["number","number"]);var Vb=f.cwrap("sqlite3_column_text","string",["number","number"]);var Xb=f.cwrap("sqlite3_column_blob","number",["number","number"]);var Wb=f.cwrap("sqlite3_column_bytes","number",["number","number"]);var Yb=f.cwrap("sqlite3_column_type","number",["number","number"]);var Zb=f.cwrap("sqlite3_column_name","string",["number","number"]);var dc=f.cwrap("sqlite3_reset","number",["number"]);var cc=f.cwrap("sqlite3_clear_bindings","number",["number"]);var ec=f.cwrap("sqlite3_finalize", +"number",["number"]);var sc=f.cwrap("sqlite3_create_function_v2","number","number string number number number number number number number".split(" "));var jc=f.cwrap("sqlite3_value_type","number",["number"]);var nc=f.cwrap("sqlite3_value_bytes","number",["number"]);var mc=f.cwrap("sqlite3_value_text","string",["number"]);var kc=f.cwrap("sqlite3_value_int","number",["number"]);var oc=f.cwrap("sqlite3_value_blob","number",["number"]);var lc=f.cwrap("sqlite3_value_double","number",["number"]);var pc= +f.cwrap("sqlite3_result_double","",["number","number"]);var rc=f.cwrap("sqlite3_result_null","",["number"]);var qc=f.cwrap("sqlite3_result_text","",["number","string","number","number"]);var fc=f.cwrap("RegisterExtensionFunctions","number",["number"]);this.SQL={Database:e};for(a in this.SQL)f[a]=this.SQL[a];var da=0;c.xb=0;c.we=1;c.Pe=2;c.Ze=3;c.Cc=4;c.Ec=5;c.Se=6;c.NOMEM=7;c.bf=8;c.Qe=9;c.Re=10;c.Hc=11;c.NOTFOUND=12;c.Oe=13;c.Fc=14;c.$e=15;c.EMPTY=16;c.cf=17;c.df=18;c.Gc=19;c.Te=20;c.Ue=21;c.Ve= +22;c.Dc=23;c.Ne=24;c.af=25;c.We=26;c.Xe=27;c.ef=28;c.hc=100;c.DONE=101;c.fc=1;c.FLOAT=2;c.ic=3;c.Zb=4;c.Ye=5;c.jc=1}.bind(this);f.preRun=f.preRun||[];f.preRun.push(va);var wa={},u;for(u in f)f.hasOwnProperty(u)&&(wa[u]=f[u]);f.arguments=[];f.thisProgram="./this.program";f.quit=function(a,b){throw b;};f.preRun=[];f.postRun=[];var v=!1,w=!1,x=!1,xa=!1;v="object"===typeof window;w="function"===typeof importScripts;x="object"===typeof process&&"function"===typeof require&&!v&&!w;xa=!v&&!x&&!w;var A=""; +if(x){A=__dirname+"/";var ya,za;f.read=function(a,b){ya||(ya=require("fs"));za||(za=require("path"));a=za.normalize(a);a=ya.readFileSync(a);return b?a:a.toString()};f.readBinary=function(a){a=f.read(a,!0);a.buffer||(a=new Uint8Array(a));assert(a.buffer);return a};1>2];a=b+a+15&-16;if(a<=Da())D[Ca>>2]=a;else if(!Ea(a))return 0;return b} +var Fa={"f64-rem":function(a,b){return a%b},"debugger":function(){debugger}},Ga=1,E=Array(64);function ua(a){for(var b=0;64>b;b++)if(!E[b])return E[b]=a,Ga+b;throw"Finished up all reserved function pointers. Use a higher value for RESERVED_FUNCTION_POINTERS.";}"object"!==typeof WebAssembly&&C("no native wasm support detected"); +function q(a,b){b=b||"i8";"*"===b.charAt(b.length-1)&&(b="i32");switch(b){case "i1":return l[a>>0];case "i8":return l[a>>0];case "i16":return Ha[a>>1];case "i32":return D[a>>2];case "i64":return D[a>>2];case "float":return Ja[a>>2];case "double":return Ka[a>>3];default:B("invalid type for getValue: "+b)}return null}var La,Ma=!1;function assert(a,b){a||B("Assertion failed: "+b)}function Na(a){var b=f["_"+a];assert(b,"Cannot call unknown function "+a+", make sure it is exported");return b} +function Oa(a,b,c,d){var e={string:function(a){var b=0;if(null!==a&&void 0!==a&&0!==a){var c=(a.length<<2)+1;b=h(c);r(a,F,b,c)}return b},array:function(a){var b=h(a.length);l.set(a,b);return b}},g=Na(a),k=[];a=0;if(d)for(var m=0;m>0]=0;break;case "i8":l[a>>0]=0;break;case "i16":Ha[a>>1]=0;break;case "i32":D[a>>2]=0;break;case "i64":aa=[0,1<=+Pa(0)?~~+Qa(0)>>>0:0];D[a>>2]=aa[0];D[a+4>>2]=aa[1];break;case "float":Ja[a>>2]=0;break;case "double":Ka[a>>3]=0;break;default:B("invalid type for setValue: "+b)}}var Ra=0,Sa=3; +function ea(a){var b=Ra;if("number"===typeof a){var c=!0;var d=a}else c=!1,d=a.length;b=b==Sa?e:[Ta,h,Ba][b](Math.max(d,1));if(c){var e=b;assert(0==(b&3));for(a=b+(d&-4);e>2]=0;for(a=b+d;e>0]=0;return b}a.subarray||a.slice?F.set(a,b):F.set(new Uint8Array(a),b);return b}var Ua="undefined"!==typeof TextDecoder?new TextDecoder("utf8"):void 0; +function t(a,b,c){var d=b+c;for(c=b;a[c]&&!(c>=d);)++c;if(16e?d+=String.fromCharCode(e):(e-=65536,d+=String.fromCharCode(55296|e>>10,56320|e&1023))}}else d+=String.fromCharCode(e)}return d}function G(a){return a?t(F,a,void 0):""} +function r(a,b,c,d){if(!(0=k){var m=a.charCodeAt(++g);k=65536+((k&1023)<<10)|m&1023}if(127>=k){if(c>=d)break;b[c++]=k}else{if(2047>=k){if(c+1>=d)break;b[c++]=192|k>>6}else{if(65535>=k){if(c+2>=d)break;b[c++]=224|k>>12}else{if(c+3>=d)break;b[c++]=240|k>>18;b[c++]=128|k>>12&63}b[c++]=128|k>>6&63}b[c++]=128|k&63}}b[c]=0;return c-e} +function oa(a){for(var b=0,c=0;c=d&&(d=65536+((d&1023)<<10)|a.charCodeAt(++c)&1023);127>=d?++b:b=2047>=d?b+2:65535>=d?b+3:b+4}return b}"undefined"!==typeof TextDecoder&&new TextDecoder("utf-16le");function Va(a){return a.replace(/__Z[\w\d_]+/g,function(a){return a===a?a:a+" ["+a+"]"})}function Wa(a){0Ya&&C("TOTAL_MEMORY should be larger than TOTAL_STACK, was "+Ya+"! (TOTAL_STACK=5242880)"); +f.buffer?buffer=f.buffer:"object"===typeof WebAssembly&&"function"===typeof WebAssembly.Memory?(La=new WebAssembly.Memory({initial:Ya/65536}),buffer=La.buffer):buffer=new ArrayBuffer(Ya);Xa();D[Ca>>2]=5303264;function Za(a){for(;0>2];var c=D[b>>2]}else ob.rb=!0,J.USER=J.LOGNAME="web_user",J.PATH="/",J.PWD="/",J.HOME="/home/web_user",J.LANG="C.UTF-8",J._=f.thisProgram,c=db?Ta(1024):Ba(1024),b=db?Ta(256):Ba(256),D[b>>2]=c,D[a>>2]=b;a=[];var d=0,e;for(e in J)if("string"===typeof J[e]){var g=e+"="+J[e];a.push(g);d+=g.length}if(1024>0]=d.charCodeAt(m);l[k>>0]=0;D[b+ +4*e>>2]=c;c+=g.length+1}D[b+4*a.length>>2]=0}function pb(a){f.___errno_location&&(D[f.___errno_location()>>2]=a);return a}function qb(a,b){for(var c=0,d=a.length-1;0<=d;d--){var e=a[d];"."===e?a.splice(d,1):".."===e?(a.splice(d,1),c++):c&&(a.splice(d,1),c--)}if(b)for(;c;c--)a.unshift("..");return a}function rb(a){var b="/"===a.charAt(0),c="/"===a.substr(-1);(a=qb(a.split("/").filter(function(a){return!!a}),!b).join("/"))||b||(a=".");a&&c&&(a+="/");return(b?"/":"")+a} +function sb(a){var b=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/.exec(a).slice(1);a=b[0];b=b[1];if(!a&&!b)return".";b&&(b=b.substr(0,b.length-1));return a+b}function tb(a){if("/"===a)return"/";var b=a.lastIndexOf("/");return-1===b?a:a.substr(b+1)}function ub(){var a=Array.prototype.slice.call(arguments,0);return rb(a.join("/"))}function n(a,b){return rb(a+"/"+b)} +function vb(){for(var a="",b=!1,c=arguments.length-1;-1<=c&&!b;c--){b=0<=c?arguments[c]:"/";if("string"!==typeof b)throw new TypeError("Arguments to path.resolve must be strings");if(!b)return"";a=b+"/"+a;b="/"===b.charAt(0)}a=qb(a.split("/").filter(function(a){return!!a}),!b).join("/");return(b?"/":"")+a||"."}var wb=[];function xb(a,b){wb[a]={input:[],output:[],ub:b};yb(a,zb)} +var zb={open:function(a){var b=wb[a.node.rdev];if(!b)throw new K(L.Cb);a.tty=b;a.seekable=!1},close:function(a){a.tty.ub.flush(a.tty)},flush:function(a){a.tty.ub.flush(a.tty)},read:function(a,b,c,d){if(!a.tty||!a.tty.ub.Xb)throw new K(L.Ob);for(var e=0,g=0;g=b||(b=Math.max(b,c*(1048576>c?2:1.125)|0),0!=c&&(b=Math.max(b,256)),c=a.bb,a.bb=new Uint8Array(b),0b)a.bb.length=b;else for(;a.bb.length=a.node.gb)return 0;a=Math.min(a.node.gb-e,d);if(8b)throw new K(L.ib);return b},Pb:function(a,b,c){M.Tb(a.node,b+c);a.node.gb=Math.max(a.node.gb,b+c)},zb:function(a,b,c,d,e,g,k){if(32768!== +(a.node.mode&61440))throw new K(L.Cb);c=a.node.bb;if(k&2||c.buffer!==b&&c.buffer!==b.buffer){if(0>2)}catch(c){if(!c.code)throw c; +throw new K(L[c.code]);}return b.mode},kb:function(a){for(var b=[];a.parent!==a;)b.push(a.name),a=a.parent;b.push(a.jb.Hb.root);b.reverse();return ub.apply(null,b)},qc:function(a){a&=-2656257;var b=0,c;for(c in P.Ub)a&c&&(b|=P.Ub[c],a^=c);if(a)throw new K(L.ib);return b},ab:{lb:function(a){a=P.kb(a);try{var b=fs.lstatSync(a)}catch(c){if(!c.code)throw c;throw new K(L[c.code]);}P.yb&&!b.pb&&(b.pb=4096);P.yb&&!b.blocks&&(b.blocks=(b.size+b.pb-1)/b.pb|0);return{dev:b.dev,ino:b.ino,mode:b.mode,nlink:b.nlink, +uid:b.uid,gid:b.gid,rdev:b.rdev,size:b.size,atime:b.atime,mtime:b.mtime,ctime:b.ctime,pb:b.pb,blocks:b.blocks}},hb:function(a,b){var c=P.kb(a);try{void 0!==b.mode&&(fs.chmodSync(c,b.mode),a.mode=b.mode),void 0!==b.size&&fs.truncateSync(c,b.size)}catch(d){if(!d.code)throw d;throw new K(L[d.code]);}},lookup:function(a,b){var c=n(P.kb(a),b);c=P.Wb(c);return P.createNode(a,b,c)},vb:function(a,b,c,d){a=P.createNode(a,b,c,d);b=P.kb(a);try{N(a.mode)?fs.mkdirSync(b,a.mode):fs.writeFileSync(b,"",{mode:a.mode})}catch(e){if(!e.code)throw e; +throw new K(L[e.code]);}return a},rename:function(a,b,c){a=P.kb(a);b=n(P.kb(b),c);try{fs.renameSync(a,b)}catch(d){if(!d.code)throw d;throw new K(L[d.code]);}},unlink:function(a,b){a=n(P.kb(a),b);try{fs.unlinkSync(a)}catch(c){if(!c.code)throw c;throw new K(L[c.code]);}},rmdir:function(a,b){a=n(P.kb(a),b);try{fs.rmdirSync(a)}catch(c){if(!c.code)throw c;throw new K(L[c.code]);}},readdir:function(a){a=P.kb(a);try{return fs.readdirSync(a)}catch(b){if(!b.code)throw b;throw new K(L[b.code]);}},symlink:function(a, +b,c){a=n(P.kb(a),b);try{fs.symlinkSync(c,a)}catch(d){if(!d.code)throw d;throw new K(L[d.code]);}},readlink:function(a){var b=P.kb(a);try{return b=fs.readlinkSync(b),b=Fb.relative(Fb.resolve(a.jb.Hb.root),b)}catch(c){if(!c.code)throw c;throw new K(L[c.code]);}}},cb:{open:function(a){var b=P.kb(a.node);try{32768===(a.node.mode&61440)&&(a.wb=fs.openSync(b,P.qc(a.flags)))}catch(c){if(!c.code)throw c;throw new K(L[c.code]);}},close:function(a){try{32768===(a.node.mode&61440)&&a.wb&&fs.closeSync(a.wb)}catch(b){if(!b.code)throw b; +throw new K(L[b.code]);}},read:function(a,b,c,d,e){if(0===d)return 0;try{return fs.readSync(a.wb,P.Rb(b.buffer),c,d,e)}catch(g){throw new K(L[g.code]);}},write:function(a,b,c,d,e){try{return fs.writeSync(a.wb,P.Rb(b.buffer),c,d,e)}catch(g){throw new K(L[g.code]);}},ob:function(a,b,c){if(1===c)b+=a.position;else if(2===c&&32768===(a.node.mode&61440))try{b+=fs.fstatSync(a.wb).size}catch(d){throw new K(L[d.code]);}if(0>b)throw new K(L.ib);return b}}},Gb=null,Hb={},Q=[],Ib=1,R=null,Jb=!0,S={},K=null, +Eb={};function T(a,b){a=vb("/",a);b=b||{};if(!a)return{path:"",node:null};var c={Vb:!0,Jb:0},d;for(d in c)void 0===b[d]&&(b[d]=c[d]);if(8>>0)%R.length}function Nb(a){var b=Mb(a.parent.id,a.name);a.tb=R[b];R[b]=a}function Ob(a){var b=Mb(a.parent.id,a.name);if(R[b]===a)R[b]=a.tb;else for(b=R[b];b;){if(b.tb===a){b.tb=a.tb;break}b=b.tb}} +function O(a,b){var c;if(c=(c=Pb(a,"x"))?c:a.ab.lookup?0:13)throw new K(c,a);for(c=R[Mb(a.id,b)];c;c=c.tb){var d=c.name;if(c.parent.id===a.id&&d===b)return c}return a.ab.lookup(a,b)} +function Db(a,b,c,d){Qb||(Qb=function(a,b,c,d){a||(a=this);this.parent=a;this.jb=a.jb;this.sb=null;this.id=Ib++;this.name=b;this.mode=c;this.ab={};this.cb={};this.rdev=d},Qb.prototype={},Object.defineProperties(Qb.prototype,{read:{get:function(){return 365===(this.mode&365)},set:function(a){a?this.mode|=365:this.mode&=-366}},write:{get:function(){return 146===(this.mode&146)},set:function(a){a?this.mode|=146:this.mode&=-147}}}));a=new Qb(a,b,c,d);Nb(a);return a} +function N(a){return 16384===(a&61440)}var Rb={r:0,rs:1052672,"r+":2,w:577,wx:705,xw:705,"w+":578,"wx+":706,"xw+":706,a:1089,ax:1217,xa:1217,"a+":1090,"ax+":1218,"xa+":1218};function ic(a){var b=["r","w","rw"][a&3];a&512&&(b+="w");return b}function Pb(a,b){if(Jb)return 0;if(-1===b.indexOf("r")||a.mode&292){if(-1!==b.indexOf("w")&&!(a.mode&146)||-1!==b.indexOf("x")&&!(a.mode&73))return 13}else return 13;return 0}function tc(a,b){try{return O(a,b),17}catch(c){}return Pb(a,"wx")} +function uc(a,b,c){try{var d=O(a,b)}catch(e){return e.eb}if(a=Pb(a,"wx"))return a;if(c){if(!N(d.mode))return 20;if(d===d.parent||"/"===Lb(d))return 16}else if(N(d.mode))return 21;return 0}function vc(a){var b=4096;for(a=a||0;a<=b;a++)if(!Q[a])return a;throw new K(24);} +function wc(a,b){xc||(xc=function(){},xc.prototype={},Object.defineProperties(xc.prototype,{object:{get:function(){return this.node},set:function(a){this.node=a}}}));var c=new xc,d;for(d in a)c[d]=a[d];a=c;b=vc(b);a.fd=b;return Q[b]=a}var Cb={open:function(a){a.cb=Hb[a.node.rdev].cb;a.cb.open&&a.cb.open(a)},ob:function(){throw new K(29);}};function yb(a,b){Hb[a]={cb:b}} +function yc(a,b){var c="/"===b,d=!b;if(c&&Gb)throw new K(16);if(!c&&!d){var e=T(b,{Vb:!1});b=e.path;e=e.node;if(e.sb)throw new K(16);if(!N(e.mode))throw new K(20);}b={type:a,Hb:{},Yb:b,wc:[]};a=a.jb(b);a.jb=b;b.root=a;c?Gb=a:e&&(e.sb=b,e.jb&&e.jb.wc.push(b))}function ja(a,b,c){var d=T(a,{parent:!0}).node;a=tb(a);if(!a||"."===a||".."===a)throw new K(22);var e=tc(d,a);if(e)throw new K(e);if(!d.ab.vb)throw new K(1);return d.ab.vb(d,a,b,c)}function U(a,b){ja(a,(void 0!==b?b:511)&1023|16384,0)} +function zc(a,b,c){"undefined"===typeof c&&(c=b,b=438);ja(a,b|8192,c)}function Ac(a,b){if(!vb(a))throw new K(2);var c=T(b,{parent:!0}).node;if(!c)throw new K(2);b=tb(b);var d=tc(c,b);if(d)throw new K(d);if(!c.ab.symlink)throw new K(1);c.ab.symlink(c,b,a)} +function ta(a){var b=T(a,{parent:!0}).node,c=tb(a),d=O(b,c),e=uc(b,c,!1);if(e)throw new K(e);if(!b.ab.unlink)throw new K(1);if(d.sb)throw new K(16);try{S.willDeletePath&&S.willDeletePath(a)}catch(g){console.log("FS.trackingDelegate['willDeletePath']('"+a+"') threw an exception: "+g.message)}b.ab.unlink(b,c);Ob(d);try{if(S.onDeletePath)S.onDeletePath(a)}catch(g){console.log("FS.trackingDelegate['onDeletePath']('"+a+"') threw an exception: "+g.message)}} +function Kb(a){a=T(a).node;if(!a)throw new K(2);if(!a.ab.readlink)throw new K(22);return vb(Lb(a.parent),a.ab.readlink(a))}function ra(a,b){a=T(a,{qb:!b}).node;if(!a)throw new K(2);if(!a.ab.lb)throw new K(1);return a.ab.lb(a)}function Bc(a){return ra(a,!0)}function ka(a,b){var c;"string"===typeof a?c=T(a,{qb:!0}).node:c=a;if(!c.ab.hb)throw new K(1);c.ab.hb(c,{mode:b&4095|c.mode&-4096,timestamp:Date.now()})} +function Cc(a){var b;"string"===typeof a?b=T(a,{qb:!0}).node:b=a;if(!b.ab.hb)throw new K(1);b.ab.hb(b,{timestamp:Date.now()})}function Dc(a,b){if(0>b)throw new K(22);var c;"string"===typeof a?c=T(a,{qb:!0}).node:c=a;if(!c.ab.hb)throw new K(1);if(N(c.mode))throw new K(21);if(32768!==(c.mode&61440))throw new K(22);if(a=Pb(c,"w"))throw new K(a);c.ab.hb(c,{size:b,timestamp:Date.now()})} +function p(a,b,c,d){if(""===a)throw new K(2);if("string"===typeof b){var e=Rb[b];if("undefined"===typeof e)throw Error("Unknown file open mode: "+b);b=e}c=b&64?("undefined"===typeof c?438:c)&4095|32768:0;if("object"===typeof a)var g=a;else{a=rb(a);try{g=T(a,{qb:!(b&131072)}).node}catch(k){}}e=!1;if(b&64)if(g){if(b&128)throw new K(17);}else g=ja(a,c,0),e=!0;if(!g)throw new K(2);8192===(g.mode&61440)&&(b&=-513);if(b&65536&&!N(g.mode))throw new K(20);if(!e&&(c=g?40960===(g.mode&61440)?40:N(g.mode)&& +("r"!==ic(b)||b&512)?21:Pb(g,ic(b)):2))throw new K(c);b&512&&Dc(g,0);b&=-641;d=wc({node:g,path:Lb(g),flags:b,seekable:!0,position:0,cb:g.cb,Bc:[],error:!1},d);d.cb.open&&d.cb.open(d);!f.logReadFiles||b&1||(Ec||(Ec={}),a in Ec||(Ec[a]=1,console.log("FS.trackingDelegate error on read file: "+a)));try{S.onOpenFile&&(g=0,1!==(b&2097155)&&(g|=1),0!==(b&2097155)&&(g|=2),S.onOpenFile(a,g))}catch(k){console.log("FS.trackingDelegate['onOpenFile']('"+a+"', flags) threw an exception: "+k.message)}return d} +function ma(a){if(null===a.fd)throw new K(9);a.Gb&&(a.Gb=null);try{a.cb.close&&a.cb.close(a)}catch(b){throw b;}finally{Q[a.fd]=null}a.fd=null}function Fc(a,b,c){if(null===a.fd)throw new K(9);if(!a.seekable||!a.cb.ob)throw new K(29);if(0!=c&&1!=c&&2!=c)throw new K(22);a.position=a.cb.ob(a,b,c);a.Bc=[]} +function sa(a,b,c,d,e){if(0>d||0>e)throw new K(22);if(null===a.fd)throw new K(9);if(1===(a.flags&2097155))throw new K(9);if(N(a.node.mode))throw new K(21);if(!a.cb.read)throw new K(22);var g="undefined"!==typeof e;if(!g)e=a.position;else if(!a.seekable)throw new K(29);b=a.cb.read(a,b,c,d,e);g||(a.position+=b);return b} +function la(a,b,c,d,e,g){if(0>d||0>e)throw new K(22);if(null===a.fd)throw new K(9);if(0===(a.flags&2097155))throw new K(9);if(N(a.node.mode))throw new K(21);if(!a.cb.write)throw new K(22);a.flags&1024&&Fc(a,0,2);var k="undefined"!==typeof e;if(!k)e=a.position;else if(!a.seekable)throw new K(29);b=a.cb.write(a,b,c,d,e,g);k||(a.position+=b);try{if(a.path&&S.onWriteToFile)S.onWriteToFile(a.path)}catch(m){console.log("FS.trackingDelegate['onWriteToFile']('"+a.path+"') threw an exception: "+m.message)}return b} +function Gc(){K||(K=function(a,b){this.node=b;this.zc=function(a){this.eb=a};this.zc(a);this.message="FS error";this.stack&&Object.defineProperty(this,"stack",{value:Error().stack,writable:!0})},K.prototype=Error(),K.prototype.constructor=K,[2].forEach(function(a){Eb[a]=new K(a);Eb[a].stack=""}))}var Hc;function ia(a,b){var c=0;a&&(c|=365);b&&(c|=146);return c} +function Ic(a,b,c){a=n("/dev",a);var d=ia(!!b,!!c);Jc||(Jc=64);var e=Jc++<<8|0;yb(e,{open:function(a){a.seekable=!1},close:function(){c&&c.buffer&&c.buffer.length&&c(10)},read:function(a,c,d,e){for(var g=0,k=0;k>2]=d.dev;D[c+4>>2]=0;D[c+8>>2]=d.ino;D[c+12>>2]=d.mode;D[c+16>>2]=d.nlink;D[c+20>>2]=d.uid;D[c+24>>2]=d.gid;D[c+28>>2]=d.rdev;D[c+32>>2]=0;D[c+36>>2]=d.size;D[c+40>>2]=4096;D[c+44>>2]=d.blocks;D[c+48>>2]=d.atime.getTime()/1E3|0;D[c+52>>2]=0;D[c+56>>2]=d.mtime.getTime()/1E3|0;D[c+60>>2]=0;D[c+64>>2]=d.ctime.getTime()/1E3|0;D[c+68>>2]=0;D[c+72>>2]=d.ino;return 0}var W=0; +function X(){W+=4;return D[W-4>>2]}function Y(){return G(X())}function Z(){var a=Q[X()];if(!a)throw new K(L.Kb);return a}function Da(){return l.length}function Ea(a){if(2147418112=b?b=Wa(2*b):b=Math.min(Wa((3*b+2147483648)/4),2147418112);a=Wa(b);var c=buffer.byteLength;try{var d=-1!==La.grow((a-c)/65536)?buffer=La.buffer:null}catch(e){d=null}if(!d||d.byteLength!=b)return!1;Xa();return!0} +function Mc(a){if(0===a)return 0;a=G(a);if(!J.hasOwnProperty(a))return 0;Mc.rb&&ha(Mc.rb);a=J[a];var b=oa(a)+1,c=Ta(b);c&&r(a,l,c,b);Mc.rb=c;return Mc.rb}r("GMT",F,60272,4); +function Nc(){function a(a){return(a=a.toTimeString().match(/\(([A-Za-z ]+)\)$/))?a[1]:"GMT"}if(!Oc){Oc=!0;D[Pc()>>2]=60*(new Date).getTimezoneOffset();var b=new Date(2E3,0,1),c=new Date(2E3,6,1);D[Qc()>>2]=Number(b.getTimezoneOffset()!=c.getTimezoneOffset());var d=a(b),e=a(c);d=ea(ba(d));e=ea(ba(e));c.getTimezoneOffset()>2]=d,D[Rc()+4>>2]=e):(D[Rc()>>2]=e,D[Rc()+4>>2]=d)}}var Oc; +function Sc(a){a/=1E3;if((v||w)&&self.performance&&self.performance.now)for(var b=self.performance.now();self.performance.now()-b>2]=c.position;c.Gb&&0===d&&0===g&&(c.Gb=null);return 0}catch(k){return"undefined"!==typeof V&&k instanceof K||B(k),-k.eb}},ca:function(a,b){W=b;try{var c=Y(),d=X();ka(c,d);return 0}catch(e){return"undefined"!==typeof V&&e instanceof K||B(e),-e.eb}},ba:function(a,b){W=b;try{var c=X(),d=X();if(0===d)return-L.ib;if(dd?-L.ib:p(c.path,c.flags,0,d).fd;case 1:case 2:return 0; +case 3:return c.flags;case 4:return d=X(),c.flags|=d,0;case 12:return d=X(),Ha[d+0>>1]=2,0;case 13:case 14:return 0;case 16:case 8:return-L.ib;case 9:return pb(L.ib),-1;default:return-L.ib}}catch(e){return"undefined"!==typeof V&&e instanceof K||B(e),-e.eb}},U:function(a,b){W=b;try{var c=Z(),d=X(),e=X();return sa(c,l,d,e)}catch(g){return"undefined"!==typeof V&&g instanceof K||B(g),-g.eb}},T:function(a,b){W=b;try{var c=Y();var d=X();if(d&-8)var e=-L.ib;else{var g=T(c,{qb:!0}).node;a="";d&4&&(a+="r"); +d&2&&(a+="w");d&1&&(a+="x");e=a&&Pb(g,a)?-L.$b:0}return e}catch(k){return"undefined"!==typeof V&&k instanceof K||B(k),-k.eb}},S:function(a,b){W=b;try{var c=Y(),d=X();a=c;a=rb(a);"/"===a[a.length-1]&&(a=a.substr(0,a.length-1));U(a,d);return 0}catch(e){return"undefined"!==typeof V&&e instanceof K||B(e),-e.eb}},R:function(a,b){W=b;try{var c=Z(),d=X(),e=X();return la(c,l,d,e)}catch(g){return"undefined"!==typeof V&&g instanceof K||B(g),-g.eb}},Q:function(a,b){W=b;try{var c=Y(),d=T(c,{parent:!0}).node, +e=tb(c),g=O(d,e),k=uc(d,e,!0);if(k)throw new K(k);if(!d.ab.rmdir)throw new K(1);if(g.sb)throw new K(16);try{S.willDeletePath&&S.willDeletePath(c)}catch(m){console.log("FS.trackingDelegate['willDeletePath']('"+c+"') threw an exception: "+m.message)}d.ab.rmdir(d,e);Ob(g);try{if(S.onDeletePath)S.onDeletePath(c)}catch(m){console.log("FS.trackingDelegate['onDeletePath']('"+c+"') threw an exception: "+m.message)}return 0}catch(m){return"undefined"!==typeof V&&m instanceof K||B(m),-m.eb}},P:function(a,b){W= +b;try{var c=Y(),d=X(),e=X();return p(c,d,e).fd}catch(g){return"undefined"!==typeof V&&g instanceof K||B(g),-g.eb}},s:function(a,b){W=b;try{var c=Z();ma(c);return 0}catch(d){return"undefined"!==typeof V&&d instanceof K||B(d),-d.eb}},O:function(a,b){W=b;try{var c=Y(),d=X();var e=X();if(0>=e)var g=-L.ib;else{var k=Kb(c),m=Math.min(e,oa(k)),y=l[d+m];r(k,F,d,e+1);l[d+m]=y;g=m}return g}catch(z){return"undefined"!==typeof V&&z instanceof K||B(z),-z.eb}},N:function(a,b){W=b;try{var c=X(),d=X(),e=Kc[c];if(!e)return 0; +if(d===e.uc){var g=Q[e.fd],k=e.flags,m=new Uint8Array(F.subarray(c,c+d));g&&g.cb.Ab&&g.cb.Ab(g,m,0,d,k);Kc[c]=null;e.Db&&ha(e.vc)}return 0}catch(y){return"undefined"!==typeof V&&y instanceof K||B(y),-y.eb}},M:function(a,b){W=b;try{var c=X(),d=X(),e=Q[c];if(!e)throw new K(9);ka(e.node,d);return 0}catch(g){return"undefined"!==typeof V&&g instanceof K||B(g),-g.eb}},L:Da,K:function(a,b,c){F.set(F.subarray(b,b+c),a)},J:Ea,r:Mc,q:function(a){var b=Date.now();D[a>>2]=b/1E3|0;D[a+4>>2]=b%1E3*1E3|0;return 0}, +I:function(a){return Math.log(a)/Math.LN10},p:function(){B("trap!")},H:function(a){Nc();a=new Date(1E3*D[a>>2]);D[15056]=a.getSeconds();D[15057]=a.getMinutes();D[15058]=a.getHours();D[15059]=a.getDate();D[15060]=a.getMonth();D[15061]=a.getFullYear()-1900;D[15062]=a.getDay();var b=new Date(a.getFullYear(),0,1);D[15063]=(a.getTime()-b.getTime())/864E5|0;D[15065]=-(60*a.getTimezoneOffset());var c=(new Date(2E3,6,1)).getTimezoneOffset();b=b.getTimezoneOffset();a=(c!=b&&a.getTimezoneOffset()==Math.min(b, +c))|0;D[15064]=a;a=D[Rc()+(a?4:0)>>2];D[15066]=a;return 60224},G:function(a,b){var c=D[a>>2];a=D[a+4>>2];0!==b&&(D[b>>2]=0,D[b+4>>2]=0);return Sc(1E6*c+a/1E3)},F:function(a){switch(a){case 30:return 16384;case 85:return 131068;case 132:case 133:case 12:case 137:case 138:case 15:case 235:case 16:case 17:case 18:case 19:case 20:case 149:case 13:case 10:case 236:case 153:case 9:case 21:case 22:case 159:case 154:case 14:case 77:case 78:case 139:case 80:case 81:case 82:case 68:case 67:case 164:case 11:case 29:case 47:case 48:case 95:case 52:case 51:case 46:return 200809; +case 79:return 0;case 27:case 246:case 127:case 128:case 23:case 24:case 160:case 161:case 181:case 182:case 242:case 183:case 184:case 243:case 244:case 245:case 165:case 178:case 179:case 49:case 50:case 168:case 169:case 175:case 170:case 171:case 172:case 97:case 76:case 32:case 173:case 35:return-1;case 176:case 177:case 7:case 155:case 8:case 157:case 125:case 126:case 92:case 93:case 129:case 130:case 131:case 94:case 91:return 1;case 74:case 60:case 69:case 70:case 4:return 1024;case 31:case 42:case 72:return 32; +case 87:case 26:case 33:return 2147483647;case 34:case 1:return 47839;case 38:case 36:return 99;case 43:case 37:return 2048;case 0:return 2097152;case 3:return 65536;case 28:return 32768;case 44:return 32767;case 75:return 16384;case 39:return 1E3;case 89:return 700;case 71:return 256;case 40:return 255;case 2:return 100;case 180:return 64;case 25:return 20;case 5:return 16;case 6:return 6;case 73:return 4;case 84:return"object"===typeof navigator?navigator.hardwareConcurrency||1:1}pb(22);return-1}, +E:function(a){var b=Date.now()/1E3|0;a&&(D[a>>2]=b);return b},D:function(a,b){if(b){var c=1E3*D[b+8>>2];c+=D[b+12>>2]/1E3}else c=Date.now();a=G(a);try{b=c;var d=T(a,{qb:!0}).node;d.ab.hb(d,{timestamp:Math.max(b,c)});return 0}catch(e){a=e;if(!(a instanceof K)){a+=" : ";a:{d=Error();if(!d.stack){try{throw Error(0);}catch(g){d=g}if(!d.stack){d="(no stack trace available)";break a}}d=d.stack.toString()}f.extraStackTrace&&(d+="\n"+f.extraStackTrace());d=Va(d);throw a+d;}pb(a.eb);return-1}},C:function(){B("OOM")}, +a:Ca},buffer);f.asm=Vc;f._RegisterExtensionFunctions=function(){return f.asm.ha.apply(null,arguments)};var nb=f.___emscripten_environ_constructor=function(){return f.asm.ia.apply(null,arguments)};f.___errno_location=function(){return f.asm.ja.apply(null,arguments)}; +var Qc=f.__get_daylight=function(){return f.asm.ka.apply(null,arguments)},Pc=f.__get_timezone=function(){return f.asm.la.apply(null,arguments)},Rc=f.__get_tzname=function(){return f.asm.ma.apply(null,arguments)},ha=f._free=function(){return f.asm.na.apply(null,arguments)},Ta=f._malloc=function(){return f.asm.oa.apply(null,arguments)},Tc=f._memalign=function(){return f.asm.pa.apply(null,arguments)},Uc=f._memset=function(){return f.asm.qa.apply(null,arguments)}; +f._sqlite3_bind_blob=function(){return f.asm.ra.apply(null,arguments)};f._sqlite3_bind_double=function(){return f.asm.sa.apply(null,arguments)};f._sqlite3_bind_int=function(){return f.asm.ta.apply(null,arguments)};f._sqlite3_bind_parameter_index=function(){return f.asm.ua.apply(null,arguments)};f._sqlite3_bind_text=function(){return f.asm.va.apply(null,arguments)};f._sqlite3_changes=function(){return f.asm.wa.apply(null,arguments)};f._sqlite3_clear_bindings=function(){return f.asm.xa.apply(null,arguments)}; +f._sqlite3_close_v2=function(){return f.asm.ya.apply(null,arguments)};f._sqlite3_column_blob=function(){return f.asm.za.apply(null,arguments)};f._sqlite3_column_bytes=function(){return f.asm.Aa.apply(null,arguments)};f._sqlite3_column_double=function(){return f.asm.Ba.apply(null,arguments)};f._sqlite3_column_name=function(){return f.asm.Ca.apply(null,arguments)};f._sqlite3_column_text=function(){return f.asm.Da.apply(null,arguments)};f._sqlite3_column_type=function(){return f.asm.Ea.apply(null,arguments)}; +f._sqlite3_create_function_v2=function(){return f.asm.Fa.apply(null,arguments)};f._sqlite3_data_count=function(){return f.asm.Ga.apply(null,arguments)};f._sqlite3_errmsg=function(){return f.asm.Ha.apply(null,arguments)};f._sqlite3_exec=function(){return f.asm.Ia.apply(null,arguments)};f._sqlite3_finalize=function(){return f.asm.Ja.apply(null,arguments)};f._sqlite3_free=function(){return f.asm.Ka.apply(null,arguments)};f._sqlite3_open=function(){return f.asm.La.apply(null,arguments)}; +f._sqlite3_prepare_v2=function(){return f.asm.Ma.apply(null,arguments)};f._sqlite3_reset=function(){return f.asm.Na.apply(null,arguments)};f._sqlite3_result_double=function(){return f.asm.Oa.apply(null,arguments)};f._sqlite3_result_null=function(){return f.asm.Pa.apply(null,arguments)};f._sqlite3_result_text=function(){return f.asm.Qa.apply(null,arguments)};f._sqlite3_step=function(){return f.asm.Ra.apply(null,arguments)};f._sqlite3_value_blob=function(){return f.asm.Sa.apply(null,arguments)}; +f._sqlite3_value_bytes=function(){return f.asm.Ta.apply(null,arguments)};f._sqlite3_value_double=function(){return f.asm.Ua.apply(null,arguments)};f._sqlite3_value_int=function(){return f.asm.Va.apply(null,arguments)};f._sqlite3_value_text=function(){return f.asm.Wa.apply(null,arguments)};f._sqlite3_value_type=function(){return f.asm.Xa.apply(null,arguments)}; +var h=f.stackAlloc=function(){return f.asm.Za.apply(null,arguments)},qa=f.stackRestore=function(){return f.asm._a.apply(null,arguments)},na=f.stackSave=function(){return f.asm.$a.apply(null,arguments)};f.dynCall_vi=function(){return f.asm.Ya.apply(null,arguments)};f.asm=Vc;f.cwrap=function(a,b,c,d){c=c||[];var e=c.every(function(a){return"number"===a});return"string"!==b&&e&&!d?Na(a):function(){return Oa(a,b,c,arguments)}};f.stackSave=na;f.stackRestore=qa;f.stackAlloc=h; +function Wc(a){this.name="ExitStatus";this.message="Program terminated with exit("+a+")";this.status=a}Wc.prototype=Error();Wc.prototype.constructor=Wc;gb=function Xc(){f.calledRun||Yc();f.calledRun||(gb=Xc)}; +function Yc(){function a(){if(!f.calledRun&&(f.calledRun=!0,!Ma)){db||(db=!0,f.noFSInit||Hc||(Hc=!0,Gc(),f.stdin=f.stdin,f.stdout=f.stdout,f.stderr=f.stderr,f.stdin?Ic("stdin",f.stdin):Ac("/dev/tty","/dev/stdin"),f.stdout?Ic("stdout",null,f.stdout):Ac("/dev/tty","/dev/stdout"),f.stderr?Ic("stderr",null,f.stderr):Ac("/dev/tty1","/dev/stderr"),p("/dev/stdin","r"),p("/dev/stdout","w"),p("/dev/stderr","w")),Za(ab));Jb=!1;Za(bb);if(f.onRuntimeInitialized)f.onRuntimeInitialized();if(f.postRun)for("function"== +typeof f.postRun&&(f.postRun=[f.postRun]);f.postRun.length;){var a=f.postRun.shift();cb.unshift(a)}Za(cb)}}if(!(0