import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:invoiceninja_flutter/data/web_client.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/reports/reports_actions.dart'; import 'package:invoiceninja_flutter/ui/app/dialogs/alert_dialog.dart'; import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; import 'package:invoiceninja_flutter/ui/app/loading_indicator.dart'; import 'package:invoiceninja_flutter/ui/app/resources/cached_image.dart'; import 'package:invoiceninja_flutter/ui/system/update_dialog.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:redux/redux.dart'; import 'package:invoiceninja_flutter/.env.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart'; import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart'; import 'package:invoiceninja_flutter/ui/app/menu_drawer_vm.dart'; import 'package:invoiceninja_flutter/ui/app/lists/selected_indicator.dart'; import 'package:invoiceninja_flutter/utils/icons.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:url_launcher/url_launcher.dart'; // STARTER: import - do not remove comment import 'package:invoiceninja_flutter/redux/design/design_actions.dart'; class MenuDrawer extends StatelessWidget { const MenuDrawer({ Key key, @required this.viewModel, }) : super(key: key); final MenuDrawerVM viewModel; @override Widget build(BuildContext context) { final Store store = StoreProvider.of(context); final state = store.state; final enableDarkMode = state.prefState.enableDarkMode; final localization = AppLocalization.of(context); final company = viewModel.selectedCompany; if (company == null) { return Container(); } Widget _companyLogo(CompanyEntity company) => company.settings.companyLogo != null && company.settings.companyLogo.isNotEmpty ? CachedImage( width: 32, url: company.settings.companyLogo, ) : Image.asset('assets/images/logo.png', width: 32); Widget _companyListItem(CompanyEntity company) => Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, children: [ _companyLogo(company), SizedBox(width: 28), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( company.displayName.isEmpty ? localization.untitledCompany : company.displayName, style: Theme.of(context).textTheme.headline6, overflow: TextOverflow.ellipsis, ), ], ), ), ], ); final _collapsedCompanySelector = PopupMenuButton( tooltip: localization.selectCompany, child: SizedBox( height: 48, width: double.infinity, child: _companyLogo(viewModel.selectedCompany), ), itemBuilder: (BuildContext context) => [ ...viewModel.companies .map((company) => PopupMenuItem( child: _companyListItem(company), value: company.id, )) .toList(), if (viewModel.state.userCompany.isAdmin) PopupMenuItem( value: null, child: Row( children: [ SizedBox(width: 2), Icon(Icons.add_circle, size: 32), SizedBox(width: 28), Text(localization.addCompany), ], ), ), ], onSelected: (String companyId) { if (companyId == null) { viewModel.onAddCompany(context); } else { /* viewModel.onCompanyChanged( context, value, viewModel.companies[int.parse(value)]); */ } }, ); final _expandedCompanySelector = viewModel.companies.isEmpty ? SizedBox() : DropdownButtonHideUnderline( child: DropdownButton( isExpanded: true, icon: Icon(Icons.arrow_drop_down), value: viewModel.selectedCompanyIndex, items: [ ...viewModel.companies .map((CompanyEntity company) => DropdownMenuItem( value: (viewModel.companies.indexOf(company)).toString(), child: _companyListItem(company), )) .toList(), if (viewModel.state.userCompany.isAdmin) DropdownMenuItem( value: null, child: Row( children: [ SizedBox(width: 2), Icon(Icons.add_circle, size: 32), SizedBox(width: 28), Text(localization.addCompany), ], ), ), ], onChanged: (value) { if (value == null) { viewModel.onAddCompany(context); } else { viewModel.onCompanyChanged( context, value, viewModel.companies[int.parse(value)]); } }, )); return SizedBox( width: state.prefState.isMenuCollapsed ? 65 : kDrawerWidth, child: Drawer( child: SafeArea( child: Column( mainAxisSize: MainAxisSize.max, children: [ // Hide options while refreshing data state.credentials.token.isEmpty ? Expanded( child: LoadingIndicator( height: 30, ), ) : Container( padding: EdgeInsets.symmetric(horizontal: 14, vertical: 3), color: enableDarkMode ? Colors.white10 : Colors.grey[200], child: state.prefState.isMenuCollapsed ? _collapsedCompanySelector : _expandedCompanySelector), state.credentials.token.isEmpty ? SizedBox() : Expanded( child: ListView( shrinkWrap: true, children: [ DrawerTile( company: company, icon: getEntityIcon(EntityType.dashboard), title: localization.dashboard, onTap: () => store.dispatch( ViewDashboard(navigator: Navigator.of(context))), onLongPress: () => store.dispatch(ViewDashboard( navigator: Navigator.of(context), filter: '')), ), DrawerTile( company: company, entityType: EntityType.client, icon: getEntityIcon(EntityType.client), title: localization.clients, ), DrawerTile( company: company, entityType: EntityType.product, icon: getEntityIcon(EntityType.product), title: localization.products, ), DrawerTile( company: company, entityType: EntityType.invoice, icon: getEntityIcon(EntityType.invoice), title: localization.invoices, ), DrawerTile( company: company, entityType: EntityType.payment, icon: getEntityIcon(EntityType.payment), title: localization.payments, ), DrawerTile( company: company, entityType: EntityType.quote, icon: getEntityIcon(EntityType.quote), title: localization.quotes, ), DrawerTile( company: company, entityType: EntityType.credit, icon: getEntityIcon(EntityType.credit), title: localization.credits, ), DrawerTile( company: company, entityType: EntityType.project, icon: getEntityIcon(EntityType.project), title: localization.projects, ), DrawerTile( company: company, entityType: EntityType.task, icon: getEntityIcon(EntityType.task), title: localization.tasks, ), DrawerTile( company: company, entityType: EntityType.vendor, icon: getEntityIcon(EntityType.vendor), title: localization.vendors, ), DrawerTile( company: company, entityType: EntityType.expense, icon: getEntityIcon(EntityType.expense), title: localization.expenses, ), // STARTER: menu - do not remove comment DrawerTile( company: company, icon: getEntityIcon(EntityType.reports), title: localization.reports, onTap: () { store.dispatch( ViewReports(navigator: Navigator.of(context))); }, ), DrawerTile( company: company, icon: getEntityIcon(EntityType.settings), title: localization.settings, onTap: () { store.dispatch(ViewSettings( navigator: Navigator.of(context), company: state.company)); }, ), ], )), Align( child: state.prefState.isMenuCollapsed ? SidebarFooterCollapsed() : SidebarFooter(), alignment: Alignment(0, 1), ), ], ), ), ), ); } } class DrawerTile extends StatefulWidget { const DrawerTile({ @required this.company, @required this.icon, @required this.title, this.onTap, this.entityType, this.onLongPress, this.onCreateTap, }); final CompanyEntity company; final EntityType entityType; final IconData icon; final String title; final Function onTap; final Function onLongPress; final Function onCreateTap; @override _DrawerTileState createState() => _DrawerTileState(); } class _DrawerTileState extends State { bool _isHovered = false; @override Widget build(BuildContext context) { final store = StoreProvider.of(context); final state = store.state; final uiState = state.uiState; final userCompany = state.userCompany; final NavigatorState navigator = Navigator.of(context); if (!Config.DEMO_MODE) { if (widget.entityType != null && !userCompany.canViewOrCreate(widget.entityType)) { return Container(); } else if (!widget.company.isModuleEnabled(widget.entityType)) { return Container(); } } final localization = AppLocalization.of(context); final route = widget.title == localization.dashboard ? kDashboard : widget.title == localization.settings ? kSettings : widget.title == localization.reports ? kReports : widget.entityType.name; Widget trailingWidget; if (!state.prefState.isMenuCollapsed) { if (widget.title == localization.dashboard) { trailingWidget = IconButton( icon: Icon(Icons.search), onPressed: () { if (isMobile(context)) { navigator.pop(); } store.dispatch( ViewDashboard(navigator: Navigator.of(context), filter: '')); }, ); } else if (userCompany.canCreate(widget.entityType)) { trailingWidget = IconButton( icon: Icon(Icons.add_circle_outline), onPressed: () { if (isMobile(context)) { navigator.pop(); } createEntityByType(context: context, entityType: widget.entityType); }, ); } } Widget child = SelectedIndicator( isSelected: uiState.currentRoute.startsWith('/$route'), child: ListTile( dense: true, leading: Icon(widget.icon, size: 22), title: state.prefState.isMenuCollapsed ? null : Text(widget.title), onTap: () => widget.entityType != null ? viewEntitiesByType( context: context, entityType: widget.entityType) : widget.onTap(), onLongPress: () => widget.onLongPress != null ? widget.onLongPress() : widget.entityType != null ? createEntityByType( context: context, entityType: widget.entityType) : null, /* trailing: _isHovered || !RendererBinding.instance.mouseTracker.mouseIsConnected ? trailingWidget : null, */ trailing: trailingWidget, ), ); if (state.prefState.isMenuCollapsed) { child = Tooltip( message: widget.title, child: child, ); } return MouseRegion( onEnter: (event) => setState(() => _isHovered = true), onExit: (event) => setState(() => _isHovered = false), child: child, ); } } class SidebarFooter extends StatelessWidget { @override Widget build(BuildContext context) { final state = StoreProvider.of(context).state; final localization = AppLocalization.of(context); return Container( color: Theme.of(context).bottomAppBarColor, child: Row( mainAxisSize: MainAxisSize.max, children: [ if (state.prefState.isMenuCollapsed) ...[ Expanded(child: SizedBox()) ] else ...[ if (true || isSelfHosted(context)) IconButton( icon: Icon( Icons.warning, color: Theme.of(context).accentColor, ), onPressed: () => _showUpdate(context), ), IconButton( icon: Icon(Icons.mail), onPressed: () => _showContactUs(context), tooltip: localization.contactUs, ), IconButton( icon: Icon(Icons.forum), onPressed: () => launch('https://www.invoiceninja.com/forums/forum/support'), tooltip: localization.supportForum, ), IconButton( icon: Icon(Icons.help_outline), onPressed: () => launch('https://docs.invoiceninja.com'), tooltip: localization.help, ), IconButton( icon: Icon(Icons.info_outline), onPressed: () => _showAbout(context), tooltip: localization.about, ), /* if (kDebugMode) IconButton( icon: Icon(Icons.memory), onPressed: () => showDialog( context: context, builder: (BuildContext context) { return StateInspector(); }), ), */ /* IconButton( icon: Icon(Icons.filter), onPressed: () => viewPdf(InvoiceEntity(), context), ), */ if (state.lastError.isNotEmpty && !kReleaseMode) IconButton( icon: Icon( Icons.warning, color: Colors.red, ), tooltip: localization.error, onPressed: () => showDialog( context: context, builder: (BuildContext context) { return ErrorDialog( state.lastError, clearErrorOnDismiss: true, ); }), ), /* if (!Platform.isIOS && isHosted(context) && !isPaidAccount(context)) ...[ Spacer(), FlatButton( child: Text(localization.upgrade), color: Colors.green, onPressed: () => showDialog( context: context, builder: (BuildContext context) { return UpgradeDialog(); }), ), SizedBox(width: 14) ], */ ], ], ), ); } } class SidebarFooterCollapsed extends StatelessWidget { @override Widget build(BuildContext context) { final localization = AppLocalization.of(context); return PopupMenuButton( icon: Icon(Icons.info_outline), onSelected: (value) { if (value == localization.updateAvailable) { _showUpdate(context); } else if (value == localization.about) { _showAbout(context); } else if (value == localization.contactUs) { _showContactUs(context); } }, itemBuilder: (BuildContext context) => [ if (true || isSelfHosted(context) && kIsWeb) PopupMenuItem( child: ListTile( leading: Icon( Icons.warning, color: Theme.of(context).accentColor, ), title: Text(localization.updateAvailable), ), value: localization.updateAvailable, ), PopupMenuItem( child: ListTile( leading: Icon(Icons.mail), title: Text(localization.contactUs), ), value: localization.contactUs, ), PopupMenuItem( child: ListTile( leading: Icon(Icons.help_outline), title: Text(localization.documentation), ), value: localization.documentation, ), PopupMenuItem( child: ListTile( leading: Icon(Icons.forum), title: Text(localization.supportForum), ), value: localization.supportForum, ), PopupMenuItem( child: ListTile( leading: Icon(Icons.info_outline), title: Text(localization.about), ), value: localization.about, ), ], ); } } void _showContactUs(BuildContext context) { showDialog( context: context, builder: (BuildContext context) => ContactUsDialog(), ); } void _showUpdate(BuildContext context) { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) => UpdateDialog(), ); } void _showAbout(BuildContext context) { final localization = AppLocalization.of(context); final ThemeData themeData = Theme.of(context); final TextStyle aboutTextStyle = themeData.textTheme.bodyText1; final TextStyle linkStyle = themeData.textTheme.bodyText1.copyWith(color: themeData.accentColor); showAboutDialog( context: context, applicationName: 'Invoice Ninja', applicationIcon: Image.asset( 'assets/images/logo.png', width: 40.0, height: 40.0, ), applicationVersion: 'Version: $kAppVersion', applicationLegalese: '© ${DateTime.now().year} Invoice Ninja', children: [ Padding( padding: const EdgeInsets.only(top: 24.0), child: RichText( text: TextSpan( children: [ TextSpan( style: aboutTextStyle, text: localization.thankYouForUsingOurApp + '\n\n' + localization.ifYouLikeIt, ), TextSpan( style: linkStyle, recognizer: TapGestureRecognizer() ..onTap = () { launch(getAppURL(context), forceSafariVC: false); }, text: ' ' + localization.clickHere + ' ', ), TextSpan( style: aboutTextStyle, text: localization.toRateIt, ), ], ), ), ), ], ); } class ContactUsDialog extends StatefulWidget { @override _ContactUsDialogState createState() => _ContactUsDialogState(); } class _ContactUsDialogState extends State { String _message = ''; bool _includeLogs = false; bool _isSaving = false; void _sendMessage() { if (_message.isEmpty) { return; } final localization = AppLocalization.of(context); final state = StoreProvider.of(context).state; setState(() => _isSaving = true); WebClient() .post(state.credentials.url + '/support/messages/send', state.credentials.token, data: json.encode({ 'message': _message, 'send_logs': _includeLogs ? 'true' : '', })) .then((dynamic response) async { setState(() => _isSaving = false); await showDialog( context: context, builder: (BuildContext context) { return MessageDialog(localization.yourMessageHasBeenReceived); }); Navigator.pop(context); }).catchError((dynamic error) { print('error: $error'); setState(() => _isSaving = false); showErrorDialog(context: context, message: '$error'); }); } @override Widget build(BuildContext context) { final localization = AppLocalization.of(context); final state = StoreProvider.of(context).state; final user = state.user; return AlertDialog( contentPadding: EdgeInsets.all(25), title: Text(localization.contactUs), actions: [ if (_isSaving) Padding( padding: const EdgeInsets.all(12), child: CircularProgressIndicator(), ), if (!_isSaving) FlatButton( child: Text(localization.cancel.toUpperCase()), onPressed: () => Navigator.pop(context), ), if (!_isSaving) FlatButton( child: Text(localization.send.toUpperCase()), onPressed: () => _sendMessage(), ), ], content: SingleChildScrollView( child: Container( width: isMobile(context) ? null : 500, child: Column(mainAxisSize: MainAxisSize.min, children: [ TextFormField( enabled: false, decoration: InputDecoration( labelText: localization.from, ), initialValue: '${user.fullName} <${user.email}>', ), SizedBox(height: 10), TextFormField( decoration: InputDecoration( labelText: localization.message, ), minLines: 4, maxLines: 4, onChanged: (value) => _message = value, ), SizedBox(height: 10), SwitchListTile( value: _includeLogs, onChanged: (value) { setState(() => _includeLogs = value); }, title: Text(localization.includeRecentErrors), activeColor: Theme.of(context).accentColor, ), ]), ), ), ); } }