Compare commits

..

4 Commits

Author SHA1 Message Date
03eaa16842 Split up routes 2023-04-07 11:50:02 +12:00
5584f44b92 Add template drawer page to main.dart 2023-04-05 12:24:48 +12:00
ee4febd718 Add vibrate permission 2023-04-05 12:24:31 +12:00
9188e9f813 Remove unused kotlin code 2023-04-05 12:16:12 +12:00
14 changed files with 377 additions and 310 deletions

1
.vim/coc-settings.json Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -2,6 +2,7 @@
package="com.example.fmassive"> package="com.example.fmassive">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

View File

@ -1,191 +0,0 @@
package com.example.fmassive
import android.annotation.SuppressLint
import android.app.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.CountDownTimer
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlin.math.floor
class AlarmModule(private val context: Context) : MethodCallHandler {
private var countdownTimer: CountDownTimer? = null
var currentMs: Long = 0
private var running = false
private val stopReceiver = object : BroadcastReceiver() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onReceive(context: Context?, intent: Intent?) {
Log.d("AlarmModule", "Received stop broadcast intent")
if (context != null) {
stop(context)
}
}
}
private val addReceiver = object : BroadcastReceiver() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onReceive(context: Context?, intent: Intent?) {
if (context != null) {
add(context)
}
}
}
init {
}
@RequiresApi(api = Build.VERSION_CODES.O)
fun add(context: Context) {
Log.d("AlarmModule", "Add 1 min to alarm.")
countdownTimer?.cancel()
val newMs = if (running) currentMs.toInt().plus(60000) else 60000
countdownTimer = getTimer(context, newMs)
countdownTimer?.start()
running = true
//val manager = getManager()
//manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
//val intent = Intent(context, AlarmService::class.java)
//context.stopService(intent)
}
@RequiresApi(api = Build.VERSION_CODES.O)
fun stop(context: Context) {
Log.d("AlarmModule", "Stop alarm.")
countdownTimer?.cancel()
running = false
//val intent = Intent(context, AlarmService::class.java)
//reactApplicationContext?.stopService(intent)
//val manager = getManager()
//manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
//manager.cancel(NOTIFICATION_ID_PENDING)
//val params = Arguments.createMap().apply {
// putString("minutes", "00")
// putString("seconds", "00")
//}
//reactApplicationContext
// .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
// .emit("tick", params)
}
@RequiresApi(api = Build.VERSION_CODES.O)
fun timer(context: Context, milliseconds: Int) {
context.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST))
context.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST))
Log.d("AlarmModule", "Queue alarm for $milliseconds delay")
val manager = getManager(context)
//manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
//val intent = Intent(reactApplicationContext, AlarmService::class.java)
//reactApplicationContext.stopService(intent)
countdownTimer?.cancel()
countdownTimer = getTimer(context, milliseconds)
countdownTimer?.start()
running = true
}
@RequiresApi(Build.VERSION_CODES.M)
private fun getTimer(
context: Context,
endMs: Int,
): CountDownTimer {
val builder = getBuilder(context)
return object : CountDownTimer(endMs.toLong(), 1000) {
@RequiresApi(Build.VERSION_CODES.O)
override fun onTick(current: Long) {
currentMs = current
val seconds =
floor((current / 1000).toDouble() % 60).toInt().toString().padStart(2, '0')
val minutes =
floor((current / 1000).toDouble() / 60).toInt().toString().padStart(2, '0')
builder.setContentText("$minutes:$seconds").setAutoCancel(false).setDefaults(0)
.setProgress(endMs, current.toInt(), false)
.setCategory(NotificationCompat.CATEGORY_PROGRESS).priority =
NotificationCompat.PRIORITY_LOW
val manager = getManager(context)
manager.notify(NOTIFICATION_ID_PENDING, builder.build())
//val params = Arguments.createMap().apply {
// putString("minutes", minutes)
// putString("seconds", seconds)
//}
//reactApplicationContext
// .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
// .emit("tick", params)
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onFinish() {
//val context = reactApplicationContext
//context.startForegroundService(Intent(context, AlarmService::class.java))
//context
// .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
// .emit("finish", Arguments.createMap().apply {
// putString("minutes", "00")
// putString("seconds", "00")
// })
}
}
}
@SuppressLint("UnspecifiedImmutableFlag")
@RequiresApi(Build.VERSION_CODES.M)
private fun getBuilder(context: Context): NotificationCompat.Builder {
val contentIntent = Intent(context, MainActivity::class.java)
val pendingContent =
PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE)
val addBroadcast = Intent(ADD_BROADCAST).apply {
setPackage(context.packageName)
}
val pendingAdd =
PendingIntent.getBroadcast(context, 0, addBroadcast, PendingIntent.FLAG_MUTABLE)
val stopBroadcast = Intent(STOP_BROADCAST)
stopBroadcast.setPackage(context.packageName)
val pendingStop =
PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE)
return NotificationCompat.Builder(context, CHANNEL_ID_PENDING)
.setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24).setContentTitle("Resting")
.setContentIntent(pendingContent)
.addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop)
.addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", pendingAdd)
.setDeleteIntent(pendingStop)
}
@RequiresApi(Build.VERSION_CODES.O)
private fun getManager(context: Context): NotificationManager {
val notificationManager = context.getSystemService(
NotificationManager::class.java
)
val timersChannel = NotificationChannel(
CHANNEL_ID_PENDING, CHANNEL_ID_PENDING, NotificationManager.IMPORTANCE_LOW
)
timersChannel.setSound(null, null)
timersChannel.description = "Progress on rest timers."
notificationManager.createNotificationChannel(timersChannel)
return notificationManager
}
companion object {
const val STOP_BROADCAST = "stop-timer-event"
const val ADD_BROADCAST = "add-timer-event"
const val CHANNEL_ID_PENDING = "Timer"
const val NOTIFICATION_ID_PENDING = 1
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"timer" -> {
}
else -> result.notImplemented()
}
}
}

View File

@ -25,11 +25,10 @@ class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
// This method is invoked on the main thread.
call, result -> call, result ->
if (call.method == "timer") { if (call.method == "timer") {
val args = call.arguments as ArrayList<Long> val args = call.arguments as ArrayList<*>
timer(args[0]) timer(args[0] as Long)
} else { } else {
result.notImplemented() result.notImplemented()
} }

View File

@ -1,19 +0,0 @@
package com.example.fmassive
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodChannel
class MyFlutterPlugin : FlutterPlugin {
private lateinit var channel: MethodChannel
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
val context = binding.applicationContext
val handler = AlarmModule(context)
channel = MethodChannel(binding.binaryMessenger, "my_channel")
channel.setMethodCallHandler(handler)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}

17
lib/best_page.dart Normal file
View File

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class BestPage extends StatelessWidget {
const BestPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Best'),
),
body: const Center(
child: Text('Welcome to the Best Page!'),
),
);
}
}

86
lib/edit_set.dart Normal file
View File

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:fmassive/home_page.dart';
class EditGymSetPage extends StatefulWidget {
final GymSet gymSet;
EditGymSetPage({required this.gymSet});
@override
_EditGymSetPageState createState() => _EditGymSetPageState();
}
class _EditGymSetPageState extends State<EditGymSetPage> {
final TextEditingController _nameController = TextEditingController();
final TextEditingController _repsController = TextEditingController();
final TextEditingController _weightController = TextEditingController();
late GymSet _editedGymSet;
@override
void initState() {
super.initState();
// Initialize the edited GymSet object with the values from the input GymSet object
_editedGymSet = GymSet(
name: widget.gymSet.name,
reps: widget.gymSet.reps,
weight: widget.gymSet.weight,
created: DateTime.now());
// Set the text controller values
_nameController.text = _editedGymSet.name;
_repsController.text = _editedGymSet.reps.toString();
_weightController.text = _editedGymSet.weight.toString();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Edit Gym Set'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Name'),
onChanged: (value) {
setState(() {
_editedGymSet.name = value;
});
},
),
TextFormField(
controller: _repsController,
decoration: const InputDecoration(labelText: 'Reps'),
keyboardType: TextInputType.number,
onChanged: (value) {
setState(() {
_editedGymSet.reps = int.tryParse(value) ?? 0;
});
},
),
TextFormField(
controller: _weightController,
decoration: const InputDecoration(labelText: 'Weight (kg)'),
keyboardType: TextInputType.number,
onChanged: (value) {
setState(() {
_editedGymSet.weight = int.tryParse(value) ?? 0;
});
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.pop(context, _editedGymSet);
},
child: const Icon(Icons.check),
),
);
}
}

172
lib/home_page.dart Normal file
View File

@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:fmassive/edit_set.dart';
class HomePage extends StatelessWidget {
HomePage({super.key});
final List<Map<String, dynamic>> _pageData = [
{'title': 'Home', 'icon': Icons.home},
{'title': 'Plans', 'icon': Icons.calendar_today},
{'title': 'Best', 'icon': Icons.star},
{'title': 'Workouts', 'icon': Icons.fitness_center},
{'title': 'Timer', 'icon': Icons.timer},
{'title': 'Settings', 'icon': Icons.settings},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_pageData[0]['title']),
),
drawer: Drawer(
child: ListView.builder(
itemCount: _pageData.length,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(_pageData[index]['icon']),
title: Text(_pageData[index]['title']),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(
context, '/${_pageData[index]['title'].toLowerCase()}');
},
);
},
),
),
body: const Center(
child: GymSetPage(),
),
);
}
}
class GymSet {
String name;
int reps;
int weight;
DateTime created;
GymSet(
{required this.name,
required this.reps,
required this.weight,
required this.created});
}
class GymSetPage extends StatefulWidget {
const GymSetPage({Key? key}) : super(key: key);
@override
_GymSetPageState createState() => _GymSetPageState();
}
class _GymSetPageState extends State<GymSetPage> {
final List<GymSet> _gymSets = [
GymSet(
name: "Bench press",
reps: 10,
weight: 50,
created: DateTime(2022, 1, 1)),
GymSet(
name: "Bench press",
reps: 8,
weight: 60,
created: DateTime(2022, 1, 2)),
GymSet(
name: "Bench press",
reps: 6,
weight: 70,
created: DateTime(2022, 1, 3)),
GymSet(
name: "Shoulder press",
reps: 12,
weight: 40,
created: DateTime(2022, 1, 4)),
GymSet(
name: "Shoulder press",
reps: 15,
weight: 35,
created: DateTime(2022, 1, 5)),
];
List<GymSet> _searchResults = [];
final TextEditingController _searchController = TextEditingController();
void _searchGymSets(String searchQuery) {
List<GymSet> results = [];
if (searchQuery.isEmpty) {
results = _gymSets;
} else {
for (int i = 0; i < _gymSets.length; i++) {
if (_gymSets[i].reps.toString().contains(searchQuery) ||
_gymSets[i].weight.toString().contains(searchQuery) ||
_gymSets[i].created.toString().contains(searchQuery) ||
_gymSets[i].name.contains(searchQuery)) {
results.add(_gymSets[i]);
}
}
}
setState(() {
_searchResults = results;
});
}
@override
void initState() {
super.initState();
// Initialize the search results to all the gym sets
_searchResults = _gymSets;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: TextField(
controller: _searchController,
decoration: const InputDecoration(
hintText: 'Search Gym Sets',
border: InputBorder.none,
),
onChanged: (searchQuery) {
_searchGymSets(searchQuery);
},
),
),
body: ListView.builder(
itemCount: _searchResults.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(
'${_searchResults[index].name}: ${_searchResults[index].reps}x${_searchResults[index].weight}kg'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
EditGymSetPage(gymSet: _searchResults[index]),
),
);
});
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
EditGymSetPage(gymSet: _searchResults[0]),
),
);
},
child: const Icon(Icons.add)));
}
}

View File

@ -1,111 +1,45 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:fmassive/best_page.dart';
import 'package:fmassive/edit_set.dart';
import 'package:fmassive/home_page.dart';
import 'package:fmassive/plans_page.dart';
import 'package:fmassive/settings_page.dart';
import 'package:fmassive/timer_page.dart';
import 'package:fmassive/workouts_page.dart';
void main() { void main() {
runApp(const MyApp()); runApp(MyApp());
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); MyApp({Key? key}) : super(key: key);
final navigatorKey = GlobalKey<NavigatorState>();
// This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final edit = EditGymSetPage(
gymSet: GymSet(name: '', reps: 0, weight: 0, created: DateTime.now()));
final Map<String, WidgetBuilder> routes = {
'/home': (context) => HomePage(),
'/plans': (context) => const PlansPage(),
'/best': (context) => const BestPage(),
'/workouts': (context) => const WorkoutsPage(),
'/timer': (context) => const TimerPage(),
'/settings': (context) => const SettingsPage(),
'/edit-set': (context) => edit,
};
return MaterialApp( return MaterialApp(
title: 'Flutter Demo', title: 'Gym App',
theme: ThemeData( theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
), ),
home: const MyHomePage(title: 'Flutter Demo Home Page'), initialRoute: '/home',
); routes: routes,
} navigatorKey: navigatorKey,
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const platform = MethodChannel('com.massive/android');
Future<void> _timer() async {
await platform.invokeMethod('timer', [3000]);
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'Tap to start timer',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _timer,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
); );
} }
} }

17
lib/plans_page.dart Normal file
View File

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class PlansPage extends StatelessWidget {
const PlansPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Plans'),
),
body: const Center(
child: Text('Welcome to the Plans Page!'),
),
);
}
}

17
lib/settings_page.dart Normal file
View File

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
),
body: const Center(
child: Text('Welcome to the Settings Page!'),
),
);
}
}

17
lib/timer_page.dart Normal file
View File

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class TimerPage extends StatelessWidget {
const TimerPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Timer'),
),
body: const Center(
child: Text('Welcome to the Timer Page!'),
),
);
}
}

17
lib/workouts_page.dart Normal file
View File

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class WorkoutsPage extends StatelessWidget {
const WorkoutsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Workouts'),
),
body: const Center(
child: Text('Welcome to the Workouts Page!'),
),
);
}
}

View File

@ -7,13 +7,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:fmassive/main.dart'; import 'package:fmassive/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame. // Build our app and trigger a frame.
await tester.pumpWidget(const MyApp()); await tester.pumpWidget(MyApp());
// Verify that our counter starts at 0. // Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget); expect(find.text('0'), findsOneWidget);