Skip to content

Implement Stopwatch Overlay Feature #523 #821

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<uses-permission
android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />

<queries>
<intent>
Expand Down Expand Up @@ -69,6 +70,12 @@
android:enabled="true"
android:exported="true"></service>
<service android:name="com.pravera.flutter_foreground_task.service.ForegroundService" />
<service android:name="flutter.overlay.window.flutter_overlay_window.OverlayService"
android:exported="false"
android:foregroundServiceType="specialUse">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="explanation_for_special_use"/>
</service>

<receiver
android:name=".BootReceiver"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:ultimate_alarm_clock/app/data/models/flag_model.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart';

class StopwatchController extends GetxController {
final RxBool isTimerPaused = true.obs;
Expand Down Expand Up @@ -53,22 +54,29 @@ class StopwatchController extends GetxController {
void startTimer() {
timer = Timer.periodic(const Duration(milliseconds: 30), (Timer t) {
_updateResult();
_shareDataToOverlay();
});
_stopwatch.start();
isTimerPaused.value = false;
}

void _shareDataToOverlay() {
FlutterOverlayWindow.shareData(_result.value);
}

void stopTimer() {
timer.cancel();
_stopwatch.stop();
isTimerPaused.value = true;
FlutterOverlayWindow.shareData(_result.value);
}

void resetTime() {
stopTimer();
_stopwatch.reset();
_updateResult();
clearFlags();
FlutterOverlayWindow.shareData(_result.value);
}

void _updateResult() {
Expand Down
157 changes: 157 additions & 0 deletions lib/app/modules/stopwatch/views/overylay_pop.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:ultimate_alarm_clock/app/modules/settings/controllers/theme_controller.dart';

class StopwatchOverlayPop extends StatelessWidget {
const StopwatchOverlayPop({super.key, required this.themeController});

final ThemeController themeController;

@override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
return Material(
color: Colors.transparent,
type: MaterialType.transparency,
child: Center(
child: Container(
width: width * 0.95,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: themeController.secondaryBackgroundColor.value,
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () async {
await FlutterOverlayWindow.closeOverlay();
},
icon: const Icon(
Icons.close_rounded,
),
color: themeController.primaryTextColor.value
.withOpacity(0.75),
iconSize: 27,
),
const Text(
'UAC Stopwatch',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.w500,
),
),
// Added this below icon just to shift text in center
IconButton(
onPressed: () {},
icon: const Icon(
Icons.close_rounded,
),
color: Colors.transparent,
iconSize: 27,
),
],
),
StreamBuilder(
stream: FlutterOverlayWindow.overlayListener,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.waiting ||
snapshot.hasData) {
String timer = '00:00:00';
if (snapshot.hasData) timer = snapshot.data;
return Expanded(
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Center(
child: Text(
timer.split(':')[0],
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w500,
),
),
),
),
const Center(
child: Text(
':',
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w500,
),
),
),
Expanded(
child: Center(
child: Text(
timer.split(':')[1],
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w500,
),
),
),
),
const Center(
child: Text(
':',
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.w500,
),
),
),
Expanded(
child: Center(
child: Text(
timer.split(':')[2],
style: const TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
);
} else {
return const Center(
child: Text(
'Some error occurred. Please try again.',
style: TextStyle(
fontSize: 30.0,
fontWeight: FontWeight.w500,
),
),
);
}
},
),
],
),
),
),
);
}
}

Future<bool> requestPermission() async {
bool isPermissionGranted = await FlutterOverlayWindow.isPermissionGranted();
if (!isPermissionGranted) {
isPermissionGranted =
await FlutterOverlayWindow.requestPermission() ?? false;
}
return isPermissionGranted;
}
50 changes: 49 additions & 1 deletion lib/app/modules/stopwatch/views/stopwatch_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:ultimate_alarm_clock/app/modules/settings/controllers/theme_controller.dart';
import 'package:ultimate_alarm_clock/app/modules/stopwatch/controllers/stopwatch_controller.dart';
import 'package:ultimate_alarm_clock/app/modules/stopwatch/views/overylay_pop.dart';
import 'package:ultimate_alarm_clock/app/utils/end_drawer.dart';
import '../../../utils/utils.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart';

// ignore: must_be_immutable
class StopwatchView extends GetView<StopwatchController> {
StopwatchView({Key? key}) : super(key: key);
StopwatchView({super.key});
ThemeController themeController = Get.find<ThemeController>();
@override
Widget build(BuildContext context) {
Expand All @@ -22,6 +24,52 @@ class StopwatchView extends GetView<StopwatchController> {
toolbarHeight: height / 7.9,
elevation: 0.0,
centerTitle: true,
leading: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Obx(
() => IconButton(
onPressed: () async {
Utils.hapticFeedback();
bool isGranted = await requestPermission();
if (!isGranted) {
Get.snackbar(
'Permission Denied'.tr,
'Please enable overlay permission'.tr,
duration: const Duration(seconds: 2),
snackPosition: SnackPosition.BOTTOM,
barBlur: 15,
colorText: themeController.primaryTextColor.value,
margin: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 15,
),
);
} else if (await FlutterOverlayWindow.isActive()) {
await FlutterOverlayWindow.closeOverlay();
} else {
await FlutterOverlayWindow.showOverlay(
enableDrag: true,
overlayTitle: '',
overlayContent: '',
flag: OverlayFlag.defaultFlag,
visibility: NotificationVisibility.visibilityPublic,
positionGravity: PositionGravity.auto,
height: (height * 0.5).toInt(),
width: WindowSize.matchParent,
startPosition: const OverlayPosition(0, -259),
);
}
},
icon: const Icon(
Icons.widgets_rounded,
),
color: themeController.primaryTextColor.value
.withOpacity(0.75),
iconSize: 27,
),
);
},
),
actions: [
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
Expand Down
20 changes: 18 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:ultimate_alarm_clock/app/data/providers/get_storage_provider.dart';
import 'package:ultimate_alarm_clock/app/modules/settings/controllers/theme_controller.dart';
import 'package:sqflite/sqflite.dart';
import 'package:ultimate_alarm_clock/app/modules/stopwatch/views/overylay_pop.dart';
import 'package:ultimate_alarm_clock/app/utils/language.dart';
import 'package:ultimate_alarm_clock/app/utils/constants.dart';
import 'package:ultimate_alarm_clock/app/utils/custom_error_screen.dart';
Expand All @@ -29,7 +29,7 @@ void main() async {
final storage = Get.find<GetStorageProvider>();
loc = await storage.readLocale();

final ThemeController themeController = Get.put(ThemeController());
// final ThemeController themeController = Get.put(ThemeController());

AudioPlayer.global.setAudioContext(
const AudioContext(
Expand All @@ -54,6 +54,22 @@ void main() async {
);
}

@pragma('vm:entry-point')
void overlayMain() async {
WidgetsFlutterBinding.ensureInitialized();

final ThemeController themeController = Get.put(ThemeController());
runApp(
MaterialApp(
theme: kLightThemeData,
darkTheme: kThemeData,
themeMode: ThemeMode.system,
title: 'UltiClock',
debugShowCheckedModeBanner: false,
home: StopwatchOverlayPop(themeController: themeController),
),
);
}

class UltimateAlarmClockApp extends StatelessWidget {
const UltimateAlarmClockApp({super.key});
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ dependencies:
intl_phone_number_input: ^0.7.4
firebase_messaging: ^14.7.19
shared_preferences: ^2.2.3
flutter_overlay_window: ^0.4.5

dev_dependencies:
flutter_lints: ^4.0.0
Expand Down