stock_keyboard_by_tun 1.0.0 copy "stock_keyboard_by_tun: ^1.0.0" to clipboard
stock_keyboard_by_tun: ^1.0.0 copied to clipboard

A highly customizable special stock numeric keyboard for trading dashboards with native selection and caret support.

example/lib/main.dart

// ignore_for_file: deprecated_member_use

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:stock_keyboard_by_tun/stock_keyboard_by_tun.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool _isDarkMode = false;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Stock Keyboard Demo',
      debugShowCheckedModeBanner: false,
      themeMode: _isDarkMode ? ThemeMode.dark : ThemeMode.light,
      theme: ThemeData(
        brightness: Brightness.light,
        scaffoldBackgroundColor: const Color(0xFFF6F8FA),
        primaryColor: const Color(0xFF8B1E22),
        colorScheme: const ColorScheme.light(
          primary: Color(0xFF8B1E22),
          secondary: Color(0xFFE53935),
        ),
        fontFamily: 'Inter',
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        brightness: Brightness.dark,
        scaffoldBackgroundColor: const Color(0xFF0F0F0F),
        primaryColor: const Color(0xFFD32F2F),
        colorScheme: const ColorScheme.dark(
          primary: Color(0xFFD32F2F),
          secondary: Color(0xFFE53935),
        ),
        fontFamily: 'Inter',
        useMaterial3: true,
      ),
      home: StockOrderScreen(
        isDarkMode: _isDarkMode,
        onThemeChanged: (val) {
          setState(() {
            _isDarkMode = val;
          });
        },
      ),
    );
  }
}

/// The fields we want to edit in our stock trading dashboard
enum OrderFields { price, volume }

class StockOrderScreen extends StatefulWidget {
  final bool isDarkMode;
  final ValueChanged<bool> onThemeChanged;

  const StockOrderScreen({
    super.key,
    required this.isDarkMode,
    required this.onThemeChanged,
  });

  @override
  State<StockOrderScreen> createState() => _StockOrderScreenState();
}

class _StockOrderScreenState extends State<StockOrderScreen> {
  final ScrollController _scrollController = ScrollController();

  // Controllers & Nodes
  final TextEditingController _priceController = TextEditingController();
  final TextEditingController _volumeController = TextEditingController();

  final FocusNode _priceFocusNode = FocusNode();
  final FocusNode _volumeFocusNode = FocusNode();

  late final CustomKeyboardController<OrderFields> _keyboardController;

  @override
  void initState() {
    super.initState();

    // Initialize the single generic controller with layouts and controllers
    _keyboardController = CustomKeyboardController<OrderFields>(
      controllers: {
        OrderFields.price: _priceController,
        OrderFields.volume: _volumeController,
      },
      focusNodes: {
        OrderFields.price: _priceFocusNode,
        OrderFields.volume: _volumeFocusNode,
      },
      layouts: {
        OrderFields.price: KeyboardLayout.decimal(),
        OrderFields.volume: KeyboardLayout.integer(),
      },
      defaultLanguage: KeyboardLanguage.vi,
    );
  }

  @override
  void dispose() {
    _keyboardController.dispose();
    _priceController.dispose();
    _volumeController.dispose();
    _priceFocusNode.dispose();
    _volumeFocusNode.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final theme =
        widget.isDarkMode ? KeyboardTheme.dark() : KeyboardTheme.light();

    // Suggestions configs for our fields
    final Map<OrderFields, List<KeyboardSuggestionModel>> suggestions = {
      OrderFields.price: [
        const KeyboardSuggestionModel(
          displayLabel: 'LO',
          value: 'LO',
          leadingIcon: Icon(Icons.lock_outline, size: 14, color: Colors.blue),
        ),
        const KeyboardSuggestionModel(displayLabel: 'MP', value: 'MP'),
        const KeyboardSuggestionModel(displayLabel: 'ATC', value: 'ATC'),
        KeyboardSuggestionModel(
          displayLabel: 'Sàn 60.9',
          value: '60.9',
          leadingIcon:
              Icon(Icons.arrow_downward, size: 14, color: Colors.cyan.shade600),
        ),
        KeyboardSuggestionModel(
          displayLabel: 'TC 61.5',
          value: '61.5',
          leadingIcon: Icon(Icons.fiber_manual_record,
              size: 12, color: Colors.orange.shade600),
        ),
        KeyboardSuggestionModel(
          displayLabel: 'Trần 63.5',
          value: '63.5',
          leadingIcon:
              Icon(Icons.arrow_upward, size: 14, color: Colors.purple.shade400),
        ),
      ],
      OrderFields.volume: [
        const KeyboardSuggestionModel(displayLabel: '100', value: '100'),
        const KeyboardSuggestionModel(displayLabel: '500', value: '500'),
        const KeyboardSuggestionModel(displayLabel: '1,000', value: '1000'),
        const KeyboardSuggestionModel(displayLabel: '5,000', value: '5000'),
        const KeyboardSuggestionModel(displayLabel: '10,000', value: '10000'),
      ],
    };

    return ListenableBuilder(
      listenable: _keyboardController,
      builder: (context, child) {
        final currentLang = _keyboardController.language;

        // Custom chips for volume input matching screenshot style
        List<Widget>? customChips;
        if (_keyboardController.focusedField == OrderFields.volume) {
          customChips = [
            _buildPercentageChip(
              prefix: 'M: ',
              percentage: '50%',
              value: '5000',
              prefixColor: Colors.green.shade600,
              theme: theme,
            ),
            _buildPercentageChip(
              prefix: 'M: ',
              percentage: '100%',
              value: '10000',
              prefixColor: Colors.green.shade600,
              theme: theme,
            ),
            _buildPercentageChip(
              prefix: 'B: ',
              percentage: '50%',
              value: '2500',
              prefixColor: Colors.red.shade600,
              theme: theme,
            ),
            _buildPercentageChip(
              prefix: 'B: ',
              percentage: '100%',
              value: '5000',
              prefixColor: Colors.red.shade600,
              theme: theme,
            ),
          ];
        }

        return Scaffold(
          appBar: AppBar(
            title: Text(
              currentLang == KeyboardLanguage.vi
                  ? 'Đặt Lệnh Chứng Khoán'
                  : 'Stock Order Terminal',
              style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 20),
            ),
            elevation: 0,
            scrolledUnderElevation: 0,
            backgroundColor:
                widget.isDarkMode ? const Color(0xFF161616) : Colors.white,
            actions: [
              // Vibration Toggle (Live test haptics flag)
              IconButton(
                icon: Icon(
                  _keyboardController.enableHapticFeedback
                      ? Icons.vibration
                      : Icons.phone_android,
                ),
                tooltip: currentLang == KeyboardLanguage.vi
                    ? 'Bật/Tắt Rung'
                    : 'Toggle Haptics',
                onPressed: () {
                  _keyboardController.enableHapticFeedback =
                      !_keyboardController.enableHapticFeedback;
                },
              ),
              // Language Switcher Toggle
              TextButton(
                onPressed: () {
                  _keyboardController.setLanguage(
                    currentLang == KeyboardLanguage.vi
                        ? KeyboardLanguage.en
                        : KeyboardLanguage.vi,
                  );
                },
                child: Text(
                  currentLang == KeyboardLanguage.vi ? 'EN' : 'VI',
                  style: const TextStyle(fontWeight: FontWeight.bold),
                ),
              ),
              // Theme Toggle
              IconButton(
                icon: Icon(
                    widget.isDarkMode ? Icons.light_mode : Icons.dark_mode),
                onPressed: () => widget.onThemeChanged(!widget.isDarkMode),
              ),
            ],
          ),
          body: StockKeyboardOverlay<OrderFields>(
            controller: _keyboardController,
            theme: theme,
            suggestions: suggestions,
            customSuggestionChips: customChips,
            centerSuggestions: true,
            suggestionBorderRadius: BorderRadius.circular(8.0),
            backspaceIcon: const Icon(
              Icons.backspace,
              size: 20,
            ),
            child: SingleChildScrollView(
              controller: _scrollController,
              physics: const AlwaysScrollableScrollPhysics(),
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  // Stock info panel
                  _buildStockInfoCard(currentLang),
                  const SizedBox(height: 16.0),

                  // Form card
                  _buildOrderForm(context, currentLang, theme),
                  const SizedBox(height: 24.0),

                  // Extra dummy inputs to test auto-scrolling
                  _buildScrollTestingPlaceholder(currentLang),
                ],
              ),
            ),
          ),

        );
      },
    );
  }

  Widget _buildStockInfoCard(KeyboardLanguage lang) {
    final isVi = lang == KeyboardLanguage.vi;
    return Card(
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
        side: BorderSide(
          color: widget.isDarkMode
              ? const Color(0xFF2C2C2C)
              : Colors.black.withOpacity(0.05),
        ),
      ),
      color: widget.isDarkMode ? const Color(0xFF161616) : Colors.white,
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      'HPG',
                      style: TextStyle(
                        fontSize: 24,
                        fontWeight: FontWeight.w800,
                        letterSpacing: -0.5,
                      ),
                    ),
                    Text(
                      isVi ? 'Tập đoàn Hòa Phát' : 'Hoa Phat Group JS Co.',
                      style: TextStyle(
                        fontSize: 12,
                        color:
                            widget.isDarkMode ? Colors.white70 : Colors.black54,
                      ),
                    ),
                  ],
                ),
                Column(
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [
                    const Text(
                      '61.5',
                      style: TextStyle(
                        fontSize: 24,
                        fontWeight: FontWeight.w800,
                        color: Color(
                            0xFFE5A93C), // Reference color (Orange/Yellow)
                      ),
                    ),
                    Row(
                      children: [
                        Icon(
                          Icons.arrow_drop_up,
                          color: Colors.green.shade600,
                          size: 18,
                        ),
                        Text(
                          '+0.7 (+1.15%)',
                          style: TextStyle(
                            fontSize: 12,
                            fontWeight: FontWeight.w600,
                            color: Colors.green.shade600,
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ],
            ),
            const Divider(height: 24),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                _buildInfoColumn(
                    isVi ? 'Trần' : 'Ceiling', '63.5', Colors.purple.shade400),
                _buildInfoColumn(
                    isVi ? 'TC' : 'Ref', '61.5', const Color(0xFFE5A93C)),
                _buildInfoColumn(
                    isVi ? 'Sàn' : 'Floor', '60.9', Colors.cyan.shade500),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoColumn(String label, String value, Color color) {
    return Column(
      children: [
        Text(
          label,
          style: TextStyle(
            fontSize: 11,
            color: widget.isDarkMode ? Colors.white60 : Colors.black54,
            fontWeight: FontWeight.w500,
          ),
        ),
        const SizedBox(height: 2),
        Text(
          value,
          style: TextStyle(
            fontSize: 15,
            fontWeight: FontWeight.w700,
            color: color,
          ),
        ),
      ],
    );
  }

  Widget _buildOrderForm(
      BuildContext context, KeyboardLanguage lang, KeyboardTheme theme) {
    final isVi = lang == KeyboardLanguage.vi;

    return Container(
      padding: const EdgeInsets.all(16.0),
      decoration: BoxDecoration(
        color: widget.isDarkMode ? const Color(0xFF161616) : Colors.white,
        borderRadius: BorderRadius.circular(16),
        border: Border.all(
          color: widget.isDarkMode
              ? const Color(0xFF2C2C2C)
              : Colors.black.withOpacity(0.05),
        ),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // Price textfield wrapper
          Text(
            isVi ? 'Giá đặt (VND)' : 'Order Price (VND)',
            style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
          ),
          const SizedBox(height: 6),
          TextField(
            controller: _priceController,
            focusNode: _priceFocusNode,
            showCursor: true,
            keyboardType: TextInputType.none,
            style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
            decoration: InputDecoration(
              hintText: isVi ? 'Nhập giá đặt' : 'Enter order price',
              hintStyle:
                  const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
              contentPadding:
                  const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
              filled: true,
              fillColor: widget.isDarkMode
                  ? const Color(0xFF222222)
                  : const Color(0xFFF9FBFC),
              focusedBorder: OutlineInputBorder(
                borderSide: BorderSide(color: theme.actionKeyColor, width: 2),
                borderRadius: BorderRadius.circular(12),
              ),
              enabledBorder: OutlineInputBorder(
                borderSide: BorderSide(
                  color: widget.isDarkMode
                      ? const Color(0xFF333333)
                      : Colors.black.withOpacity(0.08),
                  width: 1.5,
                ),
                borderRadius: BorderRadius.circular(12),
              ),
            ),
          ),
          const SizedBox(height: 16.0),

          // Volume textfield wrapper
          Text(
            isVi ? 'Khối lượng' : 'Volume',
            style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
          ),
          const SizedBox(height: 6),
          TextField(
            controller: _volumeController,
            focusNode: _volumeFocusNode,
            showCursor: true,
            keyboardType: TextInputType.none,
            style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
            decoration: InputDecoration(
              hintText: isVi ? 'Nhập khối lượng' : 'Enter volume',
              hintStyle:
                  const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
              contentPadding:
                  const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
              filled: true,
              fillColor: widget.isDarkMode
                  ? const Color(0xFF222222)
                  : const Color(0xFFF9FBFC),
              focusedBorder: OutlineInputBorder(
                borderSide: BorderSide(color: theme.actionKeyColor, width: 2),
                borderRadius: BorderRadius.circular(12),
              ),
              enabledBorder: OutlineInputBorder(
                borderSide: BorderSide(
                  color: widget.isDarkMode
                      ? const Color(0xFF333333)
                      : Colors.black.withOpacity(0.08),
                  width: 1.5,
                ),
                borderRadius: BorderRadius.circular(12),
              ),
            ),
          ),
          const SizedBox(height: 20.0),

          // Action Buttons
          Row(
            children: [
              Expanded(
                child: ElevatedButton(
                  onPressed: () {
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(
                        content: Text(
                          isVi
                              ? 'Đã gửi lệnh MUA HPG giá ${_priceController.text} SL ${_volumeController.text}'
                              : 'Submitted BUY order HPG at ${_priceController.text} Qty ${_volumeController.text}',
                        ),
                      ),
                    );
                    _keyboardController.unfocus();
                  },
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.green.shade600,
                    foregroundColor: Colors.white,
                    padding: const EdgeInsets.symmetric(vertical: 14),
                    shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(12)),
                    elevation: 0,
                  ),
                  child: Text(
                    isVi ? 'MUA' : 'BUY',
                    style: const TextStyle(
                        fontWeight: FontWeight.w800, fontSize: 16),
                  ),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: ElevatedButton(
                  onPressed: () {
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(
                        content: Text(
                          isVi
                              ? 'Đã gửi lệnh BÁN HPG giá ${_priceController.text} SL ${_volumeController.text}'
                              : 'Submitted SELL order HPG at ${_priceController.text} Qty ${_volumeController.text}',
                        ),
                      ),
                    );
                    _keyboardController.unfocus();
                  },
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.red.shade600,
                    foregroundColor: Colors.white,
                    padding: const EdgeInsets.symmetric(vertical: 14),
                    shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(12)),
                    elevation: 0,
                  ),
                  child: Text(
                    isVi ? 'BÁN' : 'SELL',
                    style: const TextStyle(
                        fontWeight: FontWeight.w800, fontSize: 16),
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildScrollTestingPlaceholder(KeyboardLanguage lang) {
    final isVi = lang == KeyboardLanguage.vi;
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        const SizedBox(height: 32.0),
        Center(
          child: Text(
            isVi
                ? '--- Kéo xuống để kiểm thử tính năng Auto-Scroll ---'
                : '--- Scroll down to test Auto-Scroll ---',
            style: const TextStyle(
                fontSize: 11, color: Colors.grey, fontWeight: FontWeight.w500),
          ),
        ),
        const SizedBox(
            height: 100.0), // Large spacer to make scroll view scrollable
        // Additional dummy inputs near the bottom
        Card(
          elevation: 0,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
            side: BorderSide(
              color: widget.isDarkMode
                  ? const Color(0xFF2C2C2C)
                  : Colors.black.withOpacity(0.04),
            ),
          ),
          color: widget.isDarkMode ? const Color(0xFF161616) : Colors.white,
          child: Padding(
            padding: const EdgeInsets.all(12.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  isVi ? 'Thông tin tài khoản' : 'Account Details',
                  style: const TextStyle(
                      fontSize: 13, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 8),
                Text(
                  isVi
                      ? 'Sức mua: 150,000,000 VND\nTỷ lệ ký quỹ: 40%\nTài sản ròng: 280,000,000 VND'
                      : 'Purchasing Power: 150,000,000 VND\nMargin Ratio: 40%\nNet Asset Value: 280,000,000 VND',
                  style: const TextStyle(
                      fontSize: 12, height: 1.6, color: Colors.grey),
                ),
              ],
            ),
          ),
        ),
        const SizedBox(
            height:
                200.0), // Massive padding to check how text inputs behave when pushed
      ],
    );
  }

  Widget _buildPercentageChip({
    required String prefix,
    required String percentage,
    required String value,
    required Color prefixColor,
    required KeyboardTheme theme,
  }) {
    final currentText = _volumeController.text;
    final displayLabel = '$prefix$percentage';
    final isActive = currentText == value || currentText == displayLabel;

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
      child: Center(
        child: GestureDetector(
          onTap: () {
            if (_keyboardController.enableHapticFeedback) {
              HapticFeedback.lightImpact();
            }
            _keyboardController.fillSuggestionValue(value);
          },
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 150),
            padding:
                const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
            decoration: BoxDecoration(
              color: isActive
                  ? theme.activeSuggestionChipColor
                  : theme.suggestionChipColor,
              borderRadius: BorderRadius.circular(20.0),
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.04),
                  blurRadius: 1.0,
                  offset: const Offset(0, 1),
                ),
              ],
              border: Border.all(
                color: isActive
                    ? Colors.transparent
                    : Colors.black.withOpacity(0.05),
                width: 0.5,
              ),
            ),
            child: RichText(
              textScaler: TextScaler.noScaling,
              text: TextSpan(
                children: [
                  TextSpan(
                    text: prefix,
                    style: TextStyle(
                      color: isActive
                          ? theme.activeSuggestionTextColor
                          : prefixColor,
                      fontSize: 13,
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                  TextSpan(
                    text: percentage,
                    style: TextStyle(
                      color: isActive
                          ? theme.activeSuggestionTextColor
                          : theme.suggestionTextColor,
                      fontSize: 13,
                      fontWeight: isActive ? FontWeight.w700 : FontWeight.w500,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}
2
likes
160
points
3
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A highly customizable special stock numeric keyboard for trading dashboards with native selection and caret support.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

cupertino_icons, flutter

More

Packages that depend on stock_keyboard_by_tun