Add responsive scaling to main screens and primary media controls

codemagic-setup
Hank Grabowski 2023-04-13 09:30:09 -05:00
rodzic 76da2ca390
commit af1f513ed5
18 zmienionych plików z 310 dodań i 208 usunięć

Wyświetl plik

@ -22,14 +22,14 @@ final _useMediaKit = Platform.isIOS ||
class AVControl extends StatefulWidget {
final String videoUrl;
final String description;
final double width;
final double height;
final double? width;
final double? height;
const AVControl({
super.key,
required this.videoUrl,
required this.width,
required this.height,
this.width,
this.height,
required this.description,
});

Wyświetl plik

@ -5,8 +5,8 @@ import 'package:media_kit_video/media_kit_video.dart';
class MediaKitAvControl extends StatefulWidget {
final String videoUrl;
final double width;
final double height;
final double? width;
final double? height;
const MediaKitAvControl({
super.key,
@ -70,9 +70,9 @@ class _MediaKitAvControlState extends State<MediaKitAvControl> {
setState(() {});
}
double get height => widget.height - 50;
double? get height => widget.height == null ? null : widget.height! - 50;
double get width => widget.width;
double? get width => widget.width;
@override
Widget build(BuildContext context) {
@ -88,10 +88,12 @@ class _MediaKitAvControlState extends State<MediaKitAvControl> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Video(
controller: controller,
width: width,
height: height,
Expanded(
child: Video(
controller: controller,
width: width,
height: height,
),
),
Row(
children: [

Wyświetl plik

@ -5,8 +5,8 @@ import 'package:video_player/video_player.dart';
class VideoPlayerLibAvControl extends StatefulWidget {
final String videoUrl;
final double width;
final double height;
final double? width;
final double? height;
const VideoPlayerLibAvControl({
super.key,
@ -56,14 +56,16 @@ class _VideoPlayerLibAvControlState extends State<VideoPlayerLibAvControl> {
@override
Widget build(BuildContext context) {
final size = videoPlayerController.value.size;
late final double videoWidth;
late final double videoHeight;
double? videoWidth;
double? videoHeight;
if (needsToLoad) {
videoWidth = widget.width;
videoHeight = widget.height;
} else {
final horizontalScale = widget.width / size.width;
final verticalScale = widget.height / size.height;
final horizontalScale =
widget.width == null ? 1 : widget.width! / size.width;
final verticalScale =
widget.height == null ? 1 : widget.height! / size.height;
final scaling = min(horizontalScale, verticalScale);
videoWidth = scaling * size.width;
videoHeight = scaling * size.height;

Wyświetl plik

@ -9,11 +9,15 @@ import 'image_control.dart';
class MediaAttachmentViewerControl extends StatefulWidget {
final List<MediaAttachment> attachments;
final int index;
final double? width;
final double? height;
const MediaAttachmentViewerControl({
super.key,
required this.attachments,
required this.index,
this.width,
this.height,
});
@override
@ -26,13 +30,11 @@ class _MediaAttachmentViewerControlState
@override
Widget build(BuildContext context) {
final item = widget.attachments[widget.index];
const width = 250.0;
const height = 250.0;
if (item.explicitType == AttachmentMediaType.video) {
return AVControl(
videoUrl: item.uri.toString(),
width: width,
height: height,
width: widget.width,
height: widget.height,
description: item.description,
);
}
@ -41,8 +43,8 @@ class _MediaAttachmentViewerControlState
}
return ImageControl(
width: width,
height: height,
width: widget.width,
height: widget.height,
imageUrl: item.thumbnailUri.toString(),
altText: item.description,
onTap: () async {

Wyświetl plik

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import '../globals.dart';
class ResponsiveMaxWidth extends StatelessWidget {
final Widget child;
const ResponsiveMaxWidth({super.key, required this.child});
@override
Widget build(BuildContext context) {
final border =
BorderSide(color: Theme.of(context).highlightColor, width: 1);
return Center(
child: ConstrainedBox(
constraints: BoxConstraints.loose(
const Size.fromWidth(maxViewPortalWidth),
),
child: Container(
decoration:
BoxDecoration(border: Border(left: border, right: border)),
child: child),
),
);
}
}

Wyświetl plik

@ -10,6 +10,7 @@ import '../../services/timeline_manager.dart';
import '../../utils/active_profile_selector.dart';
import '../../utils/clipboard_utils.dart';
import '../../utils/html_to_edit_text_helper.dart';
import '../../utils/responsive_sizes_calculator.dart';
import '../../utils/url_opening_utils.dart';
import '../media_attachment_viewer_control.dart';
import '../padding.dart';
@ -156,13 +157,16 @@ class _StatusControlState extends State<FlattenedTreeEntryControl> {
return const SizedBox();
}
return SizedBox(
height: 300.0,
height: ResponsiveSizesCalculator(context).maxThumbnailHeight,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return MediaAttachmentViewerControl(
attachments: items,
index: index,
width: items.length > 1
? ResponsiveSizesCalculator(context).maxThumbnailWidth
: ResponsiveSizesCalculator(context).viewPortalWidth,
);
},
separatorBuilder: (context, index) {

Wyświetl plik

@ -17,6 +17,9 @@ final useImagePicker = kIsWeb || Platform.isAndroid || Platform.isIOS;
const usePhpDebugging = true;
const maxViewPortalHeight = 750.0;
const maxViewPortalWidth = 750.0;
Future<bool?> showConfirmDialog(BuildContext context, String caption) {
return showDialog<bool>(
context: context,

Wyświetl plik

@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import '../controls/app_bottom_nav_bar.dart';
import '../controls/current_profile_button.dart';
import '../controls/linear_status_indicator.dart';
import '../controls/responsive_max_width.dart';
import '../controls/standard_app_drawer.dart';
import '../globals.dart';
import '../models/connection.dart';
@ -52,21 +53,23 @@ class _ContactsScreenState extends State<ContactsScreen> {
child: Text('No contacts'),
));
} else {
body = ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
final contact = contacts[index];
return ListTile(
onTap: () {
context.pushNamed(ScreenPaths.userProfile,
params: {'id': contact.id});
},
title: Text(contact.name),
trailing: Text(contact.status.label()),
);
},
separatorBuilder: (context, index) => const Divider(),
itemCount: contacts.length);
body = ResponsiveMaxWidth(
child: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
final contact = contacts[index];
return ListTile(
onTap: () {
context.pushNamed(ScreenPaths.userProfile,
params: {'id': contact.id});
},
title: Text(contact.name),
trailing: Text(contact.status.label()),
);
},
separatorBuilder: (context, index) => const Divider(),
itemCount: contacts.length),
);
}
return Scaffold(

Wyświetl plik

@ -7,6 +7,7 @@ import '../controls/app_bottom_nav_bar.dart';
import '../controls/linear_status_indicator.dart';
import '../controls/login_aware_cached_network_image.dart';
import '../controls/padding.dart';
import '../controls/responsive_max_width.dart';
import '../controls/standard_app_drawer.dart';
import '../controls/timeline/timeline_panel.dart';
import '../globals.dart';
@ -152,7 +153,9 @@ class _HomeScreenState extends State<HomeScreen> {
child: Column(
children: [
StandardLinearProgressIndicator(nss.timelineLoadingStatus),
Expanded(child: timeline),
Expanded(
child: ResponsiveMaxWidth(child: timeline),
),
],
),
),

Wyświetl plik

@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:result_monad/result_monad.dart';
import '../controls/image_control.dart';
import '../controls/responsive_max_width.dart';
import '../controls/standard_appbar.dart';
import '../controls/status_and_refresh_button.dart';
import '../globals.dart';
@ -62,36 +63,38 @@ class InteractionsViewerScreen extends StatelessWidget {
)
]),
body: Center(
child: ListView.separated(
itemCount: connections.length,
itemBuilder: (context, index) {
final connection = connections[index];
return ListTile(
onTap: () async {
await getIt<ActiveProfileSelector<ConnectionsManager>>()
.activeEntry
.andThenSuccessAsync((cm) async {
final existingData = cm.getById(connection.id);
if (existingData.isFailure) {
await cm.fullRefresh(connection);
child: ResponsiveMaxWidth(
child: ListView.separated(
itemCount: connections.length,
itemBuilder: (context, index) {
final connection = connections[index];
return ListTile(
onTap: () async {
await getIt<ActiveProfileSelector<ConnectionsManager>>()
.activeEntry
.andThenSuccessAsync((cm) async {
final existingData = cm.getById(connection.id);
if (existingData.isFailure) {
await cm.fullRefresh(connection);
}
});
if (context.mounted) {
context.pushNamed(ScreenPaths.userProfile,
params: {'id': connection.id});
}
});
if (context.mounted) {
context.pushNamed(ScreenPaths.userProfile,
params: {'id': connection.id});
}
},
leading: ImageControl(
imageUrl: connection.avatarUrl.toString(),
iconOverride: const Icon(Icons.person),
width: 32.0,
onTap: () => context.pushNamed(ScreenPaths.userProfile,
params: {'id': connection.id}),
),
title: Text('${connection.name} (${connection.handle})'),
);
},
separatorBuilder: (_, __) => const Divider(),
},
leading: ImageControl(
imageUrl: connection.avatarUrl.toString(),
iconOverride: const Icon(Icons.person),
width: 32.0,
onTap: () => context.pushNamed(ScreenPaths.userProfile,
params: {'id': connection.id}),
),
title: Text('${connection.name} (${connection.handle})'),
);
},
separatorBuilder: (_, __) => const Divider(),
),
),
),
);

Wyświetl plik

@ -4,6 +4,7 @@ import 'package:result_monad/result_monad.dart';
import '../controls/image_control.dart';
import '../controls/padding.dart';
import '../controls/responsive_max_width.dart';
import '../controls/standard_appbar.dart';
import '../globals.dart';
import '../models/direct_message_thread.dart';
@ -59,56 +60,61 @@ class _MessageThreadScreenState extends State<MessageThreadScreen> {
child: Column(
children: [
Expanded(
child: ListView.separated(
itemBuilder: (context, index) {
final m = thread.messages[index];
final textPieces = m.text.split('...\n');
final text =
textPieces.length == 1 ? textPieces[0] : textPieces[1];
final imageUrl = m.senderId == yourId
? yourAvatarUrl
: participants[m.senderId]?.avatarUrl ?? '';
return ListTile(
onTap: m.seen
? null
: () =>
service.markMessageRead(widget.parentThreadId, m),
onLongPress: () async {
await copyToClipboard(context: context, text: m.text);
},
leading: ImageControl(
imageUrl: imageUrl,
iconOverride: const Icon(Icons.person),
width: 32.0,
onTap: null,
),
title: Text(
text,
style: m.seen
child: ResponsiveMaxWidth(
child: ListView.separated(
itemBuilder: (context, index) {
final m = thread.messages[index];
final textPieces = m.text.split('...\n');
final text = textPieces.length == 1
? textPieces[0]
: textPieces[1];
final imageUrl = m.senderId == yourId
? yourAvatarUrl
: participants[m.senderId]?.avatarUrl ?? '';
return ListTile(
onTap: m.seen
? null
: const TextStyle(fontWeight: FontWeight.bold),
),
trailing: Text(DateTime.fromMillisecondsSinceEpoch(
m.createdAt * 1000)
.toString()),
);
},
separatorBuilder: (_, __) => const Divider(),
itemCount: thread.messages.length),
: () => service.markMessageRead(
widget.parentThreadId, m),
onLongPress: () async {
await copyToClipboard(context: context, text: m.text);
},
leading: ImageControl(
imageUrl: imageUrl,
iconOverride: const Icon(Icons.person),
width: 32.0,
onTap: null,
),
title: Text(
text,
style: m.seen
? null
: const TextStyle(fontWeight: FontWeight.bold),
),
trailing: Text(DateTime.fromMillisecondsSinceEpoch(
m.createdAt * 1000)
.toString()),
);
},
separatorBuilder: (_, __) => const Divider(),
itemCount: thread.messages.length),
),
),
const VerticalDivider(),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
controller: textController,
maxLines: 4,
decoration: InputDecoration(
labelText: 'Reply Text',
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.background,
child: ResponsiveMaxWidth(
child: TextFormField(
controller: textController,
maxLines: 4,
decoration: InputDecoration(
labelText: 'Reply Text',
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.background,
),
borderRadius: BorderRadius.circular(5.0),
),
borderRadius: BorderRadius.circular(5.0),
),
),
),

Wyświetl plik

@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../controls/image_control.dart';
import '../controls/responsive_max_width.dart';
import '../controls/standard_appbar.dart';
import '../controls/status_and_refresh_button.dart';
import '../globals.dart';
@ -49,44 +50,47 @@ class MessagesScreen extends StatelessWidget {
t2.messages.last.createdAt.compareTo(t1.messages.last.createdAt));
return threads.isEmpty
? const Text('No Direct Message Threads')
: ListView.separated(
itemCount: threads.length,
itemBuilder: (context, index) {
final thread = threads[index];
final style = thread.allSeen
? null
: const TextStyle(fontWeight: FontWeight.bold);
return ListTile(
onTap: () => context.pushNamed(
ScreenPaths.thread,
queryParams: {'uri': thread.parentUri},
),
leading: ImageControl(
imageUrl: thread.participants.first.avatarUrl.toString(),
iconOverride: const Icon(Icons.person),
width: 32.0,
onTap: null,
),
title: Text(
[
'You',
...thread.participants.map((p) => '${p.name}(${p.handle})')
].join(thread.participants.length == 1 ? ' & ' : ', '),
softWrap: true,
style: style,
),
subtitle: Text(
thread.title,
style: style,
),
trailing: Text(
ElapsedDateUtils.epochSecondsToString(
thread.messages.last.createdAt),
style: style,
),
);
},
separatorBuilder: (_, __) => const Divider(),
: ResponsiveMaxWidth(
child: ListView.separated(
itemCount: threads.length,
itemBuilder: (context, index) {
final thread = threads[index];
final style = thread.allSeen
? null
: const TextStyle(fontWeight: FontWeight.bold);
return ListTile(
onTap: () => context.pushNamed(
ScreenPaths.thread,
queryParams: {'uri': thread.parentUri},
),
leading: ImageControl(
imageUrl: thread.participants.first.avatarUrl.toString(),
iconOverride: const Icon(Icons.person),
width: 32.0,
onTap: null,
),
title: Text(
[
'You',
...thread.participants
.map((p) => '${p.name}(${p.handle})')
].join(thread.participants.length == 1 ? ' & ' : ', '),
softWrap: true,
style: style,
),
subtitle: Text(
thread.title,
style: style,
),
trailing: Text(
ElapsedDateUtils.epochSecondsToString(
thread.messages.last.createdAt),
style: style,
),
);
},
separatorBuilder: (_, __) => const Divider(),
),
);
}
}

Wyświetl plik

@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import '../controls/app_bottom_nav_bar.dart';
import '../controls/notifications_control.dart';
import '../controls/responsive_max_width.dart';
import '../controls/standard_app_drawer.dart';
import '../controls/standard_appbar.dart';
import '../controls/status_and_refresh_button.dart';
@ -47,12 +48,18 @@ class NotificationsScreen extends StatelessWidget {
];
if (notifications.isEmpty) {
title = 'Notifications';
body = Center(
child: Column(
children: const [
Center(child: Text('No notifications')),
],
));
body = RefreshIndicator(
onRefresh: () async {
update(manager);
return;
},
child: Center(
child: Column(
children: const [
Center(child: Text('No notifications')),
],
)),
);
} else {
final unreadCount = notifications.where((e) => !e.dismissed).length;
title =
@ -62,47 +69,49 @@ class NotificationsScreen extends StatelessWidget {
update(manager);
return;
},
child: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
if (index == 0) {
return TextButton(
onPressed: () async {
final result = await manager.loadNewerNotifications();
final noMore = result.fold(
onSuccess: (values) => values.isEmpty,
onError: (_) => true);
if (context.mounted && noMore) {
buildSnackbar(
context, 'No newer notifications to load');
}
},
child: const Text('Load newer notifications'));
}
if (index == notifications.length + 1) {
return TextButton(
onPressed: () async {
final result = await manager.loadOlderNotifications();
final noMore = result.fold(
onSuccess: (values) => values.isEmpty,
onError: (_) => true);
if (context.mounted && noMore) {
buildSnackbar(
context, 'No older notifications to load');
}
},
child: const Text('Load older notifications'));
}
return NotificationControl(
notification: notifications[index - 1]);
},
separatorBuilder: (context, index) {
return const Divider(
color: Colors.black54,
height: 0.0,
);
},
itemCount: notifications.length + 2),
child: ResponsiveMaxWidth(
child: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
if (index == 0) {
return TextButton(
onPressed: () async {
final result = await manager.loadNewerNotifications();
final noMore = result.fold(
onSuccess: (values) => values.isEmpty,
onError: (_) => true);
if (context.mounted && noMore) {
buildSnackbar(
context, 'No newer notifications to load');
}
},
child: const Text('Load newer notifications'));
}
if (index == notifications.length + 1) {
return TextButton(
onPressed: () async {
final result = await manager.loadOlderNotifications();
final noMore = result.fold(
onSuccess: (values) => values.isEmpty,
onError: (_) => true);
if (context.mounted && noMore) {
buildSnackbar(
context, 'No older notifications to load');
}
},
child: const Text('Load older notifications'));
}
return NotificationControl(
notification: notifications[index - 1]);
},
separatorBuilder: (context, index) {
return const Divider(
color: Colors.black54,
height: 0.0,
);
},
itemCount: notifications.length + 2),
),
);
}
}, onError: (error) {

Wyświetl plik

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../controls/linear_status_indicator.dart';
import '../controls/responsive_max_width.dart' show ResponsiveMaxWidth;
import '../controls/standard_appbar.dart';
import '../controls/timeline/post_control.dart';
import '../globals.dart';
@ -65,7 +66,7 @@ class _PostScreenState extends State<PostScreen> {
child: Column(
children: [
StandardLinearProgressIndicator(nss.timelineLoadingStatus),
Expanded(child: body),
Expanded(child: ResponsiveMaxWidth(child: body)),
],
),
));

Wyświetl plik

@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import '../controls/app_bottom_nav_bar.dart';
import '../controls/current_profile_button.dart';
import '../controls/image_control.dart';
import '../controls/responsive_max_width.dart';
import '../controls/search_result_status_control.dart';
import '../controls/standard_app_drawer.dart';
import '../friendica_client/friendica_client.dart';
@ -105,7 +106,7 @@ class _SearchScreenState extends State<SearchScreen> {
),
);
} else {
body = buildResultBody(profile);
body = ResponsiveMaxWidth(child: buildResultBody(profile));
}
return Scaffold(
@ -154,11 +155,11 @@ class _SearchScreenState extends State<SearchScreen> {
),
),
),
ElevatedButton(
IconButton(
onPressed: () {
updateSearchResults(profile);
},
child: const Text('Search'),
icon: const Icon(Icons.search),
),
PopupMenuButton<SearchTypes>(
initialValue: searchType,

Wyświetl plik

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../controls/responsive_max_width.dart';
import '../controls/standard_appbar.dart';
import '../services/setting_service.dart';
import '../utils/theme_mode_extensions.dart';
@ -14,11 +15,13 @@ class SettingsScreen extends StatelessWidget {
body: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
children: [
buildLowBandwidthWidget(settings),
buildThemeWidget(settings),
],
child: ResponsiveMaxWidth(
child: ListView(
children: [
buildLowBandwidthWidget(settings),
buildThemeWidget(settings),
],
),
),
),
));

Wyświetl plik

@ -0,0 +1,24 @@
import 'dart:math';
import 'package:flutter/cupertino.dart';
import '../globals.dart';
class ResponsiveSizesCalculator {
final BuildContext context;
const ResponsiveSizesCalculator(this.context);
double get viewPortalWidth => min(_screenSize.width, maxViewPortalWidth);
double get maxThumbnailHeight =>
min(_screenSize.height * 0.5, maxViewPortalHeight);
double get maxThumbnailWidth => min(
_screenSize.width < 600
? _screenSize.width * 0.5
: _screenSize.width * 0.33,
maxViewPortalHeight);
Size get _screenSize => MediaQuery.of(context).size;
}

Wyświetl plik

@ -32,6 +32,8 @@ PODS:
- FMDB (>= 2.7.5)
- url_launcher_macos (0.0.1):
- FlutterMacOS
- window_to_front (0.0.1):
- FlutterMacOS
DEPENDENCIES:
- desktop_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_window/macos`)
@ -47,6 +49,7 @@ DEPENDENCIES:
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- window_to_front (from `Flutter/ephemeral/.symlinks/plugins/window_to_front/macos`)
SPEC REPOS:
trunk:
@ -80,6 +83,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
window_to_front:
:path: Flutter/ephemeral/.symlinks/plugins/window_to_front/macos
SPEC CHECKSUMS:
desktop_window: fb7c4f12c1129f947ac482296b6f14059d57a3c3
@ -97,6 +102,7 @@ SPEC CHECKSUMS:
shared_preferences_foundation: 986fc17f3d3251412d18b0265f9c64113a8c2472
sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451
window_to_front: 4cdc24ddd8461ad1a55fa06286d6a79d8b29e8d8
PODFILE CHECKSUM: 8d40c19d3cbdb380d870685c3a564c989f1efa52