otapp_bus_seat_map 0.1.2 copy "otapp_bus_seat_map: ^0.1.2" to clipboard
otapp_bus_seat_map: ^0.1.2 copied to clipboard

A flexible bus seat map widget for Flutter. Designed for bus booking apps using Otapp Services API.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:otapp_bus_seat_map/otapp_bus_seat_map.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Seat Map Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const SeatMapDemo(),
    );
  }
}

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

  @override
  State<SeatMapDemo> createState() => _SeatMapDemoState();
}

class _SeatMapDemoState extends State<SeatMapDemo>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;
  List<SelectedSeat> _selectedSeats = [];

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Seat Map Examples'),
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(text: 'Bus'),
            Tab(text: 'Cinema'),
            Tab(text: 'Custom'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          _buildBusExample(),
          _buildCinemaExample(),
          _buildCustomExample(),
        ],
      ),
      bottomNavigationBar: _selectedSeats.isNotEmpty
          ? Container(
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Colors.white,
                boxShadow: [
                  BoxShadow(
                    color: Colors.black.withValues(alpha: 0.1),
                    blurRadius: 8,
                    offset: const Offset(0, -2),
                  ),
                ],
              ),
              child: SafeArea(
                child: Row(
                  children: [
                    Expanded(
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            '${_selectedSeats.length} seats selected',
                            style: const TextStyle(fontWeight: FontWeight.bold),
                          ),
                          Text(
                            _selectedSeats.map((s) => s.label).join(', '),
                            style: TextStyle(color: Colors.grey.shade600),
                          ),
                        ],
                      ),
                    ),
                    ElevatedButton(
                      onPressed: () {
                        // Handle booking
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(
                            content: Text(
                              'Booked: ${_selectedSeats.map((s) => s.label).join(", ")}',
                            ),
                          ),
                        );
                      },
                      child: const Text('Book'),
                    ),
                  ],
                ),
              ),
            )
          : null,
    );
  }

  Widget _buildBusExample() {
    // Example bus seat layout (similar to your API format)
    final busRows = [
      'L-1-1-1,L-1-1-2,0,L-1-1-3,L-1-1-4',
      'L-1-2-5,L-1-2-6,0,L-1-2-7,L-1-2-8',
      '@,0,0,L-1-3-9,L-1-3-10', // Door row
      'L-1-4-11,L-1-4-12,0,L-1-4-13,L-1-4-14',
      'L-1-5-15,L-1-5-16,0,L-1-5-17,L-1-5-18',
      '*,0,0,L-1-6-19,L-1-6-20', // Toilet row
      'L-1-7-21,L-1-7-22,0,L-1-7-23,L-1-7-24',
      'L-1-8-25,L-1-8-26,0,L-1-8-27,L-1-8-28',
      'L-1-9-29,L-1-9-30,L-1-9-31,L-1-9-32,L-1-9-33', // Back row (5 seats)
    ];

    // Simulated seat statuses (from API)
    final availableSeats =
        'L-1-1-1,L-1-1-2,L-1-1-3,L-1-2-5,L-1-2-7,L-1-2-8,L-1-3-9,L-1-4-11,L-1-4-12,L-1-4-13,L-1-5-15,L-1-5-16,L-1-5-17,L-1-6-19,L-1-7-21,L-1-7-22,L-1-7-23,L-1-8-25,L-1-8-26,L-1-9-29,L-1-9-30,L-1-9-31';
    final bookedSeats = 'L-1-1-4,L-1-2-6,L-1-3-10,L-1-4-14,L-1-5-18,L-1-6-20';
    final processingSeats = 'L-1-7-24,L-1-8-27,L-1-8-28,L-1-9-32,L-1-9-33';

    // VIP seats
    final vipSeats = {'L-1-1-1', 'L-1-1-2', 'L-1-1-3', 'L-1-1-4'};

    final layout = SeatLayout.fromCsvRowsWithStatus(
      busRows,
      config: SeatLayoutConfig.bus(
        defaultPrice: 25000,
        categoryResolver: (code, metadata) {
          if (vipSeats.contains(code)) return 'VIP';
          return 'Standard';
        },
        priceResolver: (code, category, metadata) {
          if (category == 'VIP') return 35000;
          return 25000;
        },
      ),
      availableSeats: availableSeats,
      bookedSeats: bookedSeats,
      processingSeats: processingSeats,
    );

    return Column(
      children: [
        // Driver section
        Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              const DriverWidget(size: 50),
              const SizedBox(width: 8),
            ],
          ),
        ),

        // Legend
        _buildLegend(),

        // Seat map
        Expanded(
          child: SeatMapWidget(
            layout: layout,
            selectedSeats: _selectedSeats,
            onSeatTap: (seat) => _handleSeatTap(seat),
            seatSize: 50,
            seatSpacing: 4,
            rowSpacing: 4,
          ),
        ),
      ],
    );
  }

  Widget _buildCinemaExample() {
    // Cinema layout with gaps for aisles
    final cinemaRows = [
      'A1,A2,A3,A4,,A5,A6,A7,A8,,A9,A10,A11,A12',
      'B1,B2,B3,B4,,B5,B6,B7,B8,,B9,B10,B11,B12',
      'C1,C2,C3,C4,,C5,C6,C7,C8,,C9,C10,C11,C12',
      ',,,,,,,,,,,,', // Gap row
      'D1,D2,D3,D4,,D5,D6,D7,D8,,D9,D10,D11,D12',
      'E1,E2,E3,E4,,E5,E6,E7,E8,,E9,E10,E11,E12',
      'F1,F2,F3,F4,,F5,F6,F7,F8,,F9,F10,F11,F12',
    ];

    final bookedSeats = 'B5,B6,B7,D6,D7,E6,E7';

    final layout = SeatLayout.fromCsvRowsWithStatus(
      cinemaRows,
      config: SeatLayoutConfig.cinema(defaultPrice: 15000),
      bookedSeats: bookedSeats,
    );

    return Column(
      children: [
        // Screen
        Container(
          margin: const EdgeInsets.all(24),
          padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 32),
          decoration: BoxDecoration(
            gradient: LinearGradient(
              colors: [Colors.blue.shade200, Colors.blue.shade400],
            ),
            borderRadius: BorderRadius.circular(4),
          ),
          child: const Text(
            'SCREEN',
            style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
          ),
        ),

        _buildLegend(),

        // Seat map
        Expanded(
          child: SeatMapWidget(
            layout: layout,
            selectedSeats: _selectedSeats,
            onSeatTap: (seat) => _handleSeatTap(seat),
            seatSize: 40,
            seatSpacing: 2,
            rowSpacing: 4,
            showRowLabels: true,
          ),
        ),
      ],
    );
  }

  Widget _buildCustomExample() {
    // Custom layout with special markers
    final customRows = [
      'S1,S2,S3,0,S4,S5,S6',
      'S7,S8,S9,0,S10,S11,S12',
      '#,0,0,0,0,0,#', // Stairs on both sides
      'S13,S14,S15,0,S16,S17,S18',
      'S19,S20,S21,0,S22,S23,S24',
    ];

    final layout = SeatLayout.fromCsvRows(
      customRows,
      config: SeatLayoutConfig(
        emptyMarkers: {'0', ''},
        stairsMarker: '#',
        autoDetectAisle: true,
        defaultPrice: 50,
        labelExtractor: (code) => code.replaceAll('S', ''),
      ),
    );

    return Column(
      children: [
        const Padding(
          padding: EdgeInsets.all(16),
          child: Text(
            'Double Decker Upper Floor',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
        ),
        _buildLegend(),
        Expanded(
          child: SeatMapWidget(
            layout: layout,
            selectedSeats: _selectedSeats,
            onSeatTap: (seat) => _handleSeatTap(seat),
            seatSize: 55,
            seatSpacing: 6,
            rowSpacing: 8,
            enableZoom: true,
          ),
        ),
      ],
    );
  }

  Widget _buildLegend() {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: Wrap(
        spacing: 16,
        runSpacing: 8,
        alignment: WrapAlignment.center,
        children: [
          _legendItem(Colors.white, 'Available'),
          _legendItem(Colors.blue.shade600, 'Selected'),
          _legendItem(Colors.grey.shade400, 'Booked'),
          _legendItem(Colors.orange.shade300, 'Processing'),
        ],
      ),
    );
  }

  Widget _legendItem(Color color, String label) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          width: 20,
          height: 20,
          decoration: BoxDecoration(
            color: color,
            borderRadius: BorderRadius.circular(4),
            border: Border.all(color: Colors.grey.shade300),
          ),
        ),
        const SizedBox(width: 4),
        Text(label, style: const TextStyle(fontSize: 12)),
      ],
    );
  }

  void _handleSeatTap(SeatElement seat) {
    if (!seat.isSelectable) return;

    setState(() {
      final existingIndex =
          _selectedSeats.indexWhere((s) => s.id == seat.id);

      if (existingIndex >= 0) {
        _selectedSeats.removeAt(existingIndex);
      } else {
        _selectedSeats.add(SelectedSeat.fromElement(seat));
      }
    });
  }
}
0
likes
160
points
--
downloads

Publisher

unverified uploader

Weekly Downloads

A flexible bus seat map widget for Flutter. Designed for bus booking apps using Otapp Services API.

Repository (GitHub)
View/report issues

Topics

#widget #seat-map #booking #ui #bus-booking

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on otapp_bus_seat_map