add tab disconnect option via context menu
X button on secondary tabs now opens a MenuAnchor popup with Disconnect (deactivates without removing) and Remove options. Menu styled to match app overlay surfaces (12px radius, outline border). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
89f8859ba3
commit
4c696745b0
2 changed files with 118 additions and 24 deletions
|
|
@ -531,6 +531,39 @@ abstract class ChatTabsStoreBase with Store {
|
|||
return true;
|
||||
}
|
||||
|
||||
/// Deactivates a tab by disposing its ChatStore without removing the tab.
|
||||
/// The tab stays in the tab bar but appears dimmed. Tapping reactivates it.
|
||||
@action
|
||||
void deactivateTab(int index) {
|
||||
if (index < 0 || index >= _tabs.length) return;
|
||||
final tab = _tabs[index];
|
||||
if (tab.isPrimary) return;
|
||||
if (tab.chatStore == null) return;
|
||||
|
||||
// If this is the active tab, switch to primary
|
||||
if (index == activeTabIndex) {
|
||||
activeTabIndex = 0;
|
||||
}
|
||||
|
||||
// Clean up merged mode state
|
||||
if (mergedMode) {
|
||||
_tabChannelProfiles.remove(tab.channelId);
|
||||
_mergedSnapshot = null;
|
||||
}
|
||||
|
||||
// Dispose the ChatStore (closes IRC connection)
|
||||
tab.chatStore!.dispose();
|
||||
tab.chatStore = null;
|
||||
|
||||
// Exit merged mode if fewer than 2 tabs remain activated
|
||||
if (mergedMode && _tabs.where((t) => t.chatStore != null).length < 2) {
|
||||
mergedMode = false;
|
||||
_mergedRenderTimer?.cancel();
|
||||
_mergedRenderTimer = null;
|
||||
_mergedRenderedMessages = [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the tab at the given index.
|
||||
/// Returns true if removed, false if primary tab or invalid index.
|
||||
@action
|
||||
|
|
|
|||
|
|
@ -245,32 +245,93 @@ class ChatTabs extends StatelessWidget {
|
|||
|
||||
final avatar = ProfilePicture(userLogin: tabInfo.channelLogin, radius: 12);
|
||||
|
||||
return InputChip(
|
||||
avatar: isActivated ? avatar : Opacity(opacity: 0.5, child: avatar),
|
||||
label: Text(
|
||||
displayName,
|
||||
style: isActivated
|
||||
? null
|
||||
: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.color?.withValues(alpha: 0.5),
|
||||
),
|
||||
if (tabInfo.isPrimary) {
|
||||
return InputChip(
|
||||
avatar: avatar,
|
||||
label: Text(displayName),
|
||||
selected: isActive,
|
||||
showCheckmark: false,
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed: () {
|
||||
if (!isActive) {
|
||||
HapticFeedback.selectionClick();
|
||||
chatTabsStore.setActiveTab(index);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final borderColor =
|
||||
Theme.of(context).colorScheme.outlineVariant.withValues(alpha: 0.5);
|
||||
|
||||
return MenuAnchor(
|
||||
style: MenuStyle(
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 4)),
|
||||
shape: WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: borderColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
selected: isActive,
|
||||
showCheckmark: false,
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed: () {
|
||||
if (!isActive) {
|
||||
HapticFeedback.selectionClick();
|
||||
chatTabsStore.setActiveTab(index);
|
||||
}
|
||||
menuChildren: [
|
||||
if (isActivated)
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.power_off_rounded, size: 18),
|
||||
style: MenuItemButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
child: const Text('Disconnect'),
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
chatTabsStore.deactivateTab(index);
|
||||
},
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: Icon(
|
||||
Icons.delete_outline_rounded,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
style: MenuItemButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
child: Text(
|
||||
'Remove',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
onPressed: () => _confirmRemoveTab(context, index, displayName),
|
||||
),
|
||||
],
|
||||
builder: (context, controller, child) {
|
||||
return InputChip(
|
||||
avatar:
|
||||
isActivated ? avatar : Opacity(opacity: 0.5, child: avatar),
|
||||
label: Text(
|
||||
displayName,
|
||||
style: isActivated
|
||||
? null
|
||||
: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.color?.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
selected: isActive,
|
||||
showCheckmark: false,
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed: () {
|
||||
if (!isActive) {
|
||||
HapticFeedback.selectionClick();
|
||||
chatTabsStore.setActiveTab(index);
|
||||
}
|
||||
},
|
||||
onDeleted: () => controller.open(),
|
||||
deleteButtonTooltipMessage: 'Tab options',
|
||||
);
|
||||
},
|
||||
onDeleted: tabInfo.isPrimary
|
||||
? null
|
||||
: () => _confirmRemoveTab(context, index, displayName),
|
||||
deleteButtonTooltipMessage: 'Close chat',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue