Trae Assistant commited on
Commit
de7f206
·
1 Parent(s): 8d11656
Files changed (8) hide show
  1. Dockerfile +12 -0
  2. README.md +45 -5
  3. Web/app.py +31 -0
  4. Web/templates/index.html +92 -0
  5. lib/logic.dart +76 -0
  6. lib/main.dart +218 -0
  7. lib/models.dart +46 -0
  8. pubspec.yaml +23 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY Web/ .
6
+
7
+ RUN pip install --no-cache-dir flask
8
+
9
+ ENV PORT=7860
10
+ EXPOSE 7860
11
+
12
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,10 +1,50 @@
1
  ---
2
- title: Time Voyager Flutter
3
- emoji: 📉
4
- colorFrom: red
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: TimeVoyager Flutter Showcase
3
+ emoji: 💙
4
+ colorFrom: blue
5
+ colorTo: cyan
6
  sdk: docker
7
  pinned: false
8
+ short_description: 基于 Flutter 的跨平台资产与时间追踪系统
9
  ---
10
 
11
+ # TimeVoyager Flutter
12
+
13
+ TimeVoyager 的 Flutter 版本,旨在通过一套代码库实现 iOS、Android、Web、Windows、macOS 和 Linux 的全平台覆盖。
14
+
15
+ ## 核心功能
16
+
17
+ * **跨平台**: 使用 Dart 和 Flutter 构建,UI 与业务逻辑高度复用。
18
+ * **状态管理**: 使用 `Provider` + `ChangeNotifier` 进行高效的状态管理。
19
+ * **资产量化**: 实时计算时间价值,支持多币种。
20
+ * **Material 3**: 遵循最新的 Material Design 3 设计规范。
21
+
22
+ ## 目录结构
23
+
24
+ ```
25
+ TimeVoyager-Flutter/
26
+ ├── lib/
27
+ │ ├── main.dart # 应用入口与UI界面
28
+ │ ├── models.dart # 数据模型 (Project, TimeEntry)
29
+ │ └── logic.dart # 业务逻辑 (TimeManager)
30
+ ├── pubspec.yaml # 项目依赖配置
31
+ ├── Web/ # Web 演示端 (Flask + Vue)
32
+ ├── Dockerfile # Hugging Face 部署配置
33
+ └── README.md
34
+ ```
35
+
36
+ ## 如何运行 Flutter 项目
37
+
38
+ 1. 确保已安装 Flutter SDK (3.0+)。
39
+ 2. 进入项目目录:`cd TimeVoyager-Flutter`
40
+ 3. 获取依赖:`flutter pub get`
41
+ 4. 运行:`flutter run`
42
+
43
+ ## Web 演示 (Hugging Face)
44
+
45
+ 本项目包含一个 Web 演示版本(位于 `Web/` 目录),用于在 Hugging Face Spaces 上展示核心计费逻辑。
46
+ 演示版使用 Python Flask 模拟后端计算,前端使用 Vue.js 实现交互,界面风格与 Flutter 版保持一致。
47
+
48
+ ## License
49
+
50
+ MIT
Web/app.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, jsonify, request
2
+ import os
3
+
4
+ app = Flask(__name__)
5
+
6
+ @app.route('/')
7
+ def index():
8
+ return render_template('index.html')
9
+
10
+ @app.route('/api/calculate', methods=['POST'])
11
+ def calculate():
12
+ data = request.json or {}
13
+ rate = float(data.get('hourlyRate', 0))
14
+ duration = float(data.get('duration', 0))
15
+ currency = data.get('currency', 'CNY')
16
+ hours = duration / 3600.0
17
+ earnings = hours * rate
18
+ return jsonify({
19
+ 'earnings': round(earnings, 2),
20
+ 'currency': currency,
21
+ 'hours': round(hours, 4),
22
+ 'formatted_earnings': f"{earnings:,.2f} {currency}"
23
+ })
24
+
25
+ @app.route('/health')
26
+ def health():
27
+ return jsonify(status="ok"), 200
28
+
29
+ if __name__ == '__main__':
30
+ port = int(os.environ.get("PORT", "7860"))
31
+ app.run(host='0.0.0.0', port=port)
Web/templates/index.html ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>TimeVoyager Flutter 展示</title>
7
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <style>
10
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
11
+ </style>
12
+ </head>
13
+ <body class="bg-gray-50 text-gray-900">
14
+ <section class="py-16 container mx-auto px-6">
15
+ <h1 class="text-4xl font-bold text-center mb-6">TimeVoyager Flutter</h1>
16
+ <p class="text-center text-gray-600 mb-8">核心计费逻辑演示 (Dart 移植版)</p>
17
+ <div id="app" class="max-w-md mx-auto bg-white rounded-2xl shadow-xl overflow-hidden">
18
+ <div class="p-8">
19
+ <div class="mb-6">
20
+ <label class="block text-sm font-medium text-gray-700 mb-2">项目名称</label>
21
+ <input type="text" v-model="project.name" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none">
22
+ </div>
23
+ <div class="flex space-x-4 mb-8">
24
+ <div class="w-1/2">
25
+ <label class="block text-sm font-medium text-gray-700 mb-2">时薪</label>
26
+ <input type="number" v-model.number="project.hourlyRate" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none">
27
+ </div>
28
+ <div class="w-1/2">
29
+ <label class="block text-sm font-medium text-gray-700 mb-2">货币</label>
30
+ <select v-model="project.currency" class="w-full px-4 py-2 border rounded-lg bg-white">
31
+ <option>USD</option>
32
+ <option>CNY</option>
33
+ <option>EUR</option>
34
+ </select>
35
+ </div>
36
+ </div>
37
+ <div class="text-center mb-8 bg-gray-50 py-6 rounded-xl border border-gray-200">
38
+ <div class="text-5xl font-mono font-light text-gray-800 mb-2">${ formattedTime }</div>
39
+ <div class="text-2xl font-bold text-green-600">${ earningsDisplay }</div>
40
+ </div>
41
+ <button @click="toggleTimer"
42
+ :class="isRunning ? 'bg-red-500 hover:bg-red-600' : 'bg-green-500 hover:bg-green-600'"
43
+ class="w-full text-white font-bold py-4 rounded-xl transition shadow-lg transform active:scale-95">
44
+ ${ isRunning ? '停止计时' : '开始工作' }
45
+ </button>
46
+ </div>
47
+ </div>
48
+ </section>
49
+ <script>
50
+ const { createApp, ref, computed, onUnmounted } = Vue;
51
+ createApp({
52
+ delimiters: ['${', '}'],
53
+ setup() {
54
+ const project = ref({ name: 'Flutter 跨平台示例', hourlyRate: 150, currency: 'CNY' });
55
+ const isRunning = ref(false);
56
+ const startTime = ref(null);
57
+ const elapsedTime = ref(0);
58
+ const timerInterval = ref(null);
59
+ const earnings = ref(0);
60
+ const formatTime = (seconds) => {
61
+ const h = Math.floor(seconds / 3600);
62
+ const m = Math.floor((seconds % 3600) / 60);
63
+ const s = Math.floor(seconds % 60);
64
+ return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
65
+ };
66
+ const formattedTime = computed(() => formatTime(elapsedTime.value));
67
+ const earningsDisplay = computed(() => {
68
+ return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: project.value.currency }).format(earnings.value);
69
+ });
70
+ const updateTick = () => {
71
+ const now = new Date();
72
+ elapsedTime.value = (now - startTime.value) / 1000;
73
+ const hours = elapsedTime.value / 3600;
74
+ earnings.value = hours * project.value.hourlyRate;
75
+ };
76
+ const toggleTimer = () => {
77
+ if (isRunning.value) {
78
+ clearInterval(timerInterval.value);
79
+ isRunning.value = false;
80
+ } else {
81
+ startTime.value = new Date() - (elapsedTime.value * 1000);
82
+ timerInterval.value = setInterval(updateTick, 100);
83
+ isRunning.value = true;
84
+ }
85
+ };
86
+ onUnmounted(() => { if (timerInterval.value) clearInterval(timerInterval.value); });
87
+ return { project, isRunning, formattedTime, earningsDisplay, toggleTimer };
88
+ }
89
+ }).mount('#app');
90
+ </script>
91
+ </body>
92
+ </html>
lib/logic.dart ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import 'dart:async';
2
+ import 'package:flutter/foundation.dart';
3
+ import 'models.dart';
4
+
5
+ class TimeManager extends ChangeNotifier {
6
+ List<Project> _projects = [];
7
+ List<TimeEntry> _entries = [];
8
+
9
+ TimeEntry? _activeEntry;
10
+ Timer? _timer;
11
+
12
+ // Public Getters
13
+ List<Project> get projects => List.unmodifiable(_projects);
14
+ TimeEntry? get activeEntry => _activeEntry;
15
+ bool get isTracking => _activeEntry != null;
16
+
17
+ Duration get currentDuration => _activeEntry?.duration ?? Duration.zero;
18
+ double get currentEarnings {
19
+ if (_activeEntry == null) return 0.0;
20
+ final project = getProjectById(_activeEntry!.projectId);
21
+ if (project == null) return 0.0;
22
+ return _activeEntry!.calculateEarnings(project.hourlyRate);
23
+ }
24
+
25
+ TimeManager() {
26
+ // Demo Data
27
+ addProject(Project(name: '示例项目 Alpha', hourlyRate: 150, currency: 'CNY'));
28
+ addProject(Project(name: '咨询服务 Beta', hourlyRate: 300, currency: 'USD', colorHex: '#4CAF50'));
29
+ }
30
+
31
+ void addProject(Project project) {
32
+ _projects.add(project);
33
+ notifyListeners();
34
+ }
35
+
36
+ Project? getProjectById(String id) {
37
+ try {
38
+ return _projects.firstWhere((p) => p.id == id);
39
+ } catch (e) {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ void startTracking(Project project) {
45
+ stopTracking(); // Stop current if any
46
+
47
+ _activeEntry = TimeEntry(
48
+ projectId: project.id,
49
+ startTime: DateTime.now(),
50
+ );
51
+
52
+ _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
53
+ notifyListeners(); // Update UI every second
54
+ });
55
+
56
+ notifyListeners();
57
+ }
58
+
59
+ void stopTracking() {
60
+ if (_activeEntry != null) {
61
+ _activeEntry!.endTime = DateTime.now();
62
+ _entries.add(_activeEntry!);
63
+ _activeEntry = null;
64
+ }
65
+
66
+ _timer?.cancel();
67
+ _timer = null;
68
+ notifyListeners();
69
+ }
70
+
71
+ @override
72
+ void dispose() {
73
+ _timer?.cancel();
74
+ super.dispose();
75
+ }
76
+ }
lib/main.dart ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import 'package:flutter/material.dart';
2
+ import 'package:provider/provider.dart';
3
+ import 'package:intl/intl.dart';
4
+ import 'models.dart';
5
+ import 'logic.dart';
6
+
7
+ void main() {
8
+ runApp(
9
+ ChangeNotifierProvider(
10
+ create: (context) => TimeManager(),
11
+ child: const TimeVoyagerApp(),
12
+ ),
13
+ );
14
+ }
15
+
16
+ class TimeVoyagerApp extends StatelessWidget {
17
+ const TimeVoyagerApp({super.key});
18
+
19
+ @override
20
+ Widget build(BuildContext context) {
21
+ return MaterialApp(
22
+ title: 'TimeVoyager',
23
+ theme: ThemeData(
24
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
25
+ useMaterial3: true,
26
+ ),
27
+ home: const ProjectListPage(),
28
+ );
29
+ }
30
+ }
31
+
32
+ class ProjectListPage extends StatelessWidget {
33
+ const ProjectListPage({super.key});
34
+
35
+ @override
36
+ Widget build(BuildContext context) {
37
+ final manager = context.watch<TimeManager>();
38
+
39
+ return Scaffold(
40
+ appBar: AppBar(
41
+ title: const Text('TimeVoyager'),
42
+ backgroundColor: Theme.of(context).colorScheme.inversePrimary,
43
+ ),
44
+ body: ListView.builder(
45
+ itemCount: manager.projects.length,
46
+ itemBuilder: (context, index) {
47
+ final project = manager.projects[index];
48
+ final isActive = manager.activeEntry?.projectId == project.id;
49
+
50
+ return Card(
51
+ margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
52
+ child: ListTile(
53
+ leading: CircleAvatar(
54
+ backgroundColor: isActive ? Colors.green : Colors.blueGrey,
55
+ child: Icon(isActive ? Icons.play_arrow : Icons.folder),
56
+ ),
57
+ title: Text(project.name),
58
+ subtitle: Text('${project.hourlyRate} ${project.currency} / 小时'),
59
+ trailing: isActive
60
+ ? const Text('正在计时...', style: TextStyle(color: Colors.green))
61
+ : const Icon(Icons.chevron_right),
62
+ onTap: () {
63
+ Navigator.push(
64
+ context,
65
+ MaterialPageRoute(
66
+ builder: (context) => ProjectDetailPage(projectId: project.id),
67
+ ),
68
+ );
69
+ },
70
+ ),
71
+ );
72
+ },
73
+ ),
74
+ floatingActionButton: FloatingActionButton(
75
+ onPressed: () {
76
+ // TODO: Implement Add Project Dialog
77
+ ScaffoldMessenger.of(context).showSnackBar(
78
+ const SnackBar(content: Text('演示模式:默认项目已预置')),
79
+ );
80
+ },
81
+ child: const Icon(Icons.add),
82
+ ),
83
+ );
84
+ }
85
+ }
86
+
87
+ class ProjectDetailPage extends StatelessWidget {
88
+ final String projectId;
89
+
90
+ const ProjectDetailPage({super.key, required this.projectId});
91
+
92
+ @override
93
+ Widget build(BuildContext context) {
94
+ final manager = context.watch<TimeManager>();
95
+ final project = manager.getProjectById(projectId);
96
+
97
+ if (project == null) return const Scaffold(body: Center(child: Text('项目不存在')));
98
+
99
+ final isActive = manager.activeEntry?.projectId == project.id;
100
+
101
+ return Scaffold(
102
+ appBar: AppBar(title: Text(project.name)),
103
+ body: Padding(
104
+ padding: const EdgeInsets.all(24.0),
105
+ child: Column(
106
+ children: [
107
+ _buildStatusCard(context, project, manager, isActive),
108
+ const SizedBox(height: 40),
109
+ _buildControlButtons(context, project, manager, isActive),
110
+ ],
111
+ ),
112
+ ),
113
+ );
114
+ }
115
+
116
+ Widget _buildStatusCard(BuildContext context, Project project, TimeManager manager, bool isActive) {
117
+ final duration = isActive ? manager.currentDuration : Duration.zero;
118
+ final earnings = isActive ? manager.currentEarnings : 0.0;
119
+
120
+ final formattedTime = _formatDuration(duration);
121
+ final formattedEarnings = NumberFormat.currency(symbol: project.currency == 'CNY' ? '¥' : '\$')
122
+ .format(earnings);
123
+
124
+ return Container(
125
+ width: double.infinity,
126
+ padding: const EdgeInsets.all(32),
127
+ decoration: BoxDecoration(
128
+ color: isActive ? Colors.blue.shade50 : Colors.grey.shade100,
129
+ borderRadius: BorderRadius.circular(24),
130
+ border: Border.all(
131
+ color: isActive ? Colors.blue.shade200 : Colors.transparent,
132
+ width: 2,
133
+ ),
134
+ ),
135
+ child: Column(
136
+ children: [
137
+ Text(
138
+ isActive ? '工作中' : '就绪',
139
+ style: TextStyle(
140
+ color: isActive ? Colors.blue : Colors.grey,
141
+ fontWeight: FontWeight.bold,
142
+ letterSpacing: 1.2,
143
+ ),
144
+ ),
145
+ const SizedBox(height: 16),
146
+ Text(
147
+ formattedTime,
148
+ style: const TextStyle(
149
+ fontSize: 64,
150
+ fontFamily: 'Monospace',
151
+ fontWeight: FontWeight.w300,
152
+ ),
153
+ ),
154
+ const SizedBox(height: 8),
155
+ Text(
156
+ formattedEarnings,
157
+ style: TextStyle(
158
+ fontSize: 32,
159
+ fontWeight: FontWeight.bold,
160
+ color: isActive ? Colors.green : Colors.grey,
161
+ ),
162
+ ),
163
+ ],
164
+ ),
165
+ );
166
+ }
167
+
168
+ Widget _buildControlButtons(BuildContext context, Project project, TimeManager manager, bool isActive) {
169
+ if (isActive) {
170
+ return SizedBox(
171
+ width: double.infinity,
172
+ height: 60,
173
+ child: ElevatedButton.icon(
174
+ onPressed: () => manager.stopTracking(),
175
+ style: ElevatedButton.styleFrom(
176
+ backgroundColor: Colors.red.shade100,
177
+ foregroundColor: Colors.red,
178
+ ),
179
+ icon: const Icon(Icons.stop_circle),
180
+ label: const Text('停止计时', style: TextStyle(fontSize: 20)),
181
+ ),
182
+ );
183
+ } else {
184
+ // Check if another project is running
185
+ final isOtherRunning = manager.isTracking && !isActive;
186
+
187
+ return SizedBox(
188
+ width: double.infinity,
189
+ height: 60,
190
+ child: ElevatedButton.icon(
191
+ onPressed: isOtherRunning
192
+ ? () {
193
+ ScaffoldMessenger.of(context).showSnackBar(
194
+ const SnackBar(content: Text('请先停止当前正在进行的项目')),
195
+ );
196
+ }
197
+ : () => manager.startTracking(project),
198
+ style: ElevatedButton.styleFrom(
199
+ backgroundColor: isOtherRunning ? Colors.grey : Colors.green.shade100,
200
+ foregroundColor: isOtherRunning ? Colors.white : Colors.green,
201
+ ),
202
+ icon: const Icon(Icons.play_circle),
203
+ label: Text(
204
+ isOtherRunning ? '其他项目进行中' : '开始工作',
205
+ style: const TextStyle(fontSize: 20)
206
+ ),
207
+ ),
208
+ );
209
+ }
210
+ }
211
+
212
+ String _formatDuration(Duration d) {
213
+ String twoDigits(int n) => n.toString().padLeft(2, "0");
214
+ String twoDigitMinutes = twoDigits(d.inMinutes.remainder(60));
215
+ String twoDigitSeconds = twoDigits(d.inSeconds.remainder(60));
216
+ return "${twoDigits(d.inHours)}:$twoDigitMinutes:$twoDigitSeconds";
217
+ }
218
+ }
lib/models.dart ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import 'package:uuid/uuid.dart';
2
+
3
+ class Project {
4
+ final String id;
5
+ String name;
6
+ double hourlyRate;
7
+ String currency;
8
+ String colorHex;
9
+ DateTime createdAt;
10
+
11
+ Project({
12
+ String? id,
13
+ required this.name,
14
+ this.hourlyRate = 0.0,
15
+ this.currency = 'CNY',
16
+ this.colorHex = '#2196F3',
17
+ DateTime? createdAt,
18
+ }) : id = id ?? const Uuid().v4(),
19
+ createdAt = createdAt ?? DateTime.now();
20
+ }
21
+
22
+ class TimeEntry {
23
+ final String id;
24
+ final String projectId;
25
+ DateTime startTime;
26
+ DateTime? endTime;
27
+ String notes;
28
+
29
+ TimeEntry({
30
+ String? id,
31
+ required this.projectId,
32
+ required this.startTime,
33
+ this.endTime,
34
+ this.notes = '',
35
+ }) : id = id ?? const Uuid().v4();
36
+
37
+ Duration get duration {
38
+ final end = endTime ?? DateTime.now();
39
+ return end.difference(startTime);
40
+ }
41
+
42
+ double calculateEarnings(double hourlyRate) {
43
+ final hours = duration.inSeconds / 3600.0;
44
+ return hours * hourlyRate;
45
+ }
46
+ }
pubspec.yaml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: time_voyager_flutter
2
+ description: A productivity app for tracking time and asset value.
3
+ publish_to: 'none'
4
+ version: 1.0.0+1
5
+
6
+ environment:
7
+ sdk: '>=3.0.0 <4.0.0'
8
+
9
+ dependencies:
10
+ flutter:
11
+ sdk: flutter
12
+ provider: ^6.0.0
13
+ intl: ^0.18.0
14
+ uuid: ^4.0.0
15
+ shared_preferences: ^2.2.0
16
+
17
+ dev_dependencies:
18
+ flutter_test:
19
+ sdk: flutter
20
+ flutter_lints: ^3.0.0
21
+
22
+ flutter:
23
+ uses-material-design: true