Compare commits

...

7 Commits

9 changed files with 507 additions and 216 deletions

View File

@@ -1317,5 +1317,7 @@
"fromDate": "From Date", "fromDate": "From Date",
"toDate": "To Date", "toDate": "To Date",
"popularity": "Popularity", "popularity": "Popularity",
"descendingOrder": "Descending Order" "descendingOrder": "Descending Order",
"selectDate": "Select Date",
"pinnedPosts": "Pinned Posts"
} }

View File

@@ -1,4 +1,5 @@
import "dart:async"; import "dart:async";
import "dart:math" as math;
import "package:easy_localization/easy_localization.dart"; import "package:easy_localization/easy_localization.dart";
import "package:file_picker/file_picker.dart"; import "package:file_picker/file_picker.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
@@ -140,6 +141,9 @@ class ChatRoomScreen extends HookConsumerWidget {
final messageController = useTextEditingController(); final messageController = useTextEditingController();
final scrollController = useScrollController(); final scrollController = useScrollController();
// Scroll animation notifiers
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
final messageReplyingTo = useState<SnChatMessage?>(null); final messageReplyingTo = useState<SnChatMessage?>(null);
final messageForwardingTo = useState<SnChatMessage?>(null); final messageForwardingTo = useState<SnChatMessage?>(null);
final messageEditingTo = useState<SnChatMessage?>(null); final messageEditingTo = useState<SnChatMessage?>(null);
@@ -164,6 +168,12 @@ class ChatRoomScreen extends HookConsumerWidget {
isLoading = true; isLoading = true;
messagesNotifier.loadMore().then((_) => isLoading = false); messagesNotifier.loadMore().then((_) => isLoading = false);
} }
// Update gradient animations
final pixels = scrollController.position.pixels;
// Bottom gradient: appears when not at bottom (pixels > 0)
bottomGradientNotifier.value.value = (pixels / 500.0).clamp(0.0, 1.0);
} }
scrollController.addListener(onScroll); scrollController.addListener(onScroll);
@@ -589,7 +599,9 @@ class ChatRoomScreen extends HookConsumerWidget {
listController: listController, listController: listController,
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: 16, top: 16,
bottom: MediaQuery.of(context).padding.bottom + 16, bottom:
MediaQuery.of(context).padding.bottom +
80, // Leave space for chat input
), ),
controller: scrollController, controller: scrollController,
reverse: true, // Show newest messages at the bottom reverse: true, // Show newest messages at the bottom
@@ -828,7 +840,7 @@ class ChatRoomScreen extends HookConsumerWidget {
), ),
body: Stack( body: Stack(
children: [ children: [
// Messages and Input in Column // Messages only in Column
Positioned.fill( Positioned.fill(
child: Column( child: Column(
children: [ children: [
@@ -872,73 +884,6 @@ class ChatRoomScreen extends HookConsumerWidget {
), ),
), ),
), ),
if (!isSelectionMode.value)
chatRoom.when(
data:
(room) => Column(
mainAxisSize: MainAxisSize.min,
children: [
ChatInput(
messageController: messageController,
chatRoom: room!,
onSend: sendMessage,
onClear: () {
if (messageEditingTo.value != null) {
attachments.value.clear();
messageController.clear();
}
messageEditingTo.value = null;
messageReplyingTo.value = null;
messageForwardingTo.value = null;
},
messageEditingTo: messageEditingTo.value,
messageReplyingTo: messageReplyingTo.value,
messageForwardingTo: messageForwardingTo.value,
onPickFile: (bool isPhoto) {
if (isPhoto) {
pickPhotoMedia();
} else {
pickVideoMedia();
}
},
onPickAudio: pickAudioMedia,
onPickGeneralFile: pickGeneralFile,
onLinkAttachment: linkAttachment,
attachments: attachments.value,
onUploadAttachment: uploadAttachment,
onDeleteAttachment: (index) async {
final attachment = attachments.value[index];
if (attachment.isOnCloud &&
!attachment.isLink) {
final client = ref.watch(apiClientProvider);
await client.delete(
'/drive/files/${attachment.data.id}',
);
}
final clone = List.of(attachments.value);
clone.removeAt(index);
attachments.value = clone;
},
onMoveAttachment: (idx, delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(idx + delta, clone.removeAt(idx));
attachments.value = clone;
},
onAttachmentsChanged: (newAttachments) {
attachments.value = newAttachments;
},
attachmentProgress: attachmentProgress.value,
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
error: (_, _) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
),
], ],
), ),
), ),
@@ -977,6 +922,112 @@ class ChatRoomScreen extends HookConsumerWidget {
), ),
), ),
), ),
// Bottom gradient - appears when scrolling towards newer messages (behind chat input)
if (!isSelectionMode.value)
AnimatedBuilder(
animation: bottomGradientNotifier.value,
builder:
(context, child) => Positioned(
left: 0,
right: 0,
bottom: 0,
child: Opacity(
opacity: bottomGradientNotifier.value.value,
child: Container(
height: math.min(
MediaQuery.of(context).size.height * 0.1,
128,
),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.8),
Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.0),
],
),
),
),
),
),
),
// Chat Input positioned above gradient (higher z-index)
if (!isSelectionMode.value)
Positioned(
left: 0,
right: 0,
bottom: 0, // At the very bottom, above gradient
child: chatRoom.when(
data:
(room) => Column(
mainAxisSize: MainAxisSize.min,
children: [
ChatInput(
messageController: messageController,
chatRoom: room!,
onSend: sendMessage,
onClear: () {
if (messageEditingTo.value != null) {
attachments.value.clear();
messageController.clear();
}
messageEditingTo.value = null;
messageReplyingTo.value = null;
messageForwardingTo.value = null;
},
messageEditingTo: messageEditingTo.value,
messageReplyingTo: messageReplyingTo.value,
messageForwardingTo: messageForwardingTo.value,
onPickFile: (bool isPhoto) {
if (isPhoto) {
pickPhotoMedia();
} else {
pickVideoMedia();
}
},
onPickAudio: pickAudioMedia,
onPickGeneralFile: pickGeneralFile,
onLinkAttachment: linkAttachment,
attachments: attachments.value,
onUploadAttachment: uploadAttachment,
onDeleteAttachment: (index) async {
final attachment = attachments.value[index];
if (attachment.isOnCloud && !attachment.isLink) {
final client = ref.watch(apiClientProvider);
await client.delete(
'/drive/files/${attachment.data.id}',
);
}
final clone = List.of(attachments.value);
clone.removeAt(index);
attachments.value = clone;
},
onMoveAttachment: (idx, delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(idx + delta, clone.removeAt(idx));
attachments.value = clone;
},
onAttachmentsChanged: (newAttachments) {
attachments.value = newAttachments;
},
attachmentProgress: attachmentProgress.value,
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
error: (_, _) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
),
),
// Selection mode toolbar // Selection mode toolbar
if (isSelectionMode.value) if (isSelectionMode.value)
Positioned( Positioned(

View File

@@ -201,7 +201,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
); );
}, },
icon: const Icon( icon: const Icon(
Symbols.add_circle, Symbols.remove_circle,
), ),
label: Text('unsubscribe'.tr()), label: Text('unsubscribe'.tr()),
) )
@@ -214,7 +214,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
); );
}, },
icon: const Icon( icon: const Icon(
Symbols.remove_circle, Symbols.add_circle,
), ),
label: Text('subscribe'.tr()), label: Text('subscribe'.tr()),
), ),

View File

@@ -487,10 +487,29 @@ class _PublisherCategoryTabWidget extends StatelessWidget {
Row( Row(
children: [ children: [
Expanded( Expanded(
child: TextField( child: InkWell(
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate:
periodStart.value != null
? DateTime.fromMillisecondsSinceEpoch(
periodStart.value! * 1000,
)
: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now().add(
const Duration(days: 365),
),
);
if (pickedDate != null) {
periodStart.value =
pickedDate.millisecondsSinceEpoch ~/ 1000;
}
},
child: InputDecorator(
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'fromDate'.tr(), labelText: 'fromDate'.tr(),
hintText: 'YYYY-MM-DD',
border: const OutlineInputBorder( border: const OutlineInputBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
Radius.circular(12), Radius.circular(12),
@@ -500,28 +519,43 @@ class _PublisherCategoryTabWidget extends StatelessWidget {
horizontal: 12, horizontal: 12,
vertical: 8, vertical: 8,
), ),
suffixIcon: const Icon(Symbols.calendar_today),
),
child: Text(
periodStart.value != null
? DateTime.fromMillisecondsSinceEpoch(
periodStart.value! * 1000,
).toString().split(' ')[0]
: 'selectDate'.tr(),
),
), ),
onChanged: (value) {
if (value.isEmpty) {
periodStart.value = null;
} else {
try {
final date = DateTime.parse(value);
periodStart.value =
date.millisecondsSinceEpoch ~/ 1000;
} catch (_) {
periodStart.value = null;
}
}
},
), ),
), ),
const Gap(8), const Gap(8),
Expanded( Expanded(
child: TextField( child: InkWell(
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate:
periodEnd.value != null
? DateTime.fromMillisecondsSinceEpoch(
periodEnd.value! * 1000,
)
: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now().add(
const Duration(days: 365),
),
);
if (pickedDate != null) {
periodEnd.value =
pickedDate.millisecondsSinceEpoch ~/ 1000;
}
},
child: InputDecorator(
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'toDate'.tr(), labelText: 'toDate'.tr(),
hintText: 'YYYY-MM-DD',
border: const OutlineInputBorder( border: const OutlineInputBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
Radius.circular(12), Radius.circular(12),
@@ -531,20 +565,16 @@ class _PublisherCategoryTabWidget extends StatelessWidget {
horizontal: 12, horizontal: 12,
vertical: 8, vertical: 8,
), ),
suffixIcon: const Icon(Symbols.calendar_today),
),
child: Text(
periodEnd.value != null
? DateTime.fromMillisecondsSinceEpoch(
periodEnd.value! * 1000,
).toString().split(' ')[0]
: 'selectDate'.tr(),
),
), ),
onChanged: (value) {
if (value.isEmpty) {
periodEnd.value = null;
} else {
try {
final date = DateTime.parse(value);
periodEnd.value =
date.millisecondsSinceEpoch ~/ 1000;
} catch (_) {
periodEnd.value = null;
}
}
},
), ),
), ),
], ],
@@ -646,6 +676,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
final periodEnd = useState<int?>(null); final periodEnd = useState<int?>(null);
final showAdvancedFilters = useState(false); final showAdvancedFilters = useState(false);
final subscribing = useState(false); final subscribing = useState(false);
final isPinnedExpanded = useState(true);
Future<void> subscribe() async { Future<void> subscribe() async {
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);
@@ -716,7 +747,36 @@ class PublisherProfileScreen extends HookConsumerWidget {
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
SliverGap(16), SliverGap(16),
SliverToBoxAdapter(
child: Card(
margin: EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
child: ListTile(
title: Text('pinnedPosts'.tr()),
leading: const Icon(Symbols.push_pin),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
trailing: Icon(
isPinnedExpanded.value
? Symbols.expand_less
: Symbols.expand_more,
),
onTap:
() =>
isPinnedExpanded.value =
!isPinnedExpanded.value,
),
),
),
...[
if (isPinnedExpanded.value)
SliverPostList(pubName: name, pinned: true), SliverPostList(pubName: name, pinned: true),
],
SliverToBoxAdapter( SliverToBoxAdapter(
child: _PublisherCategoryTabWidget( child: _PublisherCategoryTabWidget(
categoryTabController: categoryTabController, categoryTabController: categoryTabController,
@@ -855,7 +915,30 @@ class PublisherProfileScreen extends HookConsumerWidget {
heatmap: heatmap, heatmap: heatmap,
).padding(vertical: 4), ).padding(vertical: 4),
), ),
SliverToBoxAdapter(
child: Card(
margin: EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
child: ListTile(
title: Text('pinnedPosts'.tr()),
trailing: Icon(
isPinnedExpanded.value
? Symbols.expand_less
: Symbols.expand_more,
),
onTap:
() =>
isPinnedExpanded.value =
!isPinnedExpanded.value,
),
),
),
...[
if (isPinnedExpanded.value)
SliverPostList(pubName: name, pinned: true), SliverPostList(pubName: name, pinned: true),
],
SliverToBoxAdapter( SliverToBoxAdapter(
child: _PublisherCategoryTabWidget( child: _PublisherCategoryTabWidget(
categoryTabController: categoryTabController, categoryTabController: categoryTabController,

View File

@@ -1,4 +1,5 @@
import "dart:convert"; import "dart:convert";
import "dart:math" as math;
import "package:dio/dio.dart"; import "package:dio/dio.dart";
import "package:easy_localization/easy_localization.dart"; import "package:easy_localization/easy_localization.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
@@ -57,6 +58,9 @@ class ThoughtScreen extends HookConsumerWidget {
final listController = useMemoized(() => ListController(), []); final listController = useMemoized(() => ListController(), []);
// Scroll animation notifiers
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
// Update local thoughts when provider data changes // Update local thoughts when provider data changes
useEffect(() { useEffect(() {
thoughts.whenData((data) { thoughts.whenData((data) {
@@ -86,6 +90,20 @@ class ThoughtScreen extends HookConsumerWidget {
return null; return null;
}, [localThoughts.value.length, isStreaming.value]); }, [localThoughts.value.length, isStreaming.value]);
// Add scroll listener for gradient animations
useEffect(() {
void onScroll() {
// Update gradient animations
final pixels = scrollController.position.pixels;
// Bottom gradient: appears when not at bottom (pixels > 0)
bottomGradientNotifier.value.value = (pixels / 500.0).clamp(0.0, 1.0);
}
scrollController.addListener(onScroll);
return () => scrollController.removeListener(onScroll);
}, [scrollController]);
void sendMessage() async { void sendMessage() async {
if (messageController.text.trim().isEmpty) return; if (messageController.text.trim().isEmpty) return;
@@ -258,7 +276,10 @@ class ThoughtScreen extends HookConsumerWidget {
const Gap(8), const Gap(8),
], ],
), ),
body: Center( body: Stack(
children: [
// Thoughts list
Center(
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: 640), constraints: BoxConstraints(maxWidth: 640),
child: Column( child: Column(
@@ -269,7 +290,12 @@ class ThoughtScreen extends HookConsumerWidget {
(thoughtList) => SuperListView.builder( (thoughtList) => SuperListView.builder(
listController: listController, listController: listController,
controller: scrollController, controller: scrollController,
padding: const EdgeInsets.only(top: 16, bottom: 16), padding: EdgeInsets.only(
top: 16,
bottom:
MediaQuery.of(context).padding.bottom +
80, // Leave space for thought input
),
reverse: true, reverse: true,
itemCount: itemCount:
localThoughts.value.length + localThoughts.value.length +
@@ -293,7 +319,8 @@ class ThoughtScreen extends HookConsumerWidget {
}, },
), ),
loading: loading:
() => const Center(child: CircularProgressIndicator()), () =>
const Center(child: CircularProgressIndicator()),
error: error:
(error, _) => ResponseErrorWidget( (error, _) => ResponseErrorWidget(
error: error, error: error,
@@ -309,15 +336,61 @@ class ThoughtScreen extends HookConsumerWidget {
), ),
), ),
), ),
ThoughtInput(
messageController: messageController,
isStreaming: isStreaming.value,
onSend: sendMessage,
),
], ],
), ),
), ),
), ),
// Bottom gradient - appears when scrolling towards newer thoughts (behind thought input)
AnimatedBuilder(
animation: bottomGradientNotifier.value,
builder:
(context, child) => Positioned(
left: 0,
right: 0,
bottom: 0,
child: Opacity(
opacity: bottomGradientNotifier.value.value,
child: Container(
height: math.min(
MediaQuery.of(context).size.height * 0.1,
128,
),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.8),
Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.0),
],
),
),
),
),
),
),
// Thought Input positioned above gradient (higher z-index)
Positioned(
left: 0,
right: 0,
bottom: 0, // At the very bottom, above gradient
child: Center(
child: Container(
constraints: BoxConstraints(maxWidth: 640),
child: ThoughtInput(
messageController: messageController,
isStreaming: isStreaming.value,
onSend: sendMessage,
),
),
),
),
],
),
); );
} }
} }

View File

@@ -1,4 +1,5 @@
import "dart:convert"; import "dart:convert";
import "dart:math" as math;
import "package:dio/dio.dart"; import "package:dio/dio.dart";
import "package:easy_localization/easy_localization.dart"; import "package:easy_localization/easy_localization.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
@@ -54,6 +55,9 @@ class ThoughtSheet extends HookConsumerWidget {
final listController = useMemoized(() => ListController(), []); final listController = useMemoized(() => ListController(), []);
// Scroll animation notifiers
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
// Scroll to bottom when thoughts change or streaming state changes // Scroll to bottom when thoughts change or streaming state changes
useEffect(() { useEffect(() {
if (localThoughts.value.isNotEmpty || isStreaming.value) { if (localThoughts.value.isNotEmpty || isStreaming.value) {
@@ -68,6 +72,20 @@ class ThoughtSheet extends HookConsumerWidget {
return null; return null;
}, [localThoughts.value.length, isStreaming.value]); }, [localThoughts.value.length, isStreaming.value]);
// Add scroll listener for gradient animations
useEffect(() {
void onScroll() {
// Update gradient animations
final pixels = scrollController.position.pixels;
// Bottom gradient: appears when not at bottom (pixels > 0)
bottomGradientNotifier.value.value = (pixels / 500.0).clamp(0.0, 1.0);
}
scrollController.addListener(onScroll);
return () => scrollController.removeListener(onScroll);
}, [scrollController]);
void sendMessage() async { void sendMessage() async {
if (messageController.text.trim().isEmpty) return; if (messageController.text.trim().isEmpty) return;
@@ -196,7 +214,10 @@ class ThoughtSheet extends HookConsumerWidget {
return SheetScaffold( return SheetScaffold(
titleText: currentTopic.value ?? 'aiThought'.tr(), titleText: currentTopic.value ?? 'aiThought'.tr(),
child: Center( child: Stack(
children: [
// Thoughts list
Center(
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: 640), constraints: BoxConstraints(maxWidth: 640),
child: Column( child: Column(
@@ -205,10 +226,16 @@ class ThoughtSheet extends HookConsumerWidget {
child: SuperListView.builder( child: SuperListView.builder(
listController: listController, listController: listController,
controller: scrollController, controller: scrollController,
padding: const EdgeInsets.only(top: 16, bottom: 16), padding: EdgeInsets.only(
top: 16,
bottom:
MediaQuery.of(context).padding.bottom +
80, // Leave space for thought input
),
reverse: true, reverse: true,
itemCount: itemCount:
localThoughts.value.length + (isStreaming.value ? 1 : 0), localThoughts.value.length +
(isStreaming.value ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (isStreaming.value && index == 0) { if (isStreaming.value && index == 0) {
return ThoughtItem( return ThoughtItem(
@@ -218,7 +245,8 @@ class ThoughtSheet extends HookConsumerWidget {
streamingFunctionCalls: functionCalls.value, streamingFunctionCalls: functionCalls.value,
); );
} }
final thoughtIndex = isStreaming.value ? index - 1 : index; final thoughtIndex =
isStreaming.value ? index - 1 : index;
final thought = localThoughts.value[thoughtIndex]; final thought = localThoughts.value[thoughtIndex];
return ThoughtItem( return ThoughtItem(
thought: thought, thought: thought,
@@ -227,17 +255,63 @@ class ThoughtSheet extends HookConsumerWidget {
}, },
), ),
), ),
ThoughtInput( ],
),
),
),
// Bottom gradient - appears when scrolling towards newer thoughts (behind thought input)
AnimatedBuilder(
animation: bottomGradientNotifier.value,
builder:
(context, child) => Positioned(
left: 0,
right: 0,
bottom: 0,
child: Opacity(
opacity: bottomGradientNotifier.value.value,
child: Container(
height: math.min(
MediaQuery.of(context).size.height * 0.1,
128,
),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.8),
Theme.of(
context,
).colorScheme.surfaceContainer.withOpacity(0.0),
],
),
),
),
),
),
),
// Thought Input positioned above gradient (higher z-index)
Positioned(
left: 0,
right: 0,
bottom: 0, // At the very bottom, above gradient
child: Center(
child: Container(
constraints: BoxConstraints(maxWidth: 640),
child: ThoughtInput(
messageController: messageController, messageController: messageController,
isStreaming: isStreaming.value, isStreaming: isStreaming.value,
onSend: sendMessage, onSend: sendMessage,
attachedMessages: attachedMessages, attachedMessages: attachedMessages,
attachedPosts: attachedPosts, attachedPosts: attachedPosts,
), ),
),
),
),
], ],
), ),
),
),
); );
} }
} }

View File

@@ -423,6 +423,7 @@ class PostComposeCard extends HookConsumerWidget {
state: composeState, state: composeState,
originalPost: originalPost, originalPost: originalPost,
isCompact: true, isCompact: true,
useSafeArea: isContained,
), ),
), ),
), ),

View File

@@ -14,12 +14,14 @@ class ComposeToolbar extends HookConsumerWidget {
final ComposeState state; final ComposeState state;
final SnPost? originalPost; final SnPost? originalPost;
final bool isCompact; final bool isCompact;
final bool useSafeArea;
const ComposeToolbar({ const ComposeToolbar({
super.key, super.key,
required this.state, required this.state,
this.originalPost, this.originalPost,
this.isCompact = false, this.isCompact = false,
this.useSafeArea = false,
}); });
@override @override
@@ -200,7 +202,12 @@ class ComposeToolbar extends HookConsumerWidget {
), ),
), ),
], ],
).padding(horizontal: 8, vertical: 4), ).padding(
horizontal: 8,
top: 4,
bottom:
useSafeArea ? MediaQuery.of(context).padding.bottom + 4 : 4,
),
), ),
), ),
); );

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 3.3.0+143 version: 3.3.0+144
environment: environment:
sdk: ^3.7.2 sdk: ^3.7.2