17 Commits

Author SHA1 Message Date
marco 81222de7fe Merge pull request 'Added app theming' (#2) from development into main
Flutter APK Build / Build Flutter APK (push) Successful in 5m44s
Reviewed-on: #2
2026-06-19 01:37:42 +02:00
marco 6dc7161b41 added title 2026-06-19 01:36:22 +02:00
marco f1756b30d1 added padding to views 2026-06-19 01:35:46 +02:00
marco 77a524f3ec added basic app theme 2026-06-19 01:25:56 +02:00
marco 392ec22dcd Merge pull request 'Feature: dismissible tasks' (#1) from development into main
Flutter APK Build / Build Flutter APK (push) Successful in 5m43s
Reviewed-on: #1
2026-06-19 00:36:43 +02:00
marco 064c014f8b removed unused import 2026-06-19 00:35:38 +02:00
marco 410a7eb843 made tasks dismissible 2026-06-19 00:35:20 +02:00
marco 20b017b066 added alarmkey and locationkey to allowlist 2026-06-19 00:21:34 +02:00
marco a3258b84fe added controllers to context 2026-06-19 00:02:35 +02:00
marco f088b84c54 updated imports 2026-06-18 23:20:38 +02:00
marco 5a39148577 moved controllers to folder 2026-06-18 23:19:44 +02:00
marco 2b718b5bd8 added controller for locations 2026-06-18 23:18:09 +02:00
marco 023610804d added location handling to storage 2026-06-18 23:16:20 +02:00
marco cee5af0f84 removed alarms and location from task model 2026-06-18 23:02:20 +02:00
marco 999023e48a created controller for alarms 2026-06-18 23:00:45 +02:00
marco 82a74a66b5 added id to alarms 2026-06-18 22:59:47 +02:00
marco e645081204 changed alarm model id to taskId 2026-06-18 22:48:32 +02:00
15 changed files with 287 additions and 60 deletions
+37
View File
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
class AppTheme {
AppTheme._();
static ThemeData get lightTheme => _baseTheme(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.light,
),
);
static ThemeData get darkTheme => _baseTheme(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.dark,
),
);
static ThemeData _baseTheme({required ColorScheme colorScheme}) {
final theme = ThemeData(useMaterial3: true, colorScheme: colorScheme);
final universalBorderRadius = BorderRadius.circular(12);
return theme.copyWith(
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(borderRadius: universalBorderRadius),
),
listTileTheme: ListTileThemeData(
shape: RoundedRectangleBorder(
borderRadius: universalBorderRadius,
side: BorderSide(color: colorScheme.secondaryContainer, width: 2),
),
),
);
}
}
+14 -3
View File
@@ -1,10 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'app_theme.dart';
import 'model/repositories/local_repository.dart'; import 'model/repositories/local_repository.dart';
import 'pages/task_edit_page.dart'; import 'pages/task_edit_page.dart';
import 'pages/task_overview_page.dart'; import 'pages/task_overview_page.dart';
import 'service/controller_scope.dart'; import 'service/controller_scope.dart';
import 'service/task_controller.dart'; import 'service/controllers/alarm_controller.dart';
import 'service/controllers/location_controller.dart';
import 'service/controllers/task_controller.dart';
void main() async { void main() async {
final repository = LocalRepository(); final repository = LocalRepository();
@@ -13,8 +16,14 @@ void main() async {
runApp( runApp(
ControllerScope( ControllerScope(
controller: TaskController(repository), controller: LocationController(repository),
child: const MainApp(), child: ControllerScope(
controller: AlarmController(repository),
child: ControllerScope(
controller: TaskController(repository),
child: const MainApp(),
),
),
), ),
); );
} }
@@ -25,6 +34,8 @@ class MainApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
routes: { routes: {
TaskOverviewPage.routeName: (context) => TaskOverviewPage(), TaskOverviewPage.routeName: (context) => TaskOverviewPage(),
TaskEditPage.routeName: (context) => TaskEditPage(), TaskEditPage.routeName: (context) => TaskEditPage(),
+2 -1
View File
@@ -3,6 +3,7 @@ import 'time_alarm.dart';
abstract class Alarm { abstract class Alarm {
String get id; String get id;
String get taskId;
Map<String, dynamic> toJson(); Map<String, dynamic> toJson();
factory Alarm.fromJson(Map<String, dynamic> json) { factory Alarm.fromJson(Map<String, dynamic> json) {
@@ -25,5 +26,5 @@ abstract class Alarm {
} }
@override @override
int get hashCode => id.hashCode; int get hashCode => taskId.hashCode;
} }
@@ -1,4 +1,3 @@
import '../location.dart';
import '../task.dart'; import '../task.dart';
class CreateTaskRequest { class CreateTaskRequest {
@@ -9,8 +8,6 @@ class CreateTaskRequest {
final bool isCompleted; final bool isCompleted;
final String category; final String category;
final List<Task> subtasks; final List<Task> subtasks;
final List<DateTime> alarms;
final Location? location;
final String url; final String url;
CreateTaskRequest({ CreateTaskRequest({
@@ -21,8 +18,6 @@ class CreateTaskRequest {
required this.isCompleted, required this.isCompleted,
required this.category, required this.category,
required this.subtasks, required this.subtasks,
required this.alarms,
required this.location,
required this.url, required this.url,
}); });
@@ -34,8 +29,6 @@ class CreateTaskRequest {
isCompleted = task.isCompleted, isCompleted = task.isCompleted,
category = task.category, category = task.category,
subtasks = task.subtasks, subtasks = task.subtasks,
alarms = task.alarms,
location = task.location,
url = task.url; url = task.url;
Task toTask({required String id}) { Task toTask({required String id}) {
@@ -48,8 +41,6 @@ class CreateTaskRequest {
isCompleted: isCompleted, isCompleted: isCompleted,
category: category, category: category,
subtasks: subtasks, subtasks: subtasks,
alarms: alarms,
location: location,
url: url, url: url,
); );
} }
+6
View File
@@ -5,12 +5,16 @@ class LocationAlarm implements Alarm {
@override @override
final String id; final String id;
@override
final String taskId;
final Location location; final Location location;
final int radiusMeters; final int radiusMeters;
const LocationAlarm({ const LocationAlarm({
required this.id, required this.id,
required this.taskId,
required this.location, required this.location,
required this.radiusMeters, required this.radiusMeters,
}); });
@@ -18,6 +22,7 @@ class LocationAlarm implements Alarm {
factory LocationAlarm.fromJson(Map<String, dynamic> json) { factory LocationAlarm.fromJson(Map<String, dynamic> json) {
return LocationAlarm( return LocationAlarm(
id: json['id'] as String, id: json['id'] as String,
taskId: json['taskId'] as String,
location: Location.fromJson(json['location'] as Map<String, dynamic>), location: Location.fromJson(json['location'] as Map<String, dynamic>),
radiusMeters: json['radiusMeters'] as int, radiusMeters: json['radiusMeters'] as int,
); );
@@ -27,6 +32,7 @@ class LocationAlarm implements Alarm {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'id': id, 'id': id,
'taskId': taskId,
'location': location.toJson(), 'location': location.toJson(),
'radiusMeters': radiusMeters, 'radiusMeters': radiusMeters,
}; };
@@ -0,0 +1,19 @@
import '../../location.dart';
abstract class LocationRepository {
// Create
Future<void> createLocation(Location location);
// Read
Future<List<Location>> loadLocations();
// Update
Future<void> updateLocation(Location location);
// Delete
Future<void> deleteLocation(Location location);
}
+49 -2
View File
@@ -3,14 +3,18 @@ import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../alarm.dart'; import '../alarm.dart';
import '../location.dart';
import '../task.dart'; import '../task.dart';
import 'interfaces/alarm_repository.dart'; import 'interfaces/alarm_repository.dart';
import 'interfaces/location_repository.dart';
import 'interfaces/task_repository.dart'; import 'interfaces/task_repository.dart';
class LocalRepository implements TaskRepository, AlarmRepository { class LocalRepository
implements TaskRepository, AlarmRepository, LocationRepository {
static const String _tasksKey = 'tasks'; static const String _tasksKey = 'tasks';
static const String _taskOrderKey = 'taskOrder'; static const String _taskOrderKey = 'taskOrder';
static const String _alarmsKey = 'alarms'; static const String _alarmsKey = 'alarms';
static const String _locationsKey = 'locations';
SharedPreferencesWithCache? _prefs; SharedPreferencesWithCache? _prefs;
@@ -18,7 +22,12 @@ class LocalRepository implements TaskRepository, AlarmRepository {
if (_prefs == null) { if (_prefs == null) {
await SharedPreferencesWithCache.create( await SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions( cacheOptions: const SharedPreferencesWithCacheOptions(
allowList: <String>{_tasksKey, _taskOrderKey}, allowList: <String>{
_tasksKey,
_taskOrderKey,
_alarmsKey,
_locationsKey,
},
), ),
).then((value) => _prefs = value); ).then((value) => _prefs = value);
} }
@@ -29,6 +38,13 @@ class LocalRepository implements TaskRepository, AlarmRepository {
_prefs!.setStringList(_tasksKey, jsonList); _prefs!.setStringList(_tasksKey, jsonList);
} }
Future<void> _saveLocations(List<Location> locations) async {
final jsonList = locations
.map<String>((e) => jsonEncode(e.toJson()))
.toList();
_prefs!.setStringList(_locationsKey, jsonList);
}
Future<void> _saveTaskOrder(List<String> taskOrder) async { Future<void> _saveTaskOrder(List<String> taskOrder) async {
final jsonList = taskOrder.map((e) => jsonEncode(e)).toList(); final jsonList = taskOrder.map((e) => jsonEncode(e)).toList();
return _prefs!.setStringList(_taskOrderKey, jsonList); return _prefs!.setStringList(_taskOrderKey, jsonList);
@@ -62,6 +78,13 @@ class LocalRepository implements TaskRepository, AlarmRepository {
_saveAlarms(alarms); _saveAlarms(alarms);
} }
@override
Future<void> createLocation(Location location) async {
final locations = await loadLocations();
locations.add(location);
_saveLocations(locations);
}
// Read // Read
@override @override
@@ -83,6 +106,15 @@ class LocalRepository implements TaskRepository, AlarmRepository {
return jsonList.map<Alarm>((e) => Alarm.fromJson(jsonDecode(e))).toList(); return jsonList.map<Alarm>((e) => Alarm.fromJson(jsonDecode(e))).toList();
} }
@override
Future<List<Location>> loadLocations() async {
final Iterable<String> jsonList =
_prefs!.getStringList(_locationsKey) ?? [];
return jsonList
.map<Location>((e) => Location.fromJson(jsonDecode(e)))
.toList();
}
// Update // Update
@override @override
@@ -106,6 +138,14 @@ class LocalRepository implements TaskRepository, AlarmRepository {
_saveAlarms(alarms); _saveAlarms(alarms);
} }
@override
Future<void> updateLocation(Location location) async {
final locations = await loadLocations();
locations.remove(location);
locations.add(location);
_saveLocations(locations);
}
// Delete // Delete
@override @override
@@ -128,4 +168,11 @@ class LocalRepository implements TaskRepository, AlarmRepository {
alarms.remove(alarm); alarms.remove(alarm);
_saveAlarms(alarms); _saveAlarms(alarms);
} }
@override
Future<void> deleteLocation(Location location) async {
final locations = await loadLocations();
locations.remove(location);
_saveLocations(locations);
}
} }
-16
View File
@@ -9,8 +9,6 @@ class Task {
final bool isCompleted; final bool isCompleted;
final String category; final String category;
final List<Task> subtasks; final List<Task> subtasks;
final List<DateTime> alarms;
final Location? location;
final String url; final String url;
Task({ Task({
@@ -22,8 +20,6 @@ class Task {
this.isCompleted = false, this.isCompleted = false,
this.category = '', this.category = '',
this.subtasks = const [], this.subtasks = const [],
this.alarms = const [],
this.location,
this.url = '', this.url = '',
}); });
@@ -49,8 +45,6 @@ class Task {
isCompleted: isCompleted ?? this.isCompleted, isCompleted: isCompleted ?? this.isCompleted,
category: category ?? this.category, category: category ?? this.category,
subtasks: subtasks ?? this.subtasks, subtasks: subtasks ?? this.subtasks,
alarms: alarms ?? this.alarms,
location: location ?? this.location,
url: url ?? this.url, url: url ?? this.url,
); );
} }
@@ -71,14 +65,6 @@ class Task {
?.map((e) => Task.fromJson(e as Map<String, dynamic>)) ?.map((e) => Task.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
[], [],
alarms:
(json['alarms'] as List<dynamic>?)
?.map((e) => DateTime.parse(e as String))
.toList() ??
[],
location: json['location'] != null
? Location.fromJson(json['location'] as Map<String, dynamic>)
: null,
url: json['url'] as String? ?? '', url: json['url'] as String? ?? '',
); );
} }
@@ -93,8 +79,6 @@ class Task {
'isCompleted': isCompleted, 'isCompleted': isCompleted,
'category': category, 'category': category,
'subtasks': subtasks.map((e) => e.toJson()).toList(), 'subtasks': subtasks.map((e) => e.toJson()).toList(),
'alarms': alarms.map((e) => e.toIso8601String()).toList(),
'location': location?.toJson(),
'url': url, 'url': url,
}; };
} }
+14 -2
View File
@@ -4,19 +4,31 @@ class TimeAlarm implements Alarm {
@override @override
final String id; final String id;
@override
final String taskId;
final DateTime triggerAt; final DateTime triggerAt;
const TimeAlarm({required this.id, required this.triggerAt}); const TimeAlarm({
required this.id,
required this.taskId,
required this.triggerAt,
});
factory TimeAlarm.fromJson(Map<String, dynamic> json) { factory TimeAlarm.fromJson(Map<String, dynamic> json) {
return TimeAlarm( return TimeAlarm(
id: json['id'] as String, id: json['id'] as String,
taskId: json['taskId'] as String,
triggerAt: DateTime.parse(json['triggerAt'] as String), triggerAt: DateTime.parse(json['triggerAt'] as String),
); );
} }
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return {'id': id, 'triggerAt': triggerAt.toIso8601String()}; return {
'id': id,
'taskId': taskId,
'triggerAt': triggerAt.toIso8601String(),
};
} }
} }
+3 -3
View File
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import '../model/callback_models/create_task_request.dart'; import '../model/callback_models/create_task_request.dart';
import '../model/extensions/controller_context.dart'; import '../model/extensions/controller_context.dart';
import '../model/task.dart'; import '../model/task.dart';
import '../service/task_controller.dart'; import '../service/controllers/task_controller.dart';
import '../service/tools.dart'; import '../service/tools.dart';
import '../widgets/time_selector.dart'; import '../widgets/time_selector.dart';
@@ -80,7 +80,9 @@ class _TaskEditPageState extends State<TaskEditPage> {
horizontal: MediaQuery.of(context).size.width * 0.05, horizontal: MediaQuery.of(context).size.width * 0.05,
), ),
child: Column( child: Column(
spacing: 12,
children: [ children: [
SizedBox(height: 6),
TextFormField( TextFormField(
autofocus: true, autofocus: true,
controller: titleController, controller: titleController,
@@ -139,8 +141,6 @@ class _TaskEditPageState extends State<TaskEditPage> {
isCompleted: false, isCompleted: false,
category: categoryController.text, category: categoryController.text,
subtasks: [], subtasks: [],
alarms: [],
location: null,
url: urlController.text, url: urlController.text,
), ),
); );
+36 -22
View File
@@ -3,8 +3,9 @@ import 'package:flutter/material.dart';
import '../model/callback_models/create_task_request.dart'; import '../model/callback_models/create_task_request.dart';
import '../model/extensions/controller_context.dart'; import '../model/extensions/controller_context.dart';
import '../model/task.dart'; import '../model/task.dart';
import '../service/task_controller.dart'; import '../service/controllers/task_controller.dart';
import '../service/tools.dart'; import '../service/tools.dart';
import '../widgets/task_dismissible.dart';
import 'task_edit_page.dart'; import 'task_edit_page.dart';
class TaskOverviewPage extends StatefulWidget { class TaskOverviewPage extends StatefulWidget {
@@ -21,11 +22,16 @@ class _TaskOverviewPageState extends State<TaskOverviewPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(), appBar: AppBar(title: Text('Hallo Yannick')),
body: ReorderableListView.builder( body: Padding(
itemBuilder: itemBuilder, padding: EdgeInsetsGeometry.symmetric(
itemCount: tasks.length, horizontal: MediaQuery.of(context).size.width * 0.05,
onReorderItem: context.controller<TaskController>().reorderTask, ),
child: ReorderableListView.builder(
itemBuilder: itemBuilder,
itemCount: tasks.length,
onReorderItem: context.controller<TaskController>().reorderTask,
),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: onCreateTaskTapped, onPressed: onCreateTaskTapped,
@@ -37,24 +43,32 @@ class _TaskOverviewPageState extends State<TaskOverviewPage> {
Widget itemBuilder(BuildContext context, int index) { Widget itemBuilder(BuildContext context, int index) {
final task = tasks.elementAt(index); final task = tasks.elementAt(index);
return ListTile( return Padding(
key: Key(task.id), key: Key(task.id),
title: Text(task.title), padding: const EdgeInsets.only(bottom: 12),
subtitle: task.description.isNotEmpty ? Text(task.description) : null, child: TaskDismissible(
trailing: Checkbox( key: Key(task.id),
value: task.isCompleted, onDismissedRight: () =>
onChanged: (isCompleted) => context context.controller<TaskController>().deleteTask(task),
.controller<TaskController>() child: ListTile(
.saveTask(task.copyWith(isCompleted: isCompleted)), title: Text(task.title),
subtitle: task.description.isNotEmpty ? Text(task.description) : null,
trailing: Checkbox(
value: task.isCompleted,
onChanged: (isCompleted) => context
.controller<TaskController>()
.saveTask(task.copyWith(isCompleted: isCompleted)),
),
onTap: () async {
final result = await onTaskTapped(task);
if (result != null && context.mounted) {
context.controller<TaskController>().saveTask(
result.toTask(id: task.id),
);
}
},
),
), ),
onTap: () async {
final result = await onTaskTapped(task);
if (result != null && context.mounted) {
context.controller<TaskController>().saveTask(
result.toTask(id: task.id),
);
}
},
); );
} }
@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import '../../model/alarm.dart';
import '../../model/repositories/interfaces/alarm_repository.dart';
class AlarmController extends ChangeNotifier {
AlarmController(AlarmRepository repository) : _repository = repository {
_loadAlarms();
}
final AlarmRepository _repository;
final List<Alarm> _alarms = [];
Future<void> addAlarm(Alarm alarm) {
_alarms.add(alarm);
notifyListeners();
return _repository.createAlarm(alarm);
}
Future<void> deleteAlarm(Alarm alarm) {
_alarms.remove(alarm);
notifyListeners();
return _repository.deleteAlarm(alarm);
}
Future<void> _loadAlarms() {
_alarms.clear();
return _repository.loadAlarms().then((value) => _alarms.addAll(value));
}
}
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import '../../model/location.dart';
import '../../model/repositories/interfaces/location_repository.dart';
class LocationController extends ChangeNotifier {
LocationController(LocationRepository repository) : _repository = repository {
_loadLocations();
}
final LocationRepository _repository;
final List<Location> _locations = [];
Future<void> addLocation(Location location) {
_locations.add(location);
notifyListeners();
return _repository.createLocation(location);
}
Future<void> deleteLocation(Location location) {
_locations.remove(location);
notifyListeners();
return _repository.deleteLocation(location);
}
Future<void> _loadLocations() {
_locations.clear();
return _repository.loadLocations().then(
(value) => _locations.addAll(value),
);
}
}
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart' show ChangeNotifier; import 'package:flutter/material.dart' show ChangeNotifier;
import '../model/repositories/interfaces/task_repository.dart'; import '../../model/repositories/interfaces/task_repository.dart';
import '../model/task.dart'; import '../../model/task.dart';
class TaskController extends ChangeNotifier { class TaskController extends ChangeNotifier {
TaskController(TaskRepository repository) : _repository = repository { TaskController(TaskRepository repository) : _repository = repository {
+41
View File
@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
class TaskDismissible extends StatelessWidget {
const TaskDismissible({
required super.key,
this.onDismissedRight,
required this.child,
});
final VoidCallback? onDismissedRight;
final Widget child;
@override
Widget build(BuildContext context) {
return Dismissible(
key: key!,
direction: DismissDirection.startToEnd,
background: Container(
color: Theme.of(context).colorScheme.error,
child: Align(
alignment: AlignmentGeometry.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Icon(
Icons.delete,
color: Theme.of(context).colorScheme.onError,
),
),
),
),
onDismissed: onDismissed,
child: child,
);
}
void onDismissed(DismissDirection direction) {
if (direction == DismissDirection.startToEnd && onDismissedRight != null) {
onDismissedRight!();
}
}
}