bottom sheet redesign
This commit is contained in:
+324
-223
@@ -110,6 +110,9 @@ class _BrowserMenuSheet extends HookConsumerWidget {
|
|||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final selectedTabId = ref.watch(selectedTabProvider);
|
final selectedTabId = ref.watch(selectedTabProvider);
|
||||||
final settings = ref.watch(generalSettingsWithDefaultsProvider);
|
final settings = ref.watch(generalSettingsWithDefaultsProvider);
|
||||||
|
final gesturesEnabled = ref.watch(
|
||||||
|
gestureSettingsWithDefaultsProvider.select((s) => s.enabled),
|
||||||
|
);
|
||||||
|
|
||||||
return DraggableScrollableSheet(
|
return DraggableScrollableSheet(
|
||||||
initialChildSize: 0.85,
|
initialChildSize: 0.85,
|
||||||
@@ -143,8 +146,8 @@ class _BrowserMenuSheet extends HookConsumerWidget {
|
|||||||
vertical: 8,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
// Quick toggles (Desktop Mode / Reader Mode)
|
// Quick toggles (Desktop / Reader / Gestures)
|
||||||
if (selectedTabId != null) ...[
|
if (selectedTabId != null || gesturesEnabled) ...[
|
||||||
_QuickTogglesGrid(selectedTabId: selectedTabId),
|
_QuickTogglesGrid(selectedTabId: selectedTabId),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
@@ -171,20 +174,14 @@ class _BrowserMenuSheet extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Connection (Tor + proxy profiles)
|
||||||
|
const _ConnectionCard(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Profile
|
// Profile
|
||||||
_ProfileCard(),
|
_ProfileCard(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Gestures quick toggle (only when the master switch is on)
|
|
||||||
if (ref.watch(
|
|
||||||
gestureSettingsWithDefaultsProvider.select(
|
|
||||||
(s) => s.enabled,
|
|
||||||
),
|
|
||||||
)) ...[
|
|
||||||
const _GestureToggleTile(),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
|
|
||||||
// App
|
// App
|
||||||
const _SettingsCard(),
|
const _SettingsCard(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -585,136 +582,210 @@ class _PageActionsCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
// ─── Quick Toggles Grid ───
|
// ─── Quick Toggles Grid ───
|
||||||
|
|
||||||
class _QuickTogglesGrid extends HookConsumerWidget {
|
/// Unified bar of quick toggles. Page-scoped toggles (Desktop / Reader) appear
|
||||||
final String selectedTabId;
|
/// only when a tab is selected; the global Gestures toggle appears whenever the
|
||||||
|
/// gesture master switch is on. They are laid out as a connected, equal-width
|
||||||
|
/// segmented bar (icon over label) that wraps to a new row past four toggles.
|
||||||
|
class _QuickTogglesGrid extends ConsumerWidget {
|
||||||
|
final String? selectedTabId;
|
||||||
|
|
||||||
const _QuickTogglesGrid({required this.selectedTabId});
|
const _QuickTogglesGrid({required this.selectedTabId});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final desktopEnabled = ref.watch(desktopModeProvider(selectedTabId));
|
final gestureSettings = ref.watch(gestureSettingsWithDefaultsProvider);
|
||||||
|
|
||||||
final readerChanging = ref.watch(readerableScreenControllerProvider);
|
final toggles = <_QuickToggle>[];
|
||||||
final readerabilityState = ref.watch(
|
|
||||||
selectedTabStateProvider.select(
|
|
||||||
(state) => state?.readerableState ?? ReaderableState.$default(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final isReaderActive = readerabilityState.active;
|
|
||||||
final isReaderLoading = readerChanging.isLoading;
|
|
||||||
|
|
||||||
final enableReadability = ref.watch(
|
if (selectedTabId case final tabId?) {
|
||||||
generalSettingsWithDefaultsProvider.select(
|
final desktopEnabled = ref.watch(desktopModeProvider(tabId));
|
||||||
(value) => value.enableReadability,
|
toggles.add(
|
||||||
),
|
_QuickToggle(
|
||||||
);
|
icon: MdiIcons.monitor,
|
||||||
final enforceReadability = ref.watch(
|
label: 'Desktop',
|
||||||
generalSettingsWithDefaultsProvider.select(
|
active: desktopEnabled,
|
||||||
(value) => value.enforceReadability,
|
onTap: () {
|
||||||
),
|
ref
|
||||||
);
|
.read(desktopModeProvider(tabId).notifier)
|
||||||
final readerVisible =
|
.enabled(!desktopEnabled);
|
||||||
(readerabilityState.readerable &&
|
},
|
||||||
(enableReadability || readerabilityState.active)) ||
|
|
||||||
(enforceReadability && enableReadability);
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _ToggleTile(
|
|
||||||
icon: MdiIcons.monitor,
|
|
||||||
label: 'Desktop Mode',
|
|
||||||
active: desktopEnabled,
|
|
||||||
onTap: () {
|
|
||||||
ref
|
|
||||||
.read(desktopModeProvider(selectedTabId).notifier)
|
|
||||||
.enabled(!desktopEnabled);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
);
|
||||||
Expanded(
|
|
||||||
child: readerVisible
|
final isReaderLoading = ref
|
||||||
? _ToggleTile(
|
.watch(readerableScreenControllerProvider)
|
||||||
icon: isReaderActive
|
.isLoading;
|
||||||
? MdiIcons.bookOpen
|
final readerabilityState = ref.watch(
|
||||||
: MdiIcons.bookOpenOutline,
|
selectedTabStateProvider.select(
|
||||||
label: 'Reader Mode',
|
(state) => state?.readerableState ?? ReaderableState.$default(),
|
||||||
active: isReaderActive,
|
|
||||||
enabled: !isReaderLoading,
|
|
||||||
onTap: () async {
|
|
||||||
await ref
|
|
||||||
.read(readerableScreenControllerProvider.notifier)
|
|
||||||
.toggleReaderView(!isReaderActive);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: _ToggleTile(
|
|
||||||
icon: MdiIcons.bookOpenOutline,
|
|
||||||
label: 'Reader Mode',
|
|
||||||
active: false,
|
|
||||||
enabled: false,
|
|
||||||
onTap: () {},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
);
|
final isReaderActive = readerabilityState.active;
|
||||||
|
final enableReadability = ref.watch(
|
||||||
|
generalSettingsWithDefaultsProvider.select(
|
||||||
|
(value) => value.enableReadability,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final enforceReadability = ref.watch(
|
||||||
|
generalSettingsWithDefaultsProvider.select(
|
||||||
|
(value) => value.enforceReadability,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final readerVisible =
|
||||||
|
(readerabilityState.readerable &&
|
||||||
|
(enableReadability || readerabilityState.active)) ||
|
||||||
|
(enforceReadability && enableReadability);
|
||||||
|
|
||||||
|
toggles.add(
|
||||||
|
_QuickToggle(
|
||||||
|
icon: (readerVisible && isReaderActive)
|
||||||
|
? MdiIcons.bookOpen
|
||||||
|
: MdiIcons.bookOpenOutline,
|
||||||
|
label: 'Reader',
|
||||||
|
active: readerVisible && isReaderActive,
|
||||||
|
enabled: readerVisible && !isReaderLoading,
|
||||||
|
onTap: () async {
|
||||||
|
await ref
|
||||||
|
.read(readerableScreenControllerProvider.notifier)
|
||||||
|
.toggleReaderView(!isReaderActive);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gestureSettings.enabled) {
|
||||||
|
toggles.add(
|
||||||
|
_QuickToggle(
|
||||||
|
icon: MdiIcons.gestureSwipe,
|
||||||
|
label: 'Gestures',
|
||||||
|
active: gestureSettings.active,
|
||||||
|
onTap: () async {
|
||||||
|
await ref
|
||||||
|
.read(gestureSettingsRepositoryProvider.notifier)
|
||||||
|
.updateSettings(
|
||||||
|
(s) => s.copyWith(active: !gestureSettings.active),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toggles.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return _QuickToggleBar(toggles: toggles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ToggleTile extends StatelessWidget {
|
/// A single quick toggle's display + behavior, rendered as one segment of a
|
||||||
|
/// [_QuickToggleBar].
|
||||||
|
class _QuickToggle {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String label;
|
final String label;
|
||||||
final bool active;
|
final bool active;
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
const _ToggleTile({
|
const _QuickToggle({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.active,
|
required this.active,
|
||||||
this.enabled = true,
|
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
|
this.enabled = true,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connected, equal-width segmented bar (icon over label) for the quick
|
||||||
|
/// toggles. Segments are split across rows of at most [_maxPerRow] so the bar
|
||||||
|
/// stays compact and resizes to however many toggles are present.
|
||||||
|
class _QuickToggleBar extends StatelessWidget {
|
||||||
|
final List<_QuickToggle> toggles;
|
||||||
|
|
||||||
|
static const int _maxPerRow = 4;
|
||||||
|
|
||||||
|
const _QuickToggleBar({required this.toggles});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final rows = <List<_QuickToggle>>[];
|
||||||
|
for (var i = 0; i < toggles.length; i += _maxPerRow) {
|
||||||
|
final end = i + _maxPerRow <= toggles.length
|
||||||
|
? i + _maxPerRow
|
||||||
|
: toggles.length;
|
||||||
|
rows.add(toggles.sublist(i, end));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
for (var r = 0; r < rows.length; r++) ...[
|
||||||
|
if (r > 0) const SizedBox(height: 8),
|
||||||
|
_buildRow(context, rows[r]),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRow(BuildContext context, List<_QuickToggle> items) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: colorScheme.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < items.length; i++) ...[
|
||||||
|
if (i > 0)
|
||||||
|
VerticalDivider(
|
||||||
|
width: 1,
|
||||||
|
thickness: 1,
|
||||||
|
color: colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
Expanded(child: _QuickToggleSegment(toggle: items[i])),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuickToggleSegment extends StatelessWidget {
|
||||||
|
final _QuickToggle toggle;
|
||||||
|
|
||||||
|
const _QuickToggleSegment({required this.toggle});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
final backgroundColor = active
|
final foregroundColor = !toggle.enabled
|
||||||
? colorScheme.primary
|
? colorScheme.onSurface.withValues(alpha: 0.38)
|
||||||
: colorScheme.surfaceContainerHigh;
|
: toggle.active
|
||||||
final foregroundColor = active
|
? colorScheme.onSecondaryContainer
|
||||||
? colorScheme.onPrimary
|
: colorScheme.onSurfaceVariant;
|
||||||
: colorScheme.onSurface;
|
|
||||||
final effectiveAlpha = enabled ? 1.0 : 0.38;
|
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: backgroundColor.withValues(alpha: enabled ? 1.0 : 0.5),
|
color: toggle.active
|
||||||
borderRadius: BorderRadius.circular(28),
|
? colorScheme.secondaryContainer
|
||||||
clipBehavior: Clip.antiAlias,
|
: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: enabled ? onTap : null,
|
onTap: toggle.enabled ? toggle.onTap : null,
|
||||||
borderRadius: BorderRadius.circular(28),
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||||
child: Row(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(toggle.icon, color: foregroundColor, size: 22),
|
||||||
icon,
|
const SizedBox(height: 6),
|
||||||
color: foregroundColor.withValues(alpha: effectiveAlpha),
|
Text(
|
||||||
size: 22,
|
toggle.label,
|
||||||
),
|
textAlign: TextAlign.center,
|
||||||
const SizedBox(width: 10),
|
maxLines: 1,
|
||||||
Expanded(
|
overflow: TextOverflow.ellipsis,
|
||||||
child: Text(
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||||
label,
|
color: foregroundColor,
|
||||||
style: TextStyle(
|
fontWeight: FontWeight.w500,
|
||||||
color: foregroundColor.withValues(alpha: effectiveAlpha),
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -944,34 +1015,6 @@ class _FetchFeedsTile extends HookConsumerWidget {
|
|||||||
|
|
||||||
// ─── Gestures Quick Toggle ───
|
// ─── Gestures Quick Toggle ───
|
||||||
|
|
||||||
class _GestureToggleTile extends ConsumerWidget {
|
|
||||||
const _GestureToggleTile();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final active = ref.watch(
|
|
||||||
gestureSettingsWithDefaultsProvider.select((s) => s.active),
|
|
||||||
);
|
|
||||||
|
|
||||||
return _buildMenuCard(
|
|
||||||
context,
|
|
||||||
children: [
|
|
||||||
SwitchListTile.adaptive(
|
|
||||||
secondary: const Icon(MdiIcons.gestureSwipe),
|
|
||||||
title: const Text('Gestures'),
|
|
||||||
subtitle: Text(active ? 'Active' : 'Suspended'),
|
|
||||||
value: active,
|
|
||||||
onChanged: (value) async {
|
|
||||||
await ref
|
|
||||||
.read(gestureSettingsRepositoryProvider.notifier)
|
|
||||||
.updateSettings((s) => s.copyWith(active: value));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Tab Actions Card ───
|
// ─── Tab Actions Card ───
|
||||||
|
|
||||||
class _TabActionsCard extends HookConsumerWidget {
|
class _TabActionsCard extends HookConsumerWidget {
|
||||||
@@ -1966,12 +2009,13 @@ class _ExtensionsCard extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Quick Links Grid ───
|
// ─── Connection Card ───
|
||||||
|
|
||||||
class _QuickLinksGrid extends ConsumerWidget {
|
/// Tor and assigned sing-box proxy connections. Each row toggles its connection
|
||||||
final bool showContainerUi;
|
/// on tap and opens the corresponding editor via the trailing chevron, keeping
|
||||||
|
/// these connection *state* controls out of the navigation-only Quick Links.
|
||||||
const _QuickLinksGrid({required this.showContainerUi});
|
class _ConnectionCard extends ConsumerWidget {
|
||||||
|
const _ConnectionCard();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -1996,8 +2040,142 @@ class _QuickLinksGrid extends ConsumerWidget {
|
|||||||
)
|
)
|
||||||
.value;
|
.value;
|
||||||
|
|
||||||
final torActiveColor = AppColors.of(context).torActiveGreen;
|
final rows = <Widget>[
|
||||||
|
_ConnectionRow(
|
||||||
|
icon: TorIcons.onionAlt,
|
||||||
|
label: 'Tor™ Proxy',
|
||||||
|
active: isTorActive,
|
||||||
|
enabled: !isTorBusy,
|
||||||
|
onToggle: () async {
|
||||||
|
if (isTorBusy) return;
|
||||||
|
if (isTorActive) {
|
||||||
|
await ref.read(torProxyServiceProvider.notifier).disconnect();
|
||||||
|
} else {
|
||||||
|
await ref.read(startProxyControllerProvider.notifier).startProxy();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEdit: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
await const TorProxyRoute().push(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
for (final profile in assignedProfiles)
|
||||||
|
_ConnectionRow(
|
||||||
|
icon: MdiIcons.lanConnect,
|
||||||
|
label: profile.name,
|
||||||
|
active: runtimeEndpointIds.contains(profile.proxyConnectionId),
|
||||||
|
onToggle: () async {
|
||||||
|
final runtime = ref.read(
|
||||||
|
singboxProxyRuntimeRepositoryProvider.notifier,
|
||||||
|
);
|
||||||
|
final isRunning = runtimeEndpointIds.contains(
|
||||||
|
profile.proxyConnectionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isRunning) {
|
||||||
|
await runtime.stopProfiles([profile.id]);
|
||||||
|
} else {
|
||||||
|
await runtime.startProfile(profile.id);
|
||||||
|
}
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
logger.e(
|
||||||
|
'Failed to toggle proxy profile ${profile.id} from menu',
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showErrorMessage(context, 'Proxy error: $error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEdit: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
await SingboxProxyProfileEditorRoute(
|
||||||
|
profileId: profile.id,
|
||||||
|
).push(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return _buildMenuCard(
|
||||||
|
context,
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < rows.length; i++) ...[
|
||||||
|
if (i > 0) _buildDivider(),
|
||||||
|
rows[i],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectionRow extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final bool active;
|
||||||
|
final bool enabled;
|
||||||
|
final Future<void> Function() onToggle;
|
||||||
|
final Future<void> Function() onEdit;
|
||||||
|
|
||||||
|
const _ConnectionRow({
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.active,
|
||||||
|
required this.onToggle,
|
||||||
|
required this.onEdit,
|
||||||
|
this.enabled = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final activeColor = AppColors.of(context).torActiveGreen;
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
// Trim the right padding by the IconButton's internal inset (~12px) so the
|
||||||
|
// edit chevron's glyph lands at the same 16px from the edge as the plain
|
||||||
|
// trailing icons used by the Share/More/Containers rows, while keeping the
|
||||||
|
// button's full 48px touch target.
|
||||||
|
contentPadding: const EdgeInsets.only(left: 16, right: 16),
|
||||||
|
leading: Icon(
|
||||||
|
icon,
|
||||||
|
color: active ? activeColor : colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
title: Text(label, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
|
subtitle: Text(active ? 'Connected' : 'Off'),
|
||||||
|
onTap: enabled ? () => onToggle() : null,
|
||||||
|
dense: true,
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
active ? Icons.circle : Icons.circle_outlined,
|
||||||
|
size: 12,
|
||||||
|
color: active ? activeColor : colorScheme.outline,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
const VerticalDivider(indent: 4, endIndent: 4),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.chevron_right),
|
||||||
|
tooltip: 'Edit',
|
||||||
|
onPressed: () => onEdit(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Quick Links Grid ───
|
||||||
|
|
||||||
|
class _QuickLinksGrid extends ConsumerWidget {
|
||||||
|
final bool showContainerUi;
|
||||||
|
|
||||||
|
const _QuickLinksGrid({required this.showContainerUi});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final items = <_QuickLinkItem>[
|
final items = <_QuickLinkItem>[
|
||||||
_QuickLinkItem(
|
_QuickLinkItem(
|
||||||
icon: Icons.history,
|
icon: Icons.history,
|
||||||
@@ -2049,62 +2227,6 @@ class _QuickLinksGrid extends ConsumerWidget {
|
|||||||
await ref.read(smallWebModeControllerProvider.notifier).enter();
|
await ref.read(smallWebModeControllerProvider.notifier).enter();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_QuickLinkItem(
|
|
||||||
icon: TorIcons.onionAlt,
|
|
||||||
label: 'Tor\u2122 Proxy',
|
|
||||||
badge: isTorActive,
|
|
||||||
badgeColor: torActiveColor,
|
|
||||||
onTap: () async {
|
|
||||||
if (isTorBusy) return;
|
|
||||||
if (isTorActive) {
|
|
||||||
await ref.read(torProxyServiceProvider.notifier).disconnect();
|
|
||||||
} else {
|
|
||||||
await ref.read(startProxyControllerProvider.notifier).startProxy();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongPress: () async {
|
|
||||||
Navigator.pop(context);
|
|
||||||
await const TorProxyRoute().push(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
for (final profile in assignedProfiles)
|
|
||||||
_QuickLinkItem(
|
|
||||||
icon: MdiIcons.lanConnect,
|
|
||||||
label: profile.name,
|
|
||||||
badge: runtimeEndpointIds.contains(profile.proxyConnectionId),
|
|
||||||
badgeColor: torActiveColor,
|
|
||||||
onTap: () async {
|
|
||||||
final runtime = ref.read(
|
|
||||||
singboxProxyRuntimeRepositoryProvider.notifier,
|
|
||||||
);
|
|
||||||
final isRunning = runtimeEndpointIds.contains(
|
|
||||||
profile.proxyConnectionId,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isRunning) {
|
|
||||||
await runtime.stopProfiles([profile.id]);
|
|
||||||
} else {
|
|
||||||
await runtime.startProfile(profile.id);
|
|
||||||
}
|
|
||||||
} catch (error, stackTrace) {
|
|
||||||
logger.e(
|
|
||||||
'Failed to toggle proxy profile ${profile.id} from menu',
|
|
||||||
error: error,
|
|
||||||
stackTrace: stackTrace,
|
|
||||||
);
|
|
||||||
if (context.mounted) {
|
|
||||||
ui_helper.showErrorMessage(context, 'Proxy error: $error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongPress: () async {
|
|
||||||
Navigator.pop(context);
|
|
||||||
await SingboxProxyProfileEditorRoute(
|
|
||||||
profileId: profile.id,
|
|
||||||
).push(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const spacing = 8.0;
|
const spacing = 8.0;
|
||||||
@@ -2126,9 +2248,6 @@ class _QuickLinksGrid extends ConsumerWidget {
|
|||||||
item.icon,
|
item.icon,
|
||||||
item.label,
|
item.label,
|
||||||
item.onTap,
|
item.onTap,
|
||||||
badge: item.badge,
|
|
||||||
badgeColor: item.badgeColor,
|
|
||||||
onLongPress: item.onLongPress,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -2141,33 +2260,21 @@ class _QuickLinksGrid extends ConsumerWidget {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
IconData icon,
|
IconData icon,
|
||||||
String label,
|
String label,
|
||||||
VoidCallback onTap, {
|
VoidCallback onTap,
|
||||||
bool badge = false,
|
) {
|
||||||
Color? badgeColor,
|
|
||||||
VoidCallback? onLongPress,
|
|
||||||
}) {
|
|
||||||
final iconWidget = Icon(
|
|
||||||
icon,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onLongPress,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (badge)
|
Icon(icon, color: Theme.of(context).colorScheme.onSurface),
|
||||||
Badge(backgroundColor: badgeColor, child: iconWidget)
|
|
||||||
else
|
|
||||||
iconWidget,
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
@@ -2189,17 +2296,11 @@ class _QuickLinkItem {
|
|||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String label;
|
final String label;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final VoidCallback? onLongPress;
|
|
||||||
final bool badge;
|
|
||||||
final Color? badgeColor;
|
|
||||||
|
|
||||||
const _QuickLinkItem({
|
const _QuickLinkItem({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.onLongPress,
|
|
||||||
this.badge = false,
|
|
||||||
this.badgeColor,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user