524 lines
14 KiB
Markdown
524 lines
14 KiB
Markdown
|
|
# 🎨 Кнопки Bottom Sheet - Расположение и Иконки
|
|||
|
|
|
|||
|
|
## 📍 Где находятся эти кнопки?
|
|||
|
|
|
|||
|
|
**Файл:** [lib/features/profile/add/add_profile_modal.dart](../lib/features/profile/add/add_profile_modal.dart)
|
|||
|
|
|
|||
|
|
**Класс:** `AddProfileModal` (extends `HookConsumerWidget`)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🖼️ Структура кнопок
|
|||
|
|
|
|||
|
|
### 1. **"Добавить из буфера обмена"** (Clipboard)
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
_Button(
|
|||
|
|
key: const ValueKey("add_from_clipboard_button"),
|
|||
|
|
label: t.profile.add.fromClipboard,
|
|||
|
|
icon: FluentIcons.clipboard_paste_24_regular, // ← ИКОНКА
|
|||
|
|
size: buttonWidth,
|
|||
|
|
onTap: () async {
|
|||
|
|
final captureResult = await Clipboard.getData(Clipboard.kTextPlain).then((value) => value?.text ?? '');
|
|||
|
|
if (addProfileState.isLoading) return;
|
|||
|
|
ref.read(addProfileProvider.notifier).add(captureResult);
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Иконка:** `FluentIcons.clipboard_paste_24_regular`
|
|||
|
|
**Перевод:** `t.profile.add.fromClipboard` → "Добавить из буфера обмена"
|
|||
|
|
**Цвет:** `theme.colorScheme.primary` (основной цвет темы)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. **"Сканировать QR-код"** (QR Code)
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
_Button(
|
|||
|
|
key: const ValueKey("add_by_qr_code_button"),
|
|||
|
|
label: t.profile.add.scanQr,
|
|||
|
|
icon: FluentIcons.qr_code_24_regular, // ← ИКОНКА
|
|||
|
|
size: buttonWidth,
|
|||
|
|
onTap: () async {
|
|||
|
|
final cr = await const QRCodeScannerScreen().open(context);
|
|||
|
|
if (cr == null) return;
|
|||
|
|
if (addProfileState.isLoading) return;
|
|||
|
|
ref.read(addProfileProvider.notifier).add(cr);
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Иконка:** `FluentIcons.qr_code_24_regular`
|
|||
|
|
**Перевод:** `t.profile.add.scanQr` → "Сканировать QR-код"
|
|||
|
|
**Цвет:** `theme.colorScheme.primary`
|
|||
|
|
**Условие:** Показывается только на мобильных (`!PlatformUtils.isDesktop`)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. **"Добавить WARP"**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
Material(
|
|||
|
|
key: const ValueKey("add_warp_button"),
|
|||
|
|
elevation: 8,
|
|||
|
|
color: theme.colorScheme.surface,
|
|||
|
|
surfaceTintColor: theme.colorScheme.surfaceTint,
|
|||
|
|
shadowColor: Colors.transparent,
|
|||
|
|
borderRadius: BorderRadius.circular(8),
|
|||
|
|
clipBehavior: Clip.antiAlias,
|
|||
|
|
child: InkWell(
|
|||
|
|
onTap: () async {
|
|||
|
|
await addProfileModal(context, ref);
|
|||
|
|
},
|
|||
|
|
child: Row(
|
|||
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|||
|
|
children: [
|
|||
|
|
Icon(
|
|||
|
|
FluentIcons.add_24_regular, // ← ИКОНКА
|
|||
|
|
color: theme.colorScheme.primary,
|
|||
|
|
),
|
|||
|
|
const SizedBox(width: 8),
|
|||
|
|
Text(
|
|||
|
|
t.profile.add.addWarp, // "Добавить WARP"
|
|||
|
|
style: theme.textTheme.labelLarge?.copyWith(
|
|||
|
|
color: theme.colorScheme.primary,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Иконка:** `FluentIcons.add_24_regular`
|
|||
|
|
**Перевод:** `t.profile.add.addWarp` → "Добавить WARP"
|
|||
|
|
**Стиль:** Полная ширина кнопка с текстом и иконкой
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4. **"Ввести вручную"**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
_Button(
|
|||
|
|
key: const ValueKey("add_manually_button"),
|
|||
|
|
label: t.profile.add.manually,
|
|||
|
|
icon: FluentIcons.add_24_regular, // ← ИКОНКА
|
|||
|
|
size: buttonWidth,
|
|||
|
|
onTap: () async {
|
|||
|
|
context.pop();
|
|||
|
|
await const NewProfileRoute().push(context);
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Иконка:** `FluentIcons.add_24_regular`
|
|||
|
|
**Перевод:** `t.profile.add.manually` → "Ввести вручную"
|
|||
|
|
**Условие:**
|
|||
|
|
- На мобильных → показывается как отдельная кнопка внизу
|
|||
|
|
- На Desktop → показывается вместо кнопки "Сканировать QR-код"
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎨 Откуда берутся иконки?
|
|||
|
|
|
|||
|
|
### Пакет: **fluentui_system_icons**
|
|||
|
|
|
|||
|
|
**pubspec.yaml:**
|
|||
|
|
```yaml
|
|||
|
|
dependencies:
|
|||
|
|
fluentui_system_icons: ^1.1.229
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Импорт:**
|
|||
|
|
```dart
|
|||
|
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Используемые иконки:
|
|||
|
|
|
|||
|
|
| Иконка | Код | Описание |
|
|||
|
|
|--------|-----|----------|
|
|||
|
|
| 📋 | `FluentIcons.clipboard_paste_24_regular` | Буфер обмена |
|
|||
|
|
| 📷 | `FluentIcons.qr_code_24_regular` | QR-код |
|
|||
|
|
| ➕ | `FluentIcons.add_24_regular` | Добавить/Плюс |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎨 Как иконки меняют цвет?
|
|||
|
|
|
|||
|
|
### Автоматическая смена цвета через Theme
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
Icon(
|
|||
|
|
FluentIcons.clipboard_paste_24_regular,
|
|||
|
|
size: buttonWidth / 3,
|
|||
|
|
color: theme.colorScheme.primary, // ← ЦВЕТ ИЗ ТЕМЫ
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Откуда берётся цвет:**
|
|||
|
|
- `theme.colorScheme.primary` - основной цвет темы
|
|||
|
|
- Меняется автоматически при смене темы (светлая/тёмная)
|
|||
|
|
- Определяется в настройках темы приложения
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎨 Где определяется тема?
|
|||
|
|
|
|||
|
|
**Путь к настройкам темы:**
|
|||
|
|
```dart
|
|||
|
|
// Тема определяется в:
|
|||
|
|
lib/core/preferences/
|
|||
|
|
lib/features/config_option/
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Как работает смена цвета:**
|
|||
|
|
|
|||
|
|
1. **Пользователь выбирает тему** (светлую/тёмную/автоматическую)
|
|||
|
|
2. **Flutter Theme система** применяет `ColorScheme`
|
|||
|
|
3. **ColorScheme.primary** меняется на цвет из палитры темы
|
|||
|
|
4. **Icon виджеты** автоматически перерисовываются с новым цветом
|
|||
|
|
|
|||
|
|
**Пример:**
|
|||
|
|
```dart
|
|||
|
|
// Светлая тема
|
|||
|
|
colorScheme: ColorScheme.light(
|
|||
|
|
primary: Color(0xFF007AFF), // Синий
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Тёмная тема
|
|||
|
|
colorScheme: ColorScheme.dark(
|
|||
|
|
primary: Color(0xFF0A84FF), // Светло-синий
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📦 Формат иконок
|
|||
|
|
|
|||
|
|
### FluentUI System Icons - это векторные иконки
|
|||
|
|
|
|||
|
|
**Технические детали:**
|
|||
|
|
- **Формат:** IconData (Flutter встроенный формат)
|
|||
|
|
- **Источник:** Microsoft Fluent Design System
|
|||
|
|
- **Размер:** Масштабируемые (векторные, не растровые)
|
|||
|
|
- **Цвет:** Одноцветные, цвет задаётся программно
|
|||
|
|
- **Вариации:** `_regular`, `_filled` (обычные и заполненные)
|
|||
|
|
|
|||
|
|
**НЕ PNG, НЕ SVG файлы!** Это встроенные векторные глифы в шрифте.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎨 Как изменить иконки?
|
|||
|
|
|
|||
|
|
### Вариант 1: Использовать другую иконку из того же пакета
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// Было:
|
|||
|
|
icon: FluentIcons.clipboard_paste_24_regular,
|
|||
|
|
|
|||
|
|
// Стало:
|
|||
|
|
icon: FluentIcons.copy_24_regular, // Другая иконка
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Посмотреть все доступные иконки:**
|
|||
|
|
https://github.com/microsoft/fluentui-system-icons/tree/main/fonts
|
|||
|
|
|
|||
|
|
### Вариант 2: Использовать Material Icons
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// Изменить импорт
|
|||
|
|
import 'package:flutter/material.dart';
|
|||
|
|
|
|||
|
|
// Использовать Material иконку
|
|||
|
|
icon: Icons.content_paste,
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Вариант 3: Использовать свои SVG иконки
|
|||
|
|
|
|||
|
|
**1. Добавьте пакет flutter_svg:**
|
|||
|
|
```yaml
|
|||
|
|
dependencies:
|
|||
|
|
flutter_svg: ^2.0.10
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**2. Сохраните SVG в assets:**
|
|||
|
|
```
|
|||
|
|
assets/images/icons/clipboard.svg
|
|||
|
|
assets/images/icons/qr_code.svg
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**3. Используйте в коде:**
|
|||
|
|
```dart
|
|||
|
|
// Вместо Icon виджета
|
|||
|
|
SvgPicture.asset(
|
|||
|
|
'assets/images/icons/clipboard.svg',
|
|||
|
|
width: size / 3,
|
|||
|
|
height: size / 3,
|
|||
|
|
colorFilter: ColorFilter.mode(
|
|||
|
|
theme.colorScheme.primary,
|
|||
|
|
BlendMode.srcIn,
|
|||
|
|
),
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔧 Как изменить размер иконок?
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
Icon(
|
|||
|
|
FluentIcons.clipboard_paste_24_regular,
|
|||
|
|
size: buttonWidth / 3, // ← РАЗМЕР
|
|||
|
|
color: theme.colorScheme.primary,
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Текущий размер:** `buttonWidth / 3`
|
|||
|
|
- buttonWidth рассчитывается из ширины экрана
|
|||
|
|
- Примерно 48-56 пикселей для иконки
|
|||
|
|
|
|||
|
|
**Чтобы изменить:**
|
|||
|
|
```dart
|
|||
|
|
// Фиксированный размер
|
|||
|
|
size: 64,
|
|||
|
|
|
|||
|
|
// Относительный размер
|
|||
|
|
size: buttonWidth / 2, // Больше
|
|||
|
|
size: buttonWidth / 4, // Меньше
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎨 Как изменить цвет иконок?
|
|||
|
|
|
|||
|
|
### Вариант 1: Использовать другой цвет из темы
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
Icon(
|
|||
|
|
FluentIcons.clipboard_paste_24_regular,
|
|||
|
|
color: theme.colorScheme.secondary, // Вторичный цвет
|
|||
|
|
// или:
|
|||
|
|
// color: theme.colorScheme.tertiary,
|
|||
|
|
// color: theme.colorScheme.error,
|
|||
|
|
// color: theme.colorScheme.onSurface,
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Вариант 2: Использовать кастомный цвет
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
Icon(
|
|||
|
|
FluentIcons.clipboard_paste_24_regular,
|
|||
|
|
color: Color(0xFF00FF00), // Зелёный
|
|||
|
|
// или:
|
|||
|
|
// color: Colors.red,
|
|||
|
|
// color: Colors.amber,
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Вариант 3: Градиент (сложнее)
|
|||
|
|
|
|||
|
|
Для градиента нужно использовать `ShaderMask`:
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
ShaderMask(
|
|||
|
|
shaderCallback: (Rect bounds) {
|
|||
|
|
return LinearGradient(
|
|||
|
|
colors: [Colors.blue, Colors.purple],
|
|||
|
|
).createShader(bounds);
|
|||
|
|
},
|
|||
|
|
child: Icon(
|
|||
|
|
FluentIcons.clipboard_paste_24_regular,
|
|||
|
|
size: buttonWidth / 3,
|
|||
|
|
color: Colors.white, // Базовый цвет (будет заменён градиентом)
|
|||
|
|
),
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📱 Адаптивность кнопок
|
|||
|
|
|
|||
|
|
### Мобильные (Android/iOS):
|
|||
|
|
```dart
|
|||
|
|
if (!PlatformUtils.isDesktop)
|
|||
|
|
_Button(
|
|||
|
|
label: t.profile.add.scanQr,
|
|||
|
|
icon: FluentIcons.qr_code_24_regular,
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Показывается:**
|
|||
|
|
- ✅ Добавить из буфера обмена
|
|||
|
|
- ✅ Сканировать QR-код
|
|||
|
|
- ✅ Добавить WARP
|
|||
|
|
- ✅ Ввести вручную
|
|||
|
|
|
|||
|
|
### Desktop (Windows/macOS/Linux):
|
|||
|
|
```dart
|
|||
|
|
else
|
|||
|
|
_Button(
|
|||
|
|
label: t.profile.add.manually,
|
|||
|
|
icon: FluentIcons.add_24_regular,
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Показывается:**
|
|||
|
|
- ✅ Добавить из буфера обмена
|
|||
|
|
- ✅ Ввести вручную (вместо QR)
|
|||
|
|
- ✅ Добавить WARP
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎯 Класс _Button
|
|||
|
|
|
|||
|
|
**Виджет для квадратных кнопок:**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
class _Button extends StatelessWidget {
|
|||
|
|
final String label; // Текст кнопки
|
|||
|
|
final IconData icon; // Иконка (FluentIcons.*)
|
|||
|
|
final double size; // Размер кнопки (квадрат)
|
|||
|
|
final VoidCallback onTap; // Действие при нажатии
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
Widget build(BuildContext context) {
|
|||
|
|
return SizedBox(
|
|||
|
|
width: size,
|
|||
|
|
height: size,
|
|||
|
|
child: Material(
|
|||
|
|
elevation: 8, // Тень
|
|||
|
|
color: theme.colorScheme.surface,
|
|||
|
|
borderRadius: BorderRadius.circular(8),
|
|||
|
|
child: InkWell(
|
|||
|
|
onTap: onTap,
|
|||
|
|
child: Column(
|
|||
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|||
|
|
children: [
|
|||
|
|
Icon(icon, size: size / 3, color: color),
|
|||
|
|
const Gap(16),
|
|||
|
|
Text(label, style: labelStyle),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Свойства:**
|
|||
|
|
- Квадратная форма (`width = height = size`)
|
|||
|
|
- Иконка вверху (1/3 размера кнопки)
|
|||
|
|
- Текст внизу
|
|||
|
|
- Скругленные углы (8px)
|
|||
|
|
- Тень (elevation: 8)
|
|||
|
|
- Ripple эффект при нажатии (InkWell)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎨 Цветовая схема по умолчанию
|
|||
|
|
|
|||
|
|
**Где определяется primary цвет:**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// Вероятно в:
|
|||
|
|
lib/core/preferences/general_preferences_provider.dart
|
|||
|
|
lib/core/theme/
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Umbrix использует:**
|
|||
|
|
- **Primary color:** Бирюзовый/голубой (#00BCD4 или похожий)
|
|||
|
|
- **Surface:** Фон карточек
|
|||
|
|
- **OnSurface:** Текст на фоне
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 💡 Быстрые изменения
|
|||
|
|
|
|||
|
|
### Изменить цвет всех иконок на красный:
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
Icon(
|
|||
|
|
icon,
|
|||
|
|
size: size / 3,
|
|||
|
|
color: Colors.red, // ← ВСЕ ИКОНКИ СТАНУТ КРАСНЫМИ
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Изменить размер иконок на больший:
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
Icon(
|
|||
|
|
icon,
|
|||
|
|
size: size / 2, // ← БЫЛО /3, СТАЛО /2 (больше)
|
|||
|
|
color: color,
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Использовать заполненные (filled) иконки:
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// Было:
|
|||
|
|
icon: FluentIcons.clipboard_paste_24_regular,
|
|||
|
|
|
|||
|
|
// Стало:
|
|||
|
|
icon: FluentIcons.clipboard_paste_24_filled, // Заполненная версия
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔍 Где ещё используются FluentIcons?
|
|||
|
|
|
|||
|
|
**Поиск по проекту:**
|
|||
|
|
```bash
|
|||
|
|
grep -r "FluentIcons\." lib/ | wc -l
|
|||
|
|
# Результат: Много! Используются по всему приложению
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Примеры файлов:**
|
|||
|
|
- `lib/features/home/widget/home_page.dart`
|
|||
|
|
- `lib/features/proxy/overview/proxies_overview_page.dart`
|
|||
|
|
- `lib/features/common/adaptive_root_scaffold.dart`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📚 Полезные ссылки
|
|||
|
|
|
|||
|
|
1. **Fluent UI Icons Gallery:**
|
|||
|
|
https://aka.ms/fluentui-system-icons
|
|||
|
|
|
|||
|
|
2. **Flutter Icon класс:**
|
|||
|
|
https://api.flutter.dev/flutter/widgets/Icon-class.html
|
|||
|
|
|
|||
|
|
3. **ColorScheme документация:**
|
|||
|
|
https://api.flutter.dev/flutter/material/ColorScheme-class.html
|
|||
|
|
|
|||
|
|
4. **Пакет fluentui_system_icons:**
|
|||
|
|
https://pub.dev/packages/fluentui_system_icons
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎯 Резюме
|
|||
|
|
|
|||
|
|
**Кнопки находятся:** `lib/features/profile/add/add_profile_modal.dart`
|
|||
|
|
|
|||
|
|
**Иконки:**
|
|||
|
|
- Формат: IconData из пакета `fluentui_system_icons`
|
|||
|
|
- НЕ файлы PNG/SVG, а векторные глифы в шрифте
|
|||
|
|
- Цвет: `theme.colorScheme.primary` (меняется с темой)
|
|||
|
|
- Размер: Масштабируемые, задаются программно
|
|||
|
|
|
|||
|
|
**Смена цвета:**
|
|||
|
|
- Автоматическая через Theme системы Flutter
|
|||
|
|
- `colorScheme.primary` берётся из настроек темы
|
|||
|
|
- Светлая/тёмная тема → разные оттенки
|
|||
|
|
|
|||
|
|
**Чтобы изменить:**
|
|||
|
|
1. Цвет → поменять `color: theme.colorScheme.primary` на другой
|
|||
|
|
2. Размер → изменить `size: buttonWidth / 3`
|
|||
|
|
3. Иконку → заменить `FluentIcons.clipboard_paste_24_regular` на другую
|
|||
|
|
4. Свою иконку → использовать SVG через flutter_svg пакет
|