diff --git a/lib/presentation_layer/components/bottom_sheet_share.dart b/lib/presentation_layer/components/bottom_sheet_share.dart index f5e475a..f742241 100644 --- a/lib/presentation_layer/components/bottom_sheet_share.dart +++ b/lib/presentation_layer/components/bottom_sheet_share.dart @@ -1,4 +1,4 @@ -import 'dart:ui'; +import 'dart:ui'; import 'package:camelus/config/palette.dart'; import 'package:camelus/helpers/nevent_helper.dart'; @@ -6,89 +6,98 @@ import 'package:camelus/domain_layer/entities/nostr_note.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +/// Copies the given [data] to the system clipboard. Future _copyToClipboard(String data) async { await Clipboard.setData(ClipboardData(text: data)); } -void openBottomSheetShare(context, NostrNote note) { + +void openBottomSheetShare(BuildContext context, NostrNote note) { showModalBottomSheet( - isScrollControlled: false, - elevation: 10, - backgroundColor: Palette.background, - isDismissible: true, - enableDrag: true, - context: context, - builder: (ctx) => BackdropFilter( - filter: ImageFilter.blur(sigmaX: 2, sigmaY: 2), - child: Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Container( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + isScrollControlled: false, // Prevents the bottom sheet from taking the full screen. + elevation: 10, + backgroundColor: Palette.background, // Sets the background color. + isDismissible: true, // Allows the user to dismiss the bottom sheet by tapping outside. + enableDrag: true, // Enables the bottom sheet to be dismissed via drag. + context: context, + builder: (ctx) => BackdropFilter( + filter: ImageFilter.blur(sigmaX: 2, sigmaY: 2), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title of the share sheet. + const Text( + "share post", + style: TextStyle(color: Palette.white, fontSize: 30), + ), + const SizedBox(height: 20), // Adds vertical spacing. + Row( + children: [ + Column( children: [ + IconButton( + tooltip: 'nevent', + onPressed: () { + // Generates the bech32 nevent identifier and copies it to the clipboard. + var bech32nevent = NeventHelper().mapToBech32({ + "eventId": note.id, // Note's event ID. + "authorPubkey": note.pubkey, + "relays": note + .relayHints, + }); + _copyToClipboard(bech32nevent); + }, + icon: const Icon( + Icons.copy, + color: Palette.white, + ), + ), + const Text( - "share post", - style: TextStyle(color: Palette.white, fontSize: 30), + "nevent", + style: TextStyle(color: Palette.lightGray, fontSize: 17), ), - const SizedBox(height: 20), - Row( - children: [ - Column( - children: [ - IconButton( - tooltip: 'nevent', - onPressed: () { - var bech32nevent = - NeventHelper().mapToBech32({ - "eventId": note.id, - "authorPubkey": note.pubkey, - "relays": note - .relayHints, //! todo add relay hints from tracker - }); - _copyToClipboard(bech32nevent); - }, - icon: const Icon( - Icons.copy, - color: Palette.white, - ), - ), - const Text("nevent", - style: TextStyle( - color: Palette.lightGray, fontSize: 17)), - ], - ), - const SizedBox(width: 20), - Column( - children: [ - IconButton( - tooltip: 'njump.me', - onPressed: () { - var bech32nevent = - NeventHelper().mapToBech32({ - "eventId": note.id, - "authorPubkey": note.pubkey, - "relays": note.relayHints, - }); - _copyToClipboard( - 'https://njump.me/$bech32nevent'); - }, - icon: const Icon( - Icons.link, - color: Palette.white, - ), - ), - const Text("web link", - style: TextStyle( - color: Palette.lightGray, fontSize: 17)), - ], - ) - ], + ], + ), + const SizedBox(width: 20), // Adds horizontal spacing. + + // Column for the "web link" share option. + Column( + children: [ + IconButton( + tooltip: 'njump.me', // Tooltip for the button. + onPressed: () { + // Generates the bech32 nevent identifier and creates a web link. + var bech32nevent = NeventHelper().mapToBech32({ + "eventId": note.id, // Note's event ID. + "authorPubkey": note.pubkey, // Note's author public key. + "relays": note.relayHints, // Relays for the note. + }); + _copyToClipboard('https://njump.me/$bech32nevent'); + }, + icon: const Icon( + Icons.link, + color: Palette.white, + ), + ), + const Text( + "web link", + style: TextStyle(color: Palette.lightGray, fontSize: 17), ), - const SizedBox(height: 20), ], ), - )), - )); + ], + ), + const SizedBox(height: 20), // Adds vertical spacing. + ], + ), + ), + ), + ), + ); } diff --git a/lib/presentation_layer/components/full_screen_loading.dart b/lib/presentation_layer/components/full_screen_loading.dart index 7046d7c..6be633c 100644 --- a/lib/presentation_layer/components/full_screen_loading.dart +++ b/lib/presentation_layer/components/full_screen_loading.dart @@ -1,12 +1,12 @@ -import 'dart:math'; -import 'dart:ui'; -import 'package:flutter/material.dart'; +import 'dart:math'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +// A widget that displays a full-screen loading animation with text and animated blobs. class FullScreenLoading extends StatefulWidget { - final List loadingTexts; - final int numberOfBlobs; - - final void Function(void Function()) updateState; + final List loadingTexts; + final int numberOfBlobs; + final void Function(void Function()) updateState; const FullScreenLoading({ Key? key, @@ -21,15 +21,16 @@ class FullScreenLoading extends StatefulWidget { class _FullScreenLoadingState extends State with TickerProviderStateMixin { - late AnimationController _blobController; - late AnimationController _textController; - late Animation _textOpacity; - int _currentTextIndex = 0; - late List blobs; + late AnimationController _blobController; // Controls blob animation. + late AnimationController _textController; // Controls text fade-in/out animation. + late Animation _textOpacity; + int _currentTextIndex = 0; + late List blobs; - String? _successMessage; - bool _showSuccessMessage = false; + String? _successMessage; + bool _showSuccessMessage = false; + /// Displays a success message and updates the state. void showSuccessMessage(String message) { widget.updateState(() { _successMessage = message; @@ -40,32 +41,35 @@ class _FullScreenLoadingState extends State @override void initState() { super.initState(); + // Initialize blob animation controller with a 1-minute duration. _blobController = AnimationController( vsync: this, duration: const Duration(minutes: 1), - )..repeat(reverse: true); + )..repeat(reverse: true); // Loops the animation back and forth. + // Initialize text animation controller with a 2-second duration. _textController = AnimationController( vsync: this, duration: const Duration(seconds: 2), ); + // Define text opacity animation for fade-in and fade-out. _textOpacity = Tween(begin: 0, end: 1).animate( CurvedAnimation( parent: _textController, curve: Interval(0, 0.5, curve: Curves.easeIn), - reverseCurve: Interval(0.5, 1, curve: Curves.easeOut), + reverseCurve: Interval(0.5, 1, curve: Curves.easeOut), ), ); + // Change the text or success message when the animation completes or resets. _textController.addStatusListener((status) { if (status == AnimationStatus.completed) { _textController.reverse(); } else if (status == AnimationStatus.dismissed) { setState(() { if (_showSuccessMessage && _successMessage != null) { - _currentTextIndex = - widget.loadingTexts.length; // Use this as a flag + _currentTextIndex = widget.loadingTexts.length; // Show success message. } else { _currentTextIndex = (_currentTextIndex + 1) % widget.loadingTexts.length; @@ -75,13 +79,15 @@ class _FullScreenLoadingState extends State } }); - _textController.forward(); + _textController.forward(); // Start the text animation. + // Generate blobs for the background animation. blobs = List.generate(widget.numberOfBlobs, (_) => Blob()); } @override void dispose() { + // Dispose animation controllers to free resources. _blobController.dispose(); _textController.dispose(); super.dispose(); @@ -90,9 +96,10 @@ class _FullScreenLoadingState extends State @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.transparent, + backgroundColor: Colors.transparent, body: Stack( children: [ + // Animated blobs in the background. AnimatedBuilder( animation: _blobController, builder: (context, child) { @@ -102,16 +109,17 @@ class _FullScreenLoadingState extends State ); }, ), + // Centered text with fade animation. Center( child: AnimatedBuilder( animation: _textController, builder: (context, child) { return Opacity( - opacity: _textOpacity.value, + opacity: _textOpacity.value, child: Text( _showSuccessMessage && _successMessage != null - ? _successMessage! - : widget.loadingTexts[_currentTextIndex], + ? _successMessage! // Display success message. + : widget.loadingTexts[_currentTextIndex], // Display loading text. style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, @@ -129,31 +137,34 @@ class _FullScreenLoadingState extends State } } +// Custom painter for rendering animated blobs. class BlobPainter extends CustomPainter { - final double animationValue; - final List blobs; + final double animationValue; // Animation progress value (0 to 1). + final List blobs; // List of blobs to paint. BlobPainter(this.animationValue, this.blobs); @override void paint(Canvas canvas, Size size) { + // Paint each blob on the canvas. for (var blob in blobs) { - blob.update(animationValue, size); - _drawBlob(canvas, blob); + blob.update(animationValue, size); // Update blob properties based on animation. + _drawBlob(canvas, blob); // Draw the blob. } } + // Draws an individual blob on the canvas. void _drawBlob(Canvas canvas, Blob blob) { final paint = Paint() ..shader = RadialGradient( colors: [ - blob.color.withOpacity(0.85), + blob.color.withOpacity(0.85), blob.color.withOpacity(0.0), ], - stops: const [0.0, 1.0], + stops: const [0.0, 1.0], ).createShader( Rect.fromCircle(center: blob.position, radius: blob.radius)) - ..blendMode = BlendMode.xor; + ..blendMode = BlendMode.xor; // Blend mode for rendering blobs. canvas.drawCircle(blob.position, blob.radius, paint); } @@ -162,39 +173,41 @@ class BlobPainter extends CustomPainter { bool shouldRepaint(covariant BlobPainter oldDelegate) => true; } +// Represents an animated blob. class Blob { - late Offset position; - late double radius; - late Color color; - late Offset startPosition; - late Offset endPosition; - late double startRadius; - late double endRadius; + late Offset position; // Current position of the blob. + late double radius; // Current radius of the blob. + late Color color; // Color of the blob. + late Offset startPosition; // Start position for animation. + late Offset endPosition; // End position for animation. + late double startRadius; // Start radius for animation. + late double endRadius; // End radius for animation. - final random = Random(); + final random = Random(); // Random number generator. Blob() { - _initializeProperties(); + _initializeProperties(); // Initialize blob properties. } + // Initializes the blob's random properties. void _initializeProperties() { startPosition = Offset(random.nextDouble(), random.nextDouble()); endPosition = Offset(random.nextDouble(), random.nextDouble()); position = startPosition; - startRadius = 50 + random.nextDouble() * 400; - endRadius = 60 + random.nextDouble() * 400; + startRadius = 50 + random.nextDouble() * 400; // Random start radius. + endRadius = 60 + random.nextDouble() * 400; // Random end radius. radius = startRadius; color = Color.fromRGBO( - random.nextInt(100) + 100, - random.nextInt(100) + 100, - 255, - random.nextDouble(), + random.nextInt(100) + 100, + random.nextInt(100) + 100, + 255, // Blue component. + random.nextDouble(), ); - //color = Palette.white; } + // Updates the blob's position and radius based on animation progress. void update(double animationValue, Size size) { - const speed = 4; + const speed = 4; // Speed multiplier for blob movement. position = Offset( lerpDouble(startPosition.dx, endPosition.dx, animationValue * speed)! * size.width, diff --git a/lib/presentation_layer/components/generic_feed.dart b/lib/presentation_layer/components/generic_feed.dart index 97cf519..90a9186 100644 --- a/lib/presentation_layer/components/generic_feed.dart +++ b/lib/presentation_layer/components/generic_feed.dart @@ -1,5 +1,5 @@ -import 'dart:async'; +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -13,9 +13,12 @@ import 'note_card/no_more_notes.dart'; import 'note_card/note_card_container.dart'; import 'note_card/skeleton_note.dart'; +// Main widget for displaying a generic feed class GenericFeed extends ConsumerStatefulWidget { + // The feed filter determines the scope of the feed final FeedFilter feedFilter; + // Optional custom header and configuration for floating headers final List Function(BuildContext, bool)? customHeaderSliverBuilder; final bool floatHeaderSlivers; @@ -30,10 +33,12 @@ class GenericFeed extends ConsumerStatefulWidget { ConsumerState createState() => _GenericFeedState(); } +// State class for GenericFeed, which manages its lifecycle and behavior class _GenericFeedState extends ConsumerState { - late ScrollController _scrollController; - late StreamSubscription _homeBarSub; + late ScrollController _scrollController; // Controller for scrolling behavior + late StreamSubscription _homeBarSub; // Subscription to home tab events + // Scroll to the top of the feed _scrollToTop() { _scrollController.animateTo( 0, @@ -47,10 +52,12 @@ class _GenericFeedState extends ConsumerState { super.initState(); _scrollController = ScrollController(); + // Initialize providers for navigation and feed state final navBarP = ref.read(navigationBarProvider); final genericFeedStateNotifier = ref.read(genericFeedStateProvider(widget.feedFilter).notifier); + // Listen to home tab events and refresh the feed _homeBarSub = navBarP.onTabHome.listen((_) { genericFeedStateNotifier.integrateNewNotes(); _scrollToTop(); @@ -59,6 +66,7 @@ class _GenericFeedState extends ConsumerState { @override void dispose() { + // Dispose of resources to prevent memory leaks _scrollController.dispose(); _homeBarSub.cancel(); super.dispose(); @@ -66,13 +74,14 @@ class _GenericFeedState extends ConsumerState { @override Widget build(BuildContext context) { + // Watch the state of the generic feed and its notifier final genericFeedStateP = ref.watch(genericFeedStateProvider(widget.feedFilter)); final genericFeedStateNotifier = ref.watch(genericFeedStateProvider(widget.feedFilter).notifier); return DefaultTabController( - length: 2, + length: 2, // Two tabs for Posts and Posts with Replies child: NestedScrollView( floatHeaderSlivers: widget.floatHeaderSlivers, controller: _scrollController, @@ -100,6 +109,7 @@ class _GenericFeedState extends ConsumerState { }, body: TabBarView( children: [ + // Tab 1: Display posts Stack( children: [ RefreshIndicatorNoNeed( @@ -118,6 +128,7 @@ class _GenericFeedState extends ConsumerState { ), ], ), + // Tab 2: Display posts with replies Stack( children: [ RefreshIndicatorNoNeed( @@ -150,8 +161,10 @@ class _GenericFeedState extends ConsumerState { } } +// Widget for rendering a scrollable list of posts class ScrollablePostsList extends ConsumerWidget { final FeedFilter feedFilter; + const ScrollablePostsList({ super.key, required this.feedFilter, @@ -185,8 +198,10 @@ class ScrollablePostsList extends ConsumerWidget { } } +// Widget for rendering a scrollable list of posts with replies class ScrollablePostsAndRepliesList extends ConsumerWidget { final FeedFilter feedFilter; + const ScrollablePostsAndRepliesList({ super.key, required this.feedFilter, @@ -222,6 +237,7 @@ class ScrollablePostsAndRepliesList extends ConsumerWidget { } } +// Common scrollable list builder for posts and replies class _BuildScrollablePostsList extends StatelessWidget { final Widget Function(BuildContext, int) itemBuilder; final int itemCount; diff --git a/lib/presentation_layer/components/images_gallery.dart b/lib/presentation_layer/components/images_gallery.dart index 9450399..1ea5c2d 100644 --- a/lib/presentation_layer/components/images_gallery.dart +++ b/lib/presentation_layer/components/images_gallery.dart @@ -3,11 +3,19 @@ import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; import 'package:flutter/services.dart'; +/// A widget for displaying an image gallery with swipeable fullscreen images. class ImageGallery extends StatefulWidget { final List imageUrls; + /// Index of the image to show by default when the gallery opens. final int defaultImageIndex; + + /// Title displayed on the top bar of the gallery. final String topBarTitle; + + /// Optional hero animation tag for image transitions. final String? heroTag; + + /// Optional widget to display in the bottom bar. final Widget? bottomBarWidget; const ImageGallery({ @@ -23,27 +31,24 @@ class ImageGallery extends StatefulWidget { _ImageGalleryState createState() => _ImageGalleryState(); } +/// State class for [ImageGallery]. +/// Manages the behavior of the gallery, including swiping between images +/// and showing/hiding the status bar. class _ImageGalleryState extends State { - late PageController _pageController; - bool _hideStatusBarWhileViewing = false; - late int _currentPageIndex; + late PageController _pageController; + bool _hideStatusBarWhileViewing = false; // Tracks if the status bar is hidden. + late int _currentPageIndex; // Tracks the currently displayed image index. + /// Shows or hides the status bar based on the [show] parameter. void _showHideStatusBar(bool show) { if (show) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - - // SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - // statusBarColor: Palette.background.withOpacity(0.8), - // statusBarIconBrightness: Brightness.light, - // statusBarBrightness: Brightness.light, - // systemNavigationBarColor: Colors.transparent, - // systemNavigationBarIconBrightness: Brightness.light, - // )); } else { SystemChrome.setEnabledSystemUIMode(SystemUiMode.leanBack); } } + /// Resets the status bar visibility to default when the widget is disposed. void _resetStatusBar() { if (!_hideStatusBarWhileViewing) return; SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, @@ -53,10 +58,11 @@ class _ImageGalleryState extends State { @override void initState() { super.initState(); - _showHideStatusBar(true); + _showHideStatusBar(true); // Ensure status bar is visible initially. _currentPageIndex = widget.defaultImageIndex; _pageController = PageController(initialPage: widget.defaultImageIndex); - // notify on page change + + // Update the current page index whenever the page changes. _pageController.addListener(() { setState(() { _currentPageIndex = _pageController.page!.round(); @@ -66,18 +72,19 @@ class _ImageGalleryState extends State { @override void dispose() { - _resetStatusBar(); - _pageController.dispose(); + _resetStatusBar(); // Reset the status bar when the widget is disposed. + _pageController.dispose(); // Dispose of the PageController. super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( - extendBody: true, + extendBody: true, // Allow the body to extend behind the app bar. extendBodyBehindAppBar: true, backgroundColor: Colors.transparent, body: GestureDetector( + // Toggle the visibility of the status bar when the screen is tapped. onTap: () { setState(() { _showHideStatusBar(_hideStatusBarWhileViewing); @@ -86,22 +93,23 @@ class _ImageGalleryState extends State { }, child: Stack( children: [ - _imageGallery(), + _imageGallery(), // The main image gallery. SafeArea( child: AnimatedOpacity( - opacity: _hideStatusBarWhileViewing ? 0 : 1, + opacity: _hideStatusBarWhileViewing ? 0 : 1, // Hide UI if status bar is hidden. duration: const Duration(milliseconds: 100), curve: Curves.easeInOut, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _topBar(context), + _topBar(context), // Top bar with navigation and title. Center( child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - child: widget.bottomBarWidget), - ) + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + child: widget.bottomBarWidget, // Optional bottom bar widget. + ), + ), ], ), ), @@ -112,12 +120,14 @@ class _ImageGalleryState extends State { ); } + /// Builds the top bar with a close button, title and image count. Container _topBar(BuildContext context) { return Container( width: MediaQuery.of(context).size.width, - color: Palette.background.withOpacity(0.25), + color: Palette.background.withOpacity(0.25), // Transparent background color. child: Row( children: [ + // Close button to exit the gallery. IconButton( icon: const Icon(Icons.close, size: 30), color: Colors.white, @@ -125,6 +135,7 @@ class _ImageGalleryState extends State { Navigator.of(context).pop(); }, ), + // Title of the gallery. Text( widget.topBarTitle, style: const TextStyle( @@ -134,7 +145,7 @@ class _ImageGalleryState extends State { ), ), const Spacer(), - // image count + // Current image index out of total images. if (widget.imageUrls.length > 1) Padding( padding: const EdgeInsets.only(right: 10), @@ -152,24 +163,22 @@ class _ImageGalleryState extends State { ); } + /// Builds the image gallery as a swipeable PageView. PageView _imageGallery() { return PageView.builder( controller: _pageController, - itemCount: widget.imageUrls.length, + itemCount: widget.imageUrls.length, // Total number of images. itemBuilder: (context, index) { return PhotoView( - imageProvider: NetworkImage(widget.imageUrls[index]), + imageProvider: NetworkImage(widget.imageUrls[index]), // Display image from network. heroAttributes: widget.heroTag != null ? PhotoViewHeroAttributes( tag: 'image-${widget.imageUrls[widget.defaultImageIndex]}-${widget.heroTag}') - : null, - minScale: PhotoViewComputedScale.contained * 1, - maxScale: PhotoViewComputedScale.covered * 2, - //enablePanAlways: true, - disableGestures: false, - filterQuality: FilterQuality.high, - wantKeepAlive: false, + : null, // Optional hero animation. + minScale: PhotoViewComputedScale.contained * 1, // Minimum zoom scale. + maxScale: PhotoViewComputedScale.covered * 2, // Maximum zoom scale. + filterQuality: FilterQuality.high, // High quality image rendering. ); }, );