Compare commits
9 Commits
580c36fb89
...
3.3.0+144
| Author | SHA1 | Date | |
|---|---|---|---|
|
ac4fa5eb85
|
|||
|
8857718709
|
|||
|
dd17b2b9c1
|
|||
|
848439f664
|
|||
|
f83117424d
|
|||
|
8c19c32c76
|
|||
|
d62b2bed80
|
|||
|
5a23eb1768
|
|||
|
5f6e4763d3
|
@@ -1310,5 +1310,14 @@
|
|||||||
"presenceTypeMusic": "Listening to Music",
|
"presenceTypeMusic": "Listening to Music",
|
||||||
"presenceTypeWorkout": "Working out",
|
"presenceTypeWorkout": "Working out",
|
||||||
"articleCompose": "Compose Article",
|
"articleCompose": "Compose Article",
|
||||||
"backToHub": "Back to Hub"
|
"backToHub": "Back to Hub",
|
||||||
|
"advancedFilters": "Advanced Filters",
|
||||||
|
"searchPosts": "Search Posts",
|
||||||
|
"sortBy": "Sort by",
|
||||||
|
"fromDate": "From Date",
|
||||||
|
"toDate": "To Date",
|
||||||
|
"popularity": "Popularity",
|
||||||
|
"descendingOrder": "Descending Order",
|
||||||
|
"selectDate": "Select Date",
|
||||||
|
"pinnedPosts": "Pinned Posts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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()),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -326,21 +326,263 @@ class _PublisherHeatmapWidget extends StatelessWidget {
|
|||||||
|
|
||||||
class _PublisherCategoryTabWidget extends StatelessWidget {
|
class _PublisherCategoryTabWidget extends StatelessWidget {
|
||||||
final TabController categoryTabController;
|
final TabController categoryTabController;
|
||||||
|
final ValueNotifier<bool?> includeReplies;
|
||||||
|
final ValueNotifier<bool> mediaOnly;
|
||||||
|
final ValueNotifier<String?> queryTerm;
|
||||||
|
final ValueNotifier<String?> order;
|
||||||
|
final ValueNotifier<bool> orderDesc;
|
||||||
|
final ValueNotifier<int?> periodStart;
|
||||||
|
final ValueNotifier<int?> periodEnd;
|
||||||
|
final ValueNotifier<bool> showAdvancedFilters;
|
||||||
|
|
||||||
const _PublisherCategoryTabWidget({required this.categoryTabController});
|
const _PublisherCategoryTabWidget({
|
||||||
|
required this.categoryTabController,
|
||||||
|
required this.includeReplies,
|
||||||
|
required this.mediaOnly,
|
||||||
|
required this.queryTerm,
|
||||||
|
required this.order,
|
||||||
|
required this.orderDesc,
|
||||||
|
required this.periodStart,
|
||||||
|
required this.periodEnd,
|
||||||
|
required this.showAdvancedFilters,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
child: TabBar(
|
child: Column(
|
||||||
controller: categoryTabController,
|
children: [
|
||||||
dividerColor: Colors.transparent,
|
TabBar(
|
||||||
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
|
controller: categoryTabController,
|
||||||
tabs: [
|
dividerColor: Colors.transparent,
|
||||||
Tab(text: 'all'.tr()),
|
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
Tab(text: 'postTypePost'.tr()),
|
tabs: [
|
||||||
Tab(text: 'postArticle'.tr()),
|
Tab(text: 'all'.tr()),
|
||||||
|
Tab(text: 'postTypePost'.tr()),
|
||||||
|
Tab(text: 'postArticle'.tr()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: CheckboxListTile(
|
||||||
|
title: Text('reply'.tr()),
|
||||||
|
value: includeReplies.value,
|
||||||
|
tristate: true,
|
||||||
|
onChanged: (value) {
|
||||||
|
// Cycle through: null -> false -> true -> null
|
||||||
|
if (includeReplies.value == null) {
|
||||||
|
includeReplies.value = false;
|
||||||
|
} else if (includeReplies.value == false) {
|
||||||
|
includeReplies.value = true;
|
||||||
|
} else {
|
||||||
|
includeReplies.value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dense: true,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
secondary: const Icon(Symbols.reply),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: CheckboxListTile(
|
||||||
|
title: Text('attachments'.tr()),
|
||||||
|
value: mediaOnly.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
mediaOnly.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dense: true,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
secondary: const Icon(Symbols.attachment),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
title: Text('descendingOrder'.tr()),
|
||||||
|
value: orderDesc.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
orderDesc.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dense: true,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
secondary: const Icon(Symbols.sort),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
title: Text('advancedFilters'.tr()),
|
||||||
|
leading: const Icon(Symbols.filter_list),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(const Radius.circular(8)),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
showAdvancedFilters.value
|
||||||
|
? Symbols.expand_less
|
||||||
|
: Symbols.expand_more,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
showAdvancedFilters.value = !showAdvancedFilters.value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (showAdvancedFilters.value) ...[
|
||||||
|
const Divider(height: 1),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'search'.tr(),
|
||||||
|
hintText: 'searchPosts'.tr(),
|
||||||
|
prefixIcon: const Icon(Symbols.search),
|
||||||
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
queryTerm.value = value.isEmpty ? null : value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'sortBy'.tr(),
|
||||||
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
value: order.value,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(value: 'date', child: Text('date'.tr())),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'popularity',
|
||||||
|
child: Text('popularity'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
order.value = value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
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(
|
||||||
|
labelText: 'fromDate'.tr(),
|
||||||
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
suffixIcon: const Icon(Symbols.calendar_today),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
periodStart.value != null
|
||||||
|
? DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
periodStart.value! * 1000,
|
||||||
|
).toString().split(' ')[0]
|
||||||
|
: 'selectDate'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
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(
|
||||||
|
labelText: 'toDate'.tr(),
|
||||||
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
suffixIcon: const Icon(Symbols.calendar_today),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
periodEnd.value != null
|
||||||
|
? DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
periodEnd.value! * 1000,
|
||||||
|
).toString().split(' ')[0]
|
||||||
|
: 'selectDate'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -423,7 +665,18 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
|||||||
categoryTab.value = categoryTabController.index;
|
categoryTab.value = categoryTabController.index;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final includeReplies = useState<bool?>(null);
|
||||||
|
final mediaOnly = useState(false);
|
||||||
|
final queryTerm = useState<String?>(null);
|
||||||
|
final order = useState<String?>('date'); // 'popularity' or 'date'
|
||||||
|
final orderDesc = useState(
|
||||||
|
true,
|
||||||
|
); // true for descending, false for ascending
|
||||||
|
final periodStart = useState<int?>(null);
|
||||||
|
final periodEnd = useState<int?>(null);
|
||||||
|
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);
|
||||||
@@ -494,21 +747,66 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
|||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverGap(16),
|
SliverGap(16),
|
||||||
SliverPostList(pubName: name, pinned: true),
|
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),
|
||||||
|
],
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: _PublisherCategoryTabWidget(
|
child: _PublisherCategoryTabWidget(
|
||||||
categoryTabController: categoryTabController,
|
categoryTabController: categoryTabController,
|
||||||
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
orderDesc: orderDesc,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
showAdvancedFilters: showAdvancedFilters,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverPostList(
|
SliverPostList(
|
||||||
key: ValueKey(categoryTab.value),
|
key: ValueKey(
|
||||||
|
'${categoryTab.value}-${includeReplies.value}-${mediaOnly.value}-${queryTerm.value}-${order.value}-${orderDesc.value}-${periodStart.value}-${periodEnd.value}',
|
||||||
|
),
|
||||||
pubName: name,
|
pubName: name,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
type: switch (categoryTab.value) {
|
type:
|
||||||
1 => 0,
|
categoryTab.value == 1
|
||||||
2 => 1,
|
? 0
|
||||||
_ => null,
|
: (categoryTab.value == 2 ? 1 : null),
|
||||||
},
|
includeReplies: includeReplies.value,
|
||||||
|
mediaOnly: mediaOnly.value,
|
||||||
|
queryTerm: queryTerm.value,
|
||||||
|
order: order.value,
|
||||||
|
orderDesc: orderDesc.value,
|
||||||
|
periodStart: periodStart.value,
|
||||||
|
periodEnd: periodEnd.value,
|
||||||
),
|
),
|
||||||
SliverGap(
|
SliverGap(
|
||||||
MediaQuery.of(context).padding.bottom + 16,
|
MediaQuery.of(context).padding.bottom + 16,
|
||||||
@@ -617,21 +915,60 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
|||||||
heatmap: heatmap,
|
heatmap: heatmap,
|
||||||
).padding(vertical: 4),
|
).padding(vertical: 4),
|
||||||
),
|
),
|
||||||
SliverPostList(pubName: name, pinned: true),
|
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),
|
||||||
|
],
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: _PublisherCategoryTabWidget(
|
child: _PublisherCategoryTabWidget(
|
||||||
categoryTabController: categoryTabController,
|
categoryTabController: categoryTabController,
|
||||||
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
orderDesc: orderDesc,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
showAdvancedFilters: showAdvancedFilters,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverPostList(
|
SliverPostList(
|
||||||
key: ValueKey(categoryTab.value),
|
key: ValueKey(
|
||||||
|
'${categoryTab.value}-${includeReplies.value}-${mediaOnly.value}-${queryTerm.value}-${order.value}-${orderDesc.value}-${periodStart.value}-${periodEnd.value}',
|
||||||
|
),
|
||||||
pubName: name,
|
pubName: name,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
type: switch (categoryTab.value) {
|
type:
|
||||||
1 => 0,
|
categoryTab.value == 1
|
||||||
2 => 1,
|
? 0
|
||||||
_ => null,
|
: (categoryTab.value == 2 ? 1 : null),
|
||||||
},
|
includeReplies: includeReplies.value,
|
||||||
|
mediaOnly: mediaOnly.value,
|
||||||
|
queryTerm: queryTerm.value,
|
||||||
|
order: order.value,
|
||||||
|
orderDesc: orderDesc.value,
|
||||||
|
periodStart: periodStart.value,
|
||||||
|
periodEnd: periodEnd.value,
|
||||||
),
|
),
|
||||||
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,65 +276,120 @@ class ThoughtScreen extends HookConsumerWidget {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Stack(
|
||||||
child: Container(
|
children: [
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
// Thoughts list
|
||||||
child: Column(
|
Center(
|
||||||
children: [
|
child: Container(
|
||||||
Expanded(
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
child: thoughts.when(
|
child: Column(
|
||||||
data:
|
children: [
|
||||||
(thoughtList) => SuperListView.builder(
|
Expanded(
|
||||||
listController: listController,
|
child: thoughts.when(
|
||||||
controller: scrollController,
|
data:
|
||||||
padding: const EdgeInsets.only(top: 16, bottom: 16),
|
(thoughtList) => SuperListView.builder(
|
||||||
reverse: true,
|
listController: listController,
|
||||||
itemCount:
|
controller: scrollController,
|
||||||
localThoughts.value.length +
|
padding: EdgeInsets.only(
|
||||||
(isStreaming.value ? 1 : 0),
|
top: 16,
|
||||||
itemBuilder: (context, index) {
|
bottom:
|
||||||
if (isStreaming.value && index == 0) {
|
MediaQuery.of(context).padding.bottom +
|
||||||
return ThoughtItem(
|
80, // Leave space for thought input
|
||||||
isStreaming: true,
|
),
|
||||||
streamingText: streamingText.value,
|
reverse: true,
|
||||||
reasoningChunks: reasoningChunks.value,
|
itemCount:
|
||||||
streamingFunctionCalls: functionCalls.value,
|
localThoughts.value.length +
|
||||||
);
|
(isStreaming.value ? 1 : 0),
|
||||||
}
|
itemBuilder: (context, index) {
|
||||||
final thoughtIndex =
|
if (isStreaming.value && index == 0) {
|
||||||
isStreaming.value ? index - 1 : index;
|
return ThoughtItem(
|
||||||
final thought = localThoughts.value[thoughtIndex];
|
isStreaming: true,
|
||||||
return ThoughtItem(
|
streamingText: streamingText.value,
|
||||||
thought: thought,
|
reasoningChunks: reasoningChunks.value,
|
||||||
thoughtIndex: thoughtIndex,
|
streamingFunctionCalls: functionCalls.value,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
final thoughtIndex =
|
||||||
|
isStreaming.value ? index - 1 : index;
|
||||||
|
final thought = localThoughts.value[thoughtIndex];
|
||||||
|
return ThoughtItem(
|
||||||
|
thought: thought,
|
||||||
|
thoughtIndex: thoughtIndex,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
loading:
|
||||||
|
() =>
|
||||||
|
const Center(child: CircularProgressIndicator()),
|
||||||
|
error:
|
||||||
|
(error, _) => ResponseErrorWidget(
|
||||||
|
error: error,
|
||||||
|
onRetry:
|
||||||
|
() =>
|
||||||
|
selectedSequenceId.value != null
|
||||||
|
? ref.invalidate(
|
||||||
|
thoughtSequenceProvider(
|
||||||
|
selectedSequenceId.value!,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 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,
|
||||||
),
|
),
|
||||||
loading:
|
decoration: BoxDecoration(
|
||||||
() => const Center(child: CircularProgressIndicator()),
|
gradient: LinearGradient(
|
||||||
error:
|
begin: Alignment.bottomCenter,
|
||||||
(error, _) => ResponseErrorWidget(
|
end: Alignment.topCenter,
|
||||||
error: error,
|
colors: [
|
||||||
onRetry:
|
Theme.of(
|
||||||
() =>
|
context,
|
||||||
selectedSequenceId.value != null
|
).colorScheme.surfaceContainer.withOpacity(0.8),
|
||||||
? ref.invalidate(
|
Theme.of(
|
||||||
thoughtSequenceProvider(
|
context,
|
||||||
selectedSequenceId.value!,
|
).colorScheme.surfaceContainer.withOpacity(0.0),
|
||||||
),
|
],
|
||||||
)
|
),
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ThoughtInput(
|
),
|
||||||
messageController: messageController,
|
|
||||||
isStreaming: isStreaming.value,
|
|
||||||
onSend: sendMessage,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,47 +214,103 @@ class ThoughtSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
return SheetScaffold(
|
return SheetScaffold(
|
||||||
titleText: currentTopic.value ?? 'aiThought'.tr(),
|
titleText: currentTopic.value ?? 'aiThought'.tr(),
|
||||||
child: Center(
|
child: Stack(
|
||||||
child: Container(
|
children: [
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
// Thoughts list
|
||||||
child: Column(
|
Center(
|
||||||
children: [
|
child: Container(
|
||||||
Expanded(
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
child: SuperListView.builder(
|
child: Column(
|
||||||
listController: listController,
|
children: [
|
||||||
controller: scrollController,
|
Expanded(
|
||||||
padding: const EdgeInsets.only(top: 16, bottom: 16),
|
child: SuperListView.builder(
|
||||||
reverse: true,
|
listController: listController,
|
||||||
itemCount:
|
controller: scrollController,
|
||||||
localThoughts.value.length + (isStreaming.value ? 1 : 0),
|
padding: EdgeInsets.only(
|
||||||
itemBuilder: (context, index) {
|
top: 16,
|
||||||
if (isStreaming.value && index == 0) {
|
bottom:
|
||||||
return ThoughtItem(
|
MediaQuery.of(context).padding.bottom +
|
||||||
isStreaming: true,
|
80, // Leave space for thought input
|
||||||
streamingText: streamingText.value,
|
),
|
||||||
reasoningChunks: reasoningChunks.value,
|
reverse: true,
|
||||||
streamingFunctionCalls: functionCalls.value,
|
itemCount:
|
||||||
);
|
localThoughts.value.length +
|
||||||
}
|
(isStreaming.value ? 1 : 0),
|
||||||
final thoughtIndex = isStreaming.value ? index - 1 : index;
|
itemBuilder: (context, index) {
|
||||||
final thought = localThoughts.value[thoughtIndex];
|
if (isStreaming.value && index == 0) {
|
||||||
return ThoughtItem(
|
return ThoughtItem(
|
||||||
thought: thought,
|
isStreaming: true,
|
||||||
thoughtIndex: thoughtIndex,
|
streamingText: streamingText.value,
|
||||||
);
|
reasoningChunks: reasoningChunks.value,
|
||||||
},
|
streamingFunctionCalls: functionCalls.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final thoughtIndex =
|
||||||
|
isStreaming.value ? index - 1 : index;
|
||||||
|
final thought = localThoughts.value[thoughtIndex];
|
||||||
|
return ThoughtItem(
|
||||||
|
thought: thought,
|
||||||
|
thoughtIndex: thoughtIndex,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 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,
|
||||||
|
attachedMessages: attachedMessages,
|
||||||
|
attachedPosts: attachedPosts,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ThoughtInput(
|
),
|
||||||
messageController: messageController,
|
|
||||||
isStreaming: isStreaming.value,
|
|
||||||
onSend: sendMessage,
|
|
||||||
attachedMessages: attachedMessages,
|
|
||||||
attachedPosts: attachedPosts,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ class NotificationCard extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final icon = Symbols.info;
|
final icon = Symbols.info;
|
||||||
|
|
||||||
return InkWell(
|
return GestureDetector(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (notification.meta['action_uri'] != null) {
|
if (notification.meta['action_uri'] != null) {
|
||||||
var uri = notification.meta['action_uri'] as String;
|
var uri = notification.meta['action_uri'] as String;
|
||||||
|
|||||||
@@ -423,6 +423,7 @@ class PostComposeCard extends HookConsumerWidget {
|
|||||||
state: composeState,
|
state: composeState,
|
||||||
originalPost: originalPost,
|
originalPost: originalPost,
|
||||||
isCompact: true,
|
isCompact: true,
|
||||||
|
useSafeArea: isContained,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ class PostListNotifier extends _$PostListNotifier
|
|||||||
bool? pinned,
|
bool? pinned,
|
||||||
bool shuffle = false,
|
bool shuffle = false,
|
||||||
bool? includeReplies,
|
bool? includeReplies,
|
||||||
|
bool? mediaOnly,
|
||||||
|
String? queryTerm,
|
||||||
|
String? order,
|
||||||
|
int? periodStart,
|
||||||
|
int? periodEnd,
|
||||||
|
bool orderDesc = true,
|
||||||
}) {
|
}) {
|
||||||
return fetch(cursor: null);
|
return fetch(cursor: null);
|
||||||
}
|
}
|
||||||
@@ -36,14 +42,20 @@ class PostListNotifier extends _$PostListNotifier
|
|||||||
final queryParams = {
|
final queryParams = {
|
||||||
'offset': offset,
|
'offset': offset,
|
||||||
'take': _pageSize,
|
'take': _pageSize,
|
||||||
|
'replies': includeReplies,
|
||||||
|
'orderDesc': orderDesc,
|
||||||
|
if (shuffle) 'shuffle': shuffle,
|
||||||
if (pubName != null) 'pub': pubName,
|
if (pubName != null) 'pub': pubName,
|
||||||
if (realm != null) 'realm': realm,
|
if (realm != null) 'realm': realm,
|
||||||
if (type != null) 'type': type,
|
if (type != null) 'type': type,
|
||||||
if (tags != null) 'tags': tags,
|
if (tags != null) 'tags': tags,
|
||||||
if (categories != null) 'categories': categories,
|
if (categories != null) 'categories': categories,
|
||||||
if (shuffle) 'shuffle': true,
|
|
||||||
if (pinned != null) 'pinned': pinned,
|
if (pinned != null) 'pinned': pinned,
|
||||||
if (includeReplies != null) 'includeReplies': includeReplies,
|
if (order != null) 'order': order,
|
||||||
|
if (periodStart != null) 'periodStart': periodStart,
|
||||||
|
if (periodEnd != null) 'periodEnd': periodEnd,
|
||||||
|
if (queryTerm != null) 'query': queryTerm,
|
||||||
|
if (mediaOnly != null) 'media': mediaOnly,
|
||||||
};
|
};
|
||||||
|
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
@@ -82,6 +94,14 @@ class SliverPostList extends HookConsumerWidget {
|
|||||||
final List<String>? tags;
|
final List<String>? tags;
|
||||||
final bool shuffle;
|
final bool shuffle;
|
||||||
final bool? pinned;
|
final bool? pinned;
|
||||||
|
final bool? includeReplies;
|
||||||
|
final bool? mediaOnly;
|
||||||
|
final String? queryTerm;
|
||||||
|
// Can be "populaurity", other value will be treated as "date"
|
||||||
|
final String? order;
|
||||||
|
final int? periodStart;
|
||||||
|
final int? periodEnd;
|
||||||
|
final bool? orderDesc;
|
||||||
final PostItemType itemType;
|
final PostItemType itemType;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
final EdgeInsets? padding;
|
final EdgeInsets? padding;
|
||||||
@@ -99,6 +119,13 @@ class SliverPostList extends HookConsumerWidget {
|
|||||||
this.tags,
|
this.tags,
|
||||||
this.shuffle = false,
|
this.shuffle = false,
|
||||||
this.pinned,
|
this.pinned,
|
||||||
|
this.includeReplies,
|
||||||
|
this.mediaOnly,
|
||||||
|
this.queryTerm,
|
||||||
|
this.order,
|
||||||
|
this.orderDesc = true,
|
||||||
|
this.periodStart,
|
||||||
|
this.periodEnd,
|
||||||
this.itemType = PostItemType.regular,
|
this.itemType = PostItemType.regular,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.padding,
|
this.padding,
|
||||||
@@ -118,6 +145,13 @@ class SliverPostList extends HookConsumerWidget {
|
|||||||
tags: tags,
|
tags: tags,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc ?? true,
|
||||||
);
|
);
|
||||||
return PagingHelperSliverView(
|
return PagingHelperSliverView(
|
||||||
provider: provider,
|
provider: provider,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'post_list.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$postListNotifierHash() => r'fc139ad4df0deb67bcbb949560319f2f7fbfb503';
|
String _$postListNotifierHash() => r'8241120dc3c2004387c6cf881e5cb9224cbd3a97';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
@@ -39,6 +39,12 @@ abstract class _$PostListNotifier
|
|||||||
late final bool? pinned;
|
late final bool? pinned;
|
||||||
late final bool shuffle;
|
late final bool shuffle;
|
||||||
late final bool? includeReplies;
|
late final bool? includeReplies;
|
||||||
|
late final bool? mediaOnly;
|
||||||
|
late final String? queryTerm;
|
||||||
|
late final String? order;
|
||||||
|
late final int? periodStart;
|
||||||
|
late final int? periodEnd;
|
||||||
|
late final bool orderDesc;
|
||||||
|
|
||||||
FutureOr<CursorPagingData<SnPost>> build({
|
FutureOr<CursorPagingData<SnPost>> build({
|
||||||
String? pubName,
|
String? pubName,
|
||||||
@@ -49,6 +55,12 @@ abstract class _$PostListNotifier
|
|||||||
bool? pinned,
|
bool? pinned,
|
||||||
bool shuffle = false,
|
bool shuffle = false,
|
||||||
bool? includeReplies,
|
bool? includeReplies,
|
||||||
|
bool? mediaOnly,
|
||||||
|
String? queryTerm,
|
||||||
|
String? order,
|
||||||
|
int? periodStart,
|
||||||
|
int? periodEnd,
|
||||||
|
bool orderDesc = true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +84,12 @@ class PostListNotifierFamily
|
|||||||
bool? pinned,
|
bool? pinned,
|
||||||
bool shuffle = false,
|
bool shuffle = false,
|
||||||
bool? includeReplies,
|
bool? includeReplies,
|
||||||
|
bool? mediaOnly,
|
||||||
|
String? queryTerm,
|
||||||
|
String? order,
|
||||||
|
int? periodStart,
|
||||||
|
int? periodEnd,
|
||||||
|
bool orderDesc = true,
|
||||||
}) {
|
}) {
|
||||||
return PostListNotifierProvider(
|
return PostListNotifierProvider(
|
||||||
pubName: pubName,
|
pubName: pubName,
|
||||||
@@ -82,6 +100,12 @@ class PostListNotifierFamily
|
|||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
includeReplies: includeReplies,
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +122,12 @@ class PostListNotifierFamily
|
|||||||
pinned: provider.pinned,
|
pinned: provider.pinned,
|
||||||
shuffle: provider.shuffle,
|
shuffle: provider.shuffle,
|
||||||
includeReplies: provider.includeReplies,
|
includeReplies: provider.includeReplies,
|
||||||
|
mediaOnly: provider.mediaOnly,
|
||||||
|
queryTerm: provider.queryTerm,
|
||||||
|
order: provider.order,
|
||||||
|
periodStart: provider.periodStart,
|
||||||
|
periodEnd: provider.periodEnd,
|
||||||
|
orderDesc: provider.orderDesc,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +163,12 @@ class PostListNotifierProvider
|
|||||||
bool? pinned,
|
bool? pinned,
|
||||||
bool shuffle = false,
|
bool shuffle = false,
|
||||||
bool? includeReplies,
|
bool? includeReplies,
|
||||||
|
bool? mediaOnly,
|
||||||
|
String? queryTerm,
|
||||||
|
String? order,
|
||||||
|
int? periodStart,
|
||||||
|
int? periodEnd,
|
||||||
|
bool orderDesc = true,
|
||||||
}) : this._internal(
|
}) : this._internal(
|
||||||
() =>
|
() =>
|
||||||
PostListNotifier()
|
PostListNotifier()
|
||||||
@@ -143,7 +179,13 @@ class PostListNotifierProvider
|
|||||||
..tags = tags
|
..tags = tags
|
||||||
..pinned = pinned
|
..pinned = pinned
|
||||||
..shuffle = shuffle
|
..shuffle = shuffle
|
||||||
..includeReplies = includeReplies,
|
..includeReplies = includeReplies
|
||||||
|
..mediaOnly = mediaOnly
|
||||||
|
..queryTerm = queryTerm
|
||||||
|
..order = order
|
||||||
|
..periodStart = periodStart
|
||||||
|
..periodEnd = periodEnd
|
||||||
|
..orderDesc = orderDesc,
|
||||||
from: postListNotifierProvider,
|
from: postListNotifierProvider,
|
||||||
name: r'postListNotifierProvider',
|
name: r'postListNotifierProvider',
|
||||||
debugGetCreateSourceHash:
|
debugGetCreateSourceHash:
|
||||||
@@ -161,6 +203,12 @@ class PostListNotifierProvider
|
|||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
includeReplies: includeReplies,
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc,
|
||||||
);
|
);
|
||||||
|
|
||||||
PostListNotifierProvider._internal(
|
PostListNotifierProvider._internal(
|
||||||
@@ -178,6 +226,12 @@ class PostListNotifierProvider
|
|||||||
required this.pinned,
|
required this.pinned,
|
||||||
required this.shuffle,
|
required this.shuffle,
|
||||||
required this.includeReplies,
|
required this.includeReplies,
|
||||||
|
required this.mediaOnly,
|
||||||
|
required this.queryTerm,
|
||||||
|
required this.order,
|
||||||
|
required this.periodStart,
|
||||||
|
required this.periodEnd,
|
||||||
|
required this.orderDesc,
|
||||||
}) : super.internal();
|
}) : super.internal();
|
||||||
|
|
||||||
final String? pubName;
|
final String? pubName;
|
||||||
@@ -188,6 +242,12 @@ class PostListNotifierProvider
|
|||||||
final bool? pinned;
|
final bool? pinned;
|
||||||
final bool shuffle;
|
final bool shuffle;
|
||||||
final bool? includeReplies;
|
final bool? includeReplies;
|
||||||
|
final bool? mediaOnly;
|
||||||
|
final String? queryTerm;
|
||||||
|
final String? order;
|
||||||
|
final int? periodStart;
|
||||||
|
final int? periodEnd;
|
||||||
|
final bool orderDesc;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
|
FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
|
||||||
@@ -202,6 +262,12 @@ class PostListNotifierProvider
|
|||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
includeReplies: includeReplies,
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +285,13 @@ class PostListNotifierProvider
|
|||||||
..tags = tags
|
..tags = tags
|
||||||
..pinned = pinned
|
..pinned = pinned
|
||||||
..shuffle = shuffle
|
..shuffle = shuffle
|
||||||
..includeReplies = includeReplies,
|
..includeReplies = includeReplies
|
||||||
|
..mediaOnly = mediaOnly
|
||||||
|
..queryTerm = queryTerm
|
||||||
|
..order = order
|
||||||
|
..periodStart = periodStart
|
||||||
|
..periodEnd = periodEnd
|
||||||
|
..orderDesc = orderDesc,
|
||||||
from: from,
|
from: from,
|
||||||
name: null,
|
name: null,
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
@@ -233,6 +305,12 @@ class PostListNotifierProvider
|
|||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
includeReplies: includeReplies,
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -256,7 +334,13 @@ class PostListNotifierProvider
|
|||||||
other.tags == tags &&
|
other.tags == tags &&
|
||||||
other.pinned == pinned &&
|
other.pinned == pinned &&
|
||||||
other.shuffle == shuffle &&
|
other.shuffle == shuffle &&
|
||||||
other.includeReplies == includeReplies;
|
other.includeReplies == includeReplies &&
|
||||||
|
other.mediaOnly == mediaOnly &&
|
||||||
|
other.queryTerm == queryTerm &&
|
||||||
|
other.order == order &&
|
||||||
|
other.periodStart == periodStart &&
|
||||||
|
other.periodEnd == periodEnd &&
|
||||||
|
other.orderDesc == orderDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -270,6 +354,12 @@ class PostListNotifierProvider
|
|||||||
hash = _SystemHash.combine(hash, pinned.hashCode);
|
hash = _SystemHash.combine(hash, pinned.hashCode);
|
||||||
hash = _SystemHash.combine(hash, shuffle.hashCode);
|
hash = _SystemHash.combine(hash, shuffle.hashCode);
|
||||||
hash = _SystemHash.combine(hash, includeReplies.hashCode);
|
hash = _SystemHash.combine(hash, includeReplies.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, mediaOnly.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, queryTerm.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, order.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, periodStart.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, periodEnd.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, orderDesc.hashCode);
|
||||||
|
|
||||||
return _SystemHash.finish(hash);
|
return _SystemHash.finish(hash);
|
||||||
}
|
}
|
||||||
@@ -302,6 +392,24 @@ mixin PostListNotifierRef
|
|||||||
|
|
||||||
/// The parameter `includeReplies` of this provider.
|
/// The parameter `includeReplies` of this provider.
|
||||||
bool? get includeReplies;
|
bool? get includeReplies;
|
||||||
|
|
||||||
|
/// The parameter `mediaOnly` of this provider.
|
||||||
|
bool? get mediaOnly;
|
||||||
|
|
||||||
|
/// The parameter `queryTerm` of this provider.
|
||||||
|
String? get queryTerm;
|
||||||
|
|
||||||
|
/// The parameter `order` of this provider.
|
||||||
|
String? get order;
|
||||||
|
|
||||||
|
/// The parameter `periodStart` of this provider.
|
||||||
|
int? get periodStart;
|
||||||
|
|
||||||
|
/// The parameter `periodEnd` of this provider.
|
||||||
|
int? get periodEnd;
|
||||||
|
|
||||||
|
/// The parameter `orderDesc` of this provider.
|
||||||
|
bool get orderDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PostListNotifierProviderElement
|
class _PostListNotifierProviderElement
|
||||||
@@ -331,6 +439,18 @@ class _PostListNotifierProviderElement
|
|||||||
@override
|
@override
|
||||||
bool? get includeReplies =>
|
bool? get includeReplies =>
|
||||||
(origin as PostListNotifierProvider).includeReplies;
|
(origin as PostListNotifierProvider).includeReplies;
|
||||||
|
@override
|
||||||
|
bool? get mediaOnly => (origin as PostListNotifierProvider).mediaOnly;
|
||||||
|
@override
|
||||||
|
String? get queryTerm => (origin as PostListNotifierProvider).queryTerm;
|
||||||
|
@override
|
||||||
|
String? get order => (origin as PostListNotifierProvider).order;
|
||||||
|
@override
|
||||||
|
int? get periodStart => (origin as PostListNotifierProvider).periodStart;
|
||||||
|
@override
|
||||||
|
int? get periodEnd => (origin as PostListNotifierProvider).periodEnd;
|
||||||
|
@override
|
||||||
|
bool get orderDesc => (origin as PostListNotifierProvider).orderDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user