// Flutter imports: import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // Package imports: import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; import 'package:invoiceninja_flutter/redux/company/company_selectors.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:timeago/timeago.dart' as timeago; // Project imports: import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/models/static/color_theme_model.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart'; import 'package:invoiceninja_flutter/redux/ui/pref_state.dart'; import 'package:invoiceninja_flutter/ui/app/app_builder.dart'; import 'package:invoiceninja_flutter/ui/app/form_card.dart'; import 'package:invoiceninja_flutter/ui/app/forms/app_dropdown_button.dart'; import 'package:invoiceninja_flutter/ui/app/forms/app_form.dart'; import 'package:invoiceninja_flutter/ui/app/forms/bool_dropdown_button.dart'; import 'package:invoiceninja_flutter/ui/app/forms/color_picker.dart'; import 'package:invoiceninja_flutter/ui/app/live_text.dart'; import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; import 'package:invoiceninja_flutter/ui/settings/device_settings_vm.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:invoiceninja_flutter/utils/strings.dart'; class DeviceSettings extends StatefulWidget { const DeviceSettings({ Key key, @required this.viewModel, }) : super(key: key); final DeviceSettingsVM viewModel; @override _DeviceSettingsState createState() => _DeviceSettingsState(); } class _DeviceSettingsState extends State with SingleTickerProviderStateMixin { final GlobalKey _formKey = GlobalKey(debugLabel: '_deviceSettings'); TabController _controller; FocusScopeNode _focusNode; @override void initState() { super.initState(); final settingsUIState = widget.viewModel.state.settingsUIState; _focusNode = FocusScopeNode(); _controller = TabController( vsync: this, length: 2, initialIndex: settingsUIState.tabIndex); _controller.addListener(_onTabChanged); } void _onTabChanged() { final store = StoreProvider.of(context); store.dispatch(UpdateSettingsTab(tabIndex: _controller.index)); } @override void dispose() { _controller.removeListener(_onTabChanged); _controller.dispose(); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final localization = AppLocalization.of(context); final viewModel = widget.viewModel; final state = viewModel.state; final prefState = state.prefState; /* final countSessions = state.tokenState.list .map((tokenId) => state.tokenState.map[tokenId]) .where( (token) => token.isSystem && token.createdUserId == state.user.id) .length; */ return Scaffold( appBar: AppBar( centerTitle: false, automaticallyImplyLeading: isMobile(context), title: Text(localization.deviceSettings), bottom: TabBar( key: ValueKey(state.settingsUIState.updatedAt), controller: _controller, isScrollable: false, tabs: [ Tab(text: localization.options), Tab(text: localization.colors), ], ), ), body: AppTabForm( tabController: _controller, formKey: _formKey, focusNode: _focusNode, children: [ ScrollableListView( primary: true, children: [ FormCard( children: [ BoolDropdownButton( label: localization.layout, value: prefState.appLayout == AppLayout.mobile, onChanged: (value) { viewModel.onLayoutChanged(context, value ? AppLayout.mobile : AppLayout.desktop); }, enabledLabel: localization.mobile, disabledLabel: localization.desktop, ), if (state.prefState.isDesktop) ...[ BoolDropdownButton( label: localization.menuSidebar, value: prefState.menuSidebarMode == AppSidebarMode.float, onChanged: (value) { viewModel.onMenuModeChanged( context, value ? AppSidebarMode.float : AppSidebarMode.collapse, ); }, enabledLabel: localization.float, disabledLabel: localization.collapse, ), BoolDropdownButton( label: localization.historySidebar, value: prefState.historySidebarMode == AppSidebarMode.float, onChanged: (value) { viewModel.onHistoryModeChanged( context, value ? AppSidebarMode.float : AppSidebarMode.visible, ); }, enabledLabel: localization.float, disabledLabel: localization.showOrHide, ), ], if (isDesktop(context)) ...[ BoolDropdownButton( label: localization.clickSelected, value: prefState.tapSelectedToEdit, onChanged: (value) { viewModel.onTapSelectedChanged(context, value); }, enabledLabel: localization.editRecord, disabledLabel: localization.hidePreview, ), BoolDropdownButton( label: localization.afterSaving, value: prefState.editAfterSaving, onChanged: (value) { viewModel.onEditAfterSavingChanged(context, value); }, enabledLabel: localization.editRecord, disabledLabel: localization.viewRecord, ), ] else BoolDropdownButton( label: localization.listLongPress, value: !prefState.longPressSelectionIsDefault, onChanged: (value) { viewModel.onLongPressSelectionIsDefault( context, !value); }, enabledLabel: localization.showActions, disabledLabel: localization.startMultiselect, ), ], ), if (isDesktop(context)) FormCard( children: [ SwitchListTile( title: Text(localization.showPdfPreview), subtitle: Text(localization.showPdfPreviewHelp), value: prefState.showPdfPreview, onChanged: (value) => viewModel.onShowPdfChanged(context, value), activeColor: Theme.of(context).colorScheme.secondary, secondary: Icon(MdiIcons.filePdfBox), ), if (kIsWeb || !kReleaseMode) SwitchListTile( title: Text(localization.browserPdfViewer), subtitle: Text(localization.browserPdfViewerHelp), value: !prefState.enableJSPDF, onChanged: (value) => viewModel.onEnableJSPDFChanged(context, !value), activeColor: Theme.of(context).colorScheme.secondary, secondary: Icon(MdiIcons.filePdfBox), ), SizedBox(height: 10), BoolDropdownButton( label: localization.previewLocation, value: prefState.showPdfPreviewSideBySide, onChanged: (value) { viewModel.onShowPdfSideBySideChanged(context, value); }, disabledLabel: localization.bottom, enabledLabel: localization.side, ), ], ), FormCard( children: [ Padding( padding: const EdgeInsets.only(bottom: 10), child: AppDropdownButton( labelText: localization.fontSize, value: prefState.textScaleFactor, onChanged: (dynamic value) { viewModel.onTextScaleFactorChanged(context, value); AppBuilder.of(context).rebuild(); }, items: [ DropdownMenuItem( child: Text(localization.small), value: PrefState.TEXT_SCALING_SMALL, ), DropdownMenuItem( child: Text(localization.normal), value: PrefState.TEXT_SCALING_NORMAL, ), DropdownMenuItem( child: Text(localization.large), value: PrefState.TEXT_SCALING_LARGE, ), DropdownMenuItem( child: Text(localization.extraLarge), value: PrefState.TEXT_SCALING_EXTRA_LARGE, ), ]), ), FutureBuilder( future: viewModel.authenticationSupported, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData && snapshot.data == true) { return SwitchListTile( title: Text(localization.biometricAuthentication), value: prefState.requireAuthentication, onChanged: (value) => viewModel .onRequireAuthenticationChanged(context, value), secondary: Icon(prefState.requireAuthentication ? MdiIcons.lock : MdiIcons.lockOpen), activeColor: Theme.of(context).colorScheme.secondary, ); } else { return SizedBox(); } }, ), SwitchListTile( title: Text(localization.enableFlexibleSearch), subtitle: Text(localization.enableFlexibleSearchHelp), value: prefState.enableFlexibleSearch, onChanged: (value) => viewModel.onEnableFlexibleSearchChanged(context, value), activeColor: Theme.of(context).colorScheme.secondary, secondary: Icon(Icons.search), ), if (isDesktop(context)) ...[ SwitchListTile( title: Text(localization.enableTooltips), subtitle: Text(localization.enableTooltipsHelp), value: prefState.enableTooltips, onChanged: (value) => viewModel.onEnableTooltipsChanged(context, value), activeColor: Theme.of(context).colorScheme.secondary, secondary: Icon(MdiIcons.tooltip), ), SwitchListTile( title: Text(localization.enableTouchEvents), subtitle: Text(localization.enableTouchEventsHelp), value: prefState.enableTouchEvents, onChanged: (value) => viewModel.onEnableTouchEventsChanged(context, value), activeColor: Theme.of(context).colorScheme.secondary, secondary: Icon(Icons.touch_app), ), ], /* SwitchListTile( title: Text(localization.persistUi), subtitle: Text(localization.persistUiHelp), value: prefState.persistUI, onChanged: (value) => viewModel.onPersistUiChanged(context, value), activeColor: Theme.of(context).colorScheme.secondary, secondary: Icon(Icons.save_alt), ), */ SwitchListTile( title: Text(localization.persistData), subtitle: Text(localization.persistDataHelp), value: prefState.persistData, onChanged: (value) => viewModel.onPersistDataChanged(context, value), activeColor: Theme.of(context).colorScheme.secondary, secondary: Icon(Icons.save_alt), ), ], ), FormCard( isLast: true, children: [ Builder(builder: (BuildContext context) { return ListTile( leading: Icon(Icons.refresh), title: Text(localization.refreshData), subtitle: LiveText(() { if (state.userCompanyState.lastUpdated == 0) { return ''; } return localization.lastUpdated + ': ' + timeago.format( convertTimestampToDate( (state.userCompanyState.lastUpdated / 1000) .round()), locale: localeSelector(state, twoLetter: true)); }), onTap: () { viewModel.onRefreshTap(context); }, ); }), ListTile( leading: Icon(Icons.logout), title: Text(localization.endAllSessions), /* subtitle: Text(countSessions == 1 ? localization.countSession : localization.countSession .replaceFirst(':count', '$countSessions')), */ onTap: () { confirmCallback( context: context, callback: (_) { viewModel.onLogoutTap(context); }); }, ), ], ) ], ), ScrollableListView( primary: true, children: [ FormCard(children: [ SwitchListTile( title: Text(localization.darkMode), value: prefState.enableDarkMode, onChanged: (value) => viewModel.onDarkModeChanged(context, value), secondary: Icon(kIsWeb ? Icons.lightbulb_outline : MdiIcons.themeLightDark), activeColor: Theme.of(context).colorScheme.secondary, ), SizedBox(height: 16), AppDropdownButton( labelText: localization.statusColorTheme, value: state.prefState.colorTheme, items: [ ...colorThemesMap.keys .map((key) => DropdownMenuItem( child: Row( children: [ SizedBox( child: Text(toTitleCase(key)), width: 120, ), Expanded( child: Container( color: colorThemesMap[key].colorInfo, height: 50, ), ), Expanded( child: Container( color: colorThemesMap[key].colorPrimary, height: 50, ), ), Expanded( child: Container( color: colorThemesMap[key].colorSuccess, height: 50, ), ), Expanded( child: Container( color: colorThemesMap[key].colorWarning, height: 50, ), ), Expanded( child: Container( color: colorThemesMap[key].colorDanger, height: 50, ), ), ], ), value: key, )) .toList() ], onChanged: (dynamic value) => viewModel.onColorThemeChanged(context, value), ), ]), FormCard( isLast: true, children: [ AppDropdownButton( labelText: localization.loadColorTheme, value: '', onChanged: (dynamic value) { if (value == 'clear_all') { viewModel.onCustomColorsChanged( context, prefState.customColors .rebuild((b) => b..clear())); } else if (value == 'contrast') { final enableDarkMode = state.prefState.enableDarkMode; viewModel.onCustomColorsChanged( context, prefState.customColors.rebuild( (b) => b.addAll( { PrefState .THEME_SIDEBAR_ACTIVE_BACKGROUND_COLOR: '#2F2E2E', PrefState.THEME_SIDEBAR_ACTIVE_FONT_COLOR: '#FFFFFF', PrefState .THEME_SIDEBAR_INACTIVE_BACKGROUND_COLOR: '#454544', PrefState.THEME_SIDEBAR_INACTIVE_FONT_COLOR: '#FFFFFF', PrefState .THEME_INVOICE_HEADER_BACKGROUND_COLOR: '#777777', PrefState.THEME_INVOICE_HEADER_FONT_COLOR: '#FFFFFF', PrefState .THEME_TABLE_ALTERNATE_ROW_BACKGROUND_COLOR: enableDarkMode ? '#090909' : '#F9F9F9', }, ), ), ); } }, items: [ DropdownMenuItem( child: Text(localization.clearAll), value: 'clear_all'), DropdownMenuItem( child: Text(localization.contrast), value: 'contrast'), ]), ...PrefState.THEME_COLORS .map( (selector) => FormColorPicker( labelText: localization.lookup(selector), initialValue: prefState.customColors[selector], onSelected: (value) { viewModel.onCustomColorsChanged( context, prefState.customColors .rebuild((b) => b[selector] = value ?? '')); }, ), ) .toList(), SizedBox(height: 20), Row( children: [ Expanded( child: OutlinedButton( onPressed: () { final colors = PrefState.THEME_COLORS .map((selector) => prefState.customColors[selector] ?? '') .toList(); Clipboard.setData( ClipboardData(text: colors.join(','))); showToast(localization.copiedToClipboard .replaceFirst(':value', colors.join(','))); }, child: Text(localization.exportColors.toUpperCase()), ), ), SizedBox(width: kTableColumnGap), Expanded( child: OutlinedButton( onPressed: () { fieldCallback( context: context, field: localization.colors, callback: (value) { final colors = value.split(','); var customColors = prefState.customColors; for (var i = 0; i < colors.length; i++) { customColors = customColors.rebuild((b) => b[PrefState.THEME_COLORS[i]] = colors[i]); } viewModel.onCustomColorsChanged( context, customColors); }, title: localization.importColors, ); }, child: Text(localization.importColors.toUpperCase()), ), ), ], ) ], ) ], ) ], ), ); } }