tostore 2.1.4 copy "tostore: ^2.1.4" to clipboard
tostore: ^2.1.4 copied to clipboard

A advanced storage engine that supports relational and NoSQL database,data smart cache, multi-space architecture,file/local storage, SQL & key-value persistent store.

example/lib/main.dart

import 'dart:developer' as developer;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tostore/tostore.dart';
import 'database_tester.dart';

/// This example demonstrates the core features of Tostore using a user management system
/// with global settings. It shows how to:
/// - Create tables (both regular and global)
/// - Work with multi-space architecture
/// - Distributed example
/// - Handle global data
class TostoreExample {
  late ToStore db;

  /// Initialize database and create tables
  Future<void> initialize() async {
    db = ToStore(
      config: DataStoreConfig(
        enableLog: true,
        logLevel: LogLevel.debug,
      ),
      schemas: [
        // suitable for table structure definition in frequent startup scenarios of mobile applications, accurately identifying table structure changes, automatically upgrading and migrating data
        const TableSchema(
          name: 'users',
          primaryKeyConfig:
              PrimaryKeyConfig(name: 'id', type: PrimaryKeyType.sequential),
          fields: [
            FieldSchema(name: 'username', type: DataType.text, nullable: false),
            FieldSchema(name: 'email', type: DataType.text, nullable: false),
            FieldSchema(name: 'last_login', type: DataType.datetime),
            FieldSchema(
                name: 'is_active', type: DataType.boolean, defaultValue: true),
            FieldSchema(name: 'age', type: DataType.integer),
            FieldSchema(name: 'tags', type: DataType.text),
            FieldSchema(name: 'type', type: DataType.text),
            FieldSchema(name: 'fans', type: DataType.integer, defaultValue: 10),
          ],
          indexes: [
            IndexSchema(fields: ['username'], unique: true),
            IndexSchema(fields: ['email'], unique: true),
            IndexSchema(fields: ['last_login'], unique: false),
            IndexSchema(fields: ['is_active']),
            IndexSchema(fields: ['type']),
          ],
        ),
        const TableSchema(
          name: 'posts',
          primaryKeyConfig: PrimaryKeyConfig(
            name: 'id',
          ),
          fields: [
            FieldSchema(name: 'title', type: DataType.text, nullable: false),
            FieldSchema(name: 'content', type: DataType.text),
            FieldSchema(
                name: 'user_id', type: DataType.integer, nullable: false),
            FieldSchema(name: 'created_at', type: DataType.datetime),
            FieldSchema(
                name: 'is_published',
                type: DataType.boolean,
                defaultValue: true),
          ],
          indexes: [
            IndexSchema(fields: ['user_id']),
            IndexSchema(fields: ['created_at']),
          ],
        ),
        const TableSchema(
          name: 'comments',
          primaryKeyConfig: PrimaryKeyConfig(
            name: 'id',
          ),
          fields: [
            FieldSchema(
                name: 'post_id', type: DataType.integer, nullable: false),
            FieldSchema(
                name: 'user_id', type: DataType.integer, nullable: false),
            FieldSchema(name: 'content', type: DataType.text, nullable: false),
            FieldSchema(name: 'created_at', type: DataType.datetime),
          ],
          indexes: [
            IndexSchema(fields: ['post_id']),
            IndexSchema(fields: ['user_id']),
          ],
        ),
        const TableSchema(
          name: 'settings',
          primaryKeyConfig: PrimaryKeyConfig(),
          isGlobal: true,
          fields: [
            FieldSchema(
                name: 'key',
                type: DataType.text,
                nullable: false,
                unique: true),
            FieldSchema(name: 'value', type: DataType.text),
            FieldSchema(name: 'updated_at', type: DataType.datetime),
          ],
          indexes: [
            IndexSchema(fields: ['key'], unique: true),
            IndexSchema(fields: ['updated_at'], unique: false),
          ],
        ),
      ],
    );
    await db.initialize();
  }

  /// Example: Basic CRUD operations for users
  Future<void> userExamples() async {
    // Create: Insert a new user
    await db.insert('users', {
      'username': 'john_doe',
      'email': 'john@example.com',
      'last_login': DateTime.now().toIso8601String(),
    });

    // Read: Query user using chain style
    await db.query('users').where('username', '=', 'john_doe');

    // Update: Modify user data
    await db.update('users', {
      'last_login': DateTime.now().toIso8601String(),
    }).where('username', '=', 'john_doe');

    // Automatically store data,Support batch upsert
    await db.upsert('users', {'name': 'John'}).where(
        'email', '=', 'john@example.com');

    // Auto insert or update based on primary key
    await db.upsert(
        'users', {'id': 1, 'name': 'John', 'email': 'john@example.com'});

    // Delete: Remove user
    await db.delete('users').where('username', '=', 'john_doe');

    // use stream query to handle large data
    db
        .streamQuery('users')
        .where('email', 'like', '%@example.com')
        .listen((userData) {
      // handle each data as needed, avoid memory pressure
      logService.add('handle user: ${userData['username']}');
    });
  }

  /// Example: Working with global settings
  Future<void> settingsExamples() async {
    // Set global theme (accessible from any space)
    await db.insert('settings', {
      'key': 'theme',
      'value': 'dark',
      'updated_at': DateTime.now().toIso8601String(),
    });

    // Alternative: Using setValue for simple key-value storage
    await db.setValue('language', 'en', isGlobal: true);

    // Read settings using different methods
    await db.query('settings').where('key', '=', 'theme');

    await db.getValue('language', isGlobal: true);

    // Update setting with conflict resolution
    await db.update('settings', {
      'value': 'light',
      'updated_at': DateTime.now().toIso8601String(),
    }).where('key', '=', 'theme');

    // Delete setting
    await db.delete('settings').where('key', '=', 'theme');
  }

  /// Example: Multi-space feature for user data isolation
  Future<void> multiSpaceExamples() async {
    // Switch to user1's space
    await db.switchSpace(spaceName: 'user1');
    await db.insert('users', {
      'username': 'user1',
      'email': 'user1@example.com',
      'last_login': DateTime.now().toIso8601String(),
    });

    // Switch to user2's space
    await db.switchSpace(spaceName: 'user2');
    await db.insert('users', {
      'username': 'user2',
      'email': 'user2@example.com',
      'last_login': DateTime.now().toIso8601String(),
    });

    // Global settings remain accessible in any space
    await db.getValue('theme', isGlobal: true);

    // get current space info
    final spaceInfo = await db.getSpaceInfo();
    logService.add("${spaceInfo.toJson()}");
  }

  /// Example: Advanced queries
  Future<void> advancedQueryExamples() async {
    // Complex conditions
    await db
        .query('users')
        .where(
            'last_login',
            '>',
            DateTime.now()
                .subtract(
                  const Duration(days: 7),
                )
                .toIso8601String())
        .or()
        .where('email', 'LIKE', '%@example.com')
        .orderByDesc('last_login')
        .limit(10);

    // Count users
    await db.query('users').count();

    // Batch operations
    await db.batchInsert('users', [
      {
        'username': 'user3',
        'email': 'user3@example.com',
        'last_login': DateTime.now().toIso8601String(),
      },
      {
        'username': 'user4',
        'email': 'user4@example.com',
        'last_login': DateTime.now().toIso8601String(),
      },
    ]);
  }

  /// Example: Backup and restore
  Future<void> backupExample() async {
    // Create backup
    final backupPath = await db.backup(compress: false);
    logService.add('Backup created at: $backupPath');

    // Restore from backup
    await db.restore(backupPath, deleteAfterRestore: true);
  }

  /// Example: Working with vector data
  Future<void> vectorExamples() async {
    // create table structure
    await db.createTables([
      const TableSchema(
        name: 'embeddings',
        primaryKeyConfig: PrimaryKeyConfig(
          name: 'id',
          type: PrimaryKeyType.timestampBased,
        ),
        fields: [
          FieldSchema(
            name: 'document_title',
            type: DataType.text,
            nullable: false,
          ),
          FieldSchema(
            name: 'embedding',
            type: DataType.vector,
            vectorConfig: VectorFieldConfig(
              dimensions: 1536, // 1536 dimensions
              precision: VectorPrecision.float64, // 64-bit precision
            ),
          ),
        ],
        indexes: [
          IndexSchema(
            fields: ['embedding'],
            type: IndexType.vector,
            vectorConfig: VectorIndexConfig(
              indexType:
                  VectorIndexType.hnsw, // HNSW for fast approximate search
              distanceMetric: VectorDistanceMetric.cosine, // Cosine similarity
              parameters: {
                'M': 16, // Max number of connections per layer
                'efConstruction': 200, // Controls index quality
                'efSearch': 100, // Controls search accuracy/speed trade-off
              },
            ),
          ),
        ],
      ),
    ]);

    // create vector data
    final sampleVector1 = VectorData.fromList([0.1, 0.2, 0.3, 0.4]);
    final sampleVector2 = VectorData.fromList([0.5, 0.6, 0.7, 0.8]);

    // store documents with vector embeddings
    await db.insert('embeddings', {
      'document_title': 'Introduction to vector databases',
      'embedding': sampleVector1,
    });

    await db.insert('embeddings', {
      'document_title': 'Machine Learning with embeddings',
      'embedding': sampleVector2,
    });

    // query stored vector data
    final result = await db.query('embeddings');
    final documents = result.data;
    List<VectorData> vectors = [];

    for (var doc in documents) {
      if (doc['embedding'] is List) {
        VectorData vector =
            VectorData.fromJson(doc['embedding'] as List<dynamic>);
        vectors.add(vector);
        doc['embedding'] = vector;

        logService.add(
            'document title: ${doc['document_title']}, vector dimensions: ${vector.dimensions}');
      }
    }

    // at least two vectors can be compared
    if (vectors.length >= 2) {
      final vector1 = vectors[0];
      final vector2 = vectors[1];

      logService.add('vector1: ${vector1.toString()}');
      logService.add('vector2: ${vector2.toString()}');

      // calculate vector similarity
      final similarity = vector1.cosineSimilarity(vector2);
      logService.add('vector similarity: $similarity');

      // calculate euclidean distance
      final distance = vector1.euclideanDistance(vector2);
      logService.add('vector distance: $distance');
    } else {
      logService.add('not enough vector data for comparison');
    }
  }

  /// backend server or distributed example
  Future<void> distributedExample() async {
    // create database instance
    final db = ToStore(
      config: DataStoreConfig(
        enableEncoding: true, // enable security encoding for table data
        encodingKey: 'YouEncodingKey', // encoding key, can be adjusted
        encryptionKey:
            'YouEncryptionKey', // encryption key, note: adjusting this key will make it impossible to decode old data
        distributedNodeConfig: const DistributedNodeConfig(
          enableDistributed: true, // enable distributed mode
          clusterId: 1, // configure cluster id
          centralServerUrl: 'http://127.0.0.1:8080',
          accessToken: 'b7628a4f9b4d269b98649129',
        ),
        enableLog: true, // enable log
        logLevel: LogLevel.warn, // log level
      ),
    );

    // create tables
    await db.createTables(
      [
        const TableSchema(
            name: 'users',
            primaryKeyConfig: PrimaryKeyConfig(
              name: 'id',
              type: PrimaryKeyType.sequential, // sequential key type
              sequentialConfig: SequentialIdConfig(
                initialValue: 10000, // initial value
                increment: 50, // increment
                useRandomIncrement:
                    true, // use random increment, avoid exposing business volume
              ),
            ),
            // field and index definition ...
            fields: []),
      ],
      // other tables ...
    );

    // update table structure
    final taskId = await db
        .updateSchema('users')
        .renameTable('newTableName') // rename table
        .modifyField('username',
            minLength: 5,
            maxLength: 20,
            unique: true) // modify field attributes
        .renameField('oldName', 'newName') // rename field
        .removeField('fieldName') // remove field
        .addField('name', type: DataType.text) // add field
        .removeIndex(fields: ['age']) // remove index
        .setPrimaryKeyConfig(// set primary key config
            const PrimaryKeyConfig(type: PrimaryKeyType.shortCode));

    // query migration task status
    final status = await db.queryMigrationTaskStatus(taskId);
    logService.add('migration progress: ${status?.progressPercentage}%');
  }

  /// Example: Complex nested queries with predefined conditions
  Future<void> complexQueryExamples() async {
    // prepare some test data
    await db.insert('users', {
      'username': 'active_user',
      'email': 'active@example.com',
      'is_active': true,
      'age': 25,
      'type': 'app',
      'fans': 300,
      'tags': 'recommend,hot,featured',
      'last_login': DateTime.now().toIso8601String(),
    });

    await db.insert('users', {
      'username': 'inactive_user',
      'email': 'inactive@example.com',
      'is_active': false,
      'age': 30,
      'type': 'web',
      'fans': 150,
      'tags': 'normal,newbie',
      'last_login':
          DateTime.now().subtract(const Duration(days: 30)).toIso8601String(),
    });

    // complex query condition nesting - pre-defined query condition module
    final recentLoginCondition = QueryCondition().where('fans', '>=', 200);

    final idCondition = QueryCondition()
        .where('id', '>=', 1)
        .orCondition(// orCondition is equivalent to OR condition combination
            recentLoginCondition);

    // custom condition function - flexible handling of any complex logic
    final customCondition = QueryCondition().whereCustom((record) {
      // for example: check if the tag contains 'recommend'
      return record['tags'] != null &&
          record['tags'].toString().contains('recommend');
    });

    // query condition nesting example - show infinite nesting ability
    final result = await db
        .query('users')
        .where('is_active', '=', true)
        .condition(QueryCondition() // query condition construction
                .whereEqual('type', 'app')
                .condition(idCondition) // nest again the defined conditions
            )
        .orCondition(customCondition) // or satisfy custom complex conditions
        .limit(20);

    for (var user in result.data) {
      logService.add(
          'user: ${user['username']}, type: ${user['type']}, fans: ${user['fans']}, tags: ${user['tags']}');
    }

    // equivalent SQL:
    // SELECT * FROM users
    // WHERE is_active = true
    //   AND (type = 'app' OR id >= 1 OR fans >= 200)
    //   OR ([custom condition: tag contains 'recommend'])
    // LIMIT 20
  }

  Future<int> clearExamples() async {
    final stopwatch = Stopwatch()..start();
    await db.clear('users');
    stopwatch.stop();
    final elapsed = stopwatch.elapsedMilliseconds;
    logService.add('Table "users" cleared in ${elapsed}ms');
    return elapsed;
  }

  /// Example: Join queries with table relationships
  Future<void> joinQueryExamples() async {
    // prepare test data
    // insert user
    await db.insert('users', {
      'username': 'blogger',
      'email': 'blogger@example.com',
      'is_active': true,
    });

    // insert post
    await db.insert('posts', {
      'title': 'how to use join to query',
      'content': 'this is a post about join...',
      'user_id': 1,
      'created_at': DateTime.now().toIso8601String(),
    });

    // insert comment
    await db.insert('comments', {
      'post_id': 1,
      'user_id': 1,
      'content': 'this is my own post comment',
      'created_at': DateTime.now().toIso8601String(),
    });

    // multi-table join query - post, author and comment
    final postsWithComments = await db
        .query('posts')
        .select([
          'posts.id as post_id',
          'posts.title',
          'users.username as author',
          'comments.content as comment',
          'comments.created_at as comment_time'
        ])
        .join('users', 'posts.user_id', '=', 'users.id')
        .join('comments', 'posts.user_id', '=', 'comments.user_id')
        .where('posts.is_published', '=', true)
        .orderByDesc('comments.created_at');

    for (var item in postsWithComments.data) {
      logService.add(
          'post: ${item['title']}, author: ${item['author']}, comment: ${item['comment']}');
    }
  }

  Future<int> addExamples() async {
    await db.clear('users');
    logService.add('Preparing 10,000 records for batch insert...');
    Stopwatch stopwatch = Stopwatch()..start();

    // batch insert, 1000 records per batch
    const int batchSize = 1000;
    const int totalRecords = 10000;

    for (int start = 0; start < totalRecords; start += batchSize) {
      final int end =
          (start + batchSize < totalRecords) ? start + batchSize : totalRecords;
      // prepare records for current batch
      final users = <Map<String, dynamic>>[];
      for (var i = start; i < end; i++) {
        users.add({
          'username': 'user_$i',
          'email': 'user_$i@example.com',
          'tags': '4f4h3fd4fi33JlIDN2lh3777',
          'is_active': i > 5,
        });
      }

      // batch insert current batch
      await db.batchInsert('users', users);

      // yield to event loop, keep UI responsive
      await Future.delayed(Duration.zero);
    }

    stopwatch.stop();
    final elapsed = stopwatch.elapsedMilliseconds;
    logService.add('insert time: ${elapsed}ms');

    final queryResult = await db.query('users').count();
    logService.add('query count: $queryResult');
    return elapsed;
  }

  Future<int> addExamplesOneByOne() async {
    await db.clear('users');
    logService.add('Starting to add 10,000 records one by one...');
    final stopwatch = Stopwatch()..start();
    for (var i = 0; i < 10000; i++) {
      await db.insert('users', {
        'username': 'user_$i',
        'email': 'user_$i@example.com',
        'tags': '4f4h3fd4fi33JlIDN2lh3777',
        'is_active': i > 5,
      });
      // Yield to the event loop to prevent UI freezing
      if (i % 100 == 0) {
        // Yield more sparingly to improve performance while keeping UI responsive
        await Future.delayed(Duration.zero);
      }
    }
    stopwatch.stop();
    final elapsed = stopwatch.elapsedMilliseconds;
    logService.add('Finished adding 10k records one-by-one in ${elapsed}ms');
    final queryResult = await db.query('users').count();
    logService.add('query count: $queryResult');
    return elapsed;
  }

  Future<int> deleteExamples() async {
    final stopwatch = Stopwatch()..start();
    final deleteResult = await db.delete('users').where('id', '>', '5');
    stopwatch.stop();
    final elapsed = stopwatch.elapsedMilliseconds;
    logService.add('delete : ${deleteResult.toJson()}');
    logService.add('delete time: ${elapsed}ms');
    return elapsed;
  }

  Future<int> queryExamples() async {
    Stopwatch stopwatch = Stopwatch()..start();
    final queryResult = await db.query('users').where('id', '<', '6').limit(8);
    stopwatch.stop();
    final elapsed = stopwatch.elapsedMilliseconds;

    logService.add('query time: ${elapsed}ms');
    logService
        .add('query result: ${queryResult.length} ${queryResult.toJson()}');
    final queryCount = await db.query('users').count();
    logService.add('query count: $queryCount');
    return elapsed;
  }
}

/// Simple UI to run examples
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // It's crucial to set the log handler *before* any potential errors can occur.
  // This ensures that initialization logs are captured and displayed in the UI.
  LogConfig.setConfig(
    onLogHandler: (message, type, label) {
      logService.add('[$label] $message', type, true);
    },
  );

  final example = TostoreExample();

  // The example app will now run even if initialization fails,
  // allowing the user to see the error logs in the ListView.
  runApp(MyApp(example: example));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key, required this.example});
  final TostoreExample example;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Tostore Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xff0aa6e8)),
        useMaterial3: true,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: TostoreExamplePage(example: example),
    );
  }
}

class TostoreExamplePage extends StatefulWidget {
  const TostoreExamplePage({super.key, required this.example});
  final TostoreExample example;
  @override
  State<TostoreExamplePage> createState() => _TostoreExamplePageState();
}

class _TostoreExamplePageState extends State<TostoreExamplePage> {
  final ScrollController _scrollController = ScrollController();
  final TextEditingController _searchController = TextEditingController();
  LogType? _selectedLogType;
  String _lastOperationInfo = 'Please initialize the database first.';
  bool _isDbInitialized = false;
  bool _isInitializing = true;
  bool _canScrollUp = false;
  bool _canScrollDown = false;
  bool _isAtBottom = true; // Assume we start at the bottom

  @override
  void initState() {
    super.initState();
    _initializeDatabase();
    logService.logs.addListener(_onLogsChanged);
    _scrollController.addListener(_scrollListener);
    _searchController.addListener(() {
      setState(() {
        // Just rebuild the widget when text changes
      });
    });
    // Set the initial state of the scroll buttons after the first frame.
    WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
  }

  void _onLogsChanged() {
    // If we were at the bottom before the new logs were added, maintain that position.
    if (_isAtBottom) {
      _scrollToBottom();
    }
    // After logs are added, the scroll extent might change, so re-evaluate button states.
    WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
  }

  void _scrollListener() {
    if (!_scrollController.hasClients) return;
    final position = _scrollController.position;

    // Use a small tolerance for floating point comparisons
    _isAtBottom = position.pixels >= position.maxScrollExtent - 1.0;
    final atTop = position.pixels <= position.minScrollExtent + 1.0;

    final canScrollUp = !atTop;
    final canScrollDown = !_isAtBottom;

    if (_canScrollUp != canScrollUp || _canScrollDown != canScrollDown) {
      if (mounted) {
        setState(() {
          _canScrollUp = canScrollUp;
          _canScrollDown = canScrollDown;
        });
      }
    }
  }

  void _updateOperationInfo(String info) {
    if (mounted) {
      setState(() {
        _lastOperationInfo = info;
      });
    }
  }

  Future<void> _initializeDatabase() async {
    setState(() {
      _isInitializing = true;
      _lastOperationInfo = 'Initializing Database...';
    });

    final stopwatch = Stopwatch()..start();
    try {
      await widget.example.initialize();
      stopwatch.stop();
      if (mounted) {
        setState(() {
          _isDbInitialized = true;
          _isInitializing = false;
          _lastOperationInfo =
              'DB Initialized: ${stopwatch.elapsedMilliseconds}ms';
        });
      }
    } catch (e, s) {
      stopwatch.stop();
      logService.add(
          '!!!!!! DATABASE INITIALIZATION FAILED !!!!!!', LogType.error);
      logService.add('Error: $e', LogType.error);
      logService.add('StackTrace: $s', LogType.error);
      if (mounted) {
        setState(() {
          _isDbInitialized = false;
          _isInitializing = false;
          _lastOperationInfo = 'DB Initialization FAILED!';
        });
      }
    }
  }

  @override
  void dispose() {
    logService.logs.removeListener(_onLogsChanged);
    _scrollController.removeListener(_scrollListener);
    _scrollController.dispose();
    _searchController.dispose();
    super.dispose();
  }

  void _scrollToTop() {
    if (_scrollController.hasClients) {
      _scrollController.animateTo(
        0,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOut,
      );
    }
  }

  void _scrollToBottom() {
    // its scroll extents after the widget tree has been rebuilt with new log entries.
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_scrollController.hasClients) {
        final maxScroll = _scrollController.position.maxScrollExtent;
        _scrollController.jumpTo(maxScroll);

        // Sometimes, the maxScrollExtent is not updated in a single frame, especially
        // with a large number of list items. We schedule a second check to ensure
        // we are at the very bottom.
        WidgetsBinding.instance.addPostFrameCallback((_) {
          if (_scrollController.hasClients &&
              _scrollController.position.pixels !=
                  _scrollController.position.maxScrollExtent) {
            _scrollController
                .jumpTo(_scrollController.position.maxScrollExtent);
          }
        });
      }
    });
  }

  Widget _buildActionButton({required String text, VoidCallback? onPressed}) {
    return ElevatedButton(
      onPressed: onPressed,
      style: ElevatedButton.styleFrom(
        foregroundColor: Colors.white,
        backgroundColor: const Color.fromARGB(255, 10, 150, 210),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
        elevation: 2,
        shadowColor: const Color.fromARGB(255, 6, 126, 177).withOpacity(0.4),
        padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
      ),
      child: Text(
        text,
        textAlign: TextAlign.center,
        style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
        maxLines: 1,
        overflow: TextOverflow.visible,
      ),
    );
  }

  Widget _buildFilterButton(
      String text, LogType? type, int count, BuildContext context) {
    if (count == 0 && type != null) {
      // Don't show the button if there are no logs of this type (except for 'All')
      return const SizedBox.shrink();
    }
    final isSelected = _selectedLogType == type;
    final Color backgroundColor;
    final Color foregroundColor;
    final double elevation;
    final Color? shadowColor;
    Color? countColor;

    if (isSelected) {
      backgroundColor = const Color.fromARGB(255, 10, 150, 210);
      foregroundColor = Colors.white;
      elevation = 2;
      shadowColor = const Color.fromARGB(255, 6, 126, 177).withOpacity(0.4);
    } else {
      backgroundColor = const Color.fromARGB(255, 227, 232, 235);
      foregroundColor = Theme.of(context).colorScheme.onSecondaryContainer;
      elevation = 0;
      shadowColor = null;

      // Set count color for non-selected buttons
      if (type == LogType.error) {
        countColor = Colors.red;
      } else if (type == LogType.warn) {
        countColor = Colors.orange;
      }
    }

    return ElevatedButton(
      onPressed: () {
        setState(() {
          _selectedLogType = type;
        });
      },
      style: ElevatedButton.styleFrom(
        backgroundColor: backgroundColor,
        foregroundColor: foregroundColor,
        elevation: elevation,
        shadowColor: shadowColor,
        padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
        minimumSize: Size.zero,
        tapTargetSize: MaterialTapTargetSize.shrinkWrap,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
      ),
      child: RichText(
        text: TextSpan(
          style: TextStyle(
            color: foregroundColor,
            fontWeight: FontWeight.normal,
          ),
          children: [
            TextSpan(text: '$text ('),
            TextSpan(
              text: '$count',
              style: TextStyle(
                fontWeight: FontWeight.bold,
                color:
                    countColor, // This will be null for selected, which is fine
              ),
            ),
            const TextSpan(text: ')'),
          ],
        ),
      ),
    );
  }

  Color _getLogColor(LogType type) {
    switch (type) {
      case LogType.error:
        return Colors.red;
      case LogType.warn:
        return Colors.orange;
      case LogType.debug:
        return Colors.blueAccent;
      case LogType.info:
      default:
        return Colors.black;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        title: const Text('Tostore Demo'),
        actions: [
          IconButton(
            icon: const Icon(Icons.arrow_upward),
            tooltip: 'Scroll to Top',
            onPressed: _canScrollUp ? _scrollToTop : null,
          ),
          IconButton(
            icon: const Icon(Icons.arrow_downward),
            tooltip: 'Scroll to Bottom',
            onPressed: _canScrollDown ? _scrollToBottom : null,
          ),
          IconButton(
            icon: const Icon(Icons.copy_outlined, size: 24),
            tooltip: 'Copy Visible Logs',
            onPressed: () {
              // Get the current list of all logs
              final allLogs = logService.logs.value;

              // Apply the same filtering logic as the list view
              final filteredByType = _selectedLogType == null
                  ? allLogs
                  : allLogs
                      .where((log) => log.type == _selectedLogType)
                      .toList();

              final searchText = _searchController.text.toLowerCase();
              final filteredLogs = searchText.isEmpty
                  ? filteredByType
                  : filteredByType
                      .where((log) =>
                          log.message.toLowerCase().contains(searchText))
                      .toList();

              if (filteredLogs.isEmpty) {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('No logs to copy.')),
                );
                return;
              }

              // Format logs into a single string
              final logsText =
                  filteredLogs.map((log) => log.message).join('\n');

              // Copy to clipboard
              Clipboard.setData(ClipboardData(text: logsText));

              // Show confirmation
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(
                    content: Text('Visible logs copied to clipboard!')),
              );
            },
          ),
          IconButton(
            icon: const Icon(
              Icons.cleaning_services_rounded,
              size: 24,
            ),
            tooltip: 'Clear Logs',
            onPressed: () {
              logService.clear();
            },
          ),
        ],
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.surface.withOpacity(0.1),
              padding: const EdgeInsets.all(8.0),
              child: ValueListenableBuilder<List<LogEntry>>(
                valueListenable: logService.logs,
                builder: (context, logs, child) {
                  // First, filter by selected log type
                  final filteredByType = _selectedLogType == null
                      ? logs
                      : logs
                          .where((log) => log.type == _selectedLogType)
                          .toList();

                  // Then, filter by search text
                  final searchText = _searchController.text.toLowerCase();
                  final filteredLogs = searchText.isEmpty
                      ? filteredByType
                      : filteredByType
                          .where((log) =>
                              log.message.toLowerCase().contains(searchText))
                          .toList();

                  if (filteredLogs.isEmpty) {
                    return const Center(
                        child: Text('No logs match your filter.'));
                  }
                  return ListView.builder(
                    controller: _scrollController,
                    itemCount: filteredLogs.length,
                    itemBuilder: (context, index) {
                      final logEntry = filteredLogs[index];
                      return Padding(
                        padding: const EdgeInsets.symmetric(
                            horizontal: 8.0, vertical: 4.0),
                        child: Text(
                          logEntry.message,
                          style: TextStyle(
                            fontSize: 12,
                            color: _getLogColor(logEntry.type),
                          ),
                        ),
                      );
                    },
                  );
                },
              ),
            ),
          ),
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              TextField(
                controller: _searchController,
                decoration: InputDecoration(
                  hintText: 'Search in logs...',
                  prefixIcon: const Icon(Icons.search),
                  suffixIcon: _searchController.text.isNotEmpty
                      ? IconButton(
                          icon: const Icon(Icons.clear),
                          onPressed: () {
                            _searchController.clear();
                          },
                        )
                      : null,
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(8),
                  ),
                  enabledBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(8),
                    borderSide:
                        BorderSide(color: Colors.grey.shade300, width: 0.8),
                  ),
                  contentPadding:
                      const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
                ),
              ),
              const SizedBox(height: 14),
              ValueListenableBuilder<List<LogEntry>>(
                valueListenable: logService.logs,
                builder: (context, logs, child) {
                  // Calculate counts for each log type
                  final allCount = logs.length;
                  final infoCount =
                      logs.where((log) => log.type == LogType.info).length;
                  final debugCount =
                      logs.where((log) => log.type == LogType.debug).length;
                  final warnCount =
                      logs.where((log) => log.type == LogType.warn).length;
                  final errorCount =
                      logs.where((log) => log.type == LogType.error).length;

                  return Wrap(
                    spacing: 8.0,
                    runSpacing: 8.0,
                    alignment: WrapAlignment.start,
                    children: [
                      _buildFilterButton('All', null, allCount, context),
                      _buildFilterButton(
                          'Info', LogType.info, infoCount, context),
                      _buildFilterButton(
                          'Debug', LogType.debug, debugCount, context),
                      _buildFilterButton(
                          'Warn', LogType.warn, warnCount, context),
                      _buildFilterButton(
                          'Error', LogType.error, errorCount, context),
                    ],
                  );
                },
              ),
            ],
          ),
          const SizedBox(height: 20),
          const Divider(height: 0.1),
          SingleChildScrollView(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  if (_isInitializing)
                    const Center(
                      child: Padding(
                        padding: EdgeInsets.symmetric(
                            horizontal: 16.0, vertical: 24),
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            CircularProgressIndicator(),
                            SizedBox(width: 20),
                            Text('Initializing...'),
                          ],
                        ),
                      ),
                    )
                  else
                    Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Container(
                          height: 40,
                          alignment: Alignment.center,
                          child: Text(
                            _lastOperationInfo,
                            style: Theme.of(context).textTheme.titleMedium,
                            overflow: TextOverflow.ellipsis,
                          ),
                        ),
                        Wrap(
                          spacing: 12,
                          runSpacing: 12,
                          alignment: WrapAlignment.center,
                          children: [
                            SizedBox(
                              width: MediaQuery.of(context).size.width / 2 - 22,
                              child: _buildActionButton(
                                  text: 'Clear Table',
                                  onPressed: !_isDbInitialized
                                      ? null
                                      : () async {
                                          final elapsed = await widget.example
                                              .clearExamples();
                                          if (mounted) {
                                            setState(() {
                                              _lastOperationInfo =
                                                  'Clear Table: ${elapsed}ms';
                                            });
                                          }
                                        }),
                            ),
                            SizedBox(
                              width: MediaQuery.of(context).size.width / 2 - 22,
                              child: _buildActionButton(
                                  text: 'Query',
                                  onPressed: !_isDbInitialized
                                      ? null
                                      : () async {
                                          final elapsed = await widget.example
                                              .queryExamples();
                                          if (mounted) {
                                            setState(() {
                                              _lastOperationInfo =
                                                  'Query: ${elapsed}ms';
                                            });
                                          }
                                        }),
                            ),
                            SizedBox(
                              width: MediaQuery.of(context).size.width / 2 - 22,
                              child: _buildActionButton(
                                  text: 'Batch Add 10k',
                                  onPressed: !_isDbInitialized
                                      ? null
                                      : () async {
                                          final elapsed = await widget.example
                                              .addExamples();
                                          if (mounted) {
                                            setState(() {
                                              _lastOperationInfo =
                                                  'Batch Add 10k: ${elapsed}ms';
                                            });
                                          }
                                        }),
                            ),
                            SizedBox(
                              width: MediaQuery.of(context).size.width / 2 - 22,
                              child: _buildActionButton(
                                  text: 'Slow Add 10k',
                                  onPressed: !_isDbInitialized
                                      ? null
                                      : () async {
                                          final elapsed = await widget.example
                                              .addExamplesOneByOne();
                                          if (mounted) {
                                            setState(() {
                                              _lastOperationInfo =
                                                  'Slow Add 10k: ${elapsed}ms';
                                            });
                                          }
                                        }),
                            ),
                            SizedBox(
                              width: MediaQuery.of(context).size.width / 2 - 22,
                              child: _buildActionButton(
                                  text: 'Delete Many',
                                  onPressed: !_isDbInitialized
                                      ? null
                                      : () async {
                                          final elapsed = await widget.example
                                              .deleteExamples();
                                          if (mounted) {
                                            setState(() {
                                              _lastOperationInfo =
                                                  'Delete Many: ${elapsed}ms';
                                            });
                                          }
                                        }),
                            ),
                            SizedBox(
                              width: MediaQuery.of(context).size.width / 2 - 22,
                              child: _buildActionButton(
                                text: 'Run Integrity Tests',
                                onPressed: !_isDbInitialized
                                    ? null
                                    : () async {
                                        final tester = DatabaseTester(
                                            widget.example.db,
                                            logService,
                                            _updateOperationInfo);
                                        await tester.runAllTests();
                                      },
                              ),
                            ),
                          ],
                        ),
                      ],
                    ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

/// A simple class to hold log data along with its type.
class LogEntry {
  final String message;
  final LogType type;
  final DateTime timestamp;

  LogEntry(
      {required this.message, required this.type, required this.timestamp});
}

/// A simple service to manage and notify about logs.
class LogService {
  final ValueNotifier<List<LogEntry>> _logs = ValueNotifier([]);
  ValueNotifier<List<LogEntry>> get logs => _logs;

  void add(String message,
      [LogType type = LogType.info, bool fromCallback = false]) {
    // To avoid UI freezing with a large number of logs, we keep a reasonable limit.
    const maxLogs = 200;
    final now = DateTime.now();
    final timestampString =
        '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
    final newLogs = List<LogEntry>.from(_logs.value);
    newLogs.add(LogEntry(
        message: '[$timestampString] $message', type: type, timestamp: now));
    if (newLogs.length > maxLogs) {
      newLogs.removeRange(0, newLogs.length - maxLogs);
    }
    _logs.value = newLogs;

    // Only print to the developer console if the log is NOT from the internal callback.
    if (!fromCallback) {
      developer.log(message);
    }
  }

  void clear() {
    _logs.value = [];
  }
}

final logService = LogService();
31
likes
140
points
147k
downloads

Publisher

verified publishertoway.world

Weekly Downloads

A advanced storage engine that supports relational and NoSQL database,data smart cache, multi-space architecture,file/local storage, SQL & key-value persistent store.

Repository (GitHub)
Contributing

Topics

#database #storage #sql #nosql #distributed

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

archive, flutter, path, path_provider, web

More

Packages that depend on tostore