Compare commits
4 Commits
596d212593
...
v3
Author | SHA1 | Date | |
---|---|---|---|
|
c9b71701c8 | ||
|
28e98488f1 | ||
|
b4d476613e | ||
|
b48a1aac44 |
@@ -944,5 +944,21 @@
|
|||||||
"unpinPostHint": "Are you sure you want to unpin this post?",
|
"unpinPostHint": "Are you sure you want to unpin this post?",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"statusPresent": "Present",
|
"statusPresent": "Present",
|
||||||
"accountAutomated": "Automated"
|
"accountAutomated": "Automated",
|
||||||
}
|
"chatBreakClearButton": "Clear",
|
||||||
|
"chatBreak5m": "5m",
|
||||||
|
"chatBreak10m": "10m",
|
||||||
|
"chatBreak15m": "15m",
|
||||||
|
"chatBreak30m": "30m",
|
||||||
|
"chatBreakCustomMinutes": "Custom (minutes)",
|
||||||
|
"chatBreakEnterMinutes": "Enter minutes",
|
||||||
|
"errorGeneric": "Error: {}",
|
||||||
|
"searchMessages": "Search Messages",
|
||||||
|
"messagesCount": "{} messages",
|
||||||
|
"dotSeparator": "·",
|
||||||
|
"roleValidationHint": "Role must be between 0 and 100",
|
||||||
|
"searchMessagesHint": "Search messages...",
|
||||||
|
"searchLinks": "Links",
|
||||||
|
"searchAttachments": "Attachments",
|
||||||
|
"noMessagesFound": "No messages found"
|
||||||
|
}
|
@@ -40,6 +40,8 @@ PODS:
|
|||||||
- file_picker (0.0.1):
|
- file_picker (0.0.1):
|
||||||
- DKImagePickerController/PhotoGallery
|
- DKImagePickerController/PhotoGallery
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- file_saver (0.0.1):
|
||||||
|
- Flutter
|
||||||
- Firebase/CoreOnly (12.0.0):
|
- Firebase/CoreOnly (12.0.0):
|
||||||
- FirebaseCore (~> 12.0.0)
|
- FirebaseCore (~> 12.0.0)
|
||||||
- Firebase/Crashlytics (12.0.0):
|
- Firebase/Crashlytics (12.0.0):
|
||||||
@@ -303,6 +305,7 @@ DEPENDENCIES:
|
|||||||
- croppy (from `.symlinks/plugins/croppy/ios`)
|
- croppy (from `.symlinks/plugins/croppy/ios`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
|
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
||||||
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
|
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
|
||||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||||
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
|
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
|
||||||
@@ -381,6 +384,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||||
file_picker:
|
file_picker:
|
||||||
:path: ".symlinks/plugins/file_picker/ios"
|
:path: ".symlinks/plugins/file_picker/ios"
|
||||||
|
file_saver:
|
||||||
|
:path: ".symlinks/plugins/file_saver/ios"
|
||||||
firebase_analytics:
|
firebase_analytics:
|
||||||
:path: ".symlinks/plugins/firebase_analytics/ios"
|
:path: ".symlinks/plugins/firebase_analytics/ios"
|
||||||
firebase_core:
|
firebase_core:
|
||||||
@@ -464,6 +469,7 @@ SPEC CHECKSUMS:
|
|||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||||
|
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||||
Firebase: 800d487043c0557d9faed71477a38d9aafb08a41
|
Firebase: 800d487043c0557d9faed71477a38d9aafb08a41
|
||||||
firebase_analytics: cd56fc56f75c1df30a6ff5290cd56e230996a76d
|
firebase_analytics: cd56fc56f75c1df30a6ff5290cd56e230996a76d
|
||||||
firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4
|
firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4
|
||||||
|
@@ -68,6 +68,34 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
return (delete(chatMessages)..where((m) => m.id.equals(id))).go();
|
return (delete(chatMessages)..where((m) => m.id.equals(id))).go();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> getTotalMessagesForRoom(String roomId) {
|
||||||
|
return (select(chatMessages)..where((m) => m.roomId.equals(roomId))).get().then((list) => list.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<LocalChatMessage>> searchMessages(
|
||||||
|
String roomId,
|
||||||
|
String query,
|
||||||
|
) async {
|
||||||
|
var selectStatement = select(chatMessages)
|
||||||
|
..where((m) => m.roomId.equals(roomId));
|
||||||
|
|
||||||
|
if (query.isNotEmpty) {
|
||||||
|
selectStatement =
|
||||||
|
selectStatement
|
||||||
|
..where((m) => m.content.like('%${query.toLowerCase()}%'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
final messages =
|
||||||
|
await (selectStatement
|
||||||
|
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
|
||||||
|
.get();
|
||||||
|
return messages.map((msg) => companionToMessage(msg)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
// Convert between Drift and model objects
|
// Convert between Drift and model objects
|
||||||
ChatMessagesCompanion messageToCompanion(LocalChatMessage message) {
|
ChatMessagesCompanion messageToCompanion(LocalChatMessage message) {
|
||||||
return ChatMessagesCompanion(
|
return ChatMessagesCompanion(
|
||||||
|
@@ -8,7 +8,7 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
|||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
@@ -38,6 +38,7 @@ import 'package:island/screens/chat/chat.dart';
|
|||||||
import 'package:island/screens/chat/room.dart';
|
import 'package:island/screens/chat/room.dart';
|
||||||
import 'package:island/screens/chat/room_detail.dart';
|
import 'package:island/screens/chat/room_detail.dart';
|
||||||
import 'package:island/screens/chat/call.dart';
|
import 'package:island/screens/chat/call.dart';
|
||||||
|
import 'package:island/screens/chat/search_messages_screen.dart';
|
||||||
import 'package:island/screens/creators/hub.dart';
|
import 'package:island/screens/creators/hub.dart';
|
||||||
import 'package:island/screens/creators/posts/post_manage_list.dart';
|
import 'package:island/screens/creators/posts/post_manage_list.dart';
|
||||||
import 'package:island/screens/creators/stickers/stickers.dart';
|
import 'package:island/screens/creators/stickers/stickers.dart';
|
||||||
@@ -555,6 +556,14 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return ChatDetailScreen(id: id);
|
return ChatDetailScreen(id: id);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'searchMessages',
|
||||||
|
path: '/chat/:id/search',
|
||||||
|
builder: (context, state) {
|
||||||
|
final id = state.pathParameters['id']!;
|
||||||
|
return SearchMessagesScreen(roomId: id);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@@ -95,8 +95,24 @@ class LevelingScreen extends HookConsumerWidget {
|
|||||||
title: Text('levelingProgress'.tr()),
|
title: Text('levelingProgress'.tr()),
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(text: 'leveling'.tr()),
|
Tab(
|
||||||
Tab(text: 'stellarProgram'.tr()),
|
child: Text(
|
||||||
|
'leveling'.tr(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
'stellarProgram'.tr(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -283,6 +283,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
final Map<String, LocalChatMessage> _pendingMessages = {};
|
final Map<String, LocalChatMessage> _pendingMessages = {};
|
||||||
final Map<String, Map<int, double>> _fileUploadProgress = {};
|
final Map<String, Map<int, double>> _fileUploadProgress = {};
|
||||||
int? _totalCount;
|
int? _totalCount;
|
||||||
|
String? _searchQuery;
|
||||||
|
bool? _withLinks;
|
||||||
|
bool? _withAttachments;
|
||||||
|
|
||||||
late final String _roomId;
|
late final String _roomId;
|
||||||
int _currentPage = 0;
|
int _currentPage = 0;
|
||||||
@@ -326,7 +329,13 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return await loadInitial();
|
loadInitial();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LocalChatMessage> _sortMessages(List<LocalChatMessage> messages) {
|
||||||
|
messages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<LocalChatMessage>> _getCachedMessages({
|
Future<List<LocalChatMessage>> _getCachedMessages({
|
||||||
@@ -337,13 +346,32 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
'Getting cached messages from offset $offset, take $take',
|
'Getting cached messages from offset $offset, take $take',
|
||||||
name: 'MessagesNotifier',
|
name: 'MessagesNotifier',
|
||||||
);
|
);
|
||||||
final dbMessages = await _database.getMessagesForRoom(
|
final List<LocalChatMessage> dbMessages;
|
||||||
_roomId,
|
if (_searchQuery != null && _searchQuery!.isNotEmpty) {
|
||||||
offset: offset,
|
dbMessages = await _database.searchMessages(_roomId, _searchQuery ?? '');
|
||||||
limit: take,
|
} else {
|
||||||
);
|
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
||||||
final dbLocalMessages =
|
_roomId,
|
||||||
dbMessages.map(_database.companionToMessage).toList();
|
offset: offset,
|
||||||
|
limit: take,
|
||||||
|
);
|
||||||
|
dbMessages =
|
||||||
|
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LocalChatMessage> filteredMessages = dbMessages;
|
||||||
|
|
||||||
|
if (_withLinks == true) {
|
||||||
|
filteredMessages =
|
||||||
|
filteredMessages.where((msg) => _hasLink(msg)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_withAttachments == true) {
|
||||||
|
filteredMessages =
|
||||||
|
filteredMessages.where((msg) => _hasAttachment(msg)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
final dbLocalMessages = filteredMessages;
|
||||||
|
|
||||||
if (offset == 0) {
|
if (offset == 0) {
|
||||||
final pendingForRoom =
|
final pendingForRoom =
|
||||||
@@ -352,7 +380,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final allMessages = [...pendingForRoom, ...dbLocalMessages];
|
final allMessages = [...pendingForRoom, ...dbLocalMessages];
|
||||||
allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
_sortMessages(allMessages); // Use the helper function
|
||||||
|
|
||||||
final uniqueMessages = <LocalChatMessage>[];
|
final uniqueMessages = <LocalChatMessage>[];
|
||||||
final seenIds = <String>{};
|
final seenIds = <String>{};
|
||||||
@@ -427,7 +455,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
_isSyncing = true;
|
_isSyncing = true;
|
||||||
|
|
||||||
developer.log('Starting message sync', name: 'MessagesNotifier');
|
developer.log('Starting message sync', name: 'MessagesNotifier');
|
||||||
ref.read(isSyncingProvider.notifier).state = true;
|
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
|
||||||
try {
|
try {
|
||||||
final dbMessages = await _database.getMessagesForRoom(
|
final dbMessages = await _database.getMessagesForRoom(
|
||||||
_room.id,
|
_room.id,
|
||||||
@@ -488,7 +516,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
showErrorAlert(err);
|
showErrorAlert(err);
|
||||||
} finally {
|
} finally {
|
||||||
developer.log('Finished message sync', name: 'MessagesNotifier');
|
developer.log('Finished message sync', name: 'MessagesNotifier');
|
||||||
ref.read(isSyncingProvider.notifier).state = false;
|
Future.microtask(
|
||||||
|
() => ref.read(isSyncingProvider.notifier).state = false,
|
||||||
|
);
|
||||||
_isSyncing = false;
|
_isSyncing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -499,7 +529,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
bool synced = false,
|
bool synced = false,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
if (offset == 0 && !synced) {
|
if (offset == 0 &&
|
||||||
|
!synced &&
|
||||||
|
(_searchQuery == null || _searchQuery!.isEmpty)) {
|
||||||
_fetchAndCacheMessages(offset: 0, take: take).catchError((_) {
|
_fetchAndCacheMessages(offset: 0, take: take).catchError((_) {
|
||||||
return <LocalChatMessage>[];
|
return <LocalChatMessage>[];
|
||||||
});
|
});
|
||||||
@@ -514,7 +546,11 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
return localMessages;
|
return localMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _fetchAndCacheMessages(offset: offset, take: take);
|
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
||||||
|
return await _fetchAndCacheMessages(offset: offset, take: take);
|
||||||
|
} else {
|
||||||
|
return []; // If searching, and no local messages, don't fetch from network
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
final localMessages = await _getCachedMessages(
|
final localMessages = await _getCachedMessages(
|
||||||
offset: offset,
|
offset: offset,
|
||||||
@@ -528,13 +564,15 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<LocalChatMessage>> loadInitial() async {
|
Future<void> loadInitial() async {
|
||||||
developer.log('Loading initial messages', name: 'MessagesNotifier');
|
developer.log('Loading initial messages', name: 'MessagesNotifier');
|
||||||
syncMessages();
|
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
||||||
|
syncMessages();
|
||||||
|
}
|
||||||
final messages = await _getCachedMessages(offset: 0, take: 100);
|
final messages = await _getCachedMessages(offset: 0, take: 100);
|
||||||
_currentPage = 0;
|
_currentPage = 0;
|
||||||
_hasMore = messages.length == _pageSize;
|
_hasMore = messages.length == _pageSize;
|
||||||
return messages;
|
state = AsyncValue.data(messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadMore() async {
|
Future<void> loadMore() async {
|
||||||
@@ -553,7 +591,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
_hasMore = false;
|
_hasMore = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
state = AsyncValue.data([...currentMessages, ...newMessages]);
|
state = AsyncValue.data(
|
||||||
|
_sortMessages([...currentMessages, ...newMessages]),
|
||||||
|
);
|
||||||
} catch (err, stackTrace) {
|
} catch (err, stackTrace) {
|
||||||
developer.log(
|
developer.log(
|
||||||
'Error loading more messages',
|
'Error loading more messages',
|
||||||
@@ -778,7 +818,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
return m;
|
return m;
|
||||||
}).toList();
|
}).toList();
|
||||||
state = AsyncValue.data(newMessages);
|
state = AsyncValue.data(_sortMessages(newMessages));
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -838,7 +878,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
final newList = [...currentMessages];
|
final newList = [...currentMessages];
|
||||||
newList[index] = updatedMessage;
|
newList[index] = updatedMessage;
|
||||||
state = AsyncValue.data(newList);
|
state = AsyncValue.data(_sortMessages(newList));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -898,6 +938,20 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void searchMessages(String query, {bool? withLinks, bool? withAttachments}) {
|
||||||
|
_searchQuery = query.trim();
|
||||||
|
_withLinks = withLinks;
|
||||||
|
_withAttachments = withAttachments;
|
||||||
|
loadInitial();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearSearch() {
|
||||||
|
_searchQuery = null;
|
||||||
|
_withLinks = null;
|
||||||
|
_withAttachments = null;
|
||||||
|
loadInitial();
|
||||||
|
}
|
||||||
|
|
||||||
Future<LocalChatMessage?> fetchMessageById(String messageId) async {
|
Future<LocalChatMessage?> fetchMessageById(String messageId) async {
|
||||||
developer.log(
|
developer.log(
|
||||||
'Fetching message by id $messageId',
|
'Fetching message by id $messageId',
|
||||||
@@ -927,6 +981,18 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _hasLink(LocalChatMessage message) {
|
||||||
|
final content = message.toRemoteMessage().content;
|
||||||
|
if (content == null) return false;
|
||||||
|
final urlRegex = RegExp(r'https?://[^\s/$.?#].[^\s]*');
|
||||||
|
return urlRegex.hasMatch(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasAttachment(LocalChatMessage message) {
|
||||||
|
final remoteMessage = message.toRemoteMessage();
|
||||||
|
return remoteMessage.attachments.isNotEmpty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatRoomScreen extends HookConsumerWidget {
|
class ChatRoomScreen extends HookConsumerWidget {
|
||||||
|
@@ -6,7 +6,7 @@ part of 'room.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$messagesNotifierHash() => r'dda98f5bf525f3b2bc0a7c89bc6eaa3c8b95f142';
|
String _$messagesNotifierHash() => r'fc3b66dfb8dd3fc55d142dae5c5e7bdc67eca5d4';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
@@ -20,10 +20,17 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:island/pods/database.dart';
|
||||||
|
|
||||||
part 'room_detail.freezed.dart';
|
part 'room_detail.freezed.dart';
|
||||||
part 'room_detail.g.dart';
|
part 'room_detail.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<int> totalMessagesCount(Ref ref, String roomId) async {
|
||||||
|
final database = ref.watch(databaseProvider);
|
||||||
|
return database.getTotalMessagesForRoom(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
class ChatDetailScreen extends HookConsumerWidget {
|
class ChatDetailScreen extends HookConsumerWidget {
|
||||||
final String id;
|
final String id;
|
||||||
const ChatDetailScreen({super.key, required this.id});
|
const ChatDetailScreen({super.key, required this.id});
|
||||||
@@ -32,6 +39,7 @@ class ChatDetailScreen extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final roomState = ref.watch(chatroomProvider(id));
|
final roomState = ref.watch(chatroomProvider(id));
|
||||||
final roomIdentity = ref.watch(chatroomIdentityProvider(id));
|
final roomIdentity = ref.watch(chatroomIdentityProvider(id));
|
||||||
|
final totalMessages = ref.watch(totalMessagesCountProvider(id));
|
||||||
|
|
||||||
const kNotifyLevelText = [
|
const kNotifyLevelText = [
|
||||||
'chatNotifyLevelAll',
|
'chatNotifyLevelAll',
|
||||||
@@ -132,7 +140,7 @@ class ChatDetailScreen extends HookConsumerWidget {
|
|||||||
const Text('chatBreakDescription').tr(),
|
const Text('chatBreakDescription').tr(),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Clear').tr(),
|
title: const Text('chatBreakClearButton').tr(),
|
||||||
subtitle: const Text('chatBreakClear').tr(),
|
subtitle: const Text('chatBreakClear').tr(),
|
||||||
leading: const Icon(Icons.notifications_active),
|
leading: const Icon(Icons.notifications_active),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -144,8 +152,8 @@ class ChatDetailScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('5m'),
|
title: const Text('chatBreak5m').tr(),
|
||||||
subtitle: const Text('chatBreakHour').tr(args: ['5m']),
|
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak5m'.tr()]),
|
||||||
leading: const Icon(Symbols.circle),
|
leading: const Icon(Symbols.circle),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setChatBreak(now.add(const Duration(minutes: 5)));
|
setChatBreak(now.add(const Duration(minutes: 5)));
|
||||||
@@ -156,8 +164,8 @@ class ChatDetailScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('10m'),
|
title: const Text('chatBreak10m').tr(),
|
||||||
subtitle: const Text('chatBreakHour').tr(args: ['10m']),
|
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak10m'.tr()]),
|
||||||
leading: const Icon(Symbols.circle),
|
leading: const Icon(Symbols.circle),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setChatBreak(now.add(const Duration(minutes: 10)));
|
setChatBreak(now.add(const Duration(minutes: 10)));
|
||||||
@@ -168,8 +176,8 @@ class ChatDetailScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('15m'),
|
title: const Text('chatBreak15m').tr(),
|
||||||
subtitle: const Text('chatBreakHour').tr(args: ['15m']),
|
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak15m'.tr()]),
|
||||||
leading: const Icon(Symbols.timer_3),
|
leading: const Icon(Symbols.timer_3),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setChatBreak(now.add(const Duration(minutes: 15)));
|
setChatBreak(now.add(const Duration(minutes: 15)));
|
||||||
@@ -180,8 +188,8 @@ class ChatDetailScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('30m'),
|
title: const Text('chatBreak30m').tr(),
|
||||||
subtitle: const Text('chatBreakHour').tr(args: ['30m']),
|
subtitle: const Text('chatBreakHour').tr(args: ['chatBreak30m'.tr()]),
|
||||||
leading: const Icon(Symbols.timer),
|
leading: const Icon(Symbols.timer),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setChatBreak(now.add(const Duration(minutes: 30)));
|
setChatBreak(now.add(const Duration(minutes: 30)));
|
||||||
@@ -195,8 +203,8 @@ class ChatDetailScreen extends HookConsumerWidget {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: durationController,
|
controller: durationController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Custom (minutes)'.tr(),
|
labelText: 'chatBreakCustomMinutes'.tr(),
|
||||||
hintText: 'Enter minutes'.tr(),
|
hintText: 'chatBreakEnterMinutes'.tr(),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: const Icon(Icons.check),
|
icon: const Icon(Icons.check),
|
||||||
@@ -239,7 +247,7 @@ class ChatDetailScreen extends HookConsumerWidget {
|
|||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
body: roomState.when(
|
body: roomState.when(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (error, _) => Center(child: Text('Error: $error')),
|
error: (error, _) => Center(child: Text('errorGeneric'.tr(args: [error.toString()]))),
|
||||||
data:
|
data:
|
||||||
(currentRoom) => CustomScrollView(
|
(currentRoom) => CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
@@ -359,6 +367,22 @@ class ChatDetailScreen extends HookConsumerWidget {
|
|||||||
: const Text('chatBreakNone').tr(),
|
: const Text('chatBreakNone').tr(),
|
||||||
onTap: () => showChatBreakDialog(),
|
onTap: () => showChatBreakDialog(),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.search),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
title: const Text('searchMessages').tr(),
|
||||||
|
subtitle: totalMessages.when(
|
||||||
|
data: (count) => Text('messagesCount'.tr(args: [count.toString()])),
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
|
error: (err, stack) => Text('errorGeneric'.tr(args: [err.toString()])),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
context.pushNamed('searchMessages', pathParameters: {'id': id});
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
@@ -692,7 +716,7 @@ class _ChatMemberListSheet extends HookConsumerWidget {
|
|||||||
? 'permissionModerator'
|
? 'permissionModerator'
|
||||||
: 'permissionMember',
|
: 'permissionMember',
|
||||||
).tr(),
|
).tr(),
|
||||||
Text('·').bold().padding(horizontal: 6),
|
Text('dotSeparator').bold().padding(horizontal: 6),
|
||||||
Expanded(child: Text("@${member.account.name}")),
|
Expanded(child: Text("@${member.account.name}")),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -852,7 +876,7 @@ class _ChatMemberRoleSheet extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
final newRole = int.parse(roleController.text);
|
final newRole = int.parse(roleController.text);
|
||||||
if (newRole < 0 || newRole > 100) {
|
if (newRole < 0 || newRole > 100) {
|
||||||
throw 'Role must be between 0 and 100';
|
throw 'roleValidationHint'.tr();
|
||||||
}
|
}
|
||||||
|
|
||||||
final apiClient = ref.read(apiClientProvider);
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
@@ -6,8 +6,8 @@ part of 'room_detail.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$chatMemberListNotifierHash() =>
|
String _$totalMessagesCountHash() =>
|
||||||
r'3ea30150278523e9f6b23f9200ea9a9fbae9c973';
|
r'a15c03461f25c2d4d39c0926509bf626ae2550a6';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
@@ -30,6 +30,128 @@ class _SystemHash {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// See also [totalMessagesCount].
|
||||||
|
@ProviderFor(totalMessagesCount)
|
||||||
|
const totalMessagesCountProvider = TotalMessagesCountFamily();
|
||||||
|
|
||||||
|
/// See also [totalMessagesCount].
|
||||||
|
class TotalMessagesCountFamily extends Family<AsyncValue<int>> {
|
||||||
|
/// See also [totalMessagesCount].
|
||||||
|
const TotalMessagesCountFamily();
|
||||||
|
|
||||||
|
/// See also [totalMessagesCount].
|
||||||
|
TotalMessagesCountProvider call(String roomId) {
|
||||||
|
return TotalMessagesCountProvider(roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TotalMessagesCountProvider getProviderOverride(
|
||||||
|
covariant TotalMessagesCountProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'totalMessagesCountProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [totalMessagesCount].
|
||||||
|
class TotalMessagesCountProvider extends AutoDisposeFutureProvider<int> {
|
||||||
|
/// See also [totalMessagesCount].
|
||||||
|
TotalMessagesCountProvider(String roomId)
|
||||||
|
: this._internal(
|
||||||
|
(ref) => totalMessagesCount(ref as TotalMessagesCountRef, roomId),
|
||||||
|
from: totalMessagesCountProvider,
|
||||||
|
name: r'totalMessagesCountProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$totalMessagesCountHash,
|
||||||
|
dependencies: TotalMessagesCountFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
TotalMessagesCountFamily._allTransitiveDependencies,
|
||||||
|
roomId: roomId,
|
||||||
|
);
|
||||||
|
|
||||||
|
TotalMessagesCountProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.roomId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String roomId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<int> Function(TotalMessagesCountRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: TotalMessagesCountProvider._internal(
|
||||||
|
(ref) => create(ref as TotalMessagesCountRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
roomId: roomId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<int> createElement() {
|
||||||
|
return _TotalMessagesCountProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is TotalMessagesCountProvider && other.roomId == roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, roomId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin TotalMessagesCountRef on AutoDisposeFutureProviderRef<int> {
|
||||||
|
/// The parameter `roomId` of this provider.
|
||||||
|
String get roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TotalMessagesCountProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<int>
|
||||||
|
with TotalMessagesCountRef {
|
||||||
|
_TotalMessagesCountProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get roomId => (origin as TotalMessagesCountProvider).roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$chatMemberListNotifierHash() =>
|
||||||
|
r'3ea30150278523e9f6b23f9200ea9a9fbae9c973';
|
||||||
|
|
||||||
abstract class _$ChatMemberListNotifier
|
abstract class _$ChatMemberListNotifier
|
||||||
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnChatMember>> {
|
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnChatMember>> {
|
||||||
late final String roomId;
|
late final String roomId;
|
||||||
|
139
lib/screens/chat/search_messages_screen.dart
Normal file
139
lib/screens/chat/search_messages_screen.dart
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/screens/chat/room.dart';
|
||||||
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
|
import 'package:island/widgets/chat/message_item.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||||
|
|
||||||
|
class SearchMessagesScreen extends HookConsumerWidget {
|
||||||
|
final String roomId;
|
||||||
|
|
||||||
|
const SearchMessagesScreen({super.key, required this.roomId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final searchController = useTextEditingController();
|
||||||
|
final withLinks = useState(false);
|
||||||
|
final withAttachments = useState(false);
|
||||||
|
|
||||||
|
final messagesNotifier = ref.read(
|
||||||
|
messagesNotifierProvider(roomId).notifier,
|
||||||
|
);
|
||||||
|
final messages = ref.watch(messagesNotifierProvider(roomId));
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
// Clear search when screen is disposed
|
||||||
|
return () {
|
||||||
|
messagesNotifier.clearSearch();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return AppScaffold(
|
||||||
|
appBar: AppBar(title: const Text('searchMessages').tr()),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'searchMessagesHint'.tr(),
|
||||||
|
border: InputBorder.none,
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
top: 12,
|
||||||
|
bottom: 16,
|
||||||
|
),
|
||||||
|
suffix: IconButton(
|
||||||
|
iconSize: 18,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
searchController.clear();
|
||||||
|
messagesNotifier.clearSearch();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (query) {
|
||||||
|
messagesNotifier.searchMessages(
|
||||||
|
query,
|
||||||
|
withLinks: withLinks.value,
|
||||||
|
withAttachments: withAttachments.value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: CheckboxListTile(
|
||||||
|
secondary: const Icon(Symbols.link),
|
||||||
|
title: const Text('searchLinks').tr(),
|
||||||
|
value: withLinks.value,
|
||||||
|
onChanged: (bool? value) {
|
||||||
|
withLinks.value = value!;
|
||||||
|
messagesNotifier.searchMessages(
|
||||||
|
searchController.text,
|
||||||
|
withLinks: withLinks.value,
|
||||||
|
withAttachments: withAttachments.value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: CheckboxListTile(
|
||||||
|
secondary: const Icon(Symbols.file_copy),
|
||||||
|
title: const Text('searchAttachments').tr(),
|
||||||
|
value: withAttachments.value,
|
||||||
|
onChanged: (bool? value) {
|
||||||
|
withAttachments.value = value!;
|
||||||
|
messagesNotifier.searchMessages(
|
||||||
|
searchController.text,
|
||||||
|
withLinks: withLinks.value,
|
||||||
|
withAttachments: withAttachments.value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Expanded(
|
||||||
|
child: messages.when(
|
||||||
|
data:
|
||||||
|
(messageList) =>
|
||||||
|
messageList.isEmpty
|
||||||
|
? Center(child: Text('noMessagesFound'.tr()))
|
||||||
|
: SuperListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
reverse: true, // Show newest messages at the bottom
|
||||||
|
itemCount: messageList.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final message = messageList[index];
|
||||||
|
// Simplified MessageItem for search results, no grouping logic
|
||||||
|
return MessageItem(
|
||||||
|
message: message,
|
||||||
|
isCurrentUser:
|
||||||
|
false, // Or determine based on actual user
|
||||||
|
onAction: null,
|
||||||
|
onJump: (_) {},
|
||||||
|
progress: null,
|
||||||
|
showAvatar: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (error, _) => Center(child: Text('errorGeneric'.tr(args: [error.toString()]))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -27,7 +27,9 @@ class AppDetailScreen extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final tabController = useTabController(initialLength: 2);
|
final tabController = useTabController(initialLength: 2);
|
||||||
final appData = ref.watch(customAppProvider(publisherName, projectId, appId));
|
final appData = ref.watch(
|
||||||
|
customAppProvider(publisherName, projectId, appId),
|
||||||
|
);
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -35,23 +37,43 @@ class AppDetailScreen extends HookConsumerWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.edit),
|
icon: const Icon(Symbols.edit),
|
||||||
onPressed: appData.value == null
|
onPressed:
|
||||||
? null
|
appData.value == null
|
||||||
: () {
|
? null
|
||||||
context.pushNamed(
|
: () {
|
||||||
'developerAppEdit',
|
context.pushNamed(
|
||||||
pathParameters: {
|
'developerAppEdit',
|
||||||
'name': publisherName,
|
pathParameters: {
|
||||||
'projectId': projectId,
|
'name': publisherName,
|
||||||
'id': appId,
|
'projectId': projectId,
|
||||||
},
|
'id': appId,
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
tabs: [Tab(text: 'overview'.tr()), Tab(text: 'secrets'.tr())],
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
'overview'.tr(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
'secrets'.tr(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: appData.when(
|
body: appData.when(
|
||||||
@@ -70,12 +92,14 @@ class AppDetailScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (err, stack) => ResponseErrorWidget(
|
error:
|
||||||
error: err,
|
(err, stack) => ResponseErrorWidget(
|
||||||
onRetry: () => ref.invalidate(
|
error: err,
|
||||||
customAppProvider(publisherName, projectId, appId),
|
onRetry:
|
||||||
),
|
() => ref.invalidate(
|
||||||
),
|
customAppProvider(publisherName, projectId, appId),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -98,12 +122,13 @@ class _AppOverview extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
child: app.background != null
|
child:
|
||||||
? CloudFileWidget(
|
app.background != null
|
||||||
item: app.background!,
|
? CloudFileWidget(
|
||||||
fit: BoxFit.cover,
|
item: app.background!,
|
||||||
)
|
fit: BoxFit.cover,
|
||||||
: const SizedBox.shrink(),
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 20,
|
left: 20,
|
||||||
|
@@ -52,7 +52,26 @@ class BotDetailScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
tabs: [Tab(text: 'overview'.tr()), Tab(text: 'keys'.tr())],
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
'overview'.tr(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
'keys'.tr(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: botData.when(
|
body: botData.when(
|
||||||
|
@@ -58,7 +58,26 @@ class ProjectDetailScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
tabs: [Tab(text: 'customApps'.tr()), Tab(text: 'bots'.tr())],
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
'customApps'.tr(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
'bots'.tr(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: TabBarView(
|
body: TabBarView(
|
||||||
|
@@ -143,8 +143,26 @@ class ArticlesScreen extends ConsumerWidget {
|
|||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
isScrollable: true,
|
isScrollable: true,
|
||||||
tabs: [
|
tabs: [
|
||||||
const Tab(text: 'All'),
|
Tab(
|
||||||
...feeds.map((feed) => Tab(text: feed.title)),
|
child: Text(
|
||||||
|
'All',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...feeds.map(
|
||||||
|
(feed) => Tab(
|
||||||
|
child: Text(
|
||||||
|
feed.title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -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.2.0+128
|
version: 3.2.0+129
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.2
|
sdk: ^3.7.2
|
||||||
@@ -139,6 +139,7 @@ dependencies:
|
|||||||
material_color_utilities: ^0.11.1
|
material_color_utilities: ^0.11.1
|
||||||
screenshot: ^3.0.0
|
screenshot: ^3.0.0
|
||||||
flutter_card_swiper: ^7.0.2
|
flutter_card_swiper: ^7.0.2
|
||||||
|
file_saver: ^0.3.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -235,4 +236,5 @@ msix_config:
|
|||||||
identity_name: dev.solian.app
|
identity_name: dev.solian.app
|
||||||
msix_version: 3.2.0.0
|
msix_version: 3.2.0.0
|
||||||
logo_path: .\assets\icons\icon.png
|
logo_path: .\assets\icons\icon.png
|
||||||
capabilities: internetClientServer, location, microphone, webcam
|
capabilities: internetClientServer, location, microphone, webcam
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user