Merge branch 'selection_mode' into 'master'
Selection mode See merge request lukas/notes!3
This commit is contained in:
		@@ -2,6 +2,7 @@
 | 
			
		||||
    package="eu.heili.notes">
 | 
			
		||||
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 | 
			
		||||
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
 | 
			
		||||
    <uses-permission android:name="android.permission.VIBRATE"/>
 | 
			
		||||
   <application
 | 
			
		||||
           android:requestLegacyExternalStorage="true"
 | 
			
		||||
        android:label="notes"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								lib/helpers/vibrate.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								lib/helpers/vibrate.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:vibration/vibration.dart';
 | 
			
		||||
 | 
			
		||||
Future<void> shortVibrate() async {
 | 
			
		||||
  if (defaultTargetPlatform == TargetPlatform.android ||
 | 
			
		||||
      defaultTargetPlatform == TargetPlatform.iOS) {
 | 
			
		||||
    if (await Vibration.hasVibrator() ?? false) {
 | 
			
		||||
      Vibration.vibrate(duration: 50);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,12 @@
 | 
			
		||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:fluttertoast/fluttertoast.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
 | 
			
		||||
import '../context/file_change_notifier.dart';
 | 
			
		||||
import '../savesystem/note_file.dart';
 | 
			
		||||
import '../widgets/icon_material_button.dart';
 | 
			
		||||
import '../widgets/icon_text_button.dart';
 | 
			
		||||
import '../widgets/note_tile.dart';
 | 
			
		||||
import '../widgets/wip_toast.dart';
 | 
			
		||||
 | 
			
		||||
@@ -16,6 +19,8 @@ class AllNotesPage extends StatefulWidget {
 | 
			
		||||
 | 
			
		||||
class _AllNotesPageState extends State<AllNotesPage> {
 | 
			
		||||
  FToast fToast = FToast();
 | 
			
		||||
  bool selectionMode = false;
 | 
			
		||||
  List<int> selectionIdx = [];
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
@@ -23,14 +28,23 @@ class _AllNotesPageState extends State<AllNotesPage> {
 | 
			
		||||
    fToast.init(context);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Column(
 | 
			
		||||
  Widget _buildTopBar() {
 | 
			
		||||
    if (selectionMode) {
 | 
			
		||||
      return Row(
 | 
			
		||||
        children: [
 | 
			
		||||
        SizedBox(
 | 
			
		||||
          height: 25 + MediaQuery.of(context).viewPadding.top,
 | 
			
		||||
          const SizedBox(
 | 
			
		||||
            width: 20,
 | 
			
		||||
            height: 40,
 | 
			
		||||
          ),
 | 
			
		||||
        Row(
 | 
			
		||||
          Text(
 | 
			
		||||
            '${selectionIdx.length} selected',
 | 
			
		||||
            style: const TextStyle(
 | 
			
		||||
                color: Color.fromRGBO(255, 255, 255, .85), fontSize: 21),
 | 
			
		||||
          )
 | 
			
		||||
        ],
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      return Row(
 | 
			
		||||
        children: [
 | 
			
		||||
          const SizedBox(
 | 
			
		||||
            width: 20,
 | 
			
		||||
@@ -82,7 +96,18 @@ class _AllNotesPageState extends State<AllNotesPage> {
 | 
			
		||||
            width: 15,
 | 
			
		||||
          )
 | 
			
		||||
        ],
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Column(
 | 
			
		||||
      children: [
 | 
			
		||||
        SizedBox(
 | 
			
		||||
          height: 25 + MediaQuery.of(context).viewPadding.top,
 | 
			
		||||
        ),
 | 
			
		||||
        _buildTopBar(),
 | 
			
		||||
        Row(
 | 
			
		||||
          children: const [
 | 
			
		||||
            SizedBox(
 | 
			
		||||
@@ -90,26 +115,142 @@ class _AllNotesPageState extends State<AllNotesPage> {
 | 
			
		||||
            )
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        _buildNoteTiles()
 | 
			
		||||
        _buildNoteTiles(),
 | 
			
		||||
        if (selectionMode)
 | 
			
		||||
          SizedBox(
 | 
			
		||||
            height: 70,
 | 
			
		||||
            child: Row(
 | 
			
		||||
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
 | 
			
		||||
              mainAxisSize: MainAxisSize.max,
 | 
			
		||||
              children: [
 | 
			
		||||
                IconTextButton(
 | 
			
		||||
                  icon: const Icon(Icons.drive_file_move_outline),
 | 
			
		||||
                  color: const Color.fromRGBO(255, 255, 255, .85),
 | 
			
		||||
                  onPressed: () {
 | 
			
		||||
                    fToast.showToast(
 | 
			
		||||
                      child: const WIPToast(),
 | 
			
		||||
                      gravity: ToastGravity.BOTTOM,
 | 
			
		||||
                      toastDuration: const Duration(seconds: 2),
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                  iconSize: 24,
 | 
			
		||||
                  text: 'Move',
 | 
			
		||||
                ),
 | 
			
		||||
                IconTextButton(
 | 
			
		||||
                  icon: const Icon(Icons.lock_outline),
 | 
			
		||||
                  color: const Color.fromRGBO(255, 255, 255, .85),
 | 
			
		||||
                  onPressed: () {
 | 
			
		||||
                    fToast.showToast(
 | 
			
		||||
                      child: const WIPToast(),
 | 
			
		||||
                      gravity: ToastGravity.BOTTOM,
 | 
			
		||||
                      toastDuration: const Duration(seconds: 2),
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                  iconSize: 24,
 | 
			
		||||
                  text: 'Lock',
 | 
			
		||||
                ),
 | 
			
		||||
                IconTextButton(
 | 
			
		||||
                  icon: const Icon(Icons.share),
 | 
			
		||||
                  color: const Color.fromRGBO(255, 255, 255, .85),
 | 
			
		||||
                  onPressed: () {
 | 
			
		||||
                    fToast.showToast(
 | 
			
		||||
                      child: const WIPToast(),
 | 
			
		||||
                      gravity: ToastGravity.BOTTOM,
 | 
			
		||||
                      toastDuration: const Duration(seconds: 2),
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                  iconSize: 24,
 | 
			
		||||
                  text: 'Share',
 | 
			
		||||
                ),
 | 
			
		||||
                IconTextButton(
 | 
			
		||||
                  icon: const Icon(FluentIcons.delete_20_filled),
 | 
			
		||||
                  color: const Color.fromRGBO(255, 255, 255, .85),
 | 
			
		||||
                  onPressed: () async {
 | 
			
		||||
                    // todo add popup to ask if really delete
 | 
			
		||||
 | 
			
		||||
                    final filechangenotfier =
 | 
			
		||||
                        Provider.of<FileChangeNotifier>(context, listen: false);
 | 
			
		||||
                    for (final s in selectionIdx) {
 | 
			
		||||
                      final dta = filechangenotfier.tiledata[s];
 | 
			
		||||
                      // todo maybe optimize a bit and create not always new notefile instance
 | 
			
		||||
                      await NoteFile(dta.relativePath).delete();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await filechangenotfier.loadAllNotes();
 | 
			
		||||
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      selectionIdx = [];
 | 
			
		||||
                      selectionMode = false;
 | 
			
		||||
                    });
 | 
			
		||||
                  },
 | 
			
		||||
                  iconSize: 24,
 | 
			
		||||
                  text: 'Delete',
 | 
			
		||||
                ),
 | 
			
		||||
                IconTextButton(
 | 
			
		||||
                  icon: const Icon(Icons.more_vert),
 | 
			
		||||
                  color: const Color.fromRGBO(255, 255, 255, .85),
 | 
			
		||||
                  onPressed: () {
 | 
			
		||||
                    fToast.showToast(
 | 
			
		||||
                      child: const WIPToast(),
 | 
			
		||||
                      gravity: ToastGravity.BOTTOM,
 | 
			
		||||
                      toastDuration: const Duration(seconds: 2),
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                  iconSize: 24,
 | 
			
		||||
                  text: 'More',
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          )
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Widget _buildNoteTiles() {
 | 
			
		||||
    return Expanded(
 | 
			
		||||
      child: Consumer<FileChangeNotifier>(
 | 
			
		||||
    return Consumer<FileChangeNotifier>(
 | 
			
		||||
      builder: (BuildContext context, value, Widget? child) {
 | 
			
		||||
          return GridView.builder(
 | 
			
		||||
        return Expanded(
 | 
			
		||||
          child: GridView.builder(
 | 
			
		||||
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
 | 
			
		||||
              crossAxisCount: 5,
 | 
			
		||||
            ),
 | 
			
		||||
                crossAxisCount: 6, childAspectRatio: 0.7),
 | 
			
		||||
            physics: const ScrollPhysics(),
 | 
			
		||||
            padding: const EdgeInsets.all(2),
 | 
			
		||||
            itemBuilder: (BuildContext context, int idx) =>
 | 
			
		||||
                NoteTile(data: value.tiledata[idx]),
 | 
			
		||||
            itemCount: value.tiledata.length,
 | 
			
		||||
          );
 | 
			
		||||
            itemBuilder: (BuildContext context, int idx) => NoteTile(
 | 
			
		||||
              data: value.tiledata[idx],
 | 
			
		||||
              selectionMode: selectionMode,
 | 
			
		||||
              selected: selectionIdx.contains(idx),
 | 
			
		||||
              onSelectionChange: (selection) {
 | 
			
		||||
                if (selection) {
 | 
			
		||||
                  if (!selectionMode) {
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      selectionMode = true;
 | 
			
		||||
                    });
 | 
			
		||||
                  }
 | 
			
		||||
                  if (!selectionIdx.contains(idx)) {
 | 
			
		||||
                    final sel = selectionIdx;
 | 
			
		||||
                    sel.add(idx);
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      selectionIdx = sel;
 | 
			
		||||
                    });
 | 
			
		||||
                  }
 | 
			
		||||
                } else {
 | 
			
		||||
                  final sel = selectionIdx;
 | 
			
		||||
                  sel.remove(idx);
 | 
			
		||||
                  if (sel.isEmpty) {
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      selectionMode = false;
 | 
			
		||||
                    });
 | 
			
		||||
                  }
 | 
			
		||||
                  setState(() {
 | 
			
		||||
                    selectionIdx = sel;
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            itemCount: value.tiledata.length,
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,21 +6,25 @@ import 'package:sqflite/sqflite.dart';
 | 
			
		||||
import 'path.dart';
 | 
			
		||||
 | 
			
		||||
class NoteFile {
 | 
			
		||||
  late Database _db;
 | 
			
		||||
  Database? _db;
 | 
			
		||||
  String filename;
 | 
			
		||||
  late String _basePath;
 | 
			
		||||
  String? _basePath;
 | 
			
		||||
 | 
			
		||||
  String? _newFileName;
 | 
			
		||||
 | 
			
		||||
  Database db() {
 | 
			
		||||
    return _db;
 | 
			
		||||
    assert(_db != null);
 | 
			
		||||
    return _db!;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  NoteFile(this.filename);
 | 
			
		||||
 | 
			
		||||
  Future<void> init() async {
 | 
			
		||||
    _basePath = (await getSavePath()).path;
 | 
			
		||||
    final path = _basePath + Platform.pathSeparator + filename;
 | 
			
		||||
    if (_basePath == null) {
 | 
			
		||||
      await _initBasePath();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final path = _basePath! + Platform.pathSeparator + filename;
 | 
			
		||||
    _db = await openDatabase(
 | 
			
		||||
      path,
 | 
			
		||||
      onCreate: (db, version) async {
 | 
			
		||||
@@ -44,7 +48,11 @@ class NoteFile {
 | 
			
		||||
 | 
			
		||||
  Future<void> delete() async {
 | 
			
		||||
    await close();
 | 
			
		||||
    await File(_basePath + Platform.pathSeparator + filename).delete();
 | 
			
		||||
 | 
			
		||||
    if (_basePath == null) {
 | 
			
		||||
      await _initBasePath();
 | 
			
		||||
    }
 | 
			
		||||
    await File(_basePath! + Platform.pathSeparator + filename).delete();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void rename(String newname) {
 | 
			
		||||
@@ -53,19 +61,27 @@ class NoteFile {
 | 
			
		||||
 | 
			
		||||
  Future<void> close() async {
 | 
			
		||||
    // shrink the db file size
 | 
			
		||||
    if (_db.isOpen) {
 | 
			
		||||
      await _db.execute('VACUUM');
 | 
			
		||||
      await _db.close();
 | 
			
		||||
    if (_db != null && _db!.isOpen) {
 | 
			
		||||
      await _db!.execute('VACUUM');
 | 
			
		||||
      await _db!.close();
 | 
			
		||||
    } else {
 | 
			
		||||
      debugPrint('db file unexpectedly closed before shrinking');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (_basePath == null) {
 | 
			
		||||
      await _initBasePath();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // perform qued file renaming operations
 | 
			
		||||
    if (_newFileName != null) {
 | 
			
		||||
      File(_basePath + Platform.pathSeparator + filename)
 | 
			
		||||
          .rename(_basePath + Platform.pathSeparator + _newFileName!);
 | 
			
		||||
      File(_basePath! + Platform.pathSeparator + filename)
 | 
			
		||||
          .rename(_basePath! + Platform.pathSeparator + _newFileName!);
 | 
			
		||||
      filename = _newFileName!;
 | 
			
		||||
      _newFileName = null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _initBasePath() async {
 | 
			
		||||
    _basePath = (await getSavePath()).path;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								lib/widgets/icon_text_button.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								lib/widgets/icon_text_button.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'icon_material_button.dart';
 | 
			
		||||
 | 
			
		||||
class IconTextButton extends StatelessWidget {
 | 
			
		||||
  const IconTextButton(
 | 
			
		||||
      {Key? key,
 | 
			
		||||
      required this.icon,
 | 
			
		||||
      required this.color,
 | 
			
		||||
      required this.onPressed,
 | 
			
		||||
      required this.text,
 | 
			
		||||
      this.iconSize})
 | 
			
		||||
      : super(key: key);
 | 
			
		||||
  final Widget icon;
 | 
			
		||||
  final Color color;
 | 
			
		||||
  final void Function() onPressed;
 | 
			
		||||
  final String text;
 | 
			
		||||
  final double? iconSize;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Column(
 | 
			
		||||
      children: [
 | 
			
		||||
        IconMaterialButton(
 | 
			
		||||
            icon: icon, color: color, onPressed: onPressed, iconSize: iconSize),
 | 
			
		||||
        Text(
 | 
			
		||||
          text,
 | 
			
		||||
          style: TextStyle(color: color),
 | 
			
		||||
        )
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,35 +1,74 @@
 | 
			
		||||
import 'dart:math';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import '../canvas/document_types.dart';
 | 
			
		||||
import '../canvas/drawing_page.dart';
 | 
			
		||||
import '../helpers/vibrate.dart';
 | 
			
		||||
 | 
			
		||||
class NoteTile extends StatelessWidget {
 | 
			
		||||
  const NoteTile({Key? key, required this.data}) : super(key: key);
 | 
			
		||||
  const NoteTile(
 | 
			
		||||
      {Key? key,
 | 
			
		||||
      required this.data,
 | 
			
		||||
      required this.selectionMode,
 | 
			
		||||
      required this.selected,
 | 
			
		||||
      required this.onSelectionChange})
 | 
			
		||||
      : super(key: key);
 | 
			
		||||
 | 
			
		||||
  final NoteMetaData data;
 | 
			
		||||
  final bool selectionMode;
 | 
			
		||||
  final bool selected;
 | 
			
		||||
  final void Function(bool) onSelectionChange;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return GestureDetector(
 | 
			
		||||
      onTap: () {
 | 
			
		||||
        if (selectionMode) {
 | 
			
		||||
          onSelectionChange(!selected);
 | 
			
		||||
        } else {
 | 
			
		||||
          Navigator.push(
 | 
			
		||||
            context,
 | 
			
		||||
            MaterialPageRoute(
 | 
			
		||||
              builder: (context) => DrawingPage(meta: data),
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      child: SizedBox(
 | 
			
		||||
        width: 100,
 | 
			
		||||
      onLongPress: () async {
 | 
			
		||||
        shortVibrate();
 | 
			
		||||
        onSelectionChange(!selected);
 | 
			
		||||
      },
 | 
			
		||||
      child: Padding(
 | 
			
		||||
        padding: const EdgeInsets.all(20),
 | 
			
		||||
        child: Column(
 | 
			
		||||
          children: [
 | 
			
		||||
            SizedBox(
 | 
			
		||||
              height: 150,
 | 
			
		||||
              width: 100,
 | 
			
		||||
            Expanded(
 | 
			
		||||
              child: Stack(
 | 
			
		||||
                children: [
 | 
			
		||||
                  ClipRRect(
 | 
			
		||||
                    borderRadius: BorderRadius.circular(10),
 | 
			
		||||
                    child: AspectRatio(
 | 
			
		||||
                      aspectRatio: 1 / sqrt2,
 | 
			
		||||
                      child: Container(
 | 
			
		||||
                        color: Colors.white,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  if (selectionMode)
 | 
			
		||||
                    Padding(
 | 
			
		||||
                      padding: const EdgeInsets.only(left: 10, top: 10),
 | 
			
		||||
                      child: CustomPaint(
 | 
			
		||||
                        size: const Size(25, 25),
 | 
			
		||||
                        painter: _CirclePainter(selected),
 | 
			
		||||
                      ),
 | 
			
		||||
                    )
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            const SizedBox(
 | 
			
		||||
              height: 5,
 | 
			
		||||
            ),
 | 
			
		||||
            Text(
 | 
			
		||||
              data.name,
 | 
			
		||||
              style: const TextStyle(color: Colors.white),
 | 
			
		||||
@@ -43,3 +82,31 @@ class NoteTile extends StatelessWidget {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _CirclePainter extends CustomPainter {
 | 
			
		||||
  final bool selected;
 | 
			
		||||
 | 
			
		||||
  final _paint = Paint()..strokeWidth = .7;
 | 
			
		||||
 | 
			
		||||
  _CirclePainter(this.selected) {
 | 
			
		||||
    if (selected) {
 | 
			
		||||
      _paint.color = Colors.orange;
 | 
			
		||||
      _paint.style = PaintingStyle.fill;
 | 
			
		||||
    } else {
 | 
			
		||||
      _paint.color = Colors.black;
 | 
			
		||||
      _paint.style = PaintingStyle.stroke;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void paint(Canvas canvas, Size size) {
 | 
			
		||||
    canvas.drawOval(
 | 
			
		||||
      Rect.fromLTWH(0, 0, size.width, size.height),
 | 
			
		||||
      _paint,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool shouldRepaint(_CirclePainter oldDelegate) =>
 | 
			
		||||
      oldDelegate.selected != selected;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -441,6 +441,13 @@ packages:
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.2"
 | 
			
		||||
  vibration:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: vibration
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.7.6"
 | 
			
		||||
  win32:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,7 @@ dependencies:
 | 
			
		||||
  pdf: ^3.8.4
 | 
			
		||||
  permission_handler: ^10.2.0
 | 
			
		||||
  fluttertoast: ^8.1.1
 | 
			
		||||
  vibration: ^1.7.6
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user