diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index 2a439bb..bc9d0fb 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -31,7 +31,7 @@ import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector. import 'package:twonly/src/views/camera/share_image_editor.view.dart'; import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; -import 'package:twonly/src/views/components/loader.dart'; +import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/home.view.dart'; import 'package:url_launcher/url_launcher_string.dart'; diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview.layer.dart b/lib/src/views/camera/share_image_editor/layers/link_preview.layer.dart index c34065f..4de15b0 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview.layer.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview.layer.dart @@ -6,7 +6,7 @@ import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/c import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/cards/youtube.card.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; -import 'package:twonly/src/views/components/loader.dart'; +import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart'; class LinkPreviewLayer extends StatefulWidget { const LinkPreviewLayer({ @@ -32,8 +32,9 @@ class _LinkPreviewLayerState extends State { Future initAsync() async { if (widget.layerData.metadata == null) { - widget.layerData.metadata = - await getMetadata(widget.layerData.link.toString()); + widget.layerData.metadata = await getMetadata( + widget.layerData.link.toString(), + ); if (widget.layerData.metadata == null) { widget.layerData.error = true; } diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart index 3f0852c..f2fe80a 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; -import 'package:twonly/src/views/components/loader.dart'; +import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart'; class MastodonPostCard extends StatelessWidget { const MastodonPostCard({required this.info, super.key}); diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index 5501361..1f2a89a 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -9,15 +9,14 @@ import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; -import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart'; import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart'; import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/connection_status_badge.dart'; import 'package:twonly/src/views/components/notification_badge.dart'; class ChatListView extends StatefulWidget { @@ -45,8 +44,9 @@ class _ChatListViewState extends State { final stream = twonlyDB.groupsDao.watchGroupsForChatList(); _contactsSub = stream.listen((groups) { setState(() { - _groupsNotPinned = - groups.where((x) => !x.pinned && !x.archived).toList(); + _groupsNotPinned = groups + .where((x) => !x.pinned && !x.archived) + .toList(); _groupsPinned = groups.where((x) => x.pinned && !x.archived).toList(); _groupsArchived = groups.where((x) => x.archived).toList(); }); @@ -64,8 +64,10 @@ class _ChatListViewState extends State { } final changeLog = await rootBundle.loadString('CHANGELOG.md'); - final changeLogHash = - (await compute(Sha256().hash, changeLog.codeUnits)).bytes; + final changeLogHash = (await compute( + Sha256().hash, + changeLog.codeUnits, + )).bytes; if (!gUser.hideChangeLog && gUser.lastChangeLogHash.toString() != changeLogHash.toString()) { await updateUserdata((u) { @@ -93,22 +95,23 @@ class _ChatListViewState extends State { @override Widget build(BuildContext context) { - final isConnected = context.watch().isConnected; final plan = context.watch().plan; return Scaffold( appBar: AppBar( title: Row( children: [ - GestureDetector( - onTap: () async { - await context.push(Routes.settingsProfile); - if (!mounted) return; - setState(() {}); // gUser has updated - }, - child: AvatarIcon( - myAvatar: true, - fontSize: 14, - color: context.color.onSurface.withAlpha(20), + ConnectionStatusBadge( + child: GestureDetector( + onTap: () async { + await context.push(Routes.settingsProfile); + if (!mounted) return; + setState(() {}); // gUser has updated + }, + child: AvatarIcon( + myAvatar: true, + fontSize: 14, + color: context.color.onSurface.withAlpha(20), + ), ), ), const SizedBox(width: 10), @@ -121,8 +124,10 @@ class _ChatListViewState extends State { color: context.color.primary, borderRadius: BorderRadius.circular(15), ), - padding: - const EdgeInsets.symmetric(horizontal: 5, vertical: 3), + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 3, + ), child: Text( plan.name, style: TextStyle( @@ -163,87 +168,77 @@ class _ChatListViewState extends State { ), ], ), - body: Stack( - children: [ - Positioned( - top: 0, - left: 0, - right: 0, - child: isConnected ? Container() : const ConnectionInfo(), - ), - Positioned.fill( - child: RefreshIndicator( - onRefresh: () async { - await apiService.close(() {}); - await apiService.connect(); - await Future.delayed(const Duration(seconds: 1)); - }, - child: (_groupsNotPinned.isEmpty && - _groupsPinned.isEmpty && - _groupsArchived.isEmpty) - ? Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: OutlinedButton.icon( - icon: const Icon(Icons.person_add), - onPressed: () => context.push(Routes.chatsAddNewUser), - label: Text( - context.lang.chatListViewSearchUserNameBtn, - ), - ), - ), - ) - : ListView.builder( - itemCount: _groupsPinned.length + - (_groupsPinned.isNotEmpty ? 1 : 0) + - _groupsNotPinned.length + - (_groupsArchived.isNotEmpty ? 1 : 0), - itemBuilder: (context, index) { - if (index >= - _groupsNotPinned.length + - _groupsPinned.length + - (_groupsPinned.isNotEmpty ? 1 : 0)) { - if (_groupsArchived.isEmpty) return Container(); - return ListTile( - title: Text( - '${context.lang.archivedChats} (${_groupsArchived.length})', - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 13), - ), - onTap: () => context.push(Routes.chatsArchived), - ); - } - // Check if the index is for the pinned users - if (index < _groupsPinned.length) { - final group = _groupsPinned[index]; - return GroupListItem( - key: ValueKey(group.groupId), - group: group, - ); - } - - // If there are pinned users, account for the Divider - var adjustedIndex = index - _groupsPinned.length; - if (_groupsPinned.isNotEmpty && adjustedIndex == 0) { - return const Divider(); - } - - // Adjust the index for the contacts list - adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0); - - // Get the contacts that are not pinned - final group = _groupsNotPinned.elementAt( - adjustedIndex, - ); - return GroupListItem( - key: ValueKey(group.groupId), - group: group, - ); - }, + body: RefreshIndicator( + onRefresh: () async { + await apiService.close(() {}); + await apiService.connect(); + await Future.delayed(const Duration(seconds: 1)); + }, + child: + (_groupsNotPinned.isEmpty && + _groupsPinned.isEmpty && + _groupsArchived.isEmpty) + ? Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: OutlinedButton.icon( + icon: const Icon(Icons.person_add), + onPressed: () => context.push(Routes.chatsAddNewUser), + label: Text( + context.lang.chatListViewSearchUserNameBtn, ), - ), - ), - ], + ), + ), + ) + : ListView.builder( + itemCount: + _groupsPinned.length + + (_groupsPinned.isNotEmpty ? 1 : 0) + + _groupsNotPinned.length + + (_groupsArchived.isNotEmpty ? 1 : 0), + itemBuilder: (context, index) { + if (index >= + _groupsNotPinned.length + + _groupsPinned.length + + (_groupsPinned.isNotEmpty ? 1 : 0)) { + if (_groupsArchived.isEmpty) return Container(); + return ListTile( + title: Text( + '${context.lang.archivedChats} (${_groupsArchived.length})', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 13), + ), + onTap: () => context.push(Routes.chatsArchived), + ); + } + // Check if the index is for the pinned users + if (index < _groupsPinned.length) { + final group = _groupsPinned[index]; + return GroupListItem( + key: ValueKey(group.groupId), + group: group, + ); + } + + // If there are pinned users, account for the Divider + var adjustedIndex = index - _groupsPinned.length; + if (_groupsPinned.isNotEmpty && adjustedIndex == 0) { + return const Divider(); + } + + // Adjust the index for the contacts list + adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0); + + // Get the contacts that are not pinned + final group = _groupsNotPinned.elementAt( + adjustedIndex, + ); + return GroupListItem( + key: ValueKey(group.groupId), + group: group, + ); + }, + ), ), floatingActionButton: Padding( padding: const EdgeInsets.only(bottom: 30), diff --git a/lib/src/views/chats/chat_list_components/connection_info.comp.dart b/lib/src/views/chats/chat_list_components/connection_info.comp.dart deleted file mode 100644 index aa19500..0000000 --- a/lib/src/views/chats/chat_list_components/connection_info.comp.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:twonly/src/utils/misc.dart'; - -class ConnectionInfo extends StatefulWidget { - const ConnectionInfo({super.key}); - - @override - State createState() => _ConnectionInfoWidgetState(); -} - -class _ConnectionInfoWidgetState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _positionAnim; - late Animation _widthAnim; - - bool showAnimation = false; - - final double minBarWidth = 40; - final double maxBarWidth = 150; - - @override - void initState() { - super.initState(); - - _controller = AnimationController( - vsync: this, - duration: const Duration(seconds: 4), - ); - - _positionAnim = Tween(begin: 0, end: 1).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeInOut), - ); - - _widthAnim = TweenSequence([ - TweenSequenceItem( - tween: Tween(begin: minBarWidth, end: maxBarWidth), - weight: 50, - ), - TweenSequenceItem( - tween: Tween(begin: maxBarWidth, end: minBarWidth), - weight: 50, - ), - ]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); - - // Delay start by 2 seconds - Future.delayed(const Duration(seconds: 2), () { - if (mounted) { - unawaited(_controller.repeat(reverse: true)); - setState(() { - showAnimation = true; - }); - } - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (!showAnimation) return Container(); - final screenWidth = MediaQuery.of(context).size.width; - - return SizedBox( - width: screenWidth, - height: 1, - child: AnimatedBuilder( - animation: _controller, - builder: (context, child) { - final barWidth = _widthAnim.value; - final left = _positionAnim.value * (screenWidth - barWidth); - return Stack( - children: [ - Positioned( - left: left, - top: 0, - bottom: 0, - child: Container( - width: barWidth, - decoration: BoxDecoration( - color: context.color.primary, - borderRadius: BorderRadius.circular(4), - ), - ), - ), - ], - ); - }, - ), - ); - } -} diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index fec0a32..f942f4b 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -27,7 +27,7 @@ import 'package:twonly/src/views/camera/camera_send_to.view.dart'; import 'package:twonly/src/views/chats/media_viewer_components/additional_message_content.dart'; import 'package:twonly/src/views/chats/media_viewer_components/reaction_buttons.component.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; -import 'package:twonly/src/views/components/loader.dart'; +import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:video_player/video_player.dart'; diff --git a/lib/src/views/components/connection_status_badge.dart b/lib/src/views/components/connection_status_badge.dart new file mode 100644 index 0000000..dba83d1 --- /dev/null +++ b/lib/src/views/components/connection_status_badge.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:twonly/src/providers/connection.provider.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/loader/ripple.loader.dart'; + +class ConnectionStatusBadge extends StatelessWidget { + const ConnectionStatusBadge({ + required this.child, + super.key, + }); + final Widget child; + + @override + Widget build(BuildContext context) { + final isConnected = context.watch().isConnected; + return Stack( + children: [ + if (!isConnected) + const Positioned.fill( + child: SpinKitRipple( + color: Colors.red, + ), + ), + Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isConnected + ? context.color.primary.withAlpha(100) + : Colors.red, + ), + ), + padding: const EdgeInsets.all(0.5), + child: Center( + child: child, + ), + ), + ], + ); + } +} diff --git a/lib/src/views/components/loader/ripple.loader.dart b/lib/src/views/components/loader/ripple.loader.dart new file mode 100644 index 0000000..44709f7 --- /dev/null +++ b/lib/src/views/components/loader/ripple.loader.dart @@ -0,0 +1,114 @@ +// FROM: https://github.com/jogboms/flutter_spinkit/blob/master/lib/src/ripple.dart + +// ignore_for_file: prefer_int_literals + +import 'package:flutter/material.dart'; + +class SpinKitRipple extends StatefulWidget { + const SpinKitRipple({ + super.key, + this.color, + this.size = 50.0, + this.borderWidth = 6.0, + this.itemBuilder, + this.duration = const Duration(milliseconds: 1800), + this.controller, + }) : assert( + !(itemBuilder is IndexedWidgetBuilder && color is Color) && + !(itemBuilder == null && color == null), + 'You should specify either a itemBuilder or a color', + ); + + final Color? color; + final double size; + final double borderWidth; + final IndexedWidgetBuilder? itemBuilder; + final Duration duration; + final AnimationController? controller; + + @override + State createState() => _SpinKitRippleState(); +} + +class _SpinKitRippleState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation1; + late Animation _animation2; + + @override + void initState() { + super.initState(); + + _controller = + (widget.controller ?? + AnimationController(vsync: this, duration: widget.duration)) + ..addListener(() { + if (mounted) { + setState(() {}); + } + }) + ..repeat(); + _animation1 = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.75), + ), + ); + _animation2 = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.25, 1), + ), + ); + } + + @override + void dispose() { + if (widget.controller == null) { + _controller.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Center( + child: Stack( + children: [ + Opacity( + opacity: 1.0 - _animation1.value, + child: Transform.scale( + scale: _animation1.value, + child: _itemBuilder(0), + ), + ), + Opacity( + opacity: 1.0 - _animation2.value, + child: Transform.scale( + scale: _animation2.value, + child: _itemBuilder(1), + ), + ), + ], + ), + ); + } + + Widget _itemBuilder(int index) { + return SizedBox.fromSize( + size: Size.square(widget.size), + child: widget.itemBuilder != null + ? widget.itemBuilder!(context, index) + : DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: widget.color!, + width: widget.borderWidth, + ), + ), + ), + ); + } +} diff --git a/lib/src/views/components/loader.dart b/lib/src/views/components/loader/three_rotating_dots.loader.dart similarity index 100% rename from lib/src/views/components/loader.dart rename to lib/src/views/components/loader/three_rotating_dots.loader.dart diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index 3a43fd0..350e695 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -10,7 +10,7 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/components/loader.dart'; +import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart'; import 'package:twonly/src/views/memories/memories_item_thumbnail.dart'; import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; @@ -43,8 +43,8 @@ class MemoriesViewState extends State { } Future initAsync() async { - final nonHashedFiles = - await twonlyDB.mediaFilesDao.getAllNonHashedStoredMediaFiles(); + final nonHashedFiles = await twonlyDB.mediaFilesDao + .getAllNonHashedStoredMediaFiles(); if (nonHashedFiles.isNotEmpty) { setState(() { _filesToMigrate = nonHashedFiles.length; @@ -100,8 +100,9 @@ class MemoriesViewState extends State { ), ); for (var i = 0; i < galleryItems.length; i++) { - final month = DateFormat('MMMM yyyy') - .format(galleryItems[i].mediaService.mediaFile.createdAt); + final month = DateFormat( + 'MMMM yyyy', + ).format(galleryItems[i].mediaService.mediaFile.createdAt); if (lastMonth != month) { lastMonth = month; months.add(month); @@ -259,15 +260,16 @@ class MemoriesViewState extends State { int index, ) async { await Navigator.push( - context, - PageRouteBuilder( - opaque: false, - pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView( - galleryItems: galleryItems, - initialIndex: index, - ), - ), - ) as bool?; + context, + PageRouteBuilder( + opaque: false, + pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView( + galleryItems: galleryItems, + initialIndex: index, + ), + ), + ) + as bool?; if (mounted) setState(() {}); } } diff --git a/lib/src/views/settings/help/diagnostics.view.dart b/lib/src/views/settings/help/diagnostics.view.dart index faa5bc0..d8b95a3 100644 --- a/lib/src/views/settings/help/diagnostics.view.dart +++ b/lib/src/views/settings/help/diagnostics.view.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/components/loader.dart'; +import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart'; class DiagnosticsView extends StatefulWidget { const DiagnosticsView({super.key}); @@ -98,8 +98,11 @@ class _LogViewerWidgetState extends State { @override void initState() { super.initState(); - _entries = - widget.logLines.split('\n').reversed.map(_LogEntry.parse).toList(); + _entries = widget.logLines + .split('\n') + .reversed + .map(_LogEntry.parse) + .toList(); } void _setFilter(String level) => setState(() => _filterLevel = level); @@ -187,8 +190,9 @@ class _LogViewerWidgetState extends State { child: Row( children: [ IconButton( - tooltip: - _showTimestamps ? 'Hide timestamps' : 'Show timestamps', + tooltip: _showTimestamps + ? 'Hide timestamps' + : 'Show timestamps', onPressed: _toggleTimestamps, icon: Icon( _showTimestamps