From ebc4cdf7547066b0ba0dd1de0fe278b58a4b797f Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Wed, 16 Apr 2025 16:08:45 +0200 Subject: [PATCH 1/7] feat: add update and delete functionality to tags and improve report screen --- lib/main.dart | 10 +- lib/screens/main_screen.dart | 13 ++- lib/screens/report_screen.dart | 10 +- lib/screens/tags_screen.dart | 159 +++++++++++++++++++++++++++++++- lib/src/rust/api.dart | 6 ++ lib/src/rust/frb_generated.dart | 85 +++++++++++++++-- lib/time_tracking_service.dart | 72 ++++++++++++++- pubspec.lock | 8 ++ pubspec.yaml | 1 + rust/src/api.rs | 14 +++ rust/src/api/database.rs | 38 ++++++++ rust/src/frb_generated.rs | 91 ++++++++++++++++-- 12 files changed, 464 insertions(+), 43 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 8c718de..f5c288c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -68,15 +68,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Rust Time Tracker', - theme: ThemeData( - primarySwatch: Colors.blue, - useMaterial3: true, - // Optional: Theme für BottomNavigationBar anpassen - // bottomNavigationBarTheme: const BottomNavigationBarThemeData( - // selectedItemColor: Colors.deepPurple, - // unselectedItemColor: Colors.grey, - // ), - ), + theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true), home: const InitializerWidget(), ); } diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 652b864..7e42364 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:timetracker/screens/home_screen.dart'; import 'package:timetracker/screens/tags_screen.dart'; import 'package:timetracker/screens/report_screen.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class MainScreen extends StatefulWidget { const MainScreen({super.key}); @@ -37,8 +38,8 @@ class _MainScreenState extends State { items: [ BottomNavigationBarItem( - icon: Icon(context.platformIcons.clockSolid), - activeIcon: Icon(context.platformIcons.clockSolid), + icon: FaIcon(FontAwesomeIcons.clock), + activeIcon: FaIcon(FontAwesomeIcons.solidClock), label: 'Tracking', ), BottomNavigationBarItem( @@ -47,8 +48,12 @@ class _MainScreenState extends State { label: 'Tags', ), BottomNavigationBarItem( - icon: Icon(context.platformIcons.bookmarkOutline), - activeIcon: Icon(context.platformIcons.bookmarkSolid), + icon: FaIcon( + FontAwesomeIcons.chartBar, + ), //Icon(context.platformIcons.bookmarkOutline), + activeIcon: FaIcon( + FontAwesomeIcons.solidChartBar, + ), //Icon(context.platformIcons.bookmarkSolid), label: 'Reports', ), ], diff --git a/lib/screens/report_screen.dart b/lib/screens/report_screen.dart index c546259..1c56861 100644 --- a/lib/screens/report_screen.dart +++ b/lib/screens/report_screen.dart @@ -487,8 +487,8 @@ class _ReportScreenState extends State { ), ), ), - SizedBox(height: 250, child: _buildPieChart(_reportData!)), - const Divider(), + // SizedBox(height: 250, child: _buildPieChart(_reportData!)), + // const Divider(), SizedBox( height: 250, child: _buildBarChart(_reportData!, startDate, endDate), @@ -507,7 +507,7 @@ class _ReportScreenState extends State { ), ), ), - SizedBox(height: 300, child: _buildReportList(_reportData!.entries)), + SizedBox(height: 600, child: _buildReportList(_reportData!.entries)), ], ); } @@ -649,8 +649,8 @@ class _ReportScreenState extends State { Widget _buildReportList(List entries) { return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), + // shrinkWrap: true, + // physics: const NeverScrollableScrollPhysics(), itemCount: entries.length, itemBuilder: (context, index) { final entry = entries[index]; diff --git a/lib/screens/tags_screen.dart b/lib/screens/tags_screen.dart index ef079c5..a5a54c3 100644 --- a/lib/screens/tags_screen.dart +++ b/lib/screens/tags_screen.dart @@ -1,8 +1,10 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter/cupertino.dart' show CupertinoIcons; import 'package:flutter/material.dart'; +import 'package:timetracker/src/rust/api.dart'; import 'package:timetracker/time_tracking_service.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; class TagsScreen extends StatefulWidget { const TagsScreen({super.key}); @@ -40,6 +42,129 @@ class _TagsScreenState extends State { } } + void _showEditTagDialog(BuildContext context, Tag tag) { + final TextEditingController editController = TextEditingController( + text: tag.name, + ); + + showPlatformDialog( + context: context, + builder: + (dialogContext) => PlatformAlertDialog( + title: PlatformText('Tag bearbeiten'), + content: PlatformTextField( + controller: editController, + autofocus: true, + hintText: 'Neuer Tag-Name', + material: + (_, __) => MaterialTextFieldData( + decoration: const InputDecoration( + labelText: 'Neuer Tag-Name', + ), + ), + cupertino: + (_, __) => + CupertinoTextFieldData(placeholder: 'Neuer Tag-Name'), + ), + actions: [ + PlatformDialogAction( + child: PlatformText('Abbrechen'), + onPressed: () => Navigator.pop(dialogContext), + ), + PlatformDialogAction( + child: PlatformText('Speichern'), + onPressed: () async { + final newName = editController.text.trim(); + if (newName.isEmpty) { + _showPlatformFeedbackDialog( + context, + 'Fehler', + 'Tag-Name darf nicht leer sein.', + ); + return; + } + if (newName == tag.name) { + Navigator.pop(dialogContext); + return; + } + + final success = await context + .read() + .flUpdateTag(tag.id.toInt(), newName); + + if (!mounted) return; + + Navigator.pop(dialogContext); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + if (success) { + _showPlatformFeedbackDialog( + context, + 'Erfolg', + 'Tag aktualisiert.', + ); + } else { + _showPlatformFeedbackDialog( + context, + 'Fehler', + 'Tag konnte nicht aktualisiert werden (Name evtl. vergeben?).', + ); + } + }); + }, + ), + ], + ), + ); + } + + void _confirmAndDeleteTag(BuildContext context, Tag tag) { + showPlatformDialog( + context: context, + builder: + (dialogContext) => PlatformAlertDialog( + title: PlatformText('Tag löschen?'), + content: PlatformText( + 'Möchtest du den Tag "${tag.name}" wirklich löschen? Zugeordnete Zeiteinträge verlieren ihre Tag-Zuweisung.', + ), + actions: [ + PlatformDialogAction( + child: PlatformText('Abbrechen'), + onPressed: () => Navigator.pop(dialogContext), + ), + PlatformDialogAction( + child: PlatformText('Löschen'), + cupertino: + (_, __) => + CupertinoDialogActionData(isDestructiveAction: true), + onPressed: () async { + Navigator.pop(dialogContext); + + final success = await context + .read() + .flDeleteTag(tag.id.toInt()); + + if (!mounted) return; + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + if (success) { + } else { + _showPlatformFeedbackDialog( + context, + 'Fehler', + 'Tag konnte nicht gelöscht werden.', + ); + } + }); + }, + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final tags = context.watch().tags; @@ -51,6 +176,7 @@ class _TagsScreenState extends State { Padding( padding: const EdgeInsets.all(16.0), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: PlatformTextField( @@ -85,9 +211,36 @@ class _TagsScreenState extends State { itemCount: tags.length, itemBuilder: (context, index) { final tag = tags[index]; - return ListTile( - title: PlatformText(tag.name), - trailing: PlatformText('ID: ${tag.id}'), + return Slidable( + key: ValueKey(tag.id.toInt()), + endActionPane: ActionPane( + motion: const StretchMotion(), + children: [ + SlidableAction( + onPressed: (context) { + _showEditTagDialog(this.context, tag); + }, + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + icon: PlatformIcons(context).edit, + label: 'Bearbeiten', + ), + SlidableAction( + onPressed: (context) { + _confirmAndDeleteTag(this.context, tag); + }, + backgroundColor: Colors.red, + foregroundColor: Colors.white, + icon: PlatformIcons(context).delete, + label: 'Löschen', + ), + ], + ), + + child: ListTile( + title: PlatformText(tag.name), + trailing: PlatformText('ID: ${tag.id.toInt()}'), + ), ); }, ), diff --git a/lib/src/rust/api.dart b/lib/src/rust/api.dart index cf8ee78..e8ad8f1 100644 --- a/lib/src/rust/api.dart +++ b/lib/src/rust/api.dart @@ -14,6 +14,12 @@ Future initApp({required String dbDirectoryPath}) => Future createTag({required String name}) => RustLib.instance.api.crateApiCreateTag(name: name); +Future updateTag({required PlatformInt64 id, required String newName}) => + RustLib.instance.api.crateApiUpdateTag(id: id, newName: newName); + +Future deleteTag({required PlatformInt64 id}) => + RustLib.instance.api.crateApiDeleteTag(id: id); + Future> getTags() => RustLib.instance.api.crateApiGetTags(); Future startTracking({ diff --git a/lib/src/rust/frb_generated.dart b/lib/src/rust/frb_generated.dart index 9b8b5f8..0bfb96e 100644 --- a/lib/src/rust/frb_generated.dart +++ b/lib/src/rust/frb_generated.dart @@ -62,7 +62,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.9.0'; @override - int get rustContentHash => -79634774; + int get rustContentHash => 350676645; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -75,6 +75,8 @@ class RustLib extends BaseEntrypoint { abstract class RustLibApi extends BaseApi { Future crateApiCreateTag({required String name}); + Future crateApiDeleteTag({required PlatformInt64 id}); + Future crateApiDeleteTimeEntry({required PlatformInt64 id}); Future crateApiGenerateReport({ @@ -99,6 +101,11 @@ abstract class RustLibApi extends BaseApi { required PlatformInt64 endTimeUnixTs, }); + Future crateApiUpdateTag({ + required PlatformInt64 id, + required String newName, + }); + Future crateApiUpdateTimeEntry({ required PlatformInt64 entryId, PlatformInt64? newTagId, @@ -144,7 +151,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { const TaskConstMeta(debugName: "create_tag", argNames: ["name"]); @override - Future crateApiDeleteTimeEntry({required PlatformInt64 id}) { + Future crateApiDeleteTag({required PlatformInt64 id}) { return handler.executeNormal( NormalTask( callFfi: (port_) { @@ -161,6 +168,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), + constMeta: kCrateApiDeleteTagConstMeta, + argValues: [id], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDeleteTagConstMeta => + const TaskConstMeta(debugName: "delete_tag", argNames: ["id"]); + + @override + Future crateApiDeleteTimeEntry({required PlatformInt64 id}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_i_64(id, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 3, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), constMeta: kCrateApiDeleteTimeEntryConstMeta, argValues: [id], apiImpl: this, @@ -187,7 +222,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 3, + funcId: 4, port: port_, ); }, @@ -216,7 +251,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 4, + funcId: 5, port: port_, ); }, @@ -246,7 +281,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 5, + funcId: 6, port: port_, ); }, @@ -274,7 +309,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 6, + funcId: 7, port: port_, ); }, @@ -306,7 +341,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 7, + funcId: 8, port: port_, ); }, @@ -340,7 +375,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 8, + funcId: 9, port: port_, ); }, @@ -360,6 +395,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["entryId", "endTimeUnixTs"], ); + @override + Future crateApiUpdateTag({ + required PlatformInt64 id, + required String newName, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_i_64(id, serializer); + sse_encode_String(newName, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 10, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiUpdateTagConstMeta, + argValues: [id, newName], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiUpdateTagConstMeta => + const TaskConstMeta(debugName: "update_tag", argNames: ["id", "newName"]); + @override Future crateApiUpdateTimeEntry({ required PlatformInt64 entryId, @@ -378,7 +445,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 9, + funcId: 11, port: port_, ); }, diff --git a/lib/time_tracking_service.dart b/lib/time_tracking_service.dart index 7b90af7..357e464 100644 --- a/lib/time_tracking_service.dart +++ b/lib/time_tracking_service.dart @@ -105,16 +105,13 @@ class TimeTrackingService extends ChangeNotifier { Future flCreateTag(String name) async { if (name.trim().isEmpty) { - log("Cannot create tag: Name is empty."); return false; } final trimmedName = name.trim(); - try { final tagId = await createTag(name: trimmedName); log("Tag created with ID: $tagId"); - _tags = await getTags(); - notifyListeners(); + await _loadUpdatedTags(); return true; } on FrbException catch (e) { log("Error creating tag: $e"); @@ -124,6 +121,27 @@ class TimeTrackingService extends ChangeNotifier { return false; } } + // Future flCreateTag(String name) async { + // if (name.trim().isEmpty) { + // log("Cannot create tag: Name is empty."); + // return false; + // } + // final trimmedName = name.trim(); + + // try { + // final tagId = await createTag(name: trimmedName); + // log("Tag created with ID: $tagId"); + // _tags = await getTags(); + // notifyListeners(); + // return true; + // } on FrbException catch (e) { + // log("Error creating tag: $e"); + // return false; + // } catch (e) { + // log("Unexpected error creating tag: $e"); + // return false; + // } + // } Future flGetReport( int? tagId, @@ -184,4 +202,50 @@ class TimeTrackingService extends ChangeNotifier { return false; } } + + Future flUpdateTag(int tagId, String newName) async { + if (newName.trim().isEmpty) { + log('Service: Cannot update tag $tagId: New name is empty.'); + return false; + } + final trimmedName = newName.trim(); + log('Service: Attempting to update tag $tagId to "$trimmedName"'); + try { + await updateTag(id: tagId.toInt(), newName: trimmedName); + log('Service: Successfully updated tag $tagId'); + await _loadUpdatedTags(); + return true; // Erfolg + } on FrbException catch (e, s) { + log('Service: Error updating tag $tagId: $e\n$s'); + return false; + } catch (e, s) { + log('Service: Unexpected error updating tag $tagId: $e\n$s'); + return false; + } + } + + Future flDeleteTag(int tagId) async { + log('Service: Attempting to delete tag $tagId'); + try { + await deleteTag(id: tagId.toInt()); + log('Service: Successfully deleted tag $tagId'); + await _loadUpdatedTags(); + return true; + } on FrbException catch (e, s) { + log('Service: Error deleting tag $tagId: $e\n$s'); + return false; + } catch (e, s) { + log('Service: Unexpected error deleting tag $tagId: $e\n$s'); + return false; + } + } + + Future _loadUpdatedTags() async { + try { + _tags = await getTags(); + notifyListeners(); + } catch (e) { + log("Error refreshing tags after CUD operation: $e"); + } + } } diff --git a/pubspec.lock b/pubspec.lock index 0168886..eb05bdd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -152,6 +152,14 @@ packages: description: flutter source: sdk version: "0.0.0" + font_awesome_flutter: + dependency: "direct main" + description: + name: font_awesome_flutter + sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a + url: "https://pub.dev" + source: hosted + version: "10.8.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 69c0961..b4bb4df 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: fl_chart: ^0.70.2 flutter_platform_widgets: ^8.0.0 flutter_slidable: ^4.0.0 + font_awesome_flutter: ^10.8.0 dev_dependencies: flutter_test: diff --git a/rust/src/api.rs b/rust/src/api.rs index 1e61013..e725692 100644 --- a/rust/src/api.rs +++ b/rust/src/api.rs @@ -45,6 +45,20 @@ pub fn create_tag(name: String) -> Result { database::create_tag_internal(name.trim()) } +pub fn update_tag(id: i64, new_name: String) -> Result<()> { + log::debug!( + "API: update_tag called for id {} with name '{}'", + id, + new_name + ); + database::update_tag_internal(id, &new_name) +} + +pub fn delete_tag(id: i64) -> Result<()> { + log::debug!("API: delete_tag called for id {}", id); + database::delete_tag_internal(id) +} + pub fn get_tags() -> Result> { log::debug!("API: get_tags called"); database::get_tags_internal() diff --git a/rust/src/api/database.rs b/rust/src/api/database.rs index 7ee1d03..a2aa6b6 100644 --- a/rust/src/api/database.rs +++ b/rust/src/api/database.rs @@ -106,6 +106,44 @@ pub(super) fn get_tags_internal() -> Result> { }) } +pub(super) fn update_tag_internal(id: i64, new_name: &str) -> Result<()> { + log::debug!( + "RUST_DB: Attempting to update tag_id {} to name '{}'", + id, + new_name + ); + ensure!(!new_name.trim().is_empty(), "New tag name cannot be empty"); + + let name = new_name.trim(); + + with_db_connection(|conn| { + let rows_affected = conn + .execute("UPDATE tags SET name = ?1 WHERE id = ?2", params![name, id]) + .context("Failed to execute update tag SQL")?; + + ensure!(rows_affected > 0, "Tag {} not found for update", id); + + log::info!("RUST_DB: Successfully updated tag {}", id); + Ok(()) + }) + .with_context(|| format!("Failed operation for updating tag_id {}", id)) +} + +pub(super) fn delete_tag_internal(id: i64) -> Result<()> { + log::debug!("RUST_DB: Attempting to delete tag_id {}", id); + with_db_connection(|conn| { + let rows_affected = conn + .execute("DELETE FROM tags WHERE id = ?1", params![id]) + .context("Failed to execute delete tag SQL")?; + + ensure!(rows_affected > 0, "Tag {} not found for deletion", id); + + log::info!("RUST_DB: Successfully deleted tag {}", id); + Ok(()) + }) + .with_context(|| format!("Failed operation for deleting tag_id {}", id)) +} + pub(super) fn start_tracking_internal(tag_id: Option, start_time_unix_ts: i64) -> Result { with_db_connection(|conn| { conn.execute( diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index ba418ca..1f6a338 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.9.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -79634774; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 350676645; // Section: executor @@ -80,6 +80,41 @@ fn wire__crate__api__create_tag_impl( }, ) } +fn wire__crate__api__delete_tag_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "delete_tag", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let output_ok = crate::api::delete_tag(api_id)?; + Ok(output_ok) + })(), + ) + } + }, + ) +} fn wire__crate__api__delete_time_entry_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -333,6 +368,42 @@ fn wire__crate__api__stop_tracking_impl( }, ) } +fn wire__crate__api__update_tag_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "update_tag", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_id = ::sse_decode(&mut deserializer); + let api_new_name = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let output_ok = crate::api::update_tag(api_id, api_new_name)?; + Ok(output_ok) + })(), + ) + } + }, + ) +} fn wire__crate__api__update_time_entry_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -551,14 +622,16 @@ fn pde_ffi_dispatcher_primary_impl( // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { 1 => wire__crate__api__create_tag_impl(port, ptr, rust_vec_len, data_len), - 2 => wire__crate__api__delete_time_entry_impl(port, ptr, rust_vec_len, data_len), - 3 => wire__crate__api__generate_report_impl(port, ptr, rust_vec_len, data_len), - 4 => wire__crate__api__get_last_unfinished_tracking_impl(port, ptr, rust_vec_len, data_len), - 5 => wire__crate__api__get_tags_impl(port, ptr, rust_vec_len, data_len), - 6 => wire__crate__api__init_app_impl(port, ptr, rust_vec_len, data_len), - 7 => wire__crate__api__start_tracking_impl(port, ptr, rust_vec_len, data_len), - 8 => wire__crate__api__stop_tracking_impl(port, ptr, rust_vec_len, data_len), - 9 => wire__crate__api__update_time_entry_impl(port, ptr, rust_vec_len, data_len), + 2 => wire__crate__api__delete_tag_impl(port, ptr, rust_vec_len, data_len), + 3 => wire__crate__api__delete_time_entry_impl(port, ptr, rust_vec_len, data_len), + 4 => wire__crate__api__generate_report_impl(port, ptr, rust_vec_len, data_len), + 5 => wire__crate__api__get_last_unfinished_tracking_impl(port, ptr, rust_vec_len, data_len), + 6 => wire__crate__api__get_tags_impl(port, ptr, rust_vec_len, data_len), + 7 => wire__crate__api__init_app_impl(port, ptr, rust_vec_len, data_len), + 8 => wire__crate__api__start_tracking_impl(port, ptr, rust_vec_len, data_len), + 9 => wire__crate__api__stop_tracking_impl(port, ptr, rust_vec_len, data_len), + 10 => wire__crate__api__update_tag_impl(port, ptr, rust_vec_len, data_len), + 11 => wire__crate__api__update_time_entry_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } From 055b402c81c6c9ad1529bba599567023926e79e7 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Mon, 21 Apr 2025 20:19:22 +0200 Subject: [PATCH 2/7] feat: add swipe action to refresh report screen --- lib/screens/report_screen.dart | 138 +++++---------------------------- 1 file changed, 21 insertions(+), 117 deletions(-) diff --git a/lib/screens/report_screen.dart b/lib/screens/report_screen.dart index 1c56861..b24c37e 100644 --- a/lib/screens/report_screen.dart +++ b/lib/screens/report_screen.dart @@ -1,7 +1,6 @@ import 'dart:developer'; import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -16,14 +15,12 @@ DateTime unixSecondsToDateTime(int ts) => DateTime.fromMillisecondsSinceEpoch(ts * 1000, isUtc: true).toLocal(); extension TimeEntryFormatting on TimeEntry { - /// Konvertiert den startTime (Unix Timestamp) in ein lokales DateTime Objekt. DateTime get startDateTime => DateTime.fromMillisecondsSinceEpoch( startTime.toInt() * 1000, isUtc: true, ).toLocal(); - /// Konvertiert den optionalen endTime (Unix Timestamp) in ein lokales DateTime Objekt. DateTime? get endDateTime => endTime == null ? null @@ -32,7 +29,6 @@ extension TimeEntryFormatting on TimeEntry { isUtc: true, ).toLocal(); - /// Formatiert die Dauer (durationSecs) als HH:MM:SS String. String get durationFormatted { final durationInSeconds = durationSecs?.toInt(); if (durationInSeconds == null || durationInSeconds < 0) return '--:--:--'; @@ -47,7 +43,6 @@ extension TimeEntryFormatting on TimeEntry { } extension ReportDataFormatting on ReportData { - /// Formatiert die Gesamtdauer als HH:MM:SS String. String get totalDurationFormatted { final durationInSeconds = totalDurationSecs.toInt(); if (durationInSeconds < 0) return '--:--:--'; @@ -99,8 +94,9 @@ class _ReportScreenState extends State { final Map> dailyTagDurations = {}; for (final entry in entries) { - if (entry.durationSecs == null || entry.durationSecs!.toInt() <= 0) + if (entry.durationSecs == null || entry.durationSecs!.toInt() <= 0) { continue; + } final entryDay = DateTime( entry.startDateTime.year, @@ -393,14 +389,24 @@ class _ReportScreenState extends State { children: [ _buildFilterControls(tagOptions), Expanded( - child: - _isLoadingReport - ? Center(child: PlatformCircularProgressIndicator()) - : _reportData == null - ? const Center( - child: Text('Keine Reportdaten gefunden oder Fehler.'), - ) - : _buildReportView(), + child: RefreshIndicator.adaptive( + onRefresh: _generateReport, + child: + _isLoadingReport + ? Center(child: PlatformCircularProgressIndicator()) + : _reportData == null + ? ListView( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.5, + child: Text( + 'Keine Reportdaten gefunden oder Fehler.', + ), + ), + ], + ) + : _buildReportView(), + ), ), ], ), @@ -487,8 +493,6 @@ class _ReportScreenState extends State { ), ), ), - // SizedBox(height: 250, child: _buildPieChart(_reportData!)), - // const Divider(), SizedBox( height: 250, child: _buildBarChart(_reportData!, startDate, endDate), @@ -512,103 +516,6 @@ class _ReportScreenState extends State { ); } - Widget _buildPieChart(ReportData data) { - Map durationPerTag = {}; - for (var entry in data.entries) { - final tagName = entry.tagName ?? 'Ohne Tag'; - final duration = (entry.durationSecs?.toInt() ?? 0).toDouble(); - durationPerTag[tagName] = (durationPerTag[tagName] ?? 0) + duration; - } - - if (durationPerTag.isEmpty) { - return const Center(child: Text("Keine Daten für Chart")); - } - - List colors = Colors.primaries.take(durationPerTag.length).toList(); - if (durationPerTag.length > Colors.primaries.length) { - colors.addAll( - Colors.accents.take(durationPerTag.length - Colors.primaries.length), - ); - } - - int colorIndex = 0; - List sections = - durationPerTag.entries.map((entry) { - final isTouched = false; - final fontSize = isTouched ? 18.0 : 14.0; - final radius = isTouched ? 60.0 : 50.0; - final color = colors[colorIndex % colors.length]; - colorIndex++; - - final hours = (entry.value / 3600).toStringAsFixed(1); - - return PieChartSectionData( - color: color, - value: entry.value, - title: '${entry.key}\n${hours}h', - radius: radius, - titleStyle: TextStyle( - fontSize: fontSize, - fontWeight: FontWeight.bold, - color: Colors.white, - shadows: [ - Shadow(color: Colors.black.withOpacity(0.7), blurRadius: 2), - ], - ), - titlePositionPercentageOffset: 0.6, - ); - }).toList(); - - return Padding( - padding: const EdgeInsets.all(8.0), - child: PieChart( - PieChartData( - sections: sections, - centerSpaceRadius: 40, - sectionsSpace: 2, - ), - duration: const Duration(milliseconds: 150), - curve: Curves.linear, - ), - ); - } - - // Widget _buildDataTable(List entries) { - // final DateFormat timeFormatter = DateFormat('HH:mm:ss'); - // final DateFormat dateFormatter = DateFormat('dd.MM.yy'); - - // return DataTable( - // columnSpacing: 10, - // columns: [ - // DataColumn(label: PlatformText('Tag')), - // DataColumn(label: PlatformText('Start')), - // DataColumn(label: PlatformText('Ende')), - // DataColumn(label: PlatformText('Dauer'), numeric: true), - // ], - // rows: - // entries.map((entry) { - // return DataRow( - // cells: [ - // DataCell(PlatformText(entry.tagName ?? '-')), - // DataCell( - // PlatformText( - // '${dateFormatter.format(unixSecondsToDateTime(entry.startTime))}\n${timeFormatter.format(unixSecondsToDateTime(entry.startTime))}', - // ), - // ), - // DataCell( - // entry.endTime != null - // ? PlatformText( - // '${dateFormatter.format(unixSecondsToDateTime(entry.endTime!))}\n${timeFormatter.format(unixSecondsToDateTime(entry.endTime!))}', - // ) - // : PlatformText('-'), - // ), - // DataCell(Text(entry.durationFormatted)), - // ], - // ); - // }).toList(), - // ); - // } - void _confirmAndDeleteEntry(TimeEntry entry, BuildContext context) { showPlatformDialog( context: context, @@ -855,10 +762,7 @@ class _ReportScreenState extends State { actions: [ PlatformDialogAction( child: PlatformText('OK'), - onPressed: - () => Navigator.pop( - dialogContext, - ), // Pop using dialog's context + onPressed: () => Navigator.pop(dialogContext), ), ], ), From 4eed56f155b0583fc46fd3354ce7c1658b50cf75 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Mon, 21 Apr 2025 20:20:21 +0200 Subject: [PATCH 3/7] docs: update changelog --- changelog.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 changelog.md diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..d7bfb8d --- /dev/null +++ b/changelog.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [unreleased] + +### 🚀 Features + +- Add swipe action to refresh report screen + +## [0.2.0] - 2025-04-16 + +### 🚀 Features + +- Add update and delete functionality to tags and improve report screen + +## [0.1.0] - 2025-04-09 + +### 🚜 Refactor + +- Clean up +- Perform some clean up + + From 9fe11e4796a1c4ec1a6bf8935e65c43fc91467a6 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Mon, 21 Apr 2025 20:31:56 +0200 Subject: [PATCH 4/7] docs: update README --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 23fad13..a4d4d74 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,51 @@ # timetracker -A new Flutter project. +A cross-platform time tracker built with Flutter for the frontend and Rust for the backend logic. Communication is handled via `flutter_rust_bridge`, and data is stored locally on the device using SQLite. The user interface adapts to the native look-and-feel of Android and iOS thanks to `flutter_platform_widgets`. -## Getting Started +## Features -This project is a starting point for a Flutter application. +* **Time Tracking:** Start and stop time tracking entries. +* **Tag Management:** + * Create new tags. + * List all tags. + * Edit tag names (via swipe gesture). + * Delete tags (via swipe gesture, includes confirmation). Associated time entries will have their tag set to NULL. +* **Local Storage:** All data is securely stored in a local SQLite database on the device (managed by Rust). +* **Reporting:** + * Display time entries filtered by period (Day, Week, Month, Year) and optionally by tag. + * Total duration display for the filtered period. + * Visualization using charts (`fl_chart`): + * **Bar chart:** Distribution of daily time spent per tag for the selected period. + * List view for report entries. + * **Pull-to-Refresh:** Manually refresh report data. + * **Swipe-to-Delete:** Delete individual time entries directly from the report list via swipe gesture (with confirmation). +* **Platform-Adaptive UI:** Uses `flutter_platform_widgets` to provide a native appearance on Android (Material Design) and iOS (Cupertino). -A few resources to get you started if this is your first Flutter project: +## Technologies -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) +* **Frontend:** Flutter / Dart + * State Management: `provider` + * UI Adaptation: `flutter_platform_widgets` + * Charts: `fl_chart` + * Swipe Actions: `flutter_slidable` + * Intl: `intl` +* **Backend:** Rust + * Database: SQLite (via `rusqlite`) + * Error Handling: `anyhow` + * Logging: `log` +* **Bridge:** `flutter_rust_bridge` +* **Build:** Cargo, Flutter Build Tools, Gradle (Android), Xcode (iOS) -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +## Architecture + +The app follows a clear separation between UI and logic: + +`Flutter UI (Widgets)` <-> `Dart Service Layer (TimeTrackingService)` <-> `flutter_rust_bridge (Generated Bindings)` <-> `Rust API (api.rs)` <-> `Rust DB Logic (database.rs)` <-> `SQLite Database` + +## Contributing + +Contributions are welcome! Please create an issue to report bugs or suggest new features. Pull requests are also welcome. + +## License + +[MIT License] From 169669315c6dc08d468f808ce9caa6b808146cdc Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Tue, 29 Apr 2025 20:18:36 +0200 Subject: [PATCH 5/7] feat: add pickable daterange to report screen --- lib/main.dart | 7 + lib/screens/report_screen.dart | 272 +++++++++++++++++++++++++++++---- pubspec.lock | 9 +- pubspec.yaml | 5 +- 4 files changed, 258 insertions(+), 35 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f5c288c..7743e0e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:path_provider/path_provider.dart'; import 'package:timetracker/screens/main_screen.dart'; import 'package:timetracker/src/rust/api.dart'; @@ -68,6 +69,12 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Rust Time Tracker', + supportedLocales: const [Locale('de', 'DE'), Locale('en', '')], + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true), home: const InitializerWidget(), ); diff --git a/lib/screens/report_screen.dart b/lib/screens/report_screen.dart index b24c37e..dcae7ae 100644 --- a/lib/screens/report_screen.dart +++ b/lib/screens/report_screen.dart @@ -1,9 +1,11 @@ import 'dart:developer'; import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:timetracker/src/rust/api.dart'; @@ -56,7 +58,7 @@ extension ReportDataFormatting on ReportData { } } -enum ReportPeriod { day, week, month, year } +enum ReportPeriod { day, week, month, year, custom } class ReportScreen extends StatefulWidget { const ReportScreen({super.key}); @@ -68,6 +70,8 @@ class ReportScreen extends StatefulWidget { class _ReportScreenState extends State { final DateTime _selectedDate = DateTime.now(); ReportPeriod _selectedPeriod = ReportPeriod.day; + DateTime _customStartDate = DateTime.now(); + DateTime _customEndDate = DateTime.now(); Tag? _selectedTag; ReportData? _reportData; bool _isLoadingReport = false; @@ -85,6 +89,9 @@ class _ReportScreenState extends State { @override void initState() { super.initState(); + final now = DateTime.now(); + _customStartDate = DateTime(now.year, now.month, now.day); + _customEndDate = DateTime(now.year, now.month, now.day); _generateReport(); } @@ -330,7 +337,7 @@ class _ReportScreenState extends State { ); } - (DateTime, DateTime) _calculateDateRange() { + (DateTime, DateTime) _calculateDateRange(ReportPeriod period) { final nowLocal = DateTime.now().toLocal(); final startOfDay = DateTime(nowLocal.year, nowLocal.month, nowLocal.day); @@ -352,6 +359,13 @@ class _ReportScreenState extends State { final startOfYear = DateTime(nowLocal.year, 1, 1); final endOfYear = DateTime(nowLocal.year + 1, 1, 1); return (startOfYear, endOfYear); + case ReportPeriod.custom: + final endExclusive = DateTime( + _customEndDate.year, + _customEndDate.month, + _customEndDate.day, + ).add(const Duration(days: 1)); + return (_customStartDate, endExclusive); } } @@ -363,12 +377,33 @@ class _ReportScreenState extends State { }); final timeService = context.read(); - final (start, end) = _calculateDateRange(); final tagId = _selectedTag?.id; - log("Generating report for TagID: $tagId, Start: $start, End: $end"); + late DateTime reportStartDate; + late DateTime reportEndDateExclusive; - final data = await timeService.flGetReport(tagId, start, end); + if (_selectedPeriod == ReportPeriod.custom) { + reportStartDate = _customStartDate; + reportEndDateExclusive = DateTime( + _customEndDate.year, + _customEndDate.month, + _customEndDate.day, + ).add(const Duration(days: 1)); + } else { + final (start, end) = _calculateDateRange(_selectedPeriod); + reportStartDate = start; + reportEndDateExclusive = end; + } + + log( + "Generating report for Period: $_selectedPeriod, TagID: $tagId, Start: $reportStartDate, End (Exclusive): $reportEndDateExclusive", + ); + + final data = await timeService.flGetReport( + tagId, + reportStartDate, + reportEndDateExclusive, + ); if (mounted) { setState(() { @@ -378,6 +413,29 @@ class _ReportScreenState extends State { } } + // Future _generateReport() async { + // if (!mounted) return; + // setState(() { + // _isLoadingReport = true; + // _reportData = null; + // }); + + // final timeService = context.read(); + // final (start, end) = _calculateDateRange(); + // final tagId = _selectedTag?.id; + + // log("Generating report for TagID: $tagId, Start: $start, End: $end"); + + // final data = await timeService.flGetReport(tagId, start, end); + + // if (mounted) { + // setState(() { + // _reportData = data; + // _isLoadingReport = false; + // }); + // } + // } + @override Widget build(BuildContext context) { final tags = context.watch().tags; @@ -405,7 +463,7 @@ class _ReportScreenState extends State { ), ], ) - : _buildReportView(), + : _buildReportView(_reportData!), ), ), ], @@ -415,33 +473,71 @@ class _ReportScreenState extends State { Widget _buildFilterControls(List tagOptions) { final DateFormat formatter = DateFormat('dd.MM.yyyy'); - final (start, end) = _calculateDateRange(); + final String dateRangeButtonText = + '${formatter.format(_customStartDate)} - ${formatter.format(_customEndDate)}'; return Padding( padding: const EdgeInsets.all(8.0), child: Wrap( spacing: 8.0, runSpacing: 4.0, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, children: [ - DropdownButton( - value: _selectedPeriod, - onChanged: (ReportPeriod? newValue) { - if (newValue != null) { - setState(() { - _selectedPeriod = newValue; - }); - _generateReport(); - } - }, - items: - ReportPeriod.values.map((ReportPeriod period) { - return DropdownMenuItem( - value: period, - child: PlatformText( - period.toString().split('.').last.toUpperCase(), - ), - ); - }).toList(), + PlatformWidget( + material: + (_, __) => DropdownButton( + value: _selectedPeriod, + items: [ + ...ReportPeriod.values.map((period) { + String text; + switch (period) { + case ReportPeriod.day: + text = 'Tag'; + break; + case ReportPeriod.week: + text = 'Woche'; + break; + case ReportPeriod.month: + text = 'Monat'; + break; + case ReportPeriod.year: + text = 'Jahr'; + break; + case ReportPeriod.custom: + text = 'Benutzerdefiniert'; + break; + } + return DropdownMenuItem( + value: period, + child: PlatformText(text), + ); + }), + ], + onChanged: (ReportPeriod? newValue) { + if (newValue != null && newValue != _selectedPeriod) { + setState(() { + _selectedPeriod = newValue; + if (_selectedPeriod != ReportPeriod.custom) { + _generateReport(); + } else { + // Optional: Direkt den Picker öffnen, wenn "Custom" gewählt wird? + // WidgetsBinding.instance.addPostFrameCallback((_) => _selectDateRange()); + } + }); + } + }, + ), + // TODO: Cupertino Dropdown Alternative (komplexer, oft wird Button+Picker genutzt) + // Fürs Erste verwenden wir auch auf iOS das Material Dropdown + cupertino: + (_, __) => CupertinoButton( + padding: EdgeInsets.zero, + child: Text(_selectedPeriod.toString().split('.').last), + onPressed: () { + /* Hier müsste ein Picker geöffnet werden */ + }, + ), ), DropdownButton( @@ -462,23 +558,135 @@ class _ReportScreenState extends State { }).toList(), ), - Chip( - label: PlatformText( - '${formatter.format(start)} - ${formatter.format(end.subtract(const Duration(seconds: 1)))}', + if (_selectedPeriod == ReportPeriod.custom) + PlatformElevatedButton( + onPressed: _selectDateRange, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(FontAwesomeIcons.calendar), // PlatformIcons(context)), + const SizedBox(width: 8), + PlatformText(dateRangeButtonText), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), - ), ], ), ); } - Widget _buildReportView() { + // Widget _buildFilterControls(List tagOptions) { + // final DateFormat formatter = DateFormat('dd.MM.yyyy'); + // final (start, end) = _calculateDateRange(); + + // return Padding( + // padding: const EdgeInsets.all(8.0), + // child: Wrap( + // spacing: 8.0, + // runSpacing: 4.0, + // alignment: WrapAlignment.center, + // children: [ + // DropdownButton( + // value: _selectedPeriod, + // onChanged: (ReportPeriod? newValue) { + // if (newValue != null) { + // setState(() { + // _selectedPeriod = newValue; + // }); + // _generateReport(); + // } + // }, + // items: + // ReportPeriod.values.map((ReportPeriod period) { + // return DropdownMenuItem( + // value: period, + // child: PlatformText( + // period.toString().split('.').last.toUpperCase(), + // ), + // ); + // }).toList(), + // ), + + // DropdownButton( + // value: _selectedTag, + // hint: PlatformText("Alle Tags"), + // onChanged: (Tag? newValue) { + // setState(() { + // _selectedTag = newValue; + // }); + // _generateReport(); + // }, + // items: + // tagOptions.map((Tag? tag) { + // return DropdownMenuItem( + // value: tag, + // child: PlatformText(tag?.name ?? "Alle Tags"), + // ); + // }).toList(), + // ), + + // Chip( + // label: PlatformText( + // '${formatter.format(start)} - ${formatter.format(end.subtract(const Duration(seconds: 1)))}', + // ), + // ), + // ], + // ), + // ); + // } + + Future _selectDateRange() async { + final DateTimeRange initialRange = DateTimeRange( + start: _customStartDate, + end: _customEndDate, + ); + + final DateTimeRange? picked = await showDateRangePicker( + context: context, + locale: const Locale('de', 'DE'), + initialDateRange: initialRange, + firstDate: DateTime(2020), + lastDate: DateTime.now().add(const Duration(days: 365)), + helpText: 'Zeitraum auswählen', + cancelText: 'Abbrechen', + confirmText: 'Ok', + saveText: 'Speichern', + ); + + if (picked != null) { + if (picked.start != _customStartDate || + picked.end != _customEndDate || + _selectedPeriod != ReportPeriod.custom) { + setState(() { + _customStartDate = DateTime( + picked.start.year, + picked.start.month, + picked.start.day, + ); + _customEndDate = DateTime( + picked.end.year, + picked.end.month, + picked.end.day, + ); + _selectedPeriod = ReportPeriod.custom; + }); + _generateReport(); + } + } + } + + Widget _buildReportView(ReportData reportData) { if (_reportData == null) { return Center( child: PlatformText('Keine Reportdaten geladen oder Fehler.'), ); } - final (startDate, endDate) = _calculateDateRange(); + final (startDate, endDate) = _calculateDateRange( + _selectedPeriod == ReportPeriod.custom + ? ReportPeriod.custom + : _selectedPeriod, + ); return ListView( children: [ diff --git a/pubspec.lock b/pubspec.lock index eb05bdd..77a96e6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -123,6 +123,11 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_platform_widgets: dependency: "direct main" description: @@ -174,10 +179,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.19.0" leak_tracker: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b4bb4df..b081108 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter cupertino_icons: ^1.0.8 rust_lib_timetracker: @@ -17,7 +19,8 @@ dependencies: flutter_rust_bridge: 2.9.0 ffi: ^2.1.0 path_provider: ^2.1.1 - intl: ^0.20.2 + # intl: ^0.20.2 + intl: ^0.19.0 provider: ^6.1.1 fl_chart: ^0.70.2 flutter_platform_widgets: ^8.0.0 From e73e178d08b6a5bc00b654a3cb408bc2ab40ed86 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 4 Jun 2025 14:56:28 +0000 Subject: [PATCH 6/7] Add renovate.json --- renovate.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..7190a60 --- /dev/null +++ b/renovate.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json" +} From d41cedb5f05632b645999dd19e9a35d1f01f52a5 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Sun, 22 Jun 2025 13:25:31 +0200 Subject: [PATCH 7/7] deps: update dependencies --- pubspec.lock | 24 ++++++++++++------------ pubspec.yaml | 10 ++++------ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 77a96e6..8f22e82 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: "direct main" description: @@ -179,18 +179,18 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -415,10 +415,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" web: dependency: transitive description: @@ -431,10 +431,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.1.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b081108..e250767 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: timetracker description: "A simple timetracking app" -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 @@ -11,7 +11,7 @@ dependencies: flutter: sdk: flutter flutter_localizations: - sdk: flutter + sdk: flutter cupertino_icons: ^1.0.8 rust_lib_timetracker: @@ -19,8 +19,8 @@ dependencies: flutter_rust_bridge: 2.9.0 ffi: ^2.1.0 path_provider: ^2.1.1 - # intl: ^0.20.2 - intl: ^0.19.0 + intl: ^0.20.2 + # intl: ^0.19.0 provider: ^6.1.1 fl_chart: ^0.70.2 flutter_platform_widgets: ^8.0.0 @@ -36,6 +36,4 @@ dev_dependencies: sdk: flutter flutter: - uses-material-design: true -