improved websocket connection state info
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-04-09 22:51:36 +02:00
parent 587740f306
commit 527bf51bff
11 changed files with 283 additions and 222 deletions

View file

@ -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';

View file

@ -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<LinkPreviewLayer> {
Future<void> 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;
}

View file

@ -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});

View file

@ -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<ChatListView> {
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<ChatListView> {
}
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,13 +95,13 @@ class _ChatListViewState extends State<ChatListView> {
@override
Widget build(BuildContext context) {
final isConnected = context.watch<CustomChangeProvider>().isConnected;
final plan = context.watch<PurchasesProvider>().plan;
return Scaffold(
appBar: AppBar(
title: Row(
children: [
GestureDetector(
ConnectionStatusBadge(
child: GestureDetector(
onTap: () async {
await context.push(Routes.settingsProfile);
if (!mounted) return;
@ -111,6 +113,7 @@ class _ChatListViewState extends State<ChatListView> {
color: context.color.onSurface.withAlpha(20),
),
),
),
const SizedBox(width: 10),
const Text('twonly '),
if (plan != SubscriptionPlan.Free)
@ -121,8 +124,10 @@ class _ChatListViewState extends State<ChatListView> {
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,22 +168,14 @@ class _ChatListViewState extends State<ChatListView> {
),
],
),
body: Stack(
children: [
Positioned(
top: 0,
left: 0,
right: 0,
child: isConnected ? Container() : const ConnectionInfo(),
),
Positioned.fill(
child: RefreshIndicator(
body: RefreshIndicator(
onRefresh: () async {
await apiService.close(() {});
await apiService.connect();
await Future.delayed(const Duration(seconds: 1));
},
child: (_groupsNotPinned.isEmpty &&
child:
(_groupsNotPinned.isEmpty &&
_groupsPinned.isEmpty &&
_groupsArchived.isEmpty)
? Center(
@ -194,7 +191,8 @@ class _ChatListViewState extends State<ChatListView> {
),
)
: ListView.builder(
itemCount: _groupsPinned.length +
itemCount:
_groupsPinned.length +
(_groupsPinned.isNotEmpty ? 1 : 0) +
_groupsNotPinned.length +
(_groupsArchived.isNotEmpty ? 1 : 0),
@ -242,9 +240,6 @@ class _ChatListViewState extends State<ChatListView> {
},
),
),
),
],
),
floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 30),
child: Column(

View file

@ -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<ConnectionInfo> createState() => _ConnectionInfoWidgetState();
}
class _ConnectionInfoWidgetState extends State<ConnectionInfo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _positionAnim;
late Animation<double> _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<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
_widthAnim = TweenSequence([
TweenSequenceItem(
tween: Tween<double>(begin: minBarWidth, end: maxBarWidth),
weight: 50,
),
TweenSequenceItem(
tween: Tween<double>(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),
),
),
),
],
);
},
),
);
}
}

View file

@ -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';

View file

@ -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<CustomChangeProvider>().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,
),
),
],
);
}
}

View file

@ -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<SpinKitRipple> createState() => _SpinKitRippleState();
}
class _SpinKitRippleState extends State<SpinKitRipple>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation1;
late Animation<double> _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: <Widget>[
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,
),
),
),
);
}
}

View file

@ -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<MemoriesView> {
}
Future<void> 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<MemoriesView> {
),
);
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);
@ -267,7 +268,8 @@ class MemoriesViewState extends State<MemoriesView> {
initialIndex: index,
),
),
) as bool?;
)
as bool?;
if (mounted) setState(() {});
}
}

View file

@ -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<LogViewerWidget> {
@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<LogViewerWidget> {
child: Row(
children: [
IconButton(
tooltip:
_showTimestamps ? 'Hide timestamps' : 'Show timestamps',
tooltip: _showTimestamps
? 'Hide timestamps'
: 'Show timestamps',
onPressed: _toggleTimestamps,
icon: Icon(
_showTimestamps