diff --git a/lib/account/models/account.dart b/lib/account/models/account.dart index cdf42b4a1..10e6aa7ba 100644 --- a/lib/account/models/account.dart +++ b/lib/account/models/account.dart @@ -31,6 +31,7 @@ class Account { username: username, jwt: jwt, instance: instance, + anonymous: anonymous, userId: userId, index: index ?? this.index, ); @@ -53,6 +54,7 @@ class Account { username: Value(account.username), jwt: Value(account.jwt), instance: Value(account.instance), + anonymous: Value(account.anonymous), userId: Value(account.userId), listIndex: newIndex, ), @@ -81,8 +83,8 @@ class Account { username: Value(anonymousInstance.username), jwt: Value(anonymousInstance.jwt), instance: Value(anonymousInstance.instance), - userId: Value(anonymousInstance.userId), anonymous: Value(anonymousInstance.anonymous), + userId: Value(anonymousInstance.userId), listIndex: newIndex, ), ); @@ -103,6 +105,7 @@ class Account { username: account.username, jwt: account.jwt, instance: account.instance ?? '', + anonymous: account.anonymous, userId: account.userId, index: account.listIndex, )) @@ -122,6 +125,7 @@ class Account { username: account.username, jwt: account.jwt, instance: account.instance ?? '', + anonymous: account.anonymous, userId: account.userId, index: account.listIndex, )) @@ -143,6 +147,7 @@ class Account { username: account.username, jwt: account.jwt, instance: account.instance ?? '', + anonymous: account.anonymous, userId: account.userId, index: account.listIndex, ); @@ -160,6 +165,7 @@ class Account { username: Value(account.username), jwt: Value(account.jwt), instance: Value(account.instance), + anonymous: Value(account.anonymous), userId: Value(account.userId), listIndex: Value(account.index), )); diff --git a/lib/account/widgets/profile_modal_body.dart b/lib/account/widgets/profile_modal_body.dart index b6dc440b2..f0fc8199f 100644 --- a/lib/account/widgets/profile_modal_body.dart +++ b/lib/account/widgets/profile_modal_body.dart @@ -123,6 +123,11 @@ class _ProfileSelectState extends State { List? accounts; List? anonymousInstances; + bool areAccountsBeingReordered = false; + bool areAnonymousInstancesBeingReordered = false; + int? accountBeingReorderedIndex; + int? anonymousInstanceBeingReorderedIndex; + // Represents the ID of the account/instance we're currently logging out of / removing String? loggingOutId; @@ -185,368 +190,478 @@ class _ProfileSelectState extends State { title: Text(widget.customHeading ?? l10n.account(2)), centerTitle: false, scrolledUnderElevation: 0, - pinned: true, + pinned: false, actions: !widget.quickSelectMode ? [ + if ((accounts?.length ?? 0) > 1) + IconButton( + icon: areAccountsBeingReordered ? const Icon(Icons.check_rounded) : const Icon(Icons.edit_note_rounded), + tooltip: l10n.reorder, + onPressed: () => setState(() => areAccountsBeingReordered = !areAccountsBeingReordered), + ), IconButton( icon: const Icon(Icons.person_add), tooltip: l10n.addAccount, onPressed: () => widget.pushRegister(), ), - IconButton( - icon: const Icon(Icons.add), - tooltip: l10n.addAnonymousInstance, - onPressed: () => widget.pushRegister(anonymous: true), - ), const SizedBox(width: 12.0), ] : [], ), - SliverList.builder( - itemBuilder: (context, index) { - if (index < (accounts?.length ?? 0)) { - return Padding( - padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), - child: Material( - color: currentAccountId == accounts![index].account.id ? selectedColor : null, - borderRadius: BorderRadius.circular(50), - child: InkWell( - onTap: (currentAccountId == accounts![index].account.id) - ? null - : () { - context.read().add(SwitchAccount(accountId: accounts![index].account.id, reload: widget.reloadOnSave)); - context.pop(); - }, + if (accounts?.isNotEmpty == true) + SliverReorderableList( + onReorderStart: (index) => setState(() => accountBeingReorderedIndex = index), + onReorderEnd: (index) => setState(() => accountBeingReorderedIndex = null), + onReorder: (int oldIndex, int newIndex) { + setState(() { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final AccountExtended item = accounts!.removeAt(oldIndex); + accounts!.insert(newIndex, item); + }); + + _updateAccountIndices(); + }, + proxyDecorator: (child, index, animation) => Padding( + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: Material( + elevation: 5, + borderRadius: BorderRadius.circular(50), + child: child, + ), + ), + itemBuilder: (context, index) { + return ReorderableDragStartListener( + enabled: areAccountsBeingReordered, + key: Key('account-$index'), + index: index, + child: Padding( + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: Material( + color: currentAccountId == accounts![index].account.id ? selectedColor : null, borderRadius: BorderRadius.circular(50), - child: AnimatedSize( - duration: const Duration(milliseconds: 250), - child: ListTile( - leading: Stack( - children: [ - AnimatedCrossFade( - crossFadeState: accounts![index].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 500), - firstChild: const SizedBox( - child: Padding( - padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 8), - child: Icon( - Icons.person, + child: InkWell( + onTap: (currentAccountId == accounts![index].account.id) + ? null + : () { + context.read().add(SwitchAccount(accountId: accounts![index].account.id, reload: widget.reloadOnSave)); + context.pop(); + }, + borderRadius: BorderRadius.circular(50), + child: AnimatedSize( + duration: const Duration(milliseconds: 250), + child: ListTile( + leading: Stack( + children: [ + AnimatedCrossFade( + crossFadeState: accounts![index].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 500), + firstChild: const SizedBox( + child: Padding( + padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 8), + child: Icon( + Icons.person, + ), ), ), + secondChild: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundImage: accounts![index].instanceIcon == null ? null : CachedNetworkImageProvider(accounts![index].instanceIcon!), + maxRadius: 20, + ), ), - secondChild: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundImage: accounts![index].instanceIcon == null ? null : CachedNetworkImageProvider(accounts![index].instanceIcon!), - maxRadius: 20, - ), - ), - // This widget creates a slight border around the status indicator - Positioned( - right: 0, - bottom: 0, - child: SizedBox( - width: 12, - height: 12, - child: Material( - borderRadius: BorderRadius.circular(10), - color: currentAccountId == accounts![index].account.id ? selectedColor : null, + // This widget creates a slight border around the status indicator + Positioned( + right: 0, + bottom: 0, + child: SizedBox( + width: 12, + height: 12, + child: Material( + borderRadius: BorderRadius.circular(10), + color: currentAccountId == accounts![index].account.id ? selectedColor : null, + ), ), ), - ), - // This is the status indicator - Positioned( - right: 1, - bottom: 1, - child: AnimatedOpacity( - opacity: accounts![index].alive == null ? 0 : 1, - duration: const Duration(milliseconds: 500), - child: Icon( - accounts![index].alive == true ? Icons.check_circle_rounded : Icons.remove_circle_rounded, - size: 10, - color: Color.alphaBlend(theme.colorScheme.primaryContainer.withOpacity(0.6), accounts![index].alive == true ? Colors.green : Colors.red), + // This is the status indicator + Positioned( + right: 1, + bottom: 1, + child: AnimatedOpacity( + opacity: accounts![index].alive == null ? 0 : 1, + duration: const Duration(milliseconds: 500), + child: Icon( + accounts![index].alive == true ? Icons.check_circle_rounded : Icons.remove_circle_rounded, + size: 10, + color: Color.alphaBlend(theme.colorScheme.primaryContainer.withOpacity(0.6), accounts![index].alive == true ? Colors.green : Colors.red), + ), ), ), - ), - ], - ), - title: Text( - accounts![index].account.username ?? 'N/A', - style: theme.textTheme.titleMedium?.copyWith(), - ), - subtitle: Wrap( - children: [ - Text(accounts![index].account.instance.replaceAll('https://', '') ?? 'N/A'), - AnimatedSize( - duration: const Duration(milliseconds: 250), - child: accounts![index].version == null - ? const SizedBox(height: 20, width: 0) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 5), - Text( - '•', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + ], + ), + title: Text( + accounts![index].account.username ?? 'N/A', + style: theme.textTheme.titleMedium?.copyWith(), + ), + subtitle: Wrap( + children: [ + Text(accounts![index].account.instance.replaceAll('https://', '') ?? 'N/A'), + AnimatedSize( + duration: const Duration(milliseconds: 250), + child: accounts![index].version == null + ? const SizedBox(height: 20, width: 0) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 5), + Text( + '•', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + ), ), - ), - const SizedBox(width: 5), - Text( - 'v${accounts![index].version}', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + const SizedBox(width: 5), + Text( + 'v${accounts![index].version}', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + ), ), - ), - ], - ), - ), - AnimatedSize( - duration: const Duration(milliseconds: 250), - child: accounts![index].latency == null - ? const SizedBox(height: 20, width: 0) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 5), - Text( - '•', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + ], + ), + ), + AnimatedSize( + duration: const Duration(milliseconds: 250), + child: accounts![index].latency == null + ? const SizedBox(height: 20, width: 0) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 5), + Text( + '•', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + ), ), - ), - const SizedBox(width: 5), - Text( - '${accounts![index].latency?.inMilliseconds}ms', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + const SizedBox(width: 5), + Text( + '${accounts![index].latency?.inMilliseconds}ms', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + ), ), - ), - ], - ), - ), - ], + ], + ), + ), + ], + ), + trailing: !widget.quickSelectMode + ? areAccountsBeingReordered + ? const Icon(Icons.drag_handle) + : (currentAccountId == accounts![index].account.id) + ? IconButton( + icon: loggingOutId == accounts![index].account.id + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(), + ) + : Icon(Icons.logout, semanticLabel: AppLocalizations.of(context)!.logOut), + onPressed: () => _logOutOfActiveAccount(activeAccountId: accounts![index].account.id), + ) + : IconButton( + icon: loggingOutId == accounts![index].account.id + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(), + ) + : Icon( + Icons.delete, + semanticLabel: AppLocalizations.of(context)!.removeAccount, + ), + onPressed: () async { + context.read().add(RemoveAccount(accountId: accounts![index].account.id)); + + setState(() => loggingOutId = accounts![index].account.id); + + if (currentAccountId != null) { + await Future.delayed(const Duration(milliseconds: 1000), () { + context.read().add(SwitchAccount(accountId: currentAccountId)); + }); + } + + setState(() { + accounts = null; + loggingOutId = null; + }); + }) + : null, ), - trailing: !widget.quickSelectMode - ? (currentAccountId == accounts![index].account.id) - ? IconButton( - icon: loggingOutId == accounts![index].account.id - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(), - ) - : Icon(Icons.logout, semanticLabel: AppLocalizations.of(context)!.logOut), - onPressed: () => _logOutOfActiveAccount(activeAccountId: accounts![index].account.id), - ) - : IconButton( - icon: loggingOutId == accounts![index].account.id - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(), - ) - : Icon( - Icons.delete, - semanticLabel: AppLocalizations.of(context)!.removeAccount, - ), - onPressed: () async { - context.read().add(RemoveAccount(accountId: accounts![index].account.id)); - - setState(() => loggingOutId = accounts![index].account.id); - - if (currentAccountId != null) { - await Future.delayed(const Duration(milliseconds: 1000), () { - context.read().add(SwitchAccount(accountId: currentAccountId)); - }); - } - - setState(() { - accounts = null; - loggingOutId = null; - }); - }) - : null, ), ), ), ), ); - } else if (!widget.quickSelectMode) { - int realIndex = index - (accounts?.length ?? 0); - return Padding( + }, + itemCount: accounts!.length, + ), + if (accounts?.isNotEmpty != true) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(left: 24.0), + child: Text( + l10n.noAccountsAdded, + style: theme.textTheme.bodyMedium?.copyWith( + fontStyle: FontStyle.italic, + color: theme.textTheme.bodyMedium?.color?.withOpacity(0.5), + ), + ), + ), + ), + if (!widget.quickSelectMode) ...[ + SliverAppBar( + title: Text(l10n.anonymousInstances), + centerTitle: false, + scrolledUnderElevation: 0, + pinned: false, + actions: !widget.quickSelectMode + ? [ + if ((anonymousInstances?.length ?? 0) > 1) + IconButton( + icon: areAnonymousInstancesBeingReordered ? const Icon(Icons.check_rounded) : const Icon(Icons.edit_note_rounded), + tooltip: l10n.reorder, + onPressed: () => setState(() => areAnonymousInstancesBeingReordered = !areAnonymousInstancesBeingReordered), + ), + IconButton( + icon: const Icon(Icons.add), + tooltip: l10n.addAnonymousInstance, + onPressed: () => widget.pushRegister(anonymous: true), + ), + const SizedBox(width: 12.0), + ] + : [], + ), + if (anonymousInstances?.isNotEmpty == true) + SliverReorderableList( + onReorderStart: (index) => setState(() => anonymousInstanceBeingReorderedIndex = index), + onReorderEnd: (index) => setState(() => anonymousInstanceBeingReorderedIndex = null), + onReorder: (int oldIndex, int newIndex) { + setState(() { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final AnonymousInstanceExtended item = anonymousInstances!.removeAt(oldIndex); + anonymousInstances!.insert(newIndex, item); + }); + + _updateAccountIndices(); + }, + proxyDecorator: (child, index, animation) => Padding( padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), child: Material( - color: currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].anonymousInstance.instance ? selectedColor : null, + elevation: 5, borderRadius: BorderRadius.circular(50), - child: InkWell( - onTap: (currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].anonymousInstance.instance) - ? null - : () async { - context.read().add(const LogOutOfAllAccounts()); - context.read().add(OnSetCurrentAnonymousInstance(anonymousInstances![realIndex].anonymousInstance.instance)); - context.read().add(InstanceChanged(instance: anonymousInstances![realIndex].anonymousInstance.instance)); - context.pop(); - }, - borderRadius: BorderRadius.circular(50), - child: AnimatedSize( - duration: const Duration(milliseconds: 250), - child: ListTile( - leading: Stack( - children: [ - AnimatedCrossFade( - crossFadeState: anonymousInstances![realIndex].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 500), - firstChild: const SizedBox( - child: Padding( - padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 8), - child: Icon( - Icons.language, + child: child, + ), + ), + itemBuilder: (context, index) { + return ReorderableDragStartListener( + enabled: areAnonymousInstancesBeingReordered, + key: Key('anonymous-$index'), + index: index, + child: Padding( + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: Material( + elevation: anonymousInstanceBeingReorderedIndex == index ? 3 : 0, + color: currentAccountId == null && currentAnonymousInstance == anonymousInstances![index].anonymousInstance.instance ? selectedColor : null, + borderRadius: BorderRadius.circular(50), + child: InkWell( + onTap: (currentAccountId == null && currentAnonymousInstance == anonymousInstances![index].anonymousInstance.instance) + ? null + : () async { + context.read().add(const LogOutOfAllAccounts()); + context.read().add(OnSetCurrentAnonymousInstance(anonymousInstances![index].anonymousInstance.instance)); + context.read().add(InstanceChanged(instance: anonymousInstances![index].anonymousInstance.instance)); + context.pop(); + }, + borderRadius: BorderRadius.circular(50), + child: AnimatedSize( + duration: const Duration(milliseconds: 250), + child: ListTile( + leading: Stack( + children: [ + AnimatedCrossFade( + crossFadeState: anonymousInstances![index].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 500), + firstChild: const SizedBox( + child: Padding( + padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 8), + child: Icon( + Icons.language, + ), + ), + ), + secondChild: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundImage: anonymousInstances![index].instanceIcon == null ? null : CachedNetworkImageProvider(anonymousInstances![index].instanceIcon!), + maxRadius: 20, ), ), - ), - secondChild: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundImage: anonymousInstances![realIndex].instanceIcon == null ? null : CachedNetworkImageProvider(anonymousInstances![realIndex].instanceIcon!), - maxRadius: 20, - ), - ), - Positioned( - right: 0, - bottom: 0, - child: SizedBox( - width: 12, - height: 12, - child: Material( - borderRadius: BorderRadius.circular(10), - color: currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].anonymousInstance.instance ? selectedColor : null, + Positioned( + right: 0, + bottom: 0, + child: SizedBox( + width: 12, + height: 12, + child: Material( + borderRadius: BorderRadius.circular(10), + color: currentAccountId == null && currentAnonymousInstance == anonymousInstances![index].anonymousInstance.instance ? selectedColor : null, + ), + ), ), - ), - ), - // This is the status indicator - Positioned( - right: 1, - bottom: 1, - child: AnimatedOpacity( - opacity: anonymousInstances![realIndex].alive == null ? 0 : 1, - duration: const Duration(milliseconds: 500), - child: Icon( - anonymousInstances![realIndex].alive == true ? Icons.check_circle_rounded : Icons.remove_circle_rounded, - size: 10, - color: Color.alphaBlend(theme.colorScheme.primaryContainer.withOpacity(0.6), anonymousInstances![realIndex].alive == true ? Colors.green : Colors.red), + // This is the status indicator + Positioned( + right: 1, + bottom: 1, + child: AnimatedOpacity( + opacity: anonymousInstances![index].alive == null ? 0 : 1, + duration: const Duration(milliseconds: 500), + child: Icon( + anonymousInstances![index].alive == true ? Icons.check_circle_rounded : Icons.remove_circle_rounded, + size: 10, + color: Color.alphaBlend(theme.colorScheme.primaryContainer.withOpacity(0.6), anonymousInstances![index].alive == true ? Colors.green : Colors.red), + ), + ), ), - ), - ), - ], - ), - title: Row( - children: [ - const Icon( - Icons.person_off_rounded, - size: 15, + ], ), - const SizedBox(width: 5), - Text( - AppLocalizations.of(context)!.anonymous, - style: theme.textTheme.titleMedium?.copyWith(), - ), - ], - ), - subtitle: Wrap( - children: [ - Text(anonymousInstances![realIndex].anonymousInstance.instance), - AnimatedSize( - duration: const Duration(milliseconds: 250), - child: anonymousInstances![realIndex].version == null - ? const SizedBox(height: 20, width: 0) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 5), - Text( - '•', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), - ), - ), - const SizedBox(width: 5), - Text( - 'v${anonymousInstances![realIndex].version}', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), - ), - ), - ], - ), + title: Row( + children: [ + const Icon( + Icons.person_off_rounded, + size: 15, + ), + const SizedBox(width: 5), + Text( + AppLocalizations.of(context)!.anonymous, + style: theme.textTheme.titleMedium?.copyWith(), + ), + ], ), - AnimatedSize( - duration: const Duration(milliseconds: 250), - child: anonymousInstances![realIndex].latency == null - ? const SizedBox(height: 20, width: 0) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 5), - Text( - '•', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), - ), + subtitle: Wrap( + children: [ + Text(anonymousInstances![index].anonymousInstance.instance), + AnimatedSize( + duration: const Duration(milliseconds: 250), + child: anonymousInstances![index].version == null + ? const SizedBox(height: 20, width: 0) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 5), + Text( + '•', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + ), + ), + const SizedBox(width: 5), + Text( + 'v${anonymousInstances![index].version}', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + ), + ), + ], ), - const SizedBox(width: 5), - Text( - '${anonymousInstances![realIndex].latency?.inMilliseconds}ms', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), - ), + ), + AnimatedSize( + duration: const Duration(milliseconds: 250), + child: anonymousInstances![index].latency == null + ? const SizedBox(height: 20, width: 0) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 5), + Text( + '•', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + ), + ), + const SizedBox(width: 5), + Text( + '${anonymousInstances![index].latency?.inMilliseconds}ms', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + ), + ), + ], ), - ], - ), + ), + ], ), - ], + trailing: !widget.quickSelectMode + ? areAnonymousInstancesBeingReordered + ? const Icon(Icons.drag_handle) + : ((accounts?.length ?? 0) > 0 || anonymousInstances!.length > 1) + ? (currentAccountId == null && currentAnonymousInstance == anonymousInstances![index].anonymousInstance.instance) + ? IconButton( + icon: Icon(Icons.logout, semanticLabel: AppLocalizations.of(context)!.removeInstance), + onPressed: () async { + await Account.deleteAnonymousInstance(anonymousInstances![index].anonymousInstance.instance); + + if (anonymousInstances!.length > 1) { + context.read().add(OnSetCurrentAnonymousInstance( + anonymousInstances!.lastWhere((instance) => instance != anonymousInstances![index]).anonymousInstance.instance)); + context.read().add( + InstanceChanged(instance: anonymousInstances!.lastWhere((instance) => instance != anonymousInstances![index]).anonymousInstance.instance)); + } else { + context.read().add(SwitchAccount(accountId: accounts!.last.account.id)); + } + + setState(() => anonymousInstances = null); + }, + ) + : IconButton( + icon: Icon( + Icons.delete, + semanticLabel: AppLocalizations.of(context)!.removeInstance, + ), + onPressed: () async { + await Account.deleteAnonymousInstance(anonymousInstances![index].anonymousInstance.instance); + setState(() { + anonymousInstances = null; + }); + }) + : null + : null, + ), ), - trailing: !widget.quickSelectMode && ((accounts?.length ?? 0) > 0 || anonymousInstances!.length > 1) - ? (currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].anonymousInstance.instance) - ? IconButton( - icon: Icon(Icons.logout, semanticLabel: AppLocalizations.of(context)!.removeInstance), - onPressed: () async { - await Account.deleteAnonymousInstance(anonymousInstances![realIndex].anonymousInstance.instance); - - if (anonymousInstances!.length > 1) { - context - .read() - .add(OnSetCurrentAnonymousInstance(anonymousInstances!.lastWhere((instance) => instance != anonymousInstances![realIndex]).anonymousInstance.instance)); - context - .read() - .add(InstanceChanged(instance: anonymousInstances!.lastWhere((instance) => instance != anonymousInstances![realIndex]).anonymousInstance.instance)); - } else { - context.read().add(SwitchAccount(accountId: accounts!.last.account.id)); - } - - setState(() => anonymousInstances = null); - }, - ) - : IconButton( - icon: Icon( - Icons.delete, - semanticLabel: AppLocalizations.of(context)!.removeInstance, - ), - onPressed: () async { - await Account.deleteAnonymousInstance(anonymousInstances![realIndex].anonymousInstance.instance); - setState(() { - anonymousInstances = null; - }); - }) - : null, ), ), ), + ); + }, + itemCount: anonymousInstances!.length, + ), + if (anonymousInstances?.isNotEmpty != true) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(left: 24.0), + child: Text( + l10n.noAnonymousInstances, + style: theme.textTheme.bodyMedium?.copyWith( + fontStyle: FontStyle.italic, + color: theme.textTheme.bodyMedium?.color?.withOpacity(0.5), + ), ), - ); - } - return null; - }, - itemCount: (accounts?.length ?? 0) + (anonymousInstances?.length ?? 0), - ), + ), + ), + ], const SliverToBoxAdapter(child: SizedBox(height: 100)) ], ), @@ -663,6 +778,19 @@ class _ProfileSelectState extends State { setState(() => anonymousInstanceExtended.latency = pingData.response?.time); }); } + + /// Recalculates the indices of all accounts and anonymous instances in the database, given the current order in the UI. + /// We need to calculate both accounts and anonymous instances, using an offset for the latter, + /// because they are separate lists in the UI but they are in the same database table. + void _updateAccountIndices() { + for (AccountExtended accountExtended in accounts!) { + Account.updateAccount(accountExtended.account.copyWith(index: accounts!.indexOf(accountExtended))); + } + + for (AnonymousInstanceExtended anonymousInstanceExtended in anonymousInstances!) { + Account.updateAccount(anonymousInstanceExtended.anonymousInstance.copyWith(index: (accounts?.length ?? 0) + anonymousInstances!.indexOf(anonymousInstanceExtended))); + } + } } /// Wrapper class around Account with support for instance icon diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a0315bc2e..a64ddb4d2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -109,6 +109,10 @@ "@animations": {}, "anonymous": "Anonymous", "@anonymous": {}, + "anonymousInstances": "Anonymous Instances", + "@anonymousInstances": { + "description": "Heading for the anonymous instances section of the login page" + }, "appLanguage": "App Language", "@appLanguage": { "description": "Language selection in settings." @@ -1361,6 +1365,14 @@ "@no": { "description": "A negative status" }, + "noAccountsAdded": "No accounts have been added", + "@noAccountsAdded": { + "description": "Placeholder text when no accounts have been added" + }, + "noAnonymousInstances": "No anonymous instances have been added", + "@noAnonymousInstances": { + "description": "Placeholder text when no anonymous instances have been added" + }, "noComments": "Oh. There are no comments.", "@noComments": {}, "noCommentsFound": "No comments found.", @@ -1753,6 +1765,10 @@ "@removedPost": { "description": "Short decription for moderator action to remove a post" }, + "reorder": "Reorder", + "@reorder": { + "description": "Tooltip text for the list editor button" + }, "reply": "{count, plural, zero {Reply} one {Reply} other {Replies} }", "@reply": {}, "replyColor": "Reply Color",