When refreshCurrentUser throw error, show check network in ab and group tab. Signed-off-by: 21pages <sunboeasy@gmail.com>
		
			
				
	
	
		
			852 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			852 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'dart:math';
 | 
						|
 | 
						|
import 'package:dropdown_button2/dropdown_button2.dart';
 | 
						|
import 'package:dynamic_layouts/dynamic_layouts.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
 | 
						|
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
 | 
						|
import 'package:flutter_hbb/common/widgets/peer_card.dart';
 | 
						|
import 'package:flutter_hbb/common/widgets/peers_view.dart';
 | 
						|
import 'package:flutter_hbb/consts.dart';
 | 
						|
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
 | 
						|
import 'package:flutter_hbb/models/ab_model.dart';
 | 
						|
import 'package:flutter_hbb/models/platform_model.dart';
 | 
						|
import 'package:url_launcher/url_launcher_string.dart';
 | 
						|
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
 | 
						|
import 'package:get/get.dart';
 | 
						|
import 'package:flex_color_picker/flex_color_picker.dart';
 | 
						|
 | 
						|
import '../../common.dart';
 | 
						|
import 'dialog.dart';
 | 
						|
import 'login.dart';
 | 
						|
 | 
						|
final hideAbTagsPanel = false.obs;
 | 
						|
 | 
						|
class AddressBook extends StatefulWidget {
 | 
						|
  final EdgeInsets? menuPadding;
 | 
						|
  const AddressBook({Key? key, this.menuPadding}) : super(key: key);
 | 
						|
 | 
						|
  @override
 | 
						|
  State<StatefulWidget> createState() {
 | 
						|
    return _AddressBookState();
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _AddressBookState extends State<AddressBook> {
 | 
						|
  var menuPos = RelativeRect.fill;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) => Obx(() {
 | 
						|
        if (!gFFI.userModel.isLogin) {
 | 
						|
          return Center(
 | 
						|
              child: ElevatedButton(
 | 
						|
                  onPressed: loginDialog, child: Text(translate("Login"))));
 | 
						|
        } else if (gFFI.userModel.networkError.isNotEmpty) {
 | 
						|
          return netWorkErrorWidget();
 | 
						|
        } else {
 | 
						|
          return Column(
 | 
						|
            children: [
 | 
						|
              // NOT use Offstage to wrap LinearProgressIndicator
 | 
						|
              if (gFFI.abModel.currentAbLoading.value &&
 | 
						|
                  gFFI.abModel.currentAbEmpty)
 | 
						|
                const LinearProgressIndicator(),
 | 
						|
              buildErrorBanner(context,
 | 
						|
                  loading: gFFI.abModel.currentAbLoading,
 | 
						|
                  err: gFFI.abModel.currentAbPullError,
 | 
						|
                  retry: null,
 | 
						|
                  close: () => gFFI.abModel.currentAbPullError.value = ''),
 | 
						|
              buildErrorBanner(context,
 | 
						|
                  loading: gFFI.abModel.currentAbLoading,
 | 
						|
                  err: gFFI.abModel.currentAbPushError,
 | 
						|
                  retry: null, // remove retry
 | 
						|
                  close: () => gFFI.abModel.currentAbPushError.value = ''),
 | 
						|
              Expanded(
 | 
						|
                  child: (isDesktop || isWebDesktop)
 | 
						|
                      ? _buildAddressBookDesktop()
 | 
						|
                      : _buildAddressBookMobile())
 | 
						|
            ],
 | 
						|
          );
 | 
						|
        }
 | 
						|
      });
 | 
						|
 | 
						|
  Widget _buildAddressBookDesktop() {
 | 
						|
    return Row(
 | 
						|
      children: [
 | 
						|
        Offstage(
 | 
						|
            offstage: hideAbTagsPanel.value,
 | 
						|
            child: Container(
 | 
						|
              decoration: BoxDecoration(
 | 
						|
                  borderRadius: BorderRadius.circular(12),
 | 
						|
                  border: Border.all(
 | 
						|
                      color: Theme.of(context).colorScheme.background)),
 | 
						|
              child: Container(
 | 
						|
                width: 200,
 | 
						|
                height: double.infinity,
 | 
						|
                child: Column(
 | 
						|
                  children: [
 | 
						|
                    _buildAbDropdown(),
 | 
						|
                    _buildTagHeader().marginOnly(
 | 
						|
                        left: 8.0,
 | 
						|
                        right: gFFI.abModel.legacyMode.value ? 8.0 : 0,
 | 
						|
                        top: gFFI.abModel.legacyMode.value ? 8.0 : 0),
 | 
						|
                    Expanded(
 | 
						|
                      child: Container(
 | 
						|
                        width: double.infinity,
 | 
						|
                        height: double.infinity,
 | 
						|
                        child: _buildTags(),
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                    _buildAbPermission(),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ).marginOnly(right: 12.0)),
 | 
						|
        _buildPeersViews()
 | 
						|
      ],
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Widget _buildAddressBookMobile() {
 | 
						|
    const padding = 8.0;
 | 
						|
    return Column(
 | 
						|
      children: [
 | 
						|
        Offstage(
 | 
						|
            offstage: hideAbTagsPanel.value,
 | 
						|
            child: Container(
 | 
						|
              decoration: BoxDecoration(
 | 
						|
                  borderRadius: BorderRadius.circular(6),
 | 
						|
                  border: Border.all(
 | 
						|
                      color: Theme.of(context).colorScheme.background)),
 | 
						|
              child: Container(
 | 
						|
                padding:
 | 
						|
                    const EdgeInsets.fromLTRB(padding, 0, padding, padding),
 | 
						|
                child: Column(
 | 
						|
                  mainAxisSize: MainAxisSize.min,
 | 
						|
                  children: [
 | 
						|
                    _buildAbDropdown(),
 | 
						|
                    _buildTagHeader().marginOnly(left: 8.0, right: 0),
 | 
						|
                    Container(
 | 
						|
                      width: double.infinity,
 | 
						|
                      child: _buildTags(),
 | 
						|
                    ),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ).marginOnly(bottom: 12.0)),
 | 
						|
        _buildPeersViews()
 | 
						|
      ],
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Widget _buildAbPermission() {
 | 
						|
    icon(IconData data, String tooltip) {
 | 
						|
      return Tooltip(
 | 
						|
          message: translate(tooltip),
 | 
						|
          waitDuration: Duration.zero,
 | 
						|
          child: Icon(data, size: 12.0).marginSymmetric(horizontal: 2.0));
 | 
						|
    }
 | 
						|
 | 
						|
    return Obx(() {
 | 
						|
      if (gFFI.abModel.legacyMode.value) return Offstage();
 | 
						|
      if (gFFI.abModel.current.isPersonal()) {
 | 
						|
        return Row(
 | 
						|
          mainAxisAlignment: MainAxisAlignment.end,
 | 
						|
          children: [
 | 
						|
            icon(Icons.cloud_off, "Personal"),
 | 
						|
          ],
 | 
						|
        );
 | 
						|
      } else {
 | 
						|
        List<Widget> children = [];
 | 
						|
        final rule = gFFI.abModel.current.sharedProfile()?.rule;
 | 
						|
        if (rule == ShareRule.read.value) {
 | 
						|
          children.add(
 | 
						|
              icon(Icons.visibility, ShareRule.desc(ShareRule.read.value)));
 | 
						|
        } else if (rule == ShareRule.readWrite.value) {
 | 
						|
          children
 | 
						|
              .add(icon(Icons.edit, ShareRule.desc(ShareRule.readWrite.value)));
 | 
						|
        } else if (rule == ShareRule.fullControl.value) {
 | 
						|
          children.add(icon(
 | 
						|
              Icons.security, ShareRule.desc(ShareRule.fullControl.value)));
 | 
						|
        }
 | 
						|
        final owner = gFFI.abModel.current.sharedProfile()?.owner;
 | 
						|
        if (owner != null) {
 | 
						|
          children.add(icon(Icons.person, "${translate("Owner")}: $owner"));
 | 
						|
        }
 | 
						|
        return Row(
 | 
						|
          mainAxisAlignment: MainAxisAlignment.end,
 | 
						|
          children: children,
 | 
						|
        );
 | 
						|
      }
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  Widget _buildAbDropdown() {
 | 
						|
    if (gFFI.abModel.legacyMode.value) {
 | 
						|
      return Offstage();
 | 
						|
    }
 | 
						|
    final names = gFFI.abModel.addressBookNames();
 | 
						|
    if (!names.contains(gFFI.abModel.currentName.value)) {
 | 
						|
      return Offstage();
 | 
						|
    }
 | 
						|
    // order: personal, divider, character order
 | 
						|
    // https://pub.dev/packages/dropdown_button2#3-dropdownbutton2-with-items-of-different-heights-like-dividers
 | 
						|
    final personalAddressBookName = gFFI.abModel.personalAddressBookName();
 | 
						|
    bool contains = names.remove(personalAddressBookName);
 | 
						|
    names.sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
 | 
						|
    if (contains) {
 | 
						|
      names.insert(0, personalAddressBookName);
 | 
						|
    }
 | 
						|
 | 
						|
    Row buildItem(String e, {bool button = false}) {
 | 
						|
      return Row(
 | 
						|
        children: [
 | 
						|
          Expanded(
 | 
						|
            child: Tooltip(
 | 
						|
                waitDuration: Duration(milliseconds: 500),
 | 
						|
                message: gFFI.abModel.translatedName(e),
 | 
						|
                child: Text(
 | 
						|
                  gFFI.abModel.translatedName(e),
 | 
						|
                  style: button ? null : TextStyle(fontSize: 14.0),
 | 
						|
                  maxLines: 1,
 | 
						|
                  overflow: TextOverflow.ellipsis,
 | 
						|
                  textAlign: button ? TextAlign.center : null,
 | 
						|
                )),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    final items = names
 | 
						|
        .map((e) => DropdownMenuItem(value: e, child: buildItem(e)))
 | 
						|
        .toList();
 | 
						|
    var menuItemStyleData = MenuItemStyleData(height: 36);
 | 
						|
    if (contains && items.length > 1) {
 | 
						|
      items.insert(1, DropdownMenuItem(enabled: false, child: Divider()));
 | 
						|
      List<double> customHeights = List.filled(items.length, 36);
 | 
						|
      customHeights[1] = 4;
 | 
						|
      menuItemStyleData = MenuItemStyleData(customHeights: customHeights);
 | 
						|
    }
 | 
						|
    final TextEditingController textEditingController = TextEditingController();
 | 
						|
 | 
						|
    final isOptFixed = isOptionFixed(kOptionCurrentAbName);
 | 
						|
    return DropdownButton2<String>(
 | 
						|
      value: gFFI.abModel.currentName.value,
 | 
						|
      onChanged: isOptFixed
 | 
						|
          ? null
 | 
						|
          : (value) {
 | 
						|
              if (value != null) {
 | 
						|
                gFFI.abModel.setCurrentName(value);
 | 
						|
                bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
 | 
						|
              }
 | 
						|
            },
 | 
						|
      customButton: Container(
 | 
						|
        height: isDesktop ? 48 : 40,
 | 
						|
        child: Row(children: [
 | 
						|
          Expanded(
 | 
						|
              child: buildItem(gFFI.abModel.currentName.value, button: true)),
 | 
						|
          Icon(Icons.arrow_drop_down),
 | 
						|
        ]),
 | 
						|
      ),
 | 
						|
      underline: Container(
 | 
						|
        height: 0.7,
 | 
						|
        color: Theme.of(context).dividerColor.withOpacity(0.1),
 | 
						|
      ),
 | 
						|
      menuItemStyleData: menuItemStyleData,
 | 
						|
      items: items,
 | 
						|
      isExpanded: true,
 | 
						|
      isDense: true,
 | 
						|
      dropdownSearchData: DropdownSearchData(
 | 
						|
        searchController: textEditingController,
 | 
						|
        searchInnerWidgetHeight: 50,
 | 
						|
        searchInnerWidget: Container(
 | 
						|
          height: 50,
 | 
						|
          padding: const EdgeInsets.only(
 | 
						|
            top: 8,
 | 
						|
            bottom: 4,
 | 
						|
            right: 8,
 | 
						|
            left: 8,
 | 
						|
          ),
 | 
						|
          child: TextFormField(
 | 
						|
            expands: true,
 | 
						|
            maxLines: null,
 | 
						|
            controller: textEditingController,
 | 
						|
            decoration: InputDecoration(
 | 
						|
              isDense: true,
 | 
						|
              contentPadding: const EdgeInsets.symmetric(
 | 
						|
                horizontal: 10,
 | 
						|
                vertical: 8,
 | 
						|
              ),
 | 
						|
              hintText: translate('Search'),
 | 
						|
              hintStyle: const TextStyle(fontSize: 12),
 | 
						|
              border: OutlineInputBorder(
 | 
						|
                borderRadius: BorderRadius.circular(8),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
        searchMatchFn: (item, searchValue) {
 | 
						|
          return item.value
 | 
						|
              .toString()
 | 
						|
              .toLowerCase()
 | 
						|
              .contains(searchValue.toLowerCase());
 | 
						|
        },
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Widget _buildTagHeader() {
 | 
						|
    return Row(
 | 
						|
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
						|
      children: [
 | 
						|
        Text(translate('Tags')),
 | 
						|
        Listener(
 | 
						|
            onPointerDown: (e) {
 | 
						|
              final x = e.position.dx;
 | 
						|
              final y = e.position.dy;
 | 
						|
              menuPos = RelativeRect.fromLTRB(x, y, x, y);
 | 
						|
            },
 | 
						|
            onPointerUp: (_) => _showMenu(menuPos),
 | 
						|
            child: build_more(context, invert: true)),
 | 
						|
      ],
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Widget _buildTags() {
 | 
						|
    return Obx(() {
 | 
						|
      final List tags;
 | 
						|
      if (gFFI.abModel.sortTags.value) {
 | 
						|
        tags = gFFI.abModel.currentAbTags.toList();
 | 
						|
        tags.sort();
 | 
						|
      } else {
 | 
						|
        tags = gFFI.abModel.currentAbTags;
 | 
						|
      }
 | 
						|
      final editPermission = gFFI.abModel.current.canWrite();
 | 
						|
      tagBuilder(String e) {
 | 
						|
        return AddressBookTag(
 | 
						|
            name: e,
 | 
						|
            tags: gFFI.abModel.selectedTags,
 | 
						|
            onTap: () {
 | 
						|
              if (gFFI.abModel.selectedTags.contains(e)) {
 | 
						|
                gFFI.abModel.selectedTags.remove(e);
 | 
						|
              } else {
 | 
						|
                gFFI.abModel.selectedTags.add(e);
 | 
						|
              }
 | 
						|
            },
 | 
						|
            showActionMenu: editPermission);
 | 
						|
      }
 | 
						|
 | 
						|
      final gridView = DynamicGridView.builder(
 | 
						|
          shrinkWrap: isMobile,
 | 
						|
          gridDelegate: SliverGridDelegateWithWrapping(),
 | 
						|
          itemCount: tags.length,
 | 
						|
          itemBuilder: (BuildContext context, int index) {
 | 
						|
            final e = tags[index];
 | 
						|
            return tagBuilder(e);
 | 
						|
          });
 | 
						|
      final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
 | 
						|
      return (isDesktop || isWebDesktop)
 | 
						|
          ? gridView
 | 
						|
          : LimitedBox(maxHeight: maxHeight, child: gridView);
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  Widget _buildPeersViews() {
 | 
						|
    return Expanded(
 | 
						|
      child: Align(
 | 
						|
          alignment: Alignment.topLeft,
 | 
						|
          child: AddressBookPeersView(
 | 
						|
            menuPadding: widget.menuPadding,
 | 
						|
            getInitPeers: () => gFFI.abModel.currentAbPeers,
 | 
						|
          )),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  @protected
 | 
						|
  MenuEntryBase<String> syncMenuItem() {
 | 
						|
    final isOptFixed = isOptionFixed(syncAbOption);
 | 
						|
    return MenuEntrySwitch<String>(
 | 
						|
      switchType: SwitchType.scheckbox,
 | 
						|
      text: translate('Sync with recent sessions'),
 | 
						|
      getter: () async {
 | 
						|
        return shouldSyncAb();
 | 
						|
      },
 | 
						|
      setter: (bool v) async {
 | 
						|
        gFFI.abModel.setShouldAsync(v);
 | 
						|
      },
 | 
						|
      dismissOnClicked: true,
 | 
						|
      enabled: (!isOptFixed).obs,
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  @protected
 | 
						|
  MenuEntryBase<String> sortMenuItem() {
 | 
						|
    final isOptFixed = isOptionFixed(sortAbTagsOption);
 | 
						|
    return MenuEntrySwitch<String>(
 | 
						|
      switchType: SwitchType.scheckbox,
 | 
						|
      text: translate('Sort tags'),
 | 
						|
      getter: () async {
 | 
						|
        return shouldSortTags();
 | 
						|
      },
 | 
						|
      setter: (bool v) async {
 | 
						|
        bind.mainSetLocalOption(
 | 
						|
            key: sortAbTagsOption, value: v ? 'Y' : defaultOptionNo);
 | 
						|
        gFFI.abModel.sortTags.value = v;
 | 
						|
      },
 | 
						|
      dismissOnClicked: true,
 | 
						|
      enabled: (!isOptFixed).obs,
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  @protected
 | 
						|
  MenuEntryBase<String> filterMenuItem() {
 | 
						|
    final isOptFixed = isOptionFixed(filterAbTagOption);
 | 
						|
    return MenuEntrySwitch<String>(
 | 
						|
      switchType: SwitchType.scheckbox,
 | 
						|
      text: translate('Filter by intersection'),
 | 
						|
      getter: () async {
 | 
						|
        return filterAbTagByIntersection();
 | 
						|
      },
 | 
						|
      setter: (bool v) async {
 | 
						|
        bind.mainSetLocalOption(
 | 
						|
            key: filterAbTagOption, value: v ? 'Y' : defaultOptionNo);
 | 
						|
        gFFI.abModel.filterByIntersection.value = v;
 | 
						|
      },
 | 
						|
      dismissOnClicked: true,
 | 
						|
      enabled: (!isOptFixed).obs,
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  void _showMenu(RelativeRect pos) {
 | 
						|
    final canWrite = gFFI.abModel.current.canWrite();
 | 
						|
    final items = [
 | 
						|
      if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb),
 | 
						|
      if (canWrite) getEntry(translate("Add Tag"), abAddTag),
 | 
						|
      getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
 | 
						|
      if (gFFI.abModel.legacyMode.value)
 | 
						|
        sortMenuItem(), // It's already sorted after pulling down
 | 
						|
      if (canWrite) syncMenuItem(),
 | 
						|
      filterMenuItem(),
 | 
						|
      if (!gFFI.abModel.legacyMode.value && canWrite)
 | 
						|
        MenuEntryDivider<String>(),
 | 
						|
      if (!gFFI.abModel.legacyMode.value && canWrite)
 | 
						|
        getEntry(translate("ab_web_console_tip"), () async {
 | 
						|
          final url = await bind.mainGetApiServer();
 | 
						|
          if (await canLaunchUrlString(url)) {
 | 
						|
            launchUrlString(url);
 | 
						|
          }
 | 
						|
        }),
 | 
						|
    ];
 | 
						|
 | 
						|
    mod_menu.showMenu(
 | 
						|
      context: context,
 | 
						|
      position: pos,
 | 
						|
      items: items
 | 
						|
          .map((e) => e.build(
 | 
						|
              context,
 | 
						|
              MenuConfig(
 | 
						|
                  commonColor: CustomPopupMenuTheme.commonColor,
 | 
						|
                  height: CustomPopupMenuTheme.height,
 | 
						|
                  dividerHeight: CustomPopupMenuTheme.dividerHeight)))
 | 
						|
          .expand((i) => i)
 | 
						|
          .toList(),
 | 
						|
      elevation: 8,
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  void addIdToCurrentAb() async {
 | 
						|
    if (gFFI.abModel.isCurrentAbFull(true)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    var isInProgress = false;
 | 
						|
    var passwordVisible = false;
 | 
						|
    IDTextEditingController idController = IDTextEditingController(text: '');
 | 
						|
    TextEditingController aliasController = TextEditingController(text: '');
 | 
						|
    TextEditingController passwordController = TextEditingController(text: '');
 | 
						|
    final tags = List.of(gFFI.abModel.currentAbTags);
 | 
						|
    var selectedTag = List<dynamic>.empty(growable: true).obs;
 | 
						|
    final style = TextStyle(fontSize: 14.0);
 | 
						|
    String? errorMsg;
 | 
						|
    final isCurrentAbShared = !gFFI.abModel.current.isPersonal();
 | 
						|
 | 
						|
    gFFI.dialogManager.show((setState, close, context) {
 | 
						|
      submit() async {
 | 
						|
        setState(() {
 | 
						|
          isInProgress = true;
 | 
						|
          errorMsg = null;
 | 
						|
        });
 | 
						|
        String id = idController.id;
 | 
						|
        if (id.isEmpty) {
 | 
						|
          // pass
 | 
						|
        } else {
 | 
						|
          if (gFFI.abModel.idContainByCurrent(id)) {
 | 
						|
            setState(() {
 | 
						|
              isInProgress = false;
 | 
						|
              errorMsg = translate('ID already exists');
 | 
						|
            });
 | 
						|
            return;
 | 
						|
          }
 | 
						|
          var password = '';
 | 
						|
          if (isCurrentAbShared) {
 | 
						|
            password = passwordController.text;
 | 
						|
          }
 | 
						|
          String? errMsg2 = await gFFI.abModel.addIdToCurrent(
 | 
						|
              id, aliasController.text.trim(), password, selectedTag);
 | 
						|
          if (errMsg2 != null) {
 | 
						|
            setState(() {
 | 
						|
              isInProgress = false;
 | 
						|
              errorMsg = errMsg2;
 | 
						|
            });
 | 
						|
            return;
 | 
						|
          }
 | 
						|
          // final currentPeers
 | 
						|
        }
 | 
						|
        close();
 | 
						|
      }
 | 
						|
 | 
						|
      double marginBottom = 4;
 | 
						|
 | 
						|
      row({required Widget lable, required Widget input}) {
 | 
						|
        return Row(
 | 
						|
          children: [
 | 
						|
            !isMobile
 | 
						|
                ? ConstrainedBox(
 | 
						|
                    constraints: const BoxConstraints(minWidth: 100),
 | 
						|
                    child: lable.marginOnly(right: 10))
 | 
						|
                : SizedBox.shrink(),
 | 
						|
            Expanded(
 | 
						|
              child: ConstrainedBox(
 | 
						|
                  constraints: const BoxConstraints(minWidth: 200),
 | 
						|
                  child: input),
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        ).marginOnly(bottom: !isMobile ? 8 : 0);
 | 
						|
      }
 | 
						|
 | 
						|
      return CustomAlertDialog(
 | 
						|
        title: Text(translate("Add ID")),
 | 
						|
        content: Column(
 | 
						|
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
          children: [
 | 
						|
            Column(
 | 
						|
              children: [
 | 
						|
                row(
 | 
						|
                    lable: Row(
 | 
						|
                      children: [
 | 
						|
                        Text(
 | 
						|
                          '*',
 | 
						|
                          style: TextStyle(color: Colors.red, fontSize: 14),
 | 
						|
                        ),
 | 
						|
                        Text(
 | 
						|
                          'ID',
 | 
						|
                          style: style,
 | 
						|
                        ),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                    input: TextField(
 | 
						|
                      controller: idController,
 | 
						|
                      inputFormatters: [IDTextInputFormatter()],
 | 
						|
                      decoration: InputDecoration(
 | 
						|
                          labelText: !isMobile ? null : translate('ID'),
 | 
						|
                          errorText: errorMsg,
 | 
						|
                          errorMaxLines: 5),
 | 
						|
                    )),
 | 
						|
                row(
 | 
						|
                  lable: Text(
 | 
						|
                    translate('Alias'),
 | 
						|
                    style: style,
 | 
						|
                  ),
 | 
						|
                  input: TextField(
 | 
						|
                      controller: aliasController,
 | 
						|
                      decoration: InputDecoration(
 | 
						|
                        labelText: !isMobile ? null : translate('Alias'),
 | 
						|
                      )),
 | 
						|
                ),
 | 
						|
                if (isCurrentAbShared)
 | 
						|
                  row(
 | 
						|
                      lable: Text(
 | 
						|
                        translate('Password'),
 | 
						|
                        style: style,
 | 
						|
                      ),
 | 
						|
                      input: TextField(
 | 
						|
                        controller: passwordController,
 | 
						|
                        obscureText: !passwordVisible,
 | 
						|
                        decoration: InputDecoration(
 | 
						|
                          labelText: !isMobile ? null : translate('Password'),
 | 
						|
                          suffixIcon: IconButton(
 | 
						|
                            icon: Icon(
 | 
						|
                                passwordVisible
 | 
						|
                                    ? Icons.visibility
 | 
						|
                                    : Icons.visibility_off,
 | 
						|
                                color: MyTheme.lightTheme.primaryColor),
 | 
						|
                            onPressed: () {
 | 
						|
                              setState(() {
 | 
						|
                                passwordVisible = !passwordVisible;
 | 
						|
                              });
 | 
						|
                            },
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                      )),
 | 
						|
                if (gFFI.abModel.currentAbTags.isNotEmpty)
 | 
						|
                  Align(
 | 
						|
                    alignment: Alignment.centerLeft,
 | 
						|
                    child: Text(
 | 
						|
                      translate('Tags'),
 | 
						|
                      style: style,
 | 
						|
                    ),
 | 
						|
                  ).marginOnly(top: 8, bottom: marginBottom),
 | 
						|
                if (gFFI.abModel.currentAbTags.isNotEmpty)
 | 
						|
                  Align(
 | 
						|
                    alignment: Alignment.centerLeft,
 | 
						|
                    child: Wrap(
 | 
						|
                      children: tags
 | 
						|
                          .map((e) => AddressBookTag(
 | 
						|
                              name: e,
 | 
						|
                              tags: selectedTag,
 | 
						|
                              onTap: () {
 | 
						|
                                if (selectedTag.contains(e)) {
 | 
						|
                                  selectedTag.remove(e);
 | 
						|
                                } else {
 | 
						|
                                  selectedTag.add(e);
 | 
						|
                                }
 | 
						|
                              },
 | 
						|
                              showActionMenu: false))
 | 
						|
                          .toList(growable: false),
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
            const SizedBox(
 | 
						|
              height: 4.0,
 | 
						|
            ),
 | 
						|
            if (!gFFI.abModel.current.isPersonal())
 | 
						|
              Row(children: [
 | 
						|
                Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
 | 
						|
                Text(
 | 
						|
                  translate('share_warning_tip'),
 | 
						|
                  style: TextStyle(fontSize: 12),
 | 
						|
                )
 | 
						|
              ]).marginSymmetric(vertical: 10),
 | 
						|
            // NOT use Offstage to wrap LinearProgressIndicator
 | 
						|
            if (isInProgress) const LinearProgressIndicator(),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
        actions: [
 | 
						|
          dialogButton("Cancel", onPressed: close, isOutline: true),
 | 
						|
          dialogButton("OK", onPressed: submit),
 | 
						|
        ],
 | 
						|
        onSubmit: submit,
 | 
						|
        onCancel: close,
 | 
						|
      );
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  void abAddTag() async {
 | 
						|
    var field = "";
 | 
						|
    var msg = "";
 | 
						|
    var isInProgress = false;
 | 
						|
    TextEditingController controller = TextEditingController(text: field);
 | 
						|
    gFFI.dialogManager.show((setState, close, context) {
 | 
						|
      submit() async {
 | 
						|
        setState(() {
 | 
						|
          msg = "";
 | 
						|
          isInProgress = true;
 | 
						|
        });
 | 
						|
        field = controller.text.trim();
 | 
						|
        if (field.isEmpty) {
 | 
						|
          // pass
 | 
						|
        } else {
 | 
						|
          final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
 | 
						|
          field = tags.join(',');
 | 
						|
          gFFI.abModel.addTags(tags);
 | 
						|
          // final currentPeers
 | 
						|
        }
 | 
						|
        close();
 | 
						|
      }
 | 
						|
 | 
						|
      return CustomAlertDialog(
 | 
						|
        title: Text(translate("Add Tag")),
 | 
						|
        content: Column(
 | 
						|
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
          children: [
 | 
						|
            Text(translate("whitelist_sep")),
 | 
						|
            const SizedBox(
 | 
						|
              height: 8.0,
 | 
						|
            ),
 | 
						|
            Row(
 | 
						|
              children: [
 | 
						|
                Expanded(
 | 
						|
                  child: TextField(
 | 
						|
                    maxLines: null,
 | 
						|
                    decoration: InputDecoration(
 | 
						|
                      errorText: msg.isEmpty ? null : translate(msg),
 | 
						|
                    ),
 | 
						|
                    controller: controller,
 | 
						|
                    autofocus: true,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
            const SizedBox(
 | 
						|
              height: 4.0,
 | 
						|
            ),
 | 
						|
            // NOT use Offstage to wrap LinearProgressIndicator
 | 
						|
            if (isInProgress) const LinearProgressIndicator(),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
        actions: [
 | 
						|
          dialogButton("Cancel", onPressed: close, isOutline: true),
 | 
						|
          dialogButton("OK", onPressed: submit),
 | 
						|
        ],
 | 
						|
        onSubmit: submit,
 | 
						|
        onCancel: close,
 | 
						|
      );
 | 
						|
    });
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class AddressBookTag extends StatelessWidget {
 | 
						|
  final String name;
 | 
						|
  final RxList<dynamic> tags;
 | 
						|
  final Function()? onTap;
 | 
						|
  final bool showActionMenu;
 | 
						|
 | 
						|
  const AddressBookTag(
 | 
						|
      {Key? key,
 | 
						|
      required this.name,
 | 
						|
      required this.tags,
 | 
						|
      this.onTap,
 | 
						|
      this.showActionMenu = true})
 | 
						|
      : super(key: key);
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    var pos = RelativeRect.fill;
 | 
						|
 | 
						|
    void setPosition(TapDownDetails e) {
 | 
						|
      final x = e.globalPosition.dx;
 | 
						|
      final y = e.globalPosition.dy;
 | 
						|
      pos = RelativeRect.fromLTRB(x, y, x, y);
 | 
						|
    }
 | 
						|
 | 
						|
    const double radius = 8;
 | 
						|
    return GestureDetector(
 | 
						|
      onTap: onTap,
 | 
						|
      onTapDown: showActionMenu ? setPosition : null,
 | 
						|
      onSecondaryTapDown: showActionMenu ? setPosition : null,
 | 
						|
      onSecondaryTap: showActionMenu ? () => _showMenu(context, pos) : null,
 | 
						|
      onLongPress: showActionMenu ? () => _showMenu(context, pos) : null,
 | 
						|
      child: Obx(() => Container(
 | 
						|
            decoration: BoxDecoration(
 | 
						|
                color: tags.contains(name)
 | 
						|
                    ? gFFI.abModel.getCurrentAbTagColor(name)
 | 
						|
                    : Theme.of(context).colorScheme.background,
 | 
						|
                borderRadius: BorderRadius.circular(4)),
 | 
						|
            margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
 | 
						|
            padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 6.0),
 | 
						|
            child: IntrinsicWidth(
 | 
						|
              child: Row(
 | 
						|
                children: [
 | 
						|
                  Container(
 | 
						|
                    width: radius,
 | 
						|
                    height: radius,
 | 
						|
                    decoration: BoxDecoration(
 | 
						|
                        shape: BoxShape.circle,
 | 
						|
                        color: tags.contains(name)
 | 
						|
                            ? Colors.white
 | 
						|
                            : gFFI.abModel.getCurrentAbTagColor(name)),
 | 
						|
                  ).marginOnly(right: radius / 2),
 | 
						|
                  Expanded(
 | 
						|
                    child: Text(name,
 | 
						|
                        style: TextStyle(
 | 
						|
                            overflow: TextOverflow.ellipsis,
 | 
						|
                            color: tags.contains(name) ? Colors.white : null)),
 | 
						|
                  ),
 | 
						|
                ],
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          )),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  void _showMenu(BuildContext context, RelativeRect pos) {
 | 
						|
    final items = [
 | 
						|
      getEntry(translate("Rename"), () {
 | 
						|
        renameDialog(
 | 
						|
            oldName: name,
 | 
						|
            validator: (String? newName) {
 | 
						|
              if (newName == null || newName.isEmpty) {
 | 
						|
                return translate('Can not be empty');
 | 
						|
              }
 | 
						|
              if (newName != name &&
 | 
						|
                  gFFI.abModel.currentAbTags.contains(newName)) {
 | 
						|
                return translate('Already exists');
 | 
						|
              }
 | 
						|
              return null;
 | 
						|
            },
 | 
						|
            onSubmit: (String newName) {
 | 
						|
              if (name != newName) {
 | 
						|
                gFFI.abModel.renameTag(name, newName);
 | 
						|
              }
 | 
						|
              Future.delayed(Duration.zero, () => Get.back());
 | 
						|
            },
 | 
						|
            onCancel: () {
 | 
						|
              Future.delayed(Duration.zero, () => Get.back());
 | 
						|
            });
 | 
						|
      }),
 | 
						|
      getEntry(translate(translate('Change Color')), () async {
 | 
						|
        final model = gFFI.abModel;
 | 
						|
        Color oldColor = model.getCurrentAbTagColor(name);
 | 
						|
        Color newColor = await showColorPickerDialog(
 | 
						|
          context,
 | 
						|
          oldColor,
 | 
						|
          pickersEnabled: {
 | 
						|
            ColorPickerType.accent: false,
 | 
						|
            ColorPickerType.wheel: true,
 | 
						|
          },
 | 
						|
          pickerTypeLabels: {
 | 
						|
            ColorPickerType.primary: translate("Primary Color"),
 | 
						|
            ColorPickerType.wheel: translate("HSV Color"),
 | 
						|
          },
 | 
						|
          actionButtons: ColorPickerActionButtons(
 | 
						|
              dialogOkButtonLabel: translate("OK"),
 | 
						|
              dialogCancelButtonLabel: translate("Cancel")),
 | 
						|
          showColorCode: true,
 | 
						|
        );
 | 
						|
        if (oldColor != newColor) {
 | 
						|
          model.setTagColor(name, newColor);
 | 
						|
        }
 | 
						|
      }),
 | 
						|
      getEntry(translate("Delete"), () {
 | 
						|
        gFFI.abModel.deleteTag(name);
 | 
						|
        Future.delayed(Duration.zero, () => Get.back());
 | 
						|
      }),
 | 
						|
    ];
 | 
						|
 | 
						|
    mod_menu.showMenu(
 | 
						|
      context: context,
 | 
						|
      position: pos,
 | 
						|
      items: items
 | 
						|
          .map((e) => e.build(
 | 
						|
              context,
 | 
						|
              MenuConfig(
 | 
						|
                  commonColor: CustomPopupMenuTheme.commonColor,
 | 
						|
                  height: CustomPopupMenuTheme.height,
 | 
						|
                  dividerHeight: CustomPopupMenuTheme.dividerHeight)))
 | 
						|
          .expand((i) => i)
 | 
						|
          .toList(),
 | 
						|
      elevation: 8,
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
MenuEntryButton<String> getEntry(String title, VoidCallback proc) {
 | 
						|
  return MenuEntryButton<String>(
 | 
						|
    childBuilder: (TextStyle? style) => Text(
 | 
						|
      title,
 | 
						|
      style: style,
 | 
						|
    ),
 | 
						|
    proc: proc,
 | 
						|
    dismissOnClicked: true,
 | 
						|
  );
 | 
						|
}
 |