diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..c30f4437 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:1.25 + +COPY build/web/ /data/webroot +COPY nginx.conf /etc/nginx/conf.d/default.conf \ No newline at end of file diff --git a/Makefile b/Makefile index cbd6d3ed..850c2d67 100644 --- a/Makefile +++ b/Makefile @@ -24,10 +24,16 @@ build-web: rm -fr build/web/fonts/ && mkdir build/web/fonts cp -r scripts/s build/web/fonts/s +build-web-samehost: + flutter build web --web-renderer canvaskit --release --dart-define=API_SERVER_URL=/ + cd scripts && go run main.go ../build/web/main.dart.js && cd .. + rm -fr build/web/fonts/ && mkdir build/web/fonts + cp -r scripts/s build/web/fonts/s + deploy-web: build-web cd build && tar -zcvf web.tar.gz web scp build/web.tar.gz huawei-1:/data/webroot ssh huawei-1 "cd /data/webroot && tar -zxvf web.tar.gz && rm -rf web.tar.gz app && mv web app" rm -fr build/web.tar.gz -.PHONY: run build-android build-macos ipa +.PHONY: run build-android build-macos ipa build-web-samehost build-web deploy-web diff --git a/docker-build.sh b/docker-build.sh new file mode 100755 index 00000000..3b553c5e --- /dev/null +++ b/docker-build.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +VERSION=1.0.6 +VERSION_DATE=202310091100 + +rm -fr build/web + +flutter build web --web-renderer canvaskit --release --dart-define=API_SERVER_URL=/ +cd scripts && go run main.go ../build/web/main.dart.js && cd .. +rm -fr build/web/fonts/ && mkdir build/web/fonts +cp -r scripts/s build/web/fonts/s + +docker build -t mylxsw/aidea-web:$VERSION . +docker tag mylxsw/aidea-web:$VERSION mylxsw/aidea-web:$VERSION_DATE +docker tag mylxsw/aidea-web:$VERSION mylxsw/aidea-web:latest + +docker push mylxsw/aidea-web:$VERSION +docker push mylxsw/aidea-web:$VERSION_DATE +docker push mylxsw/aidea-web:latest + diff --git a/lib/bloc/background_image_bloc.dart b/lib/bloc/background_image_bloc.dart index ec78c8dc..257f0cdd 100644 --- a/lib/bloc/background_image_bloc.dart +++ b/lib/bloc/background_image_bloc.dart @@ -1,4 +1,5 @@ import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/model/misc.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; diff --git a/lib/bloc/chat_chat_bloc.dart b/lib/bloc/chat_chat_bloc.dart index 32357523..c21f95b3 100644 --- a/lib/bloc/chat_chat_bloc.dart +++ b/lib/bloc/chat_chat_bloc.dart @@ -2,6 +2,7 @@ import 'package:askaide/helper/constant.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/chat_message_repo.dart'; import 'package:askaide/repo/model/chat_history.dart'; +import 'package:askaide/repo/model/misc.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; @@ -15,7 +16,7 @@ class ChatChatBloc extends Bloc { on((event, emit) async { final histories = await _chatMessageRepository.recentChatHistories( chatAnywhereRoomId, - 30, + 4, userId: APIServer().localUserID(), ); diff --git a/lib/bloc/chat_message_bloc.dart b/lib/bloc/chat_message_bloc.dart index 9470d7c5..44e042b6 100644 --- a/lib/bloc/chat_message_bloc.dart +++ b/lib/bloc/chat_message_bloc.dart @@ -15,6 +15,7 @@ import 'package:askaide/repo/model/message.dart'; import 'package:askaide/repo/model/room.dart'; import 'package:askaide/repo/openai_repo.dart'; import 'package:askaide/repo/settings_repo.dart'; +import 'package:dart_openai/openai.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -268,7 +269,7 @@ class ChatMessageBloc extends BlocExt { // 更新 Room 最后活跃时间 // 这里没有使用 await,因为不需要等待更新完成,让 room 的更新异步的去处理吧 - if (!Ability().supportAPIServer()) { + if (!Ability().enableAPIServer()) { chatMsgRepo.updateRoomLastActiveTime(roomId); } @@ -305,6 +306,7 @@ class ChatMessageBloc extends BlocExt { // 等待监听机器人应答消息 final queue = GracefulQueue(); try { + RequestFailedException? error; var listener = queue.listen(const Duration(milliseconds: 10), (items) { final systemCmds = items.where((e) => e.role == 'system').toList(); if (systemCmds.isNotEmpty) { @@ -335,6 +337,13 @@ class ChatMessageBloc extends BlocExt { .map((e) => e.content) .join(''); emit(ChatMessageUpdated(waitMessage, processing: true)); + + // 失败处理 + for (var e in items) { + if (e.code != null && e.code! > 0) { + error = RequestFailedException(e.error ?? '请求处理失败', e.code!); + } + } }); await ModelResolver.instance @@ -348,6 +357,15 @@ class ChatMessageBloc extends BlocExt { await listener; + waitMessage.text = waitMessage.text.trim(); + if (waitMessage.text.isEmpty) { + error = RequestFailedException('机器人没有回答任何内容', 500); + } + + if (error != null) { + throw error!; + } + // 机器人应答完成,将最后一条机器人应答消息更新到数据库,替换掉思考中消息 waitMessage.isReady = true; await chatMsgRepo.updateMessage(roomId, waitMessage.id!, waitMessage); @@ -388,11 +406,13 @@ class ChatMessageBloc extends BlocExt { chatHistory: localChatHistory, )); } catch (e) { + final error = resolveErrorMessage(e, isChat: true); await chatMsgRepo.updateMessagePart( roomId, sentMessageId, [ MessagePart('status', 2), + MessagePart('extra', jsonEncode({'error': error.toString()})), ], ); @@ -403,7 +423,7 @@ class ChatMessageBloc extends BlocExt { waitMessage.id!, Message( Role.receiver, - AppLocale.robotHasSomeError, + error.toString(), id: waitMessage.id, ts: DateTime.now(), type: MessageType.system, @@ -423,7 +443,7 @@ class ChatMessageBloc extends BlocExt { userId: APIServer().localUserID(), chatHistoryId: localChatHistoryId, ), - error: resolveErrorMessage(e), + error: error, chatHistory: localChatHistory, )); @@ -439,7 +459,7 @@ class ChatMessageBloc extends BlocExt { Future queryRoomById( ChatMessageRepository chatMsgRepo, int roomId) async { Room? room; - if (Ability().supportAPIServer()) { + if (Ability().enableAPIServer()) { final roomInServer = await APIServer().room(roomId: roomId); room = Room( roomInServer.name, diff --git a/lib/bloc/free_count_bloc.dart b/lib/bloc/free_count_bloc.dart index 9181ac86..3b45df5c 100644 --- a/lib/bloc/free_count_bloc.dart +++ b/lib/bloc/free_count_bloc.dart @@ -1,5 +1,6 @@ import 'package:askaide/helper/ability.dart'; import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/model/misc.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; @@ -12,8 +13,11 @@ class FreeCountBloc extends Bloc { FreeCountBloc() : super(FreeCountInitial()) { // 重新加载所有的模型免费使用次数 on((event, emit) async { - if (Ability().supportLocalOpenAI() || !Ability().supportAPIServer()) { - emit(FreeCountLoadedState(counts: counts)); + if (!Ability().enableAPIServer()) { + emit(FreeCountLoadedState( + counts: counts, + needSignin: event.checkSigninStatus, + )); return; } @@ -23,7 +27,8 @@ class FreeCountBloc extends Bloc { // 重新加载指定模型的免费使用次数 on((event, emit) async { - if (Ability().supportLocalOpenAI() || !Ability().supportAPIServer()) { + if (Ability().usingLocalOpenAIModel(event.model) || + !Ability().enableAPIServer()) { emit(FreeCountLoadedState(counts: counts)); return; } diff --git a/lib/bloc/free_count_event.dart b/lib/bloc/free_count_event.dart index cd917f03..f509fd33 100644 --- a/lib/bloc/free_count_event.dart +++ b/lib/bloc/free_count_event.dart @@ -8,4 +8,8 @@ class FreeCountReloadEvent extends FreeCountEvent { FreeCountReloadEvent({required this.model}); } -class FreeCountReloadAllEvent extends FreeCountEvent {} +class FreeCountReloadAllEvent extends FreeCountEvent { + final bool checkSigninStatus; + + FreeCountReloadAllEvent({this.checkSigninStatus = false}); +} diff --git a/lib/bloc/free_count_state.dart b/lib/bloc/free_count_state.dart index 453fbf91..98223fff 100644 --- a/lib/bloc/free_count_state.dart +++ b/lib/bloc/free_count_state.dart @@ -7,6 +7,7 @@ final class FreeCountInitial extends FreeCountState {} class FreeCountLoadedState extends FreeCountState { final List counts; + final bool needSignin; FreeModelCount? model(String model) { model = model.split(':').last; @@ -19,5 +20,5 @@ class FreeCountLoadedState extends FreeCountState { return null; } - FreeCountLoadedState({required this.counts}); + FreeCountLoadedState({required this.counts, this.needSignin = false}); } diff --git a/lib/bloc/gallery_bloc.dart b/lib/bloc/gallery_bloc.dart index 2f83a7e3..da0b6c42 100644 --- a/lib/bloc/gallery_bloc.dart +++ b/lib/bloc/gallery_bloc.dart @@ -29,7 +29,10 @@ class GalleryBloc extends Bloc { id: event.id, ); - emit(GalleryItemLoaded(item: res)); + emit(GalleryItemLoaded( + item: res.item, + isInternalUser: res.isInternalUser, + )); }); } } diff --git a/lib/bloc/gallery_state.dart b/lib/bloc/gallery_state.dart index dd36c0e8..a0b587b8 100644 --- a/lib/bloc/gallery_state.dart +++ b/lib/bloc/gallery_state.dart @@ -13,6 +13,7 @@ class GalleryLoaded extends GalleryState { class GalleryItemLoaded extends GalleryState { final CreativeGallery item; + final bool isInternalUser; - GalleryItemLoaded({required this.item}); + GalleryItemLoaded({required this.item, this.isInternalUser = false}); } diff --git a/lib/bloc/group_chat_bloc.dart b/lib/bloc/group_chat_bloc.dart new file mode 100644 index 00000000..1e6a3b39 --- /dev/null +++ b/lib/bloc/group_chat_bloc.dart @@ -0,0 +1,197 @@ +import 'dart:convert'; + +import 'package:askaide/helper/cache.dart'; +import 'package:askaide/helper/logger.dart'; +import 'package:askaide/page/component/chat/message_state_manager.dart'; +import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/model/group.dart'; +import 'package:askaide/repo/model/message.dart'; +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; + +part 'group_chat_event.dart'; +part 'group_chat_state.dart'; + +class GroupChatBloc extends Bloc { + var messages = []; + final MessageStateManager stateManager; + + GroupChatBloc({required this.stateManager}) : super(GroupChatInitial()) { + // 加载聊天组 + on((event, emit) async { + final group = + await APIServer().chatGroup(event.groupId, cache: !event.forceUpdate); + final states = await stateManager.loadRoomStates(event.groupId); + + emit(GroupChatLoaded( + group: group, + states: states, + defaultChatMembers: await loadDefaultChatMembers(event.groupId), + )); + }); + + // 加载聊天组聊天记录 + on((event, emit) async { + if (event.isInitRequest) { + try { + final cached = + await Cache().stringGet(key: 'group:speed:${event.groupId}'); + if (cached != null) { + final messages = (jsonDecode(cached) as List) + .map((e) => GroupMessage.fromJson(e)) + .toList(); + + emit(GroupChatMessagesLoaded(messages: messages)); + } + } catch (e) { + Logger.instance.e(e); + } + } + + await refreshGroupMessages( + event.groupId, + startId: event.startId, + forceRefresh: true, + ); + + emit(GroupChatMessagesLoaded(messages: messages)); + }); + + // 发送聊天组消息 + on((event, emit) async { + try { + final resp = await APIServer().chatGroupSendMessage( + event.groupId, + GroupChatSendRequest( + message: event.message, + memberIds: event.members, + ), + ); + + Logger.instance.d(resp.toJson()); + + // 记录默认聊天成员 + updateDefaultChatMembers( + event.groupId, + resp.tasks.map((e) => e.memberId).toList(), + ).then((members) { + emit(GroupDefaultMemberSelected(members)); + }); + + await refreshGroupMessages( + event.groupId, + startId: 0, + forceRefresh: true, + ); + emit(GroupChatMessagesLoaded(messages: messages)); + } catch (e) { + await refreshGroupMessages( + event.groupId, + startId: 0, + forceRefresh: true, + ); + emit(GroupChatMessagesLoaded(messages: messages, error: e)); + } + }); + + // 发送系统消息 + on((event, emit) async { + try { + final resp = await APIServer().chatGroupSendSystemMessage( + event.groupId, + messageType: event.type.getTypeText(), + message: event.message, + ); + + Logger.instance.d(resp.toJson()); + } finally { + await refreshGroupMessages( + event.groupId, + startId: 0, + forceRefresh: true, + ); + emit(GroupChatMessagesLoaded(messages: messages)); + } + }); + + // 更新聊天组消息状态 + on((event, emit) async { + final waitMessageIds = messages + .where((msg) => msg.status == groupMessageStatusWaiting) + .map((msg) => msg.id) + .toList(); + + if (waitMessageIds.isEmpty) { + return; + } + + final resp = await APIServer() + .chatGroupMessageStatus(event.groupId, waitMessageIds); + final newMessageStatusMap = {}; + for (var msg in resp) { + newMessageStatusMap[msg.id] = msg; + } + + for (var i = 0; i < messages.length; i++) { + final msg = messages[i]; + if (newMessageStatusMap.containsKey(msg.id)) { + messages[i] = newMessageStatusMap[msg.id]!; + } + } + + emit(GroupChatMessagesLoaded(messages: messages)); + }); + + // 清空聊天组消息 + on((event, emit) async { + await APIServer().chatGroupDeleteAllMessages(event.groupId); + messages.clear(); + emit(GroupChatMessagesLoaded(messages: messages)); + }); + + // 删除聊天组消息 + on((event, emit) async { + await APIServer().chatGroupDeleteMessage(event.groupId, event.messageId); + messages.removeWhere((msg) => msg.id == event.messageId); + emit(GroupChatMessagesLoaded(messages: messages)); + }); + } + + refreshGroupMessages( + int groupId, { + int startId = 0, + bool forceRefresh = false, + }) async { + final data = await APIServer() + .chatGroupMessages(groupId, startId: startId, cache: !forceRefresh); + messages = data.data.reversed.toList(); + + if (startId == 0) { + Cache() + .setString(key: 'group:speed:$groupId', value: jsonEncode(messages)); + } + } + + Future> loadDefaultChatMembers(int groupId) async { + final defaultMembers = + await Cache().stringGet(key: 'group:$groupId:default-members'); + + return (defaultMembers ?? '') + .split(',') + .map((e) => int.tryParse(e) ?? 0) + .where((e) => e > 0) + .toList(); + } + + Future> updateDefaultChatMembers( + int groupId, List members) async { + // 记录默认聊天成员 + await Cache().setString( + key: 'group:$groupId:default-members', + value: members.join(','), + duration: const Duration(days: 365), + ); + + return members; + } +} diff --git a/lib/bloc/group_chat_event.dart b/lib/bloc/group_chat_event.dart new file mode 100644 index 00000000..00cb4f8f --- /dev/null +++ b/lib/bloc/group_chat_event.dart @@ -0,0 +1,58 @@ +part of 'group_chat_bloc.dart'; + +@immutable +sealed class GroupChatEvent {} + +class GroupChatLoadEvent extends GroupChatEvent { + final int groupId; + final bool forceUpdate; + + GroupChatLoadEvent(this.groupId, {this.forceUpdate = false}); +} + +class GroupChatMessagesLoadEvent extends GroupChatEvent { + final int groupId; + final int startId; + final bool isInitRequest; + + GroupChatMessagesLoadEvent( + this.groupId, { + this.startId = 0, + this.isInitRequest = false, + }); +} + +class GroupChatSendEvent extends GroupChatEvent { + final int groupId; + final String message; + final List members; + + GroupChatSendEvent(this.groupId, this.message, this.members); +} + +class GroupChatUpdateMessageStatusEvent extends GroupChatEvent { + final int groupId; + + GroupChatUpdateMessageStatusEvent(this.groupId); +} + +class GroupChatSendSystemEvent extends GroupChatEvent { + final int groupId; + final String? message; + final MessageType type; + + GroupChatSendSystemEvent(this.groupId, this.type, {this.message}); +} + +class GroupChatDeleteAllEvent extends GroupChatEvent { + final int groupId; + + GroupChatDeleteAllEvent(this.groupId); +} + +class GroupChatDeleteEvent extends GroupChatEvent { + final int groupId; + final int messageId; + + GroupChatDeleteEvent(this.groupId, this.messageId); +} diff --git a/lib/bloc/group_chat_state.dart b/lib/bloc/group_chat_state.dart new file mode 100644 index 00000000..d4055242 --- /dev/null +++ b/lib/bloc/group_chat_state.dart @@ -0,0 +1,39 @@ +part of 'group_chat_bloc.dart'; + +@immutable +sealed class GroupChatState {} + +final class GroupChatInitial extends GroupChatState {} + +class GroupChatLoaded extends GroupChatState { + final ChatGroup group; + final Map states; + final List? defaultChatMembers; + + GroupChatLoaded({ + required this.group, + required this.states, + this.defaultChatMembers, + }); +} + +class GroupDefaultMemberSelected extends GroupChatState { + final List members; + + GroupDefaultMemberSelected(this.members); +} + +class GroupChatMessagesLoaded extends GroupChatState { + final List messages; + final Object? _error; + + get error => _error; + + bool get hasWaitTasks => + messages.any((element) => element.status == groupMessageStatusWaiting); + + GroupChatMessagesLoaded({ + required this.messages, + Object? error, + }) : _error = error; +} diff --git a/lib/bloc/room_bloc.dart b/lib/bloc/room_bloc.dart index 43399c45..405c7b09 100644 --- a/lib/bloc/room_bloc.dart +++ b/lib/bloc/room_bloc.dart @@ -3,6 +3,8 @@ import 'package:askaide/helper/constant.dart'; import 'package:askaide/page/component/chat/message_state_manager.dart'; import 'package:askaide/repo/api/room_gallery.dart'; import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/model/group.dart'; +import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/model/room.dart'; import 'package:askaide/bloc/bloc_manager.dart'; @@ -32,11 +34,11 @@ class RoomBloc extends BlocExt { id: event.roomId, ), const {}, - cascading: event.cascading, + cascading: false, )); } - if (Ability().supportAPIServer()) { + if (Ability().enableAPIServer()) { final room = await APIServer().room(roomId: event.roomId); if (event.chatHistoryId != null && event.chatHistoryId! > 0) { final chatHistory = @@ -61,9 +63,10 @@ class RoomBloc extends BlocExt { maxContext: room.maxContext, avatarId: room.avatarId, avatarUrl: room.avatarUrl, + roomType: room.roomType, ), const {}, - cascading: event.cascading, + cascading: false, )); final states = await stateManager.loadRoomStates(event.roomId); @@ -83,6 +86,7 @@ class RoomBloc extends BlocExt { maxContext: room.maxContext, avatarId: room.avatarId, avatarUrl: room.avatarUrl, + roomType: room.roomType, ), states, examples: await APIServer().example('${room.vendor}:${room.model}'), @@ -113,7 +117,9 @@ class RoomBloc extends BlocExt { // 加载聊天室列表 on((event, emit) async { - emit(RoomsLoading()); + if (!event.forceRefresh) { + emit(RoomsLoading()); + } emit(await createRoomsLoadedState(cache: !event.forceRefresh)); }); @@ -122,7 +128,7 @@ class RoomBloc extends BlocExt { emit(RoomsLoading()); try { - if (Ability().supportAPIServer()) { + if (Ability().enableAPIServer()) { await APIServer().createRoom( name: event.name, vendor: event.model.split(':').first, @@ -155,7 +161,7 @@ class RoomBloc extends BlocExt { emit(RoomsLoading()); try { - if (Ability().supportAPIServer()) { + if (Ability().enableAPIServer()) { await APIServer().deleteRoom(roomId: event.roomId); } else { var room = await chatMsgRepo.room(event.roomId); @@ -174,7 +180,7 @@ class RoomBloc extends BlocExt { // 更新聊天室信息 on((event, emit) async { - if (Ability().supportAPIServer()) { + if (Ability().enableAPIServer()) { final room = await APIServer().updateRoom( roomId: event.roomId, name: event.name!, @@ -204,6 +210,7 @@ class RoomBloc extends BlocExt { avatarId: room.avatarId, avatarUrl: room.avatarUrl, initMessage: room.initMessage, + roomType: room.roomType, ), states, examples: await APIServer().example(room.model), @@ -263,11 +270,48 @@ class RoomBloc extends BlocExt { emit(RoomGalleriesLoaded(const [], error: e)); } }); + + // 创建群聊聊天室 + on((event, emit) async { + emit(RoomsLoading()); + + try { + await APIServer().createGroupRoom( + name: event.name, + avatarUrl: event.avatarUrl, + members: event.members, + ); + + emit(GroupRoomUpdateResultState(true)); + emit(await createRoomsLoadedState(cache: false)); + } catch (e) { + emit(GroupRoomUpdateResultState(false, error: e)); + emit(RoomsLoaded(const [], error: e.toString())); + } + }); + + // 群聊聊天室更新 + on((event, emit) async { + emit(RoomsLoading()); + + try { + await APIServer().updateGroupRoom( + groupId: event.groupId, + name: event.name, + avatarUrl: event.avatarUrl, + members: event.members, + ); + + emit(GroupRoomUpdateResultState(true)); + } catch (e) { + emit(GroupRoomUpdateResultState(false, error: e)); + } + }); } Future createRoomsLoadedState({bool cache = true}) async { try { - if (Ability().supportAPIServer()) { + if (Ability().enableAPIServer()) { final resp = await APIServer().rooms(cache: cache); return RoomsLoaded( resp.rooms @@ -284,6 +328,8 @@ class RoomBloc extends BlocExt { model: '${room.vendor}:${room.model}', avatarId: room.avatarId, avatarUrl: room.avatarUrl, + roomType: room.roomType, + members: room.members, )) .toList(), suggests: resp.suggests ?? [], diff --git a/lib/bloc/room_event.dart b/lib/bloc/room_event.dart index 8fce8999..44500cc6 100644 --- a/lib/bloc/room_event.dart +++ b/lib/bloc/room_event.dart @@ -29,6 +29,32 @@ class RoomCreateEvent extends RoomEvent { }); } +class GroupRoomCreateEvent extends RoomEvent { + final String name; + final String? avatarUrl; + final List? members; + + GroupRoomCreateEvent({ + required this.name, + this.avatarUrl, + this.members, + }); +} + +class GroupRoomUpdateEvent extends RoomEvent { + final int groupId; + final String name; + final String? avatarUrl; + final List? members; + + GroupRoomUpdateEvent({ + required this.groupId, + required this.name, + this.avatarUrl, + this.members, + }); +} + class RoomDeleteEvent extends RoomEvent { final int roomId; diff --git a/lib/bloc/room_state.dart b/lib/bloc/room_state.dart index 96bfdf8c..b822eb75 100644 --- a/lib/bloc/room_state.dart +++ b/lib/bloc/room_state.dart @@ -44,3 +44,10 @@ class RoomGalleriesLoaded extends RoomState { RoomGalleriesLoaded(this.galleries, {this.error, this.tags = const []}); } + +class GroupRoomUpdateResultState extends RoomState { + final bool success; + final Object? error; + + GroupRoomUpdateResultState(this.success, {this.error}); +} diff --git a/lib/bloc/version_bloc.dart b/lib/bloc/version_bloc.dart index eef8e992..de07f2e3 100644 --- a/lib/bloc/version_bloc.dart +++ b/lib/bloc/version_bloc.dart @@ -1,4 +1,5 @@ import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/model/misc.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; diff --git a/lib/helper/ability.dart b/lib/helper/ability.dart index d4c36ce1..2e56be09 100644 --- a/lib/helper/ability.dart +++ b/lib/helper/ability.dart @@ -5,7 +5,7 @@ import 'package:askaide/repo/settings_repo.dart'; class Ability { late final SettingRepository setting; - late final Capabilities capabilities; + late Capabilities capabilities; init(SettingRepository setting, Capabilities capabilities) { this.setting = setting; @@ -20,14 +20,55 @@ class Ability { return _instance; } + /// 是否支持 Websocket + bool supportWebSocket() { + return capabilities.supportWebsocket; + } + + /// 更新能力 + updateCapabilities(Capabilities capabilities) { + this.capabilities = capabilities; + } + /// 首页支持的模型列表 List get homeModels { return capabilities.homeModels; } + /// 是否显示首页模型描述 + bool get showHomeModelDescription { + return capabilities.showHomeModelDescription; + } + + /// 首页路由 + String get homeRoute { + return capabilities.homeRoute; + } + + /// 是否支持绘玩 + bool get enableGallery { + return !capabilities.disableGallery; + } + + /// 是否支持创作岛 + bool get enableCreationIsland { + return !capabilities.disableCreationIsland; + } + + /// 是否支持数字人 + bool get enableDigitalHuman { + return !capabilities.disableDigitalHuman; + } + + /// 是否支持聊一聊 + bool get enableChat { + return !capabilities.disableChat; + } + /// 是否支持 OpenAI bool get enableOpenAI { - return capabilities.openaiEnabled; + return capabilities.openaiEnabled && + (!capabilities.disableChat || !capabilities.disableDigitalHuman); } /// 是否支持支付宝 @@ -54,15 +95,21 @@ class Ability { } /// 是否支持API Server - bool supportAPIServer() { + bool enableAPIServer() { return setting.stringDefault(settingAPIServerToken, '') != ''; } /// 是否启用了 OpenAI 自定义设置 - bool supportLocalOpenAI() { + bool enableLocalOpenAI() { return setting.boolDefault(settingOpenAISelfHosted, false); } + /// 是否使用本地的 OpenAI 模型 + bool usingLocalOpenAIModel(String model) { + return setting.boolDefault(settingOpenAISelfHosted, false) && + (model.startsWith('openai:') || model.startsWith('gpt-')); + } + /// 是否支持翻译功能 bool supportTranslate() { return false; @@ -72,6 +119,10 @@ class Ability { /// 是否支持语音合成功能 bool supportSpeak() { // return setting.stringDefault(settingAPIServerToken, '') != ''; + if (PlatformTool.isWeb()) { + return false; + } + return true; } diff --git a/lib/helper/cache.dart b/lib/helper/cache.dart index 56a42beb..de773e3d 100644 --- a/lib/helper/cache.dart +++ b/lib/helper/cache.dart @@ -42,4 +42,16 @@ class Cache { }) async { await cacheRepo.set(key, value.toString(), duration); } + + Future setString({ + required String key, + required String value, + Duration duration = const Duration(days: 1), + }) async { + await cacheRepo.set(key, value, duration); + } + + Future stringGet({required String key}) async { + return await cacheRepo.get(key); + } } diff --git a/lib/helper/constant.dart b/lib/helper/constant.dart index aeb428f5..c564ec3a 100644 --- a/lib/helper/constant.dart +++ b/lib/helper/constant.dart @@ -1,17 +1,13 @@ import 'package:flutter/material.dart'; // 客户端应用版本号 -const clientVersion = '1.0.6'; +const clientVersion = '1.0.7'; // 本地数据库版本号 const databaseVersion = 25; const maxRoomNumForNonVIP = 50; const coinSign = '个'; -// API 服务器地址 -const apiServerURL = 'https://ai-api.aicode.cc'; -// const apiServerURL = 'http://localhost'; - const settingAPIServerToken = 'api-token'; const settingUserInfo = 'user-info'; const settingUsingGuestMode = 'using-guest-mode'; diff --git a/lib/helper/env.dart b/lib/helper/env.dart new file mode 100644 index 00000000..0346c798 --- /dev/null +++ b/lib/helper/env.dart @@ -0,0 +1,18 @@ +/// 默认 API 服务器地址 +/// 注意:当你使用自己的服务器时,请修改该地址为你自己的服务器地址 +const defaultAPIServerURL = 'https://ai-api.aicode.cc'; + +/// API 服务器地址 +String get apiServerURL { + var url = const String.fromEnvironment( + 'API_SERVER_URL', + defaultValue: defaultAPIServerURL, + ); + + // 当配置的 URL 为 / 时,自动替换为空,用于 Web 端 + if (url == '/') { + return ''; + } + + return url; +} diff --git a/lib/helper/error.dart b/lib/helper/error.dart index 2d96d0ea..12b301dd 100644 --- a/lib/helper/error.dart +++ b/lib/helper/error.dart @@ -2,10 +2,10 @@ import 'package:askaide/helper/ability.dart'; import 'package:askaide/lang/lang.dart'; import 'package:dart_openai/openai.dart'; -Object resolveErrorMessage(dynamic e) { +Object resolveErrorMessage(dynamic e, {bool isChat = false}) { // TODO if (e is RequestFailedException) { - final msg = resolveHTTPStatusCode(e.statusCode); + final msg = resolveHTTPStatusCode(e.statusCode, isChat: isChat); if (msg != null) { return msg; } @@ -16,21 +16,31 @@ Object resolveErrorMessage(dynamic e) { return e.toString(); } -Object? resolveHTTPStatusCode(int statusCode) { +Object? resolveHTTPStatusCode(int statusCode, {bool isChat = false}) { switch (statusCode) { case 400: return const LanguageText('请求参数错误'); case 401: - if (Ability().supportLocalOpenAI()) { + if (Ability().enableLocalOpenAI()) { return const LanguageText(AppLocale.openAIAuthFailed); } - if (Ability().supportAPIServer()) { + if (Ability().enableAPIServer()) { return const LanguageText(AppLocale.accountNeedReSignin, action: 're-signin'); } return const LanguageText(AppLocale.signInRequired, action: 'sign-in'); + case 404: + if (isChat) { + return const LanguageText(AppLocale.modelNotFound); + } + break; + case 429: + if (isChat) { + return const LanguageText(AppLocale.tooManyRequestsOrPaymentRequired); + } + return const LanguageText(AppLocale.tooManyRequests); case 451: return const LanguageText(AppLocale.modelNotValid); case 402: diff --git a/lib/helper/http.dart b/lib/helper/http.dart index 5a1d5294..75babc3e 100644 --- a/lib/helper/http.dart +++ b/lib/helper/http.dart @@ -1,4 +1,4 @@ -import 'package:askaide/helper/constant.dart'; +import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:dio/dio.dart'; @@ -88,6 +88,25 @@ class HttpClient { return resp; } + static Future postJSON( + String url, { + Map? queryParameters, + Map? data, + Options? options, + }) async { + final resp = await dio.post( + url, + queryParameters: queryParameters, + data: data, + options: options, + ); + // print("======================="); + // print("request: $url"); + // print("response: ${resp.data}"); + + return resp; + } + static Future put( String url, { Map? queryParameters, @@ -102,6 +121,20 @@ class HttpClient { ); } + static Future putJSON( + String url, { + Map? queryParameters, + Map? data, + Options? options, + }) async { + return await dio.put( + url, + queryParameters: queryParameters, + data: data, + options: options, + ); + } + static Future delete( String url, { Map? queryParameters, diff --git a/lib/helper/model.dart b/lib/helper/model.dart index 01d5461b..08a06793 100644 --- a/lib/helper/model.dart +++ b/lib/helper/model.dart @@ -19,23 +19,32 @@ class ModelAggregate { settings.stringDefault(settingAPIServerToken, '') != ''; final selfHostOpenAI = settings.boolDefault(settingOpenAISelfHosted, false); - if (isAPIServerSet && !selfHostOpenAI) { + if (isAPIServerSet) { models.addAll((await APIServer().models()) .map( (e) => mm.Model( e.id.split(':').last, e.name, e.category, + shortName: e.shortName, description: e.description, isChatModel: e.isChat, disabled: e.disabled, category: e.category, tag: e.tag, + avatarUrl: e.avatarUrl, ), ) .toList()); - } else { - models.addAll(OpenAIRepository.supportModels()); + } + + if (selfHostOpenAI) { + return [ + ...OpenAIRepository.supportModels(), + ...models + .where((element) => element.category != modelTypeOpenAI) + .toList() + ]; } // if (isAPIServerSet || diff --git a/lib/lang/lang.dart b/lib/lang/lang.dart index 855a017f..3a82c68c 100644 --- a/lib/lang/lang.dart +++ b/lib/lang/lang.dart @@ -84,12 +84,14 @@ mixin AppLocale { static const String quotaExceeded = 'quota-exceeded'; static const String internalServerError = 'internal-server-error'; static const String badGateway = 'bad-gateway'; + static const String emptyResponse = 'empty-response'; static const String modelNotValid = 'model-not-valid'; static const String signInRequired = 'sign-in-required'; static const String accountNeedReSignin = 'account-need-re-signin'; static const String confirmToDeleteRoom = 'confirm-to-delete-room'; static const String confirmSend = 'confirm-send'; static const String openAIAuthFailed = 'openai-auth-failed'; + static const String modelNotFound = 'model-not-found'; static const String nameRequiredMessage = 'name-required-message'; static const String promptFormatError = 'prompt-format-error'; @@ -113,6 +115,8 @@ mixin AppLocale { static const String generateResult = 'generate-result'; static const String generateExitConfirm = 'generate-exit-confirm'; static const String tooManyRequests = 'too-many-requests'; + static const String tooManyRequestsOrPaymentRequired = + 'too-many-requests-or-payment-required'; static const String promptHint = 'prompt-hint'; static const String confirmClearCache = 'confirm-clear-cache'; static const String confirmSignOut = 'confirm-sign-out'; @@ -198,6 +202,8 @@ mixin AppLocale { static const String toPay = 'to-pay'; static const String discover = 'discover'; + static const String customHomeModels = 'custom-home-models'; + static const Map zh = { required: '必填', systemInfo: '系统信息', @@ -219,7 +225,7 @@ mixin AppLocale { clearChatHistory: '清空聊天记录', examples: '示例', continueMessage: '继续', - messageInputTips: '有问题尽管问我...', + messageInputTips: '有问题尽管问我', uploadImage: '上传图片', longPressSpeak: '长按说话', send: '发送', @@ -264,7 +270,7 @@ mixin AppLocale { search: '搜索', background: '背景', backgroundSetting: '背景图', - roomSetting: '数字人设置', + roomSetting: '设置', chatHistory: '聊天记录', confirmSend: '确定发送以下内容?', questionExamples: '问题示例', @@ -278,7 +284,7 @@ mixin AppLocale { modelRequiredMessage: '请选择 AI 模型', operateSuccess: '操作成功', operateFailed: '操作失败', - confirmDelete: '确定要删除这些项目?', + confirmDelete: '确定删除?', confirmStartNewChat: '确定要开始新的对话?', confirmClearMessages: '确定要清空聊天记录?', quotaExceeded: '智慧果数量不足,请先购买', @@ -288,7 +294,8 @@ mixin AppLocale { signInRequired: '您尚未登录,请先登录', accountNeedReSignin: '账号异常,请重新登录', openAIAuthFailed: '您启用了自定义 OpenAI 服务,请检查 API Key 是否正确', - confirmToDeleteRoom: '确定删除该数字人?', + modelNotFound: '当前模型尚未开通,暂时无法使用', + confirmToDeleteRoom: '确定删除?', writeYourIdeas: '你的想法', describeYourImages: '你的想法', excludeContents: '反向提示词', @@ -307,10 +314,12 @@ mixin AppLocale { generateResult: '创作结果', generateExitConfirm: '创作中...\n退出后,可在历史记录中查看结果', tooManyRequests: '操作过于频繁,请稍后再试', + tooManyRequestsOrPaymentRequired: + '操作过于频繁(如果您使用了自定义的 OpenAI Keys,请登录 https://platform.openai.com 检查账户余额是否充足)', promptHint: '设定该数字人的角色和技能,以便为你提供更精准有效的信息。', confirmClearCache: '确定要清除缓存吗?', confirmSignOut: '确定要退出登录吗?', - askMeAnyQuestion: '有问题尽管问我~', + askMeAnyQuestion: '有问题尽管问我', askMeLikeThis: '可以这样问我:', refresh: '换一换', fastAndCostEffective: '速度快,成本低', @@ -386,6 +395,7 @@ mixin AppLocale { coinUnit: '个', toPay: '立即支付', discover: '绘玩', + customHomeModels: '常用模型', }; static const Map en = { @@ -455,7 +465,7 @@ mixin AppLocale { search: 'Search', background: 'Background', backgroundSetting: 'Background Setting', - roomSetting: 'Character Setting', + roomSetting: 'Setting', chatHistory: 'Histories', confirmSend: 'Confirm to send?', questionExamples: 'Question Examples', @@ -481,6 +491,8 @@ mixin AppLocale { accountNeedReSignin: 'Account exception, please log in again', openAIAuthFailed: 'You have enabled custom OpenAI service, please check if the API Key is correct', + modelNotFound: + 'The current model is not enabled yet, please try again later', confirmToDeleteRoom: 'Confirm to delete the character?', writeYourIdeas: 'Your ideas', describeYourImages: 'Your ideas', @@ -501,6 +513,8 @@ mixin AppLocale { generateExitConfirm: 'Generating...\nYou can view the result in the history', tooManyRequests: 'Too many requests, please try again later', + tooManyRequestsOrPaymentRequired: + 'Too many requests (If you are using your own OpenAI Keys, please log in to https://platform.openai.com to check if your account balance is sufficient)', promptHint: 'Set the role and skills of the character so that it can provide more accurate and effective information for you.', confirmClearCache: 'Confirm to clear cache?', @@ -589,6 +603,7 @@ mixin AppLocale { coinUnit: '', toPay: 'To pay', discover: 'Discover', + customHomeModels: 'Favorite Models', }; } @@ -596,6 +611,11 @@ class LanguageText { final String message; final String? action; const LanguageText(this.message, {this.action}); + + @override + String toString() { + return message; + } } final languages = { diff --git a/lib/main.dart b/lib/main.dart index e4676fcd..6470800f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:askaide/bloc/chat_chat_bloc.dart'; import 'package:askaide/bloc/creative_island_bloc.dart'; import 'package:askaide/bloc/free_count_bloc.dart'; import 'package:askaide/bloc/gallery_bloc.dart'; +import 'package:askaide/bloc/group_chat_bloc.dart'; import 'package:askaide/bloc/payment_bloc.dart'; import 'package:askaide/bloc/version_bloc.dart'; import 'package:askaide/helper/ability.dart'; @@ -14,43 +15,45 @@ import 'package:askaide/helper/model_resolver.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/data/migrate.dart'; -import 'package:askaide/page/account_security.dart'; +import 'package:askaide/page/balance/quota_usage_details.dart'; +import 'package:askaide/page/setting/account_security.dart'; import 'package:askaide/page/app_scaffold.dart'; -import 'package:askaide/page/avatar_selector.dart'; -import 'package:askaide/page/background_selector.dart'; -import 'package:askaide/page/bind_phone_page.dart'; -import 'package:askaide/page/change_password.dart'; -import 'package:askaide/page/chat_anywhere.dart'; -import 'package:askaide/page/chat_chat.dart'; -import 'package:askaide/page/chat_room_create.dart'; +import 'package:askaide/page/lab/avatar_selector.dart'; +import 'package:askaide/page/setting/background_selector.dart'; +import 'package:askaide/page/setting/bind_phone_page.dart'; +import 'package:askaide/page/setting/change_password.dart'; +import 'package:askaide/page/chat/home_chat.dart'; +import 'package:askaide/page/chat/home.dart'; +import 'package:askaide/page/chat/home_chat_history.dart'; +import 'package:askaide/page/chat/room_create.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/transition_resolver.dart'; -import 'package:askaide/page/creative_island/creative_island.dart'; -import 'package:askaide/page/creative_island/creative_island_create_page.dart'; -import 'package:askaide/page/creative_island/creative_island_gallery.dart'; -import 'package:askaide/page/creative_island/creative_island_history.dart'; -import 'package:askaide/page/creative_island/creative_island_history_all.dart'; -import 'package:askaide/page/creative_island/creative_island_history_preview.dart'; -import 'package:askaide/page/free_statistics.dart'; +import 'package:askaide/page/creative_island/my_creation.dart'; +import 'package:askaide/page/creative_island/my_creation_item.dart'; +import 'package:askaide/page/setting/custom_home_models.dart'; +import 'package:askaide/page/balance/free_statistics.dart'; +import 'package:askaide/page/chat/group/chat.dart'; +import 'package:askaide/page/chat/group/create.dart'; +import 'package:askaide/page/chat/group/edit.dart'; import 'package:askaide/page/lab/creative_models.dart'; -import 'package:askaide/page/destroy_account.dart'; -import 'package:askaide/page/diagnosis.dart'; -import 'package:askaide/page/draw/draw.dart'; -import 'package:askaide/page/draw/draw_create.dart'; -import 'package:askaide/page/draw/image_edit_direct.dart'; +import 'package:askaide/page/setting/destroy_account.dart'; +import 'package:askaide/page/setting/diagnosis.dart'; +import 'package:askaide/page/creative_island/draw/draw_list.dart'; +import 'package:askaide/page/creative_island/draw/draw_create.dart'; +import 'package:askaide/page/creative_island/draw/image_edit_direct.dart'; import 'package:askaide/page/lab/draw_board.dart'; -import 'package:askaide/page/gallery/gallery.dart'; -import 'package:askaide/page/gallery/gallery_item.dart'; -import 'package:askaide/page/openai_setting.dart'; -import 'package:askaide/page/payment.dart'; -import 'package:askaide/page/prompt.dart'; -import 'package:askaide/page/quota_usage_statistics.dart'; -import 'package:askaide/page/signin_or_signup.dart'; -import 'package:askaide/page/signin_screen.dart'; +import 'package:askaide/page/creative_island/gallery/gallery.dart'; +import 'package:askaide/page/creative_island/gallery/gallery_item.dart'; +import 'package:askaide/page/setting/openai_setting.dart'; +import 'package:askaide/page/balance/payment.dart'; +import 'package:askaide/page/lab/prompt.dart'; +import 'package:askaide/page/balance/quota_usage_statistics.dart'; +import 'package:askaide/page/auth/signin_or_signup.dart'; +import 'package:askaide/page/auth/signin_screen.dart'; import 'package:askaide/page/component/chat/message_state_manager.dart'; -import 'package:askaide/page/quota_detail_screen.dart'; -import 'package:askaide/page/retrieve_password_screen.dart'; -import 'package:askaide/page/signup_screen.dart'; +import 'package:askaide/page/balance/payment_history.dart'; +import 'package:askaide/page/setting/retrieve_password_screen.dart'; +import 'package:askaide/page/auth/signup_screen.dart'; import 'package:askaide/page/lab/user_center.dart'; import 'package:askaide/repo/api/info.dart'; import 'package:askaide/repo/api_server.dart'; @@ -63,20 +66,21 @@ import 'package:askaide/repo/deepai_repo.dart'; import 'package:askaide/repo/stabilityai_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:askaide/helper/constant.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:fluwx/fluwx.dart'; import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:askaide/bloc/bloc_manager.dart'; import 'package:askaide/bloc/chat_message_bloc.dart'; import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/bloc/notify_bloc.dart'; -import 'package:askaide/page/chat_room_setting.dart'; -import 'package:askaide/page/chat_screen.dart'; -import 'package:askaide/page/home_screen.dart'; -import 'package:askaide/page/setting_screen.dart'; +import 'package:askaide/page/chat/room_edit.dart'; +import 'package:askaide/page/chat/room_chat.dart'; +import 'package:askaide/page/chat/rooms.dart'; +import 'package:askaide/page/setting/setting_screen.dart'; import 'package:askaide/repo/data/chat_message_data.dart'; import 'package:askaide/repo/chat_message_repo.dart'; import 'package:askaide/repo/data/room_data.dart'; @@ -86,7 +90,7 @@ import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; -import 'page/theme/theme.dart'; +import 'page/component/theme/theme.dart'; import 'package:sizer/sizer.dart'; import 'package:askaide/helper/http.dart' as httpx; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; @@ -169,7 +173,7 @@ void main() async { // 从服务器获取客户端支持的能力清单 try { - final capabilities = await APIServer().capabilities(); + final capabilities = await APIServer().capabilities(cache: false); Ability().init(settingRepo, capabilities); } catch (e) { Logger.instance.e('获取客户端能力清单失败', error: e); @@ -182,6 +186,9 @@ void main() async { mailEnabled: true, openaiEnabled: true, homeModels: [], + homeRoute: '/chat-chat', + showHomeModelDescription: true, + supportWebsocket: false, ), ); } @@ -258,7 +265,7 @@ class MyApp extends StatefulWidget { !stabilityAISelfHosted; _router = GoRouter( - initialLocation: shouldLogin ? '/login' : '/chat-chat', + initialLocation: shouldLogin ? '/login' : Ability().homeRoute, observers: [ BotToastNavigatorObserver(), ], @@ -350,13 +357,18 @@ class MyApp extends StatefulWidget { BlocProvider(create: (context) => NotifyBloc()), BlocProvider.value(value: freeCountBloc), ], - child: ChatAnywhereScreen( + child: HomeChatPage( stateManager: messageStateManager, setting: settingRepo, chatId: int.tryParse(state.queryParameters['chat_id'] ?? '0'), initialMessage: state.queryParameters['init_message'], - model: state.queryParameters['model'], + model: state.queryParameters['model'] == '' + ? null + : state.queryParameters['model'], + title: state.queryParameters['title'] == '' + ? null + : state.queryParameters['title'], ), ), ), @@ -371,7 +383,7 @@ class MyApp extends StatefulWidget { create: (context) => ChatChatBloc(chatMsgRepo)), BlocProvider.value(value: freeCountBloc), ], - child: ChatChatScreen( + child: HomePage( setting: settingRepo, showInitialDialog: state.queryParameters['show_initial_dialog'] == 'true', @@ -381,6 +393,22 @@ class MyApp extends StatefulWidget { ), ), ), + GoRoute( + name: 'chat_chat_history', + path: '/chat-chat/history', + pageBuilder: (context, state) => transitionResolver( + MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => ChatChatBloc(chatMsgRepo)), + ], + child: HomeChatHistoryPage( + setting: settingRepo, + chatMessageRepo: chatMsgRepo, + ), + ), + ), + ), GoRoute( path: '/lab/avatar-selector', pageBuilder: (context, state) => transitionResolver( @@ -399,7 +427,7 @@ class MyApp extends StatefulWidget { pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [BlocProvider.value(value: chatRoomBloc)], - child: CharactersScreen(setting: settingRepo), + child: RoomsPage(setting: settingRepo), ), ), ), @@ -409,7 +437,7 @@ class MyApp extends StatefulWidget { pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [BlocProvider.value(value: chatRoomBloc)], - child: ChatRoomCreateScreen(setting: settingRepo), + child: RoomCreatePage(setting: settingRepo), ), ), ), @@ -428,7 +456,7 @@ class MyApp extends StatefulWidget { BlocProvider(create: (context) => NotifyBloc()), BlocProvider.value(value: freeCountBloc), ], - child: ChatScreen( + child: RoomChatPage( roomId: roomId, stateManager: messageStateManager, setting: settingRepo, @@ -450,8 +478,7 @@ class MyApp extends StatefulWidget { value: ChatBlocManager().getBloc(roomId), ), ], - child: ChatRoomSettingScreen( - roomId: roomId, setting: settingRepo), + child: RoomEditPage(roomId: roomId, setting: settingRepo), ), ); }, @@ -517,20 +544,6 @@ class MyApp extends StatefulWidget { ), ), ), - GoRoute( - name: 'creative-island', - path: '/creative-island', - pageBuilder: (context, state) => transitionResolver( - MultiBlocProvider( - providers: [ - BlocProvider.value(value: creativeIslandBloc), - ], - child: CreativeIsland( - setting: settingRepo, - ), - ), - ), - ), GoRoute( name: 'creative-draw', path: '/creative-draw', @@ -539,7 +552,7 @@ class MyApp extends StatefulWidget { providers: [ BlocProvider.value(value: creativeIslandBloc), ], - child: DrawScreen( + child: DrawListScreen( setting: settingRepo, ), ), @@ -613,25 +626,6 @@ class MyApp extends StatefulWidget { ), ), ), - GoRoute( - name: 'creative-island-create', - path: '/creative-island/:id/create', - pageBuilder: (context, state) { - final id = state.pathParameters['id']!; - return transitionResolver( - MultiBlocProvider( - providers: [ - BlocProvider.value(value: creativeIslandBloc), - ], - child: CreativeIslandCreatePage( - id: id, - repo: creativeIslandRepo, - setting: settingRepo, - ), - ), - ); - }, - ), GoRoute( name: 'creative-island-history-all', path: '/creative-island/history', @@ -641,7 +635,7 @@ class MyApp extends StatefulWidget { providers: [ BlocProvider.value(value: creativeIslandBloc), ], - child: CreativeIslandHistoriesAllScreen( + child: MyCreationScreen( setting: settingRepo, mode: state.queryParameters['mode'] ?? '', ), @@ -649,20 +643,6 @@ class MyApp extends StatefulWidget { ); }, ), - GoRoute( - name: 'creative-island-gallery', - path: '/creative-island/gallery', - pageBuilder: (context, state) { - return transitionResolver( - MultiBlocProvider( - providers: [ - BlocProvider.value(value: creativeIslandBloc), - ], - child: CreativeIslandGalleryScreen(setting: settingRepo), - ), - ); - }, - ), GoRoute( name: 'creative-island-models', path: '/creative-island/models', @@ -677,25 +657,6 @@ class MyApp extends StatefulWidget { ); }, ), - GoRoute( - name: 'creative-island-history', - path: '/creative-island/:id/history', - pageBuilder: (context, state) { - final id = state.pathParameters['id']!; - return transitionResolver( - MultiBlocProvider( - providers: [ - BlocProvider.value(value: creativeIslandBloc), - ], - child: CreativeIslandHistoryPage( - id: id, - repo: creativeIslandRepo, - setting: settingRepo, - ), - ), - ); - }, - ), GoRoute( name: 'creative-island-history-item', path: '/creative-island/:id/history/:item_id', @@ -709,7 +670,7 @@ class MyApp extends StatefulWidget { providers: [ BlocProvider.value(value: creativeIslandBloc), ], - child: CreativeIslandHistoryPreview( + child: MyCreationItemPage( setting: settingRepo, islandId: id, itemId: itemId!, @@ -723,7 +684,7 @@ class MyApp extends StatefulWidget { name: 'quota-details', path: '/quota-details', pageBuilder: (context, state) => transitionResolver( - QuotaDetailScreen(setting: settingRepo), + PaymentHistoryScreen(setting: settingRepo), ), ), GoRoute( @@ -733,6 +694,17 @@ class MyApp extends StatefulWidget { QuotaUsageStatisticsScreen(setting: settingRepo), ), ), + GoRoute( + name: 'quota-usage-daily-details', + path: '/quota-usage-daily-details', + pageBuilder: (context, state) => transitionResolver( + QuotaUsageDetailScreen( + setting: settingRepo, + date: state.queryParameters['date'] ?? + DateFormat('yyyy-MM-dd').format(DateTime.now()), + ), + ), + ), GoRoute( name: 'prompt-editor', path: '/prompt-editor', @@ -801,6 +773,76 @@ class MyApp extends StatefulWidget { ), ), ), + GoRoute( + name: 'custom-home-models', + path: '/setting/custom-home-models', + pageBuilder: (context, state) => transitionResolver( + CustomHomeModelsPage(setting: settingRepo), + ), + ), + GoRoute( + name: 'group-chat-chat', + path: '/group-chat/:group_id/chat', + pageBuilder: (context, state) { + final groupId = int.tryParse(state.pathParameters['group_id']!); + + return transitionResolver( + MultiBlocProvider( + providers: [ + BlocProvider( + create: ((context) => + GroupChatBloc(stateManager: messageStateManager)), + ), + BlocProvider.value(value: chatRoomBloc), + ], + child: GroupChatPage( + setting: settingRepo, + stateManager: messageStateManager, + groupId: groupId!, + ), + ), + ); + }, + ), + GoRoute( + name: 'group-chat-create', + path: '/group-chat-create', + pageBuilder: (context, state) { + return transitionResolver( + MultiBlocProvider( + providers: [ + BlocProvider( + create: ((context) => + GroupChatBloc(stateManager: messageStateManager)), + ), + BlocProvider.value(value: chatRoomBloc), + ], + child: GroupCreatePage(setting: settingRepo), + ), + ); + }, + ), + GoRoute( + name: 'group-chat-edit', + path: '/group-chat/:group_id/edit', + pageBuilder: (context, state) { + return transitionResolver( + MultiBlocProvider( + providers: [ + BlocProvider( + create: ((context) => + GroupChatBloc(stateManager: messageStateManager)), + ), + BlocProvider.value(value: chatRoomBloc), + ], + child: GroupEditPage( + setting: settingRepo, + groupId: int.tryParse(state.pathParameters['group_id']!)!, + ), + ), + ); + }, + ), ], ) ], diff --git a/lib/page/app_scaffold.dart b/lib/page/app_scaffold.dart index f567ae7e..02572cf4 100644 --- a/lib/page/app_scaffold.dart +++ b/lib/page/app_scaffold.dart @@ -1,8 +1,9 @@ +import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/event.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; @@ -44,10 +45,71 @@ class _AppScaffoldState extends State { super.initState(); } + List _bottomNavigationBarList( + {int? currentIndex}) { + return [ + if (Ability().enableChat) + BottomNavigationBarConfig( + builder: (index, customColors) => createAnimatedNavBarItem( + icon: Icons.question_answer_outlined, + activatedIcon: Icons.question_answer, + activatedColor: customColors.linkColor, + label: AppLocale.chatAnywhere.getString(context), + activated: currentIndex == index, + ), + route: '/chat-chat', + ), + if (Ability().enableDigitalHuman) + BottomNavigationBarConfig( + builder: (index, customColors) => createAnimatedNavBarItem( + icon: Icons.group_outlined, + activatedIcon: Icons.group, + activatedColor: customColors.linkColor, + label: AppLocale.homeTitle.getString(context), + activated: currentIndex == index, + ), + route: '/', + ), + if (Ability().enableGallery) + BottomNavigationBarConfig( + builder: (index, customColors) => createAnimatedNavBarItem( + icon: Icons.auto_awesome_outlined, + activatedIcon: Icons.auto_awesome, + activatedColor: customColors.linkColor, + label: AppLocale.discover.getString(context), + activated: currentIndex == index, + ), + route: '/creative-gallery', + ), + if (Ability().enableCreationIsland) + BottomNavigationBarConfig( + builder: (index, customColors) => createAnimatedNavBarItem( + icon: Icons.palette_outlined, + activatedIcon: Icons.palette, + activatedColor: customColors.linkColor, + label: AppLocale.creativeIsland.getString(context), + activated: currentIndex == index, + ), + route: '/creative-draw', + ), + BottomNavigationBarConfig( + builder: (index, customColors) => createAnimatedNavBarItem( + icon: Icons.manage_accounts_outlined, + activatedIcon: Icons.manage_accounts, + activatedColor: customColors.linkColor, + label: AppLocale.me.getString(context), + activated: currentIndex == index, + ), + route: '/setting', + ), + ]; + } + @override Widget build(BuildContext context) { final currentIndex = _calculateSelectedIndex(context); final customColors = Theme.of(context).extension()!; + final barItems = _bottomNavigationBarList(currentIndex: currentIndex); return Scaffold( backgroundColor: customColors.backgroundContainerColor, body: BackgroundContainer( @@ -72,41 +134,8 @@ class _AppScaffoldState extends State { backgroundColor: customColors.backgroundColor, elevation: 0, items: [ - createAnimatedNavBarItem( - icon: Icons.question_answer_outlined, - activatedIcon: Icons.question_answer, - activatedColor: customColors.linkColor, - label: AppLocale.chatAnywhere.getString(context), - activated: currentIndex == 0, - ), - createAnimatedNavBarItem( - icon: Icons.group_outlined, - activatedIcon: Icons.group, - activatedColor: customColors.linkColor, - label: AppLocale.homeTitle.getString(context), - activated: currentIndex == 1, - ), - createAnimatedNavBarItem( - icon: Icons.auto_awesome_outlined, - activatedIcon: Icons.auto_awesome, - activatedColor: customColors.linkColor, - label: AppLocale.discover.getString(context), - activated: currentIndex == 2, - ), - createAnimatedNavBarItem( - icon: Icons.palette_outlined, - activatedIcon: Icons.palette, - activatedColor: customColors.linkColor, - label: AppLocale.creativeIsland.getString(context), - activated: currentIndex == 3, - ), - createAnimatedNavBarItem( - icon: Icons.manage_accounts_outlined, - activatedIcon: Icons.manage_accounts, - activatedColor: customColors.linkColor, - label: AppLocale.me.getString(context), - activated: currentIndex == 4, - ), + for (var i = 0; i < barItems.length; i++) + barItems[i].builder(i, customColors), ], ) : null, @@ -117,31 +146,25 @@ class _AppScaffoldState extends State { final GoRouter route = GoRouter.of(context); final String location = route.location.split('?').first; - if (location == '/chat-chat') return 0; - if (location == '/') return 1; - if (location == '/creative-gallery') return 2; - if (location == '/creative-draw') return 3; - if (location == '/setting') return 4; + final barItems = _bottomNavigationBarList(); + for (var i = 0; i < barItems.length; i++) { + if (barItems[i].route == location) return i; + } return -1; } void onTap(int value) { - HapticFeedbackHelper.lightImpact(); - switch (value) { - case 0: - return context.go('/chat-chat'); - case 1: - return context.go('/'); - case 2: - return context.go('/creative-gallery'); - case 3: - return context.go('/creative-draw'); - case 4: - return context.go('/setting'); - default: - return context.go('/'); + if (context.canPop()) { + context.pop(); } + + HapticFeedbackHelper.lightImpact(); + + final barItems = _bottomNavigationBarList(); + if (value >= barItems.length) return context.go(Ability().homeRoute); + + return context.go(barItems[value].route); } } @@ -163,3 +186,14 @@ BottomNavigationBarItem createAnimatedNavBarItem({ ), ); } + +class BottomNavigationBarConfig { + final BottomNavigationBarItem Function(int index, CustomColors customColors) + builder; + final String route; + + BottomNavigationBarConfig({ + required this.builder, + required this.route, + }); +} diff --git a/lib/page/signin_or_signup.dart b/lib/page/auth/signin_or_signup.dart similarity index 96% rename from lib/page/signin_or_signup.dart rename to lib/page/auth/signin_or_signup.dart index 58c18dd9..3d98a1b2 100644 --- a/lib/page/signin_or_signup.dart +++ b/lib/page/auth/signin_or_signup.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; @@ -7,9 +8,9 @@ import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/password_field.dart'; import 'package:askaide/page/component/verify_code_input.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; @@ -164,7 +165,7 @@ class _SigninOrSignupScreenState extends State { await widget.settings.set(settingUserInfo, jsonEncode(value)); if (context.mounted) { context.go( - '/chat-chat?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); + '${Ability().homeRoute}?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); } }).catchError((e) { showErrorMessage(resolveError(context, e)); @@ -405,7 +406,7 @@ class _SigninOrSignupScreenState extends State { } else { if (context.mounted) { context.go( - '/chat-chat?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); + '${Ability().homeRoute}?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); } } }).catchError((e) { diff --git a/lib/page/signin_screen.dart b/lib/page/auth/signin_screen.dart similarity index 97% rename from lib/page/signin_screen.dart rename to lib/page/auth/signin_screen.dart index 981d0569..d4c2a205 100644 --- a/lib/page/signin_screen.dart +++ b/lib/page/auth/signin_screen.dart @@ -4,14 +4,15 @@ import 'package:animated_text_kit/animated_text_kit.dart'; import 'package:askaide/bloc/version_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; +import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/gestures.dart'; @@ -76,7 +77,7 @@ class _SignInScreenState extends State { if (context.canPop()) { context.pop(); } else { - context.go('/chat-chat'); + context.go(Ability().homeRoute); } }, ), @@ -409,7 +410,7 @@ class _SignInScreenState extends State { return; } else { context.go( - '/chat-chat?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); + '${Ability().homeRoute}?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); } }); }).catchError((e) { diff --git a/lib/page/signup_screen.dart b/lib/page/auth/signup_screen.dart similarity index 98% rename from lib/page/signup_screen.dart rename to lib/page/auth/signup_screen.dart index eb14aebe..0067d234 100644 --- a/lib/page/signup_screen.dart +++ b/lib/page/auth/signup_screen.dart @@ -2,14 +2,16 @@ import 'dart:async'; import 'dart:convert'; import 'package:animated_text_kit/animated_text_kit.dart'; +import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; +import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/password_field.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; @@ -557,7 +559,7 @@ class _SignupScreenState extends State { } else { if (context.mounted) { context.go( - '/chat-chat?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); + '${Ability().homeRoute}?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); } } }).catchError((e) { diff --git a/lib/page/free_statistics.dart b/lib/page/balance/free_statistics.dart similarity index 90% rename from lib/page/free_statistics.dart rename to lib/page/balance/free_statistics.dart index 0d87f67e..9021c982 100644 --- a/lib/page/free_statistics.dart +++ b/lib/page/balance/free_statistics.dart @@ -4,13 +4,14 @@ import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/message_box.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; +import 'package:go_router/go_router.dart'; import 'package:quickalert/models/quickalert_type.dart'; class FreeStatisticsPage extends StatefulWidget { @@ -26,8 +27,9 @@ class _FreeStatisticsPageState extends State { @override void initState() { super.initState(); - - context.read().add(FreeCountReloadAllEvent()); + context + .read() + .add(FreeCountReloadAllEvent(checkSigninStatus: true)); } @override @@ -57,7 +59,16 @@ class _FreeStatisticsPageState extends State { height: double.infinity, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), - child: BlocBuilder( + child: BlocConsumer( + listenWhen: (previous, current) => + current is FreeCountLoadedState, + listener: (BuildContext context, FreeCountState state) { + if (state is FreeCountLoadedState) { + if (state.needSignin) { + context.go('/login'); + } + } + }, builder: (context, state) { if (state is FreeCountLoadedState) { if (state.counts.isEmpty) { diff --git a/lib/page/payment.dart b/lib/page/balance/payment.dart similarity index 95% rename from lib/page/payment.dart rename to lib/page/balance/payment.dart index d076092c..9986a8e6 100644 --- a/lib/page/payment.dart +++ b/lib/page/balance/payment.dart @@ -11,9 +11,9 @@ import 'package:askaide/page/component/coin.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/loading.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/payment.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; @@ -330,6 +330,17 @@ class _PaymentScreenState extends State { showErrorMessage(resolveError(context, e)); } } else { + // openConfirmDialog( + // context, + // '当前终端在线支付暂不可用,预计最晚 2023 年 10 月 15 日恢复,如需充值,请使用移动端 APP(支持 Android 手机、Apple 手机)。', + // () { + // launchUrlString( + // 'https://aidea.aicode.cc', + // mode: LaunchMode.externalApplication, + // ); + // }, + // confirmText: '前往下载移动端 APP', + // ); openListSelectDialog( context, [ @@ -462,10 +473,13 @@ class _PaymentScreenState extends State { ); paymentId = created.paymentId; + // 沙箱环境支持 + final env = created.sandbox ? AliPayEvn.SANDBOX : AliPayEvn.ONLINE; + // 调起支付宝支付 final aliPayRes = await aliPay( created.params, - evn: AliPayEvn.ONLINE, + evn: env, ).whenComplete(() => _closePaymentLoading()); print("================="); print(aliPayRes); diff --git a/lib/page/quota_detail_screen.dart b/lib/page/balance/payment_history.dart similarity index 94% rename from lib/page/quota_detail_screen.dart rename to lib/page/balance/payment_history.dart index 55f5219a..8848a355 100644 --- a/lib/page/quota_detail_screen.dart +++ b/lib/page/balance/payment_history.dart @@ -2,8 +2,8 @@ import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/coin.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/quota.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; @@ -11,15 +11,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:intl/intl.dart'; -class QuotaDetailScreen extends StatefulWidget { +class PaymentHistoryScreen extends StatefulWidget { final SettingRepository setting; - const QuotaDetailScreen({super.key, required this.setting}); + const PaymentHistoryScreen({super.key, required this.setting}); @override - State createState() => _QuotaDetailScreenState(); + State createState() => _PaymentHistoryScreenState(); } -class _QuotaDetailScreenState extends State { +class _PaymentHistoryScreenState extends State { @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; diff --git a/lib/page/balance/quota_usage_details.dart b/lib/page/balance/quota_usage_details.dart new file mode 100644 index 00000000..3112f2e8 --- /dev/null +++ b/lib/page/balance/quota_usage_details.dart @@ -0,0 +1,127 @@ +import 'package:askaide/page/component/background_container.dart'; +import 'package:askaide/page/component/loading.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/model/misc.dart'; +import 'package:askaide/repo/settings_repo.dart'; +import 'package:flutter/material.dart'; + +class QuotaUsageDetailScreen extends StatefulWidget { + final SettingRepository setting; + final String date; + + const QuotaUsageDetailScreen({ + super.key, + required this.setting, + required this.date, + }); + + @override + State createState() => _QuotaUsageDetailScreenState(); +} + +class _QuotaUsageDetailScreenState extends State { + List usages = []; + bool loaded = false; + + @override + void initState() { + APIServer().quotaUsedDetails(date: widget.date).then((value) { + setState(() { + usages = value; + loaded = true; + }); + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + var customColors = Theme.of(context).extension()!; + + return Scaffold( + appBar: AppBar( + toolbarHeight: CustomSize.toolbarHeight, + title: Text( + widget.date, + style: const TextStyle(fontSize: CustomSize.appBarTitleSize), + ), + centerTitle: true, + elevation: 0, + ), + backgroundColor: customColors.backgroundContainerColor, + body: BackgroundContainer( + setting: widget.setting, + enabled: false, + child: Container( + padding: const EdgeInsets.all(16), + child: _buildQuotaUsagePage(context, customColors), + ), + ), + ); + } + + Widget _buildQuotaUsagePage( + BuildContext context, + CustomColors customColors, + ) { + if (!loaded) { + return const Center( + child: LoadingIndicator(), + ); + } + + final usageGt0 = usages.where((e) => e.used > 0).toList(); + if (usageGt0.isEmpty) { + return const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline, + size: 50, + ), + SizedBox(height: 10), + Text( + '暂无使用记录', + ), + ], + ), + ); + } + + return Column( + children: [ + Expanded( + child: ListView( + shrinkWrap: true, + children: [ + for (var item in usageGt0) + Container( + margin: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: customColors.paymentItemBackgroundColor, + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(item.createdAt, + style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(width: 20), + Expanded( + child: Text('使用 ${item.type} 消耗 ${item.used} 个智慧果'), + ), + ], + ), + ) + ], + ), + ), + ], + ); + } +} diff --git a/lib/page/balance/quota_usage_statistics.dart b/lib/page/balance/quota_usage_statistics.dart new file mode 100644 index 00000000..6ada45fb --- /dev/null +++ b/lib/page/balance/quota_usage_statistics.dart @@ -0,0 +1,146 @@ +import 'package:askaide/page/component/background_container.dart'; +import 'package:askaide/page/component/loading.dart'; +import 'package:askaide/page/component/message_box.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/settings_repo.dart'; +import 'package:flutter/material.dart'; +import 'package:askaide/repo/model/misc.dart'; +import 'package:go_router/go_router.dart'; + +class QuotaUsageStatisticsScreen extends StatefulWidget { + final SettingRepository setting; + const QuotaUsageStatisticsScreen({super.key, required this.setting}); + + @override + State createState() => + _QuotaUsageStatisticsScreenState(); +} + +class _QuotaUsageStatisticsScreenState + extends State { + List usages = []; + bool loaded = false; + + @override + void initState() { + APIServer().quotaUsedStatistics().then((value) { + setState(() { + usages = value; + loaded = true; + }); + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + var customColors = Theme.of(context).extension()!; + + return Scaffold( + appBar: AppBar( + toolbarHeight: CustomSize.toolbarHeight, + title: const Text( + '使用明细', + style: TextStyle(fontSize: CustomSize.appBarTitleSize), + ), + centerTitle: true, + elevation: 0, + ), + backgroundColor: customColors.backgroundContainerColor, + body: BackgroundContainer( + setting: widget.setting, + enabled: false, + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const MessageBox( + message: '使用明细将在次日更新,显示近 30 天的使用量。', + type: MessageBoxType.info, + ), + const SizedBox(height: 10), + Expanded( + child: _buildQuotaUsagePage(context, customColors), + ), + ], + ), + ), + ), + ); + } + + Widget _buildQuotaUsagePage( + BuildContext context, + CustomColors customColors, + ) { + if (!loaded) { + return const Center( + child: LoadingIndicator(), + ); + } + + final usageGt0 = usages.where((e) => e.used > 0 || e.used == -1).toList(); + if (usageGt0.isEmpty) { + return const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline, + size: 50, + ), + SizedBox(height: 10), + Text( + '暂无使用记录', + ), + ], + ), + ); + } + + return Column( + children: [ + Expanded( + child: ListView( + shrinkWrap: true, + children: [ + for (var item in usageGt0) + Container( + margin: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: customColors.paymentItemBackgroundColor, + borderRadius: BorderRadius.circular(10), + ), + child: InkWell( + onTap: () { + context + .push('/quota-usage-daily-details?date=${item.date}'); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + item.date, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + if (item.used == -1) + const Text('未出账') + else + Text('${item.used > 0 ? "-" : ""}${item.used}'), + ], + ), + ), + ) + ], + ), + ), + ], + ); + } +} diff --git a/lib/page/chat/component/group_avatar.dart b/lib/page/chat/component/group_avatar.dart new file mode 100644 index 00000000..b985f107 --- /dev/null +++ b/lib/page/chat/component/group_avatar.dart @@ -0,0 +1,193 @@ +import 'package:askaide/helper/image.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; + +class GroupAvatar extends StatelessWidget { + final double size; + final double padding; + final double margin; + final List avatars; + final Color? backgroundColor; + + var row = 0, column = 0; + + GroupAvatar({ + super.key, + this.size = 40, + this.padding = 2, + this.margin = 3, + required this.avatars, + this.backgroundColor, + }); + + @override + Widget build(BuildContext context) { + final avatar = buildAvatar(context); + + return Container( + padding: const EdgeInsets.all(4), + width: size, + height: size, + color: backgroundColor ?? Colors.grey.withAlpha(100), + child: avatar, + ); + } + + double get innerSize => size - 8; + + Widget buildAvatar(BuildContext context) { + var childCount = avatars.length; + int columnMax; + List icons = []; + List stacks = []; + // 五张图片之后(包含5张),每行的最大列数是3 + double imgWidth; + + if (childCount < 2) { + return Container( + width: innerSize, + height: innerSize, + color: Colors.transparent, + ); + } + + if (childCount >= 5) { + columnMax = 3; + imgWidth = (innerSize - (padding * columnMax) - margin) / columnMax; + } else { + columnMax = 2; + imgWidth = (innerSize - (padding * columnMax) - margin) / columnMax; + } + for (var i = 0; i < childCount; i++) { + icons.add(_weChatGroupChatChildIcon(avatars[i], imgWidth)); + } + row = 0; + column = 0; + var centerTop = 0.0; + if (childCount == 2 || childCount == 5 || childCount == 6) { + centerTop = imgWidth / 2; + } + for (var i = 0; i < childCount; i++) { + var left = imgWidth * row + padding * (row + 1); + var top = imgWidth * column + margin * column + centerTop; + switch (childCount) { + case 3: + case 7: + _topOneIcon(stacks, icons[i], childCount, i, imgWidth, left, top); + break; + case 5: + case 8: + _topTwoIcon(stacks, icons[i], childCount, i, imgWidth, left, top); + break; + default: + _otherIcon( + stacks, icons[i], childCount, i, imgWidth, left, top, columnMax); + break; + } + } + + return Container( + width: innerSize, + height: innerSize, + color: Colors.transparent, + padding: EdgeInsets.only(top: padding), + alignment: AlignmentDirectional.bottomCenter, + child: Stack( + children: stacks, + ), + ); + } + + _weChatGroupChatChildIcon(String avatar, double width) { + return ClipRRect( + borderRadius: BorderRadius.circular(2), + child: CachedNetworkImage( + imageUrl: imageURL(avatar, 'avatar'), + height: width, + width: width, + fit: BoxFit.fill, + ), + ); + } + + // 顶部为一张图片 + _topOneIcon(List stacks, Widget child, int childCount, i, imgWidth, + left, top) { + if (i == 0) { + var firstLeft = imgWidth / 2 + left + margin / 2; + if (childCount == 7) { + firstLeft = imgWidth + left + margin; + } + stacks.add(Positioned( + left: firstLeft, + child: child, + )); + row = 0; + // 换行 + column++; + } else { + stacks.add(Positioned( + left: left, + top: top, + child: child, + )); + // 换列 + row++; + if (i == 3) { + // 第一例 + row = 0; + // 换行 + column++; + } + } + } + +// 顶部为两张图片 + _topTwoIcon(List stacks, Widget child, int childCount, i, imgWidth, + left, top) { + if (i == 0 || i == 1) { + stacks.add(Positioned( + left: imgWidth / 2 + left + margin / 2, + top: childCount == 5 ? top : 0.0, + child: child, + )); + row++; + if (i == 1) { + row = 0; + // 换行 + column++; + } + } else { + stacks.add(Positioned( + left: left, + top: top, + child: child, + )); + // 换列 + row++; + if (i == 4) { + // 第一例 + row = 0; + // 换行 + column++; + } + } + } + + _otherIcon(List stacks, Widget child, int childCount, i, imgWidth, + left, top, columnMax) { + stacks.add(Positioned( + left: left, + top: top, + child: child, + )); + // 换列 + row++; + if ((i + 1) % columnMax == 0) { + // 第一例 + row = 0; + // 换行 + column++; + } + } +} diff --git a/lib/page/chat/component/group_empty.dart b/lib/page/chat/component/group_empty.dart new file mode 100644 index 00000000..6be860a5 --- /dev/null +++ b/lib/page/chat/component/group_empty.dart @@ -0,0 +1,108 @@ +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:flutter/material.dart'; + +class GroupEmptyBoard extends StatelessWidget { + const GroupEmptyBoard({super.key}); + + @override + Widget build(BuildContext context) { + final customColors = Theme.of(context).extension()!; + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(height: 30), + Container( + decoration: BoxDecoration( + color: customColors.backgroundColor?.withAlpha(200), + borderRadius: BorderRadius.circular(10), + ), + padding: + const EdgeInsets.only(top: 20, left: 15, right: 10, bottom: 3), + width: _resolveTipWidth(context), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.asset('assets/app-256-transparent.png', + width: 20, height: 20), + const SizedBox(width: 5), + const Text( + '小提示', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildTextLine( + customColors, + "点击 @ 按钮,快速指定应答成员", + Icons.touch_app, + ), + buildTextLine( + customColors, + '未选择成员时,系统将随机指派', + Icons.shuffle, + ), + buildTextLine( + customColors, + '系统会记住上次使用的成员', + Icons.memory, + ), + ], + ), + const SizedBox(height: 20), + ], + ), + ), + ], + ), + ); + } + + Widget buildTextLine(CustomColors customColors, String text, IconData? icon) { + return Padding( + padding: const EdgeInsets.only(bottom: 10, left: 15), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + icon, + size: 14, + color: customColors.chatExampleItemText?.withAlpha(120), + ), + const SizedBox(width: 5), + Expanded( + child: Text( + text, + maxLines: 1, + style: TextStyle( + color: customColors.weakTextColor, + height: 1.5, + fontSize: 14, + overflow: TextOverflow.ellipsis, + ), + ), + ) + ], + ), + ); + } + + double _resolveTipWidth(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + if (screenWidth < 400) { + return screenWidth / 1.15; + } + + return 400; + } +} diff --git a/lib/page/chat/component/room_item.dart b/lib/page/chat/component/room_item.dart new file mode 100644 index 00000000..7627fb4c --- /dev/null +++ b/lib/page/chat/component/room_item.dart @@ -0,0 +1,277 @@ +import 'package:askaide/bloc/room_bloc.dart'; +import 'package:askaide/helper/ability.dart'; +import 'package:askaide/helper/constant.dart'; +import 'package:askaide/helper/haptic_feedback.dart'; +import 'package:askaide/helper/helper.dart'; +import 'package:askaide/helper/image.dart'; +import 'package:askaide/lang/lang.dart'; +import 'package:askaide/page/chat/component/group_avatar.dart'; +import 'package:askaide/page/component/image.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:askaide/repo/model/room.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_initicon/flutter_initicon.dart'; +import 'package:flutter_localization/flutter_localization.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:go_router/go_router.dart'; + +class RoomItem extends StatelessWidget { + final Room room; + const RoomItem({super.key, required this.room}); + + @override + Widget build(BuildContext context) { + final customColors = Theme.of(context).extension()!; + return Container( + margin: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(customColors.borderRadius ?? 8), + ), + child: Slidable( + endActionPane: ActionPane( + motion: const ScrollMotion(), + children: [ + const SizedBox(width: 10), + SlidableAction( + label: '设置', + backgroundColor: Colors.green, + borderRadius: room.category == 'system' + ? BorderRadius.all( + Radius.circular(customColors.borderRadius ?? 8)) + : BorderRadius.only( + topLeft: Radius.circular(customColors.borderRadius ?? 8), + bottomLeft: + Radius.circular(customColors.borderRadius ?? 8), + ), + icon: Icons.settings, + onPressed: (_) { + final chatRoomBloc = context.read(); + final redirectUrl = room.roomType == 4 + ? '/group-chat/${room.id}/edit' + : '/room/${room.id}/setting'; + + context.push(redirectUrl).then((value) { + chatRoomBloc.add(RoomsLoadEvent()); + }); + }, + ), + if (room.category != 'system') + SlidableAction( + label: AppLocale.delete.getString(context), + borderRadius: BorderRadius.only( + topRight: Radius.circular(customColors.borderRadius ?? 8), + bottomRight: Radius.circular(customColors.borderRadius ?? 8), + ), + backgroundColor: Colors.red, + icon: Icons.delete, + onPressed: (_) { + openConfirmDialog( + context, + AppLocale.confirmToDeleteRoom.getString(context), + () => + context.read().add(RoomDeleteEvent(room.id!)), + danger: true, + ); + }, + ), + ], + ), + child: Material( + borderRadius: + BorderRadius.all(Radius.circular(customColors.borderRadius ?? 8)), + color: customColors.columnBlockBackgroundColor, + child: InkWell( + borderRadius: BorderRadius.all( + Radius.circular(customColors.borderRadius ?? 8)), + onTap: () { + final redirectRoute = room.roomType == 4 + ? '/group-chat/${room.id}/chat' + : '/room/${room.id}/chat'; + HapticFeedbackHelper.lightImpact(); + final chatRoomBloc = context.read(); + context.push(redirectRoute).then((value) { + chatRoomBloc.add(RoomsLoadEvent(forceRefresh: true)); + }); + }, + child: Stack( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildAvatar(room), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + room.name, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + humanTime(room.lastActiveTime), + style: TextStyle( + color: customColors.weakLinkColor + ?.withAlpha(65), + fontSize: 10, + ), + ), + ], + ), + const SizedBox(height: 5), + buildRoomDesc(customColors), + ], + ), + ), + ), + ], + ), + if (room.roomType == 4) + Positioned( + right: 0, + top: 0, + child: Container( + decoration: BoxDecoration( + color: customColors.backgroundContainerColor, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), + ), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 2), + child: Text( + '群聊', + style: TextStyle( + color: customColors.weakTextColor, + fontSize: 8, + ), + ), + ), + ), + if (Ability().usingLocalOpenAIModel(room.model)) + Positioned( + right: 0, + top: 0, + child: Container( + decoration: BoxDecoration( + color: customColors.backgroundContainerColor, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), + ), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 2), + child: Text( + 'local', + style: TextStyle( + color: customColors.weakTextColor, + fontSize: 8, + ), + ), + ), + ) + ], + ), + ), + ), + ), + ); + } + + Widget buildRoomDesc(CustomColors customColors) { + if (room.description != null && room.description != '') { + return Text( + room.description!, + style: TextStyle( + color: customColors.weakLinkColor?.withAlpha(150), + fontSize: 13, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + + if (room.systemPrompt != null && room.systemPrompt != '') { + return Text( + room.systemPrompt!, + style: TextStyle( + color: customColors.weakLinkColor?.withAlpha(150), + fontSize: 13, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + + if (room.systemPrompt == null || room.systemPrompt == '') { + Text( + room.modelName().toUpperCase().replaceAll('-TURBO', ''), + style: TextStyle( + color: customColors.weakLinkColor?.withAlpha(150), + fontSize: 13, + ), + ); + } + + return const SizedBox(); + } + + Widget _buildAvatar(Room room) { + if (room.members.length == 1 && + (room.avatarUrl == null || room.avatarUrl == '')) { + room.avatarUrl = room.members[0]; + } + + if (room.avatarUrl != null && room.avatarUrl!.startsWith('http')) { + return SizedBox( + width: 70, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), + child: CachedNetworkImageEnhanced( + imageUrl: imageURL(room.avatarUrl!, qiniuImageTypeAvatar), + fit: BoxFit.fill, + ), + ), + ); + } + + if (room.members.isNotEmpty) { + return ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), + child: GroupAvatar( + size: 70, + avatars: room.members, + ), + ); + } + + return Initicon( + text: room.name.split('、').join(' '), + size: 70, + backgroundColor: Colors.grey.withAlpha(100), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), + ); + } +} diff --git a/lib/page/chat/group/chat.dart b/lib/page/chat/group/chat.dart new file mode 100644 index 00000000..c1677e63 --- /dev/null +++ b/lib/page/chat/group/chat.dart @@ -0,0 +1,714 @@ +import 'dart:async'; + +import 'package:askaide/bloc/group_chat_bloc.dart'; +import 'package:askaide/helper/ability.dart'; +import 'package:askaide/helper/constant.dart'; +import 'package:askaide/helper/haptic_feedback.dart'; +import 'package:askaide/helper/image.dart'; +import 'package:askaide/lang/lang.dart'; +import 'package:askaide/page/chat/component/group_empty.dart'; +import 'package:askaide/page/component/audio_player.dart'; +import 'package:askaide/page/component/background_container.dart'; +import 'package:askaide/page/component/chat/chat_share.dart'; +import 'package:askaide/page/component/chat/help_tips.dart'; +import 'package:askaide/page/component/chat/message_state_manager.dart'; +import 'package:askaide/page/component/enhanced_popup_menu.dart'; +import 'package:askaide/page/component/multi_item_selector.dart'; +import 'package:askaide/page/component/random_avatar.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:askaide/page/component/chat/chat_input.dart'; +import 'package:askaide/page/component/chat/chat_preview.dart'; +import 'package:askaide/repo/model/group.dart'; +import 'package:askaide/repo/model/message.dart'; +import 'package:askaide/repo/settings_repo.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localization/flutter_localization.dart'; +import 'package:go_router/go_router.dart'; + +class GroupChatPage extends StatefulWidget { + final SettingRepository setting; + final int groupId; + final MessageStateManager stateManager; + + const GroupChatPage({ + super.key, + required this.setting, + required this.groupId, + required this.stateManager, + }); + + @override + State createState() => _GroupChatPageState(); +} + +class _GroupChatPageState extends State { + final ScrollController _scrollController = ScrollController(); + final ValueNotifier _inputEnabled = ValueNotifier(true); + final ChatPreviewController _chatPreviewController = ChatPreviewController(); + final AudioPlayerController _audioPlayerController = + AudioPlayerController(useRemoteAPI: false); + bool showAudioPlayer = false; + + List? selectedMembers = []; + List messages = []; + + ChatGroup? group; + + Timer? timer; + + @override + void initState() { + super.initState(); + + context.read().add(GroupChatLoadEvent(widget.groupId)); + + _chatPreviewController.addListener(() { + setState(() {}); + }); + + _audioPlayerController.onPlayStopped = () { + setState(() { + showAudioPlayer = false; + }); + }; + _audioPlayerController.onPlayAudioStarted = () { + setState(() { + showAudioPlayer = true; + }); + }; + } + + @override + void dispose() { + timer?.cancel(); + _scrollController.dispose(); + _chatPreviewController.dispose(); + _audioPlayerController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final customColors = Theme.of(context).extension()!; + + return BackgroundContainer( + setting: widget.setting, + child: Scaffold( + appBar: _buildAppBar(context, customColors), + backgroundColor: Colors.transparent, + body: _buildChatComponents(customColors), + ), + ); + } + + Widget _buildChatComponents(CustomColors customColors) { + return BlocConsumer( + listenWhen: (previous, current) => + current is GroupChatLoaded || current is GroupDefaultMemberSelected, + listener: (context, state) { + if (state is GroupChatLoaded) { + // 加载聊天记录列表 + context.read().add( + GroupChatMessagesLoadEvent(widget.groupId, isInitRequest: true)); + + // 选中默认的聊天成员 + selectedMembers = state.group.members + .where((e) => state.defaultChatMembers?.contains(e.id) ?? false) + .toList(); + + setState(() { + group = state.group; + }); + } + + if (state is GroupDefaultMemberSelected) { + // 选中默认的聊天成员 + if (group != null) { + selectedMembers = group?.members + .where((e) => state.members.contains(e.id)) + .toList(); + } + } + }, + buildWhen: (previous, current) => current is GroupChatLoaded, + builder: (context, groupState) { + if (groupState is GroupChatLoaded) { + return SafeArea( + top: false, + bottom: false, + child: Column( + children: [ + // 语音输出中提示 + if (showAudioPlayer) + EnhancedAudioPlayer(controller: _audioPlayerController), + // 聊天内容窗口 + Expanded( + child: _buildChatPreviewArea( + groupState, + customColors, + _chatPreviewController.selectMode, + ), + ), + + // 聊天输入窗口 + Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10), + ), + color: customColors.chatInputPanelBackground, + ), + child: SafeArea( + child: _chatPreviewController.selectMode + ? buildSelectModeToolbars( + context, + _chatPreviewController, + customColors, + ) + : ChatInput( + enableNotifier: _inputEnabled, + enableImageUpload: false, + onSubmit: _handleSubmit, + onNewChat: () => handleResetContext(context), + hintText: '有问题尽管问我', + onVoiceRecordTappedEvent: () { + _audioPlayerController.stop(); + }, + leftSideToolsBuilder: () { + return [ + Stack( + children: [ + Container( + width: 40, + height: 40, + padding: const EdgeInsets.all(5), + child: InkWell( + onTap: () { + onModelSelect( + context, + groupState, + customColors, + ); + }, + child: Icon( + Icons.alternate_email, + color: selectedMembers != null && + selectedMembers!.isNotEmpty + ? customColors.linkColor + : customColors.chatInputPanelText, + ), + ), + ), + if (selectedMembers != null && + selectedMembers!.isNotEmpty) + Positioned( + right: 2, + top: 0, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 3, vertical: 3), + child: Text( + 'x${selectedMembers!.length}', + style: TextStyle( + fontSize: 7, + color: customColors.linkColor, + )), + ), + ), + ], + ) + ]; + }, + ), + ), + ), + ], + ), + ); + } else { + return Container(); + } + }, + ); + } + + void onModelSelect( + BuildContext context, + GroupChatLoaded groupState, + CustomColors customColors, + ) { + openModalBottomSheet( + context, + (context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.only(top: 15, left: 20), + child: Text( + '选择本次对话成员', + style: TextStyle( + fontSize: 14, + color: customColors.weakLinkColor, + ), + ), + ), + Expanded( + child: MultiItemSelector( + itemBuilder: (item) { + return Text(item.modelName); + }, + items: groupState.group.members + .where((e) => e.status != 2) + .toList(), + onChanged: (selected) { + setState(() { + selectedMembers = selected; + }); + }, + itemAvatarBuilder: (item) { + return _buildAvatar( + avatarUrl: item.avatarUrl, + id: item.id, + size: 30, + ); + }, + selectedItems: selectedMembers, + ), + ), + ], + ); + }, + heightFactor: 0.6, + ); + } + + BlocConsumer _buildChatPreviewArea( + GroupChatLoaded group, + CustomColors customColors, + bool selectMode, + ) { + return BlocConsumer( + listenWhen: (previous, current) => current is GroupChatMessagesLoaded, + listener: (context, state) { + if (state is GroupChatMessagesLoaded) { + if (state.error != null) { + showErrorMessageEnhanced(context, state.error); + } + + messages = state.messages; + + // 聊天内容窗口滚动到底部 + if (!state.hasWaitTasks && _scrollController.hasClients) { + _scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeOut, + ); + } + + if (state.hasWaitTasks && _inputEnabled.value) { + // 聊天回复中时,禁止输入框编辑 + setState(() { + _inputEnabled.value = false; + }); + } else if (!state.hasWaitTasks && !_inputEnabled.value) { + // 聊天回复完成时,取消输入框的禁止编辑状态 + setState(() { + _inputEnabled.value = true; + }); + } + + // 启动定时器,定时刷新聊天记录 + timer ??= Timer.periodic(const Duration(seconds: 3), (timer) { + context + .read() + .add(GroupChatUpdateMessageStatusEvent(widget.groupId)); + }); + } + }, + buildWhen: (prv, cur) => cur is GroupChatMessagesLoaded, + builder: (context, state) { + if (state is GroupChatMessagesLoaded) { + if (state.messages.isEmpty) { + return const Padding( + padding: EdgeInsets.only(left: 15, right: 15, top: 10), + child: GroupEmptyBoard(), + ); + } + + final loadedMessages = state.messages.map((e) { + var member = + e.memberId != null ? group.group.findMember(e.memberId!) : null; + + return Message( + id: e.id, + Role.getRoleFromText(e.role), + e.message, + type: MessageType.getTypeFromText(e.type), + status: e.status, + refId: e.pid, + ts: e.createdAt, + avatarUrl: member?.avatarUrl, + senderName: member?.modelName, + roomId: e.groupId, + ); + }).toList(); + + final messages = loadedMessages.map((e) { + return MessageWithState( + e, + group.states[ + widget.stateManager.getKey(e.roomId ?? 0, e.id ?? 0)] ?? + MessageState(), + ); + }).toList(); + + _chatPreviewController.setAllMessageIds(messages); + + return ChatPreview( + supportBloc: false, + messages: messages, + scrollController: _scrollController, + controller: _chatPreviewController, + stateManager: widget.stateManager, + robotAvatar: selectMode + ? null + : _buildAvatar( + avatarUrl: group.group.group.avatarUrl, + id: group.group.group.id, + ), + avatarBuilder: selectMode + ? null + : (Message message) { + if (message.avatarUrl == null) { + return null; + } + + return _buildAvatar(avatarUrl: message.avatarUrl!); + }, + senderNameBuilder: (message) { + if (message.senderName == null) { + return null; + } + + return Container( + margin: const EdgeInsets.fromLTRB(0, 0, 10, 7), + padding: const EdgeInsets.symmetric(horizontal: 13), + child: Text( + message.senderName!, + style: TextStyle( + color: customColors.weakTextColor, + fontSize: 12, + ), + ), + ); + }, + onDeleteMessage: (id) { + handleDeleteMessage(context, id); + }, + onSpeakEvent: (message) { + _audioPlayerController.playAudio(message.text); + }, + onResentEvent: (message) { + _scrollController.animateTo(0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeOut); + _handleSubmit( + message.text, + ); + }, + helpWidgets: state.hasWaitTasks || loadedMessages.isEmpty + ? null + : [ + HelpTips( + onSubmitMessage: _handleSubmit, + onNewChat: () => handleResetContext(context), + ) + ], + ); + } + return const Center(child: CircularProgressIndicator()); + }, + ); + } + + /// 构建 AppBar + AppBar _buildAppBar(BuildContext context, CustomColors customColors) { + return _chatPreviewController.selectMode + ? AppBar( + title: Text( + AppLocale.select.getString(context), + style: const TextStyle(fontSize: CustomSize.appBarTitleSize), + ), + centerTitle: true, + elevation: 0, + leading: TextButton( + onPressed: () { + _chatPreviewController.exitSelectMode(); + }, + child: Text( + AppLocale.cancel.getString(context), + style: TextStyle(color: customColors.linkColor), + ), + ), + toolbarHeight: CustomSize.toolbarHeight, + ) + : AppBar( + centerTitle: true, + elevation: 0, + // backgroundColor: customColors.chatRoomBackground, + title: BlocBuilder( + buildWhen: (previous, current) => current is GroupChatLoaded, + builder: (context, state) { + if (state is GroupChatLoaded) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 房间名称 + Text( + state.group.group.name, + style: const TextStyle(fontSize: 16), + ), + ], + ); + } + + return Container(); + }, + ), + actions: [ + buildChatMoreMenu(context, widget.groupId), + ], + toolbarHeight: CustomSize.toolbarHeight, + ); + } + + Widget _buildAvatar({String? avatarUrl, int? id, int size = 30}) { + if (avatarUrl != null && avatarUrl.startsWith('http')) { + return RemoteAvatar( + avatarUrl: imageURL(avatarUrl, qiniuImageTypeAvatar), + size: size, + ); + } + + return RandomAvatar( + id: id ?? 0, + size: size, + usage: + Ability().enableAPIServer() ? AvatarUsage.room : AvatarUsage.legacy, + ); + } + + /// 提交新消息 + void _handleSubmit(String text) { + setState(() { + _inputEnabled.value = false; + }); + + var replyMemberIds = (selectedMembers ?? []).map((e) => e.id!).toList(); + context + .read() + .add(GroupChatSendEvent(widget.groupId, text, replyMemberIds)); + } + + /// 处理消息删除事件 + void handleDeleteMessage(BuildContext context, int id, {int? chatHistoryId}) { + openConfirmDialog( + context, + AppLocale.confirmDelete.getString(context), + () { + context + .read() + .add(GroupChatDeleteEvent(widget.groupId, id)); + HapticFeedbackHelper.mediumImpact(); + }, + danger: true, + ); + } + + /// 重置上下文 + void handleResetContext(BuildContext context) { + context.read().add(GroupChatSendSystemEvent( + widget.groupId, + MessageType.contextBreak, + message: 'context-break-message', + )); + HapticFeedbackHelper.mediumImpact(); + } + + /// 清空历史消息 + void handleClearHistory(BuildContext context) { + openConfirmDialog( + context, + AppLocale.confirmClearMessages.getString(context), + () { + context + .read() + .add(GroupChatDeleteAllEvent(widget.groupId)); + HapticFeedbackHelper.mediumImpact(); + }, + danger: true, + ); + } + + /// 构建聊天内容窗口 + Widget buildSelectModeToolbars( + BuildContext context, + ChatPreviewController chatPreviewController, + CustomColors customColors, + ) { + return Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10), + ), + color: customColors.backgroundColor, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + TextButton.icon( + onPressed: () { + var messages = chatPreviewController.selectedMessages(); + if (messages.isEmpty) { + showErrorMessageEnhanced( + context, AppLocale.noMessageSelected.getString(context)); + return; + } + + Navigator.push( + context, + MaterialPageRoute( + fullscreenDialog: true, + builder: (context) => ChatShareScreen( + messages: messages + .map((e) => ChatShareMessage( + content: e.message.text, + username: e.message.senderName, + avatarURL: e.message.avatarUrl, + leftSide: e.message.role == Role.receiver, + )) + .toList(), + ), + ), + ); + // var shareText = messages.map((e) { + // if (e.message.role == Role.sender) { + // return '我:\n${e.message.text}'; + // } + + // return '${e.message.senderName ?? "助理"}:\n${e.message.text}'; + // }).join('\n\n'); + + // shareTo( + // context, + // content: shareText, + // title: AppLocale.chatHistory.getString(context), + // ); + }, + icon: Icon(Icons.share, color: customColors.linkColor), + label: Text( + AppLocale.share.getString(context), + style: TextStyle(color: customColors.linkColor), + ), + ), + TextButton.icon( + onPressed: () { + chatPreviewController.selectAllMessage(); + }, + icon: + Icon(Icons.select_all_outlined, color: customColors.linkColor), + label: Text( + AppLocale.selectAll.getString(context), + style: TextStyle(color: customColors.linkColor), + ), + ), + TextButton.icon( + onPressed: () { + if (chatPreviewController.selectedMessageIds.isEmpty) { + showErrorMessageEnhanced( + context, AppLocale.noMessageSelected.getString(context)); + return; + } + + openConfirmDialog( + context, + AppLocale.confirmDelete.getString(context), + () { + final ids = chatPreviewController.selectedMessageIds.toList(); + if (ids.isNotEmpty) { + // context + // .read() + // .add(ChatMessageDeleteEvent(ids)); + + showErrorMessageEnhanced( + context, AppLocale.operateSuccess.getString(context)); + + chatPreviewController.exitSelectMode(); + } + }, + danger: true, + ); + }, + icon: Icon(Icons.delete, color: customColors.linkColor), + label: Text( + AppLocale.delete.getString(context), + style: TextStyle(color: customColors.linkColor), + ), + ), + ], + ), + ); + } + + /// 构建聊天设置下拉菜单 + Widget buildChatMoreMenu( + BuildContext context, + int chatRoomId, { + bool useLocalContext = true, + bool withSetting = true, + }) { + var customColors = Theme.of(context).extension()!; + + return EnhancedPopupMenu( + items: [ + EnhancedPopupMenuItem( + title: AppLocale.newChat.getString(context), + icon: Icons.post_add, + iconColor: Colors.blue, + onTap: (ctx) { + handleResetContext(useLocalContext ? ctx : context); + }, + ), + EnhancedPopupMenuItem( + title: AppLocale.clearChatHistory.getString(context), + icon: Icons.delete_forever, + iconColor: Colors.red, + onTap: (ctx) { + handleClearHistory(useLocalContext ? ctx : context); + }, + ), + if (withSetting) + EnhancedPopupMenuItem( + title: AppLocale.settings.getString(context), + icon: Icons.settings, + iconColor: customColors.linkColor, + onTap: (_) { + context.push('/group-chat/$chatRoomId/edit').whenComplete(() { + context + .read() + .add(GroupChatLoadEvent(widget.groupId, forceUpdate: true)); + }); + }, + ), + ], + ); + } +} diff --git a/lib/page/chat/group/create.dart b/lib/page/chat/group/create.dart new file mode 100644 index 00000000..804a81a2 --- /dev/null +++ b/lib/page/chat/group/create.dart @@ -0,0 +1,220 @@ +import 'package:askaide/bloc/room_bloc.dart'; +import 'package:askaide/helper/ability.dart'; +import 'package:askaide/helper/constant.dart'; +import 'package:askaide/helper/image.dart'; +import 'package:askaide/lang/lang.dart'; +import 'package:askaide/page/component/background_container.dart'; +import 'package:askaide/page/component/enhanced_button.dart'; +import 'package:askaide/page/component/loading.dart'; +import 'package:askaide/page/component/random_avatar.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/model/group.dart'; +import 'package:askaide/repo/model/misc.dart'; +import 'package:askaide/repo/settings_repo.dart'; +import 'package:bot_toast/bot_toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localization/flutter_localization.dart'; +import 'package:go_router/go_router.dart'; + +class GroupCreatePage extends StatefulWidget { + final SettingRepository setting; + + const GroupCreatePage({super.key, required this.setting}); + + @override + State createState() => _GroupCreatePageState(); +} + +class _GroupCreatePageState extends State { + List models = []; + List selectedModels = []; + + Function? globalLoadingCancel; + + @override + void initState() { + super.initState(); + + // 加载模型 + APIServer().models().then((value) { + setState(() { + models = value.where((e) => !e.disabled).toList(); + }); + }); + } + + @override + Widget build(BuildContext context) { + final customColors = Theme.of(context).extension()!; + return Scaffold( + appBar: AppBar( + title: const Text( + '发起群聊', + style: TextStyle(fontSize: CustomSize.appBarTitleSize), + ), + centerTitle: true, + elevation: 0, + toolbarHeight: CustomSize.toolbarHeight, + actions: [ + Padding( + padding: const EdgeInsets.only(right: 12), + child: EnhancedButton( + width: 50, + height: 30, + fontSize: 14, + title: AppLocale.ok.getString(context), + color: selectedModels.isEmpty ? customColors.weakTextColor : null, + backgroundColor: selectedModels.isEmpty + ? customColors.weakTextColor!.withAlpha(20) + : null, + onPressed: () { + onSave(context); + }, + ), + ), + ], + ), + backgroundColor: customColors.backgroundContainerColor, + body: BackgroundContainer( + setting: widget.setting, + enabled: false, + child: BlocListener( + listenWhen: (previous, current) => + current is GroupRoomUpdateResultState, + listener: (context, state) { + if (state is GroupRoomUpdateResultState) { + globalLoadingCancel?.call(); + if (state.success) { + showSuccessMessage(AppLocale.operateSuccess.getString(context)); + context.pop(); + } else { + showErrorMessageEnhanced(context, + state.error ?? AppLocale.operateFailed.getString(context)); + } + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.only(top: 15, left: 20, bottom: 15), + child: Text( + '选择参与群聊的成员', + style: TextStyle( + fontSize: 14, + color: customColors.weakLinkColor, + ), + ), + ), + Expanded( + child: ListView.separated( + itemCount: models.length, + itemBuilder: (context, i) { + var item = models[i]; + return CheckboxListTile( + controlAffinity: ListTileControlAffinity.leading, + checkboxShape: const CircleBorder(), + activeColor: customColors.linkColor, + side: BorderSide( + color: customColors.weakTextColor!.withAlpha(100), + ), + title: Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + _buildAvatar(avatarUrl: item.avatarUrl, size: 40), + const SizedBox(width: 20), + Expanded( + child: Container( + alignment: Alignment.centerLeft, + child: Text(item.name), + ), + ), + ], + ), + ), + onChanged: (selected) { + setState(() { + if (selectedModels.contains(item)) { + selectedModels.remove(item); + } else { + selectedModels.add(item); + } + }); + }, + value: selectedModels.contains(item), + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider( + height: 1, + color: + customColors.columnBlockDividerColor?.withAlpha(200), + ); + }, + ), + ), + ], + ), + ), + ), + ); + } + + void onSave(BuildContext context) { + if (selectedModels.isEmpty) { + return; + } + + globalLoadingCancel = BotToast.showCustomLoading( + toastBuilder: (cancel) { + return LoadingIndicator( + message: AppLocale.processingWait.getString(context), + ); + }, + allowClick: false, + duration: const Duration(seconds: 120), + ); + + try { + if (context.mounted) { + context.read().add( + GroupRoomCreateEvent( + name: selectedModels.map((e) => e.shortName).take(3).join("、"), + members: selectedModels + .map((e) => GroupMember( + modelId: e.realModelId, modelName: e.shortName)) + .toList(), + ), + ); + } + } catch (e) { + globalLoadingCancel?.call(); + // ignore: use_build_context_synchronously + showErrorMessageEnhanced(context, e); + } + } + + Widget _buildAvatar({String? avatarUrl, int? id, int size = 30}) { + if (avatarUrl != null && avatarUrl.startsWith('http')) { + return RemoteAvatar( + avatarUrl: imageURL(avatarUrl, qiniuImageTypeAvatar), + size: size, + ); + } + + return RandomAvatar( + id: id ?? 0, + size: size, + usage: + Ability().enableAPIServer() ? AvatarUsage.room : AvatarUsage.legacy, + ); + } +} diff --git a/lib/page/chat/group/edit.dart b/lib/page/chat/group/edit.dart new file mode 100644 index 00000000..1c602175 --- /dev/null +++ b/lib/page/chat/group/edit.dart @@ -0,0 +1,500 @@ +import 'package:askaide/bloc/group_chat_bloc.dart'; +import 'package:askaide/repo/model/group.dart'; +import 'package:askaide/repo/settings_repo.dart'; +import 'package:flutter/material.dart'; +import 'dart:io'; +import 'dart:math'; + +import 'package:askaide/bloc/room_bloc.dart'; +import 'package:askaide/helper/ability.dart'; +import 'package:askaide/helper/constant.dart'; +import 'package:askaide/helper/image.dart'; +import 'package:askaide/helper/upload.dart'; +import 'package:askaide/lang/lang.dart'; +import 'package:askaide/page/component/avatar_selector.dart'; +import 'package:askaide/page/component/background_container.dart'; +import 'package:askaide/page/component/column_block.dart'; +import 'package:askaide/page/component/enhanced_button.dart'; +import 'package:askaide/page/component/enhanced_input.dart'; +import 'package:askaide/page/component/enhanced_textfield.dart'; +import 'package:askaide/page/component/image.dart'; +import 'package:askaide/page/component/loading.dart'; +import 'package:askaide/page/component/multi_item_selector.dart'; +import 'package:askaide/page/component/random_avatar.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/model/misc.dart'; +import 'package:bot_toast/bot_toast.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localization/flutter_localization.dart'; +import 'package:go_router/go_router.dart'; + +class ModelWithMemberId { + final Model model; + final int? memberId; + + ModelWithMemberId(this.model, this.memberId); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ModelWithMemberId && + runtimeType == other.runtimeType && + model == other.model && + memberId == other.memberId; +} + +class GroupEditPage extends StatefulWidget { + final SettingRepository setting; + final int groupId; + + const GroupEditPage({ + super.key, + required this.groupId, + required this.setting, + }); + + @override + State createState() => _GroupEditPageState(); +} + +class _GroupEditPageState extends State { + final _nameController = TextEditingController(text: ''); + + String? _avatarUrl; + List avatarPresets = []; + + final randomSeed = Random().nextInt(10000); + + List models = []; + List selectedModels = []; + + Function? globalLoadingCancel; + + @override + void initState() { + super.initState(); + + // 加载预定义头像 + APIServer().avatars().then((value) { + avatarPresets = value; + }); + + // 加载模型 + APIServer().models().then((value) { + setState(() { + models = value; + }); + context + .read() + .add(GroupChatLoadEvent(widget.groupId, forceUpdate: true)); + }); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final customColors = Theme.of(context).extension()!; + return Scaffold( + appBar: AppBar( + title: Text( + AppLocale.roomSetting.getString(context), + style: const TextStyle(fontSize: CustomSize.appBarTitleSize), + ), + centerTitle: true, + elevation: 0, + toolbarHeight: CustomSize.toolbarHeight, + ), + backgroundColor: customColors.backgroundContainerColor, + body: BackgroundContainer( + setting: widget.setting, + enabled: false, + child: BlocListener( + listenWhen: (previous, current) => + current is GroupRoomUpdateResultState, + listener: (context, state) { + if (state is GroupRoomUpdateResultState) { + globalLoadingCancel?.call(); + if (state.success) { + showSuccessMessage(AppLocale.operateSuccess.getString(context)); + } else { + showErrorMessageEnhanced(context, + state.error ?? AppLocale.operateFailed.getString(context)); + } + + context + .read() + .add(GroupChatLoadEvent(widget.groupId, forceUpdate: true)); + } + }, + child: BlocConsumer( + listenWhen: (previous, current) => current is GroupChatLoaded, + listener: (context, state) { + if (state is GroupChatLoaded) { + _nameController.text = state.group.group.name; + _avatarUrl = state.group.group.avatarUrl; + selectedModels = state.group.members + .where((e) => e.status != 2) + .map((e) { + final mod = models + .where((em) => e.modelId == em.realModelId) + .firstOrNull; + if (mod == null) { + return null; + } + + return ModelWithMemberId(mod, e.id); + }) + .where((e) => e != null) + .map((e) => e!) + .toList(); + + final selectedModelIds = + selectedModels.map((e) => e.model.realModelId).toList(); + + models = models + .where((e) => + !e.disabled || selectedModelIds.contains(e.realModelId)) + .toList(); + } + }, + builder: (context, state) { + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + ColumnBlock( + children: [ + // 名称 + EnhancedTextField( + customColors: customColors, + controller: _nameController, + maxLength: 50, + maxLines: 1, + showCounter: false, + labelText: '名称', + labelPosition: LabelPosition.left, + hintText: AppLocale.required.getString(context), + textDirection: TextDirection.rtl, + ), + EnhancedInput( + padding: const EdgeInsets.only(top: 10, bottom: 5), + title: Text( + '头像', + style: TextStyle( + color: customColors.textfieldLabelColor, + fontSize: 16, + ), + ), + value: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 45, + height: 45, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: _avatarUrl == null + ? null + : DecorationImage( + image: (_avatarUrl!.startsWith('http') + ? CachedNetworkImageProviderEnhanced( + _avatarUrl!) + : FileImage(File( + _avatarUrl!))) as ImageProvider, + fit: BoxFit.cover, + ), + ), + child: _avatarUrl == null + ? const Center( + child: Icon( + Icons.interests, + color: Colors.grey, + ), + ) + : const SizedBox(), + ), + ], + ), + onPressed: () { + openModalBottomSheet( + context, + (context) { + return AvatarSelector( + onSelected: (selected) { + setState(() { + _avatarUrl = selected.url; + }); + context.pop(); + }, + usage: AvatarUsage.room, + randomSeed: randomSeed, + defaultAvatarUrl: _avatarUrl, + externalAvatarUrls: [ + ...avatarPresets, + ], + ); + }, + heightFactor: 0.8, + ); + }, + ), + ], + ), + ColumnBlock( + children: [ + // 成员 + EnhancedInput( + padding: const EdgeInsets.only(top: 10, bottom: 5), + title: Text( + '成员', + style: TextStyle( + color: customColors.textfieldLabelColor, + fontSize: 16, + ), + ), + value: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + children: [ + Container( + width: resolveSelectedModelsPreviewWidth( + context), + height: 45, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + clipBehavior: Clip.hardEdge, + child: buildSelectedModelsPreview(), + ), + if (selectedModels.isNotEmpty) + Positioned( + right: 0, + top: 0, + child: Container( + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: customColors.tagsBackground, + borderRadius: + BorderRadius.circular(8), + ), + child: Text( + 'x${selectedModels.length}', + style: TextStyle( + fontSize: 8, + color: customColors.weakTextColor, + ), + ), + ), + ) + ], + ), + ], + ), + onPressed: () { + openModalBottomSheet( + context, + (context) { + return MultiItemSelector( + itemBuilder: (item) { + return Text(item.model.name); + }, + items: models + .map((e) => ModelWithMemberId( + e, + selectedModels + .where( + (se) => se.model.id == e.id) + .firstOrNull + ?.memberId)) + .toList(), + onChanged: (selected) { + setState(() { + selectedModels = selected; + }); + }, + itemAvatarBuilder: (item) { + return _buildAvatar( + avatarUrl: item.model.avatarUrl, + size: 30, + ); + }, + selectedItems: selectedModels, + ); + }, + heightFactor: 0.8, + ); + }, + ), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: EnhancedButton( + title: AppLocale.save.getString(context), + color: + canSubmit() ? null : customColors.weakTextColor, + backgroundColor: canSubmit() + ? null + : customColors.weakTextColor!.withAlpha(20), + onPressed: () async { + if (!canSubmit()) { + return; + } + + globalLoadingCancel = BotToast.showCustomLoading( + toastBuilder: (cancel) { + return LoadingIndicator( + message: AppLocale.processingWait + .getString(context), + ); + }, + allowClick: false, + duration: const Duration(seconds: 120), + ); + + final name = _nameController.text.trim(); + if (name == '') { + globalLoadingCancel?.call(); + showErrorMessage('请输入群组名称'); + return; + } + + try { + if (_avatarUrl != null) { + if (!(_avatarUrl!.startsWith('http://') || + _avatarUrl!.startsWith('https://'))) { + // 上传文件,获取 URL + final cancel = BotToast.showCustomLoading( + toastBuilder: (cancel) { + return const LoadingIndicator( + message: "正在上传图片,请稍后...", + ); + }, + allowClick: false, + ); + + final uploadRes = + await ImageUploader(widget.setting) + .upload(_avatarUrl!, + usage: 'avatar') + .whenComplete(() => cancel()); + _avatarUrl = uploadRes.url; + } + } + + if (context.mounted) { + context.read().add( + GroupRoomUpdateEvent( + groupId: widget.groupId, + name: name, + avatarUrl: _avatarUrl, + members: selectedModels + .map((e) => GroupMember( + modelId: e.model.realModelId, + modelName: e.model.shortName, + id: e.memberId)) + .toList(), + ), + ); + } + } catch (e) { + globalLoadingCancel?.call(); + // ignore: use_build_context_synchronously + showErrorMessageEnhanced(context, e); + } + }, + ), + ), + ], + ), + ], + ), + ); + }, + ), + ), + ), + ); + } + + bool canSubmit() { + if (selectedModels.isEmpty) { + return false; + } + + if (_nameController.text.trim() == '') { + return false; + } + + return true; + } + + Widget buildSelectedModelsPreview() { + if (selectedModels.isEmpty) { + return const Center( + child: Icon( + Icons.group, + color: Colors.grey, + ), + ); + } + + return Stack( + clipBehavior: Clip.none, + children: [ + for (var i = 0; i < selectedModels.length; i++) + i == 0 + ? _buildAvatar( + avatarUrl: selectedModels.first.model.avatarUrl, + size: 30, + ) + : Positioned( + left: i * 15.0, + child: _buildAvatar( + avatarUrl: selectedModels[i].model.avatarUrl, + size: 30, + ), + ), + ], + ); + } + + Widget _buildAvatar({String? avatarUrl, int? id, int size = 30}) { + if (avatarUrl != null && avatarUrl.startsWith('http')) { + return RemoteAvatar( + avatarUrl: imageURL(avatarUrl, qiniuImageTypeAvatar), + size: size, + ); + } + + return RandomAvatar( + id: id ?? 0, + size: size, + usage: + Ability().enableAPIServer() ? AvatarUsage.room : AvatarUsage.legacy, + ); + } + + double resolveSelectedModelsPreviewWidth(BuildContext context) { + final maxSize = MediaQuery.of(context).size.width - 180; + final expectSize = 45.0 + selectedModels.length * 15; + + return expectSize > maxSize ? maxSize : expectSize; + } +} diff --git a/lib/page/chat_chat.dart b/lib/page/chat/home.dart similarity index 83% rename from lib/page/chat_chat.dart rename to lib/page/chat/home.dart index 07b591c5..e03f2697 100644 --- a/lib/page/chat_chat.dart +++ b/lib/page/chat/home.dart @@ -16,14 +16,16 @@ import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/model_indicator.dart'; import 'package:askaide/page/component/notify_message.dart'; import 'package:askaide/page/component/sliver_component.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/chat_history.dart'; +import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -32,11 +34,11 @@ import 'package:go_router/go_router.dart'; import 'package:quickalert/models/quickalert_type.dart'; import 'package:url_launcher/url_launcher_string.dart'; -class ChatChatScreen extends StatefulWidget { +class HomePage extends StatefulWidget { final SettingRepository setting; final bool showInitialDialog; final int? reward; - const ChatChatScreen({ + const HomePage({ super.key, required this.setting, this.showInitialDialog = false, @@ -44,7 +46,7 @@ class ChatChatScreen extends StatefulWidget { }); @override - State createState() => _ChatChatScreenState(); + State createState() => _HomePageState(); } class ChatModel { @@ -61,7 +63,7 @@ class ChatModel { }); } -class _ChatChatScreenState extends State { +class _HomePageState extends State { final TextEditingController _textController = TextEditingController(); ModelIndicatorInfo? currentModel; @@ -89,6 +91,21 @@ class _ChatChatScreenState extends State { /// 促销事件 PromotionEvent? promotionEvent; + /// 用于监听键盘事件,实现回车发送消息,Shift+Enter换行 + late final FocusNode _focusNode = FocusNode( + onKey: (node, event) { + if (!event.isShiftPressed && event.logicalKey.keyLabel == 'Enter') { + if (event is RawKeyDownEvent) { + onSubmit(context, _textController.text.trim()); + } + + return KeyEventResult.handled; + } else { + return KeyEventResult.ignored; + } + }, + ); + @override void dispose() { _textController.dispose(); @@ -112,6 +129,33 @@ class _ChatChatScreenState extends State { .toList(); } + APIServer().capabilities().then((cap) { + Ability().updateCapabilities(cap); + + if (cap.homeModels.isNotEmpty) { + models = cap.homeModels + .map((e) => ModelIndicatorInfo( + modelId: e.modelId, + modelName: e.name, + description: e.desc, + icon: e.powerful ? Icons.auto_awesome : Icons.bolt, + activeColor: stringToColor(e.color), + )) + .toList(); + + if (mounted) { + // 加载免费模型剩余使用次数 + if (currentModel != null) { + context + .read() + .add(FreeCountReloadEvent(model: currentModel!.modelId)); + } + + setState(() {}); + } + } + }); + // 是否显示免费模型提示消息 Cache().boolGet(key: 'show_home_free_model_message').then((show) async { if (show) { @@ -139,13 +183,6 @@ class _ChatChatScreenState extends State { currentModel = models[0]; }); - // 加载免费模型剩余使用次数 - if (currentModel != null) { - context - .read() - .add(FreeCountReloadEvent(model: currentModel!.modelId)); - } - if (widget.showInitialDialog) { WidgetsBinding.instance.addPostFrameCallback((_) { showBeautyDialog( @@ -194,6 +231,7 @@ class _ChatChatScreenState extends State { map[model.modelId] = ModelIndicator( model: model, selected: model.modelId == currentModel?.modelId, + showDescription: Ability().showHomeModelDescription, ); } return map; @@ -219,6 +257,18 @@ class _ChatChatScreenState extends State { color: customColors.backgroundInvertedColor, ), ), + actions: [ + IconButton( + icon: const Icon(Icons.history), + onPressed: () { + context.push('/chat-chat/history').whenComplete(() { + context + .read() + .add(ChatChatLoadRecentHistories()); + }); + }, + ), + ], backgroundImage: Image.asset( customColors.appBarBackgroundImage!, fit: BoxFit.cover, @@ -240,9 +290,9 @@ class _ChatChatScreenState extends State { bottom: false, child: Container( margin: - const EdgeInsets.only(top: 20, left: 15), + const EdgeInsets.only(top: 10, left: 15), child: Text( - '历史记录', + AppLocale.histories.getString(context), style: TextStyle( color: customColors.weakTextColor ?.withAlpha(100), @@ -252,6 +302,56 @@ class _ChatChatScreenState extends State { ), ); } + + if (index == state.histories.length && index > 3) { + return SafeArea( + top: false, + bottom: false, + child: GestureDetector( + onTap: () { + context + .push('/chat-chat/history') + .whenComplete(() { + context + .read() + .add(ChatChatLoadRecentHistories()); + }); + }, + child: Container( + alignment: Alignment.center, + margin: const EdgeInsets.only( + top: 5, bottom: 15), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons.keyboard_double_arrow_left, + size: 12, + color: customColors.weakTextColor! + .withAlpha(120), + ), + Text( + "查看更多", + style: TextStyle( + fontSize: 12, + color: customColors.weakTextColor! + .withAlpha(120), + ), + ), + Icon( + Icons.keyboard_double_arrow_right, + size: 12, + color: customColors.weakTextColor! + .withAlpha(120), + ), + ], + ), + ), + ), + ); + } + return SafeArea( top: false, bottom: false, @@ -261,7 +361,7 @@ class _ChatChatScreenState extends State { onTap: () { context .push( - '/chat-anywhere?chat_id=${state.histories[index - 1].id}') + '/chat-anywhere?chat_id=${state.histories[index - 1].id}&model=${state.histories[index - 1].model}&title=${state.histories[index - 1].title}') .whenComplete(() { FocusScope.of(context) .requestFocus(FocusNode()); @@ -319,7 +419,7 @@ class _ChatChatScreenState extends State { children: buildModelIndicators(), padding: 0, isStretch: true, - height: 60, + height: Ability().showHomeModelDescription ? 60 : 45, innerPadding: const EdgeInsets.all(0), decoration: BoxDecoration( color: customColors.columnBlockBackgroundColor?.withAlpha(150), @@ -380,6 +480,7 @@ class _ChatChatScreenState extends State { ), Expanded( child: EnhancedTextField( + focusNode: _focusNode, controller: _textController, customColors: customColors, maxLines: 10, @@ -655,10 +756,6 @@ class _ChatChatScreenState extends State { ), InkWell( onTap: () { - if (_textController.text.trim().isEmpty) { - return; - } - onSubmit(context, _textController.text.trim()); }, child: Icon( @@ -675,6 +772,10 @@ class _ChatChatScreenState extends State { } void onSubmit(BuildContext context, String text) { + if (text.trim().isEmpty) { + return; + } + context .push(Uri(path: '/chat-anywhere', queryParameters: { 'init_message': text, diff --git a/lib/page/chat_anywhere.dart b/lib/page/chat/home_chat.dart similarity index 80% rename from lib/page/chat_anywhere.dart rename to lib/page/chat/home_chat.dart index 8ad83016..b42d0e5a 100644 --- a/lib/page/chat_anywhere.dart +++ b/lib/page/chat/home_chat.dart @@ -3,8 +3,9 @@ import 'package:askaide/bloc/free_count_bloc.dart'; import 'package:askaide/bloc/notify_bloc.dart'; import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/helper/constant.dart'; +import 'package:askaide/helper/model.dart'; import 'package:askaide/lang/lang.dart'; -import 'package:askaide/page/chat_screen.dart'; +import 'package:askaide/page/chat/room_chat.dart'; import 'package:askaide/page/component/audio_player.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/chat/chat_input.dart'; @@ -14,38 +15,42 @@ import 'package:askaide/page/component/chat/help_tips.dart'; import 'package:askaide/page/component/chat/message_state_manager.dart'; import 'package:askaide/page/component/enhanced_error.dart'; import 'package:askaide/page/component/random_avatar.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; -import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:askaide/repo/model/chat_history.dart'; import 'package:askaide/repo/model/message.dart'; +import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/model/room.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; +import 'package:askaide/repo/model/model.dart' as mm; -class ChatAnywhereScreen extends StatefulWidget { +class HomeChatPage extends StatefulWidget { final MessageStateManager stateManager; final SettingRepository setting; final int? chatId; final String? initialMessage; final String? model; + final String? title; - const ChatAnywhereScreen({ + const HomeChatPage({ super.key, required this.stateManager, required this.setting, this.chatId, this.initialMessage, this.model, + this.title, }); @override - State createState() => _ChatAnywhereScreenState(); + State createState() => _HomeChatPageState(); } -class _ChatAnywhereScreenState extends State { +class _HomeChatPageState extends State { final ChatPreviewController _chatPreviewController = ChatPreviewController(); final ScrollController _scrollController = ScrollController(); final ValueNotifier _inputEnabled = ValueNotifier(true); @@ -56,10 +61,11 @@ class _ChatAnywhereScreenState extends State { bool showAudioPlayer = false; + List supportModels = []; + @override void initState() { chatId = widget.chatId; - context.read().add(RoomLoadEvent( chatAnywhereRoomId, chatHistoryId: chatId, @@ -92,6 +98,13 @@ class _ChatAnywhereScreenState extends State { }); }; + // 加载模型列表,用于查询模型名称 + ModelAggregate.models().then((value) { + setState(() { + supportModels = value; + }); + }); + super.initState(); } @@ -118,9 +131,9 @@ class _ChatAnywhereScreenState extends State { listener: (context, state) { if (state is RoomLoaded && state.cascading) { // 加载免费使用次数 - context - .read() - .add(FreeCountReloadEvent(model: state.room.model)); + context.read().add(FreeCountReloadEvent( + model: widget.model ?? state.room.model, + )); } }, buildWhen: (previous, current) => current is RoomLoaded, @@ -174,45 +187,29 @@ class _ChatAnywhereScreenState extends State { if (state is ChatMessagesLoaded) { return Column( children: [ - Text( - AppLocale.chatAnywhere.getString(context), - style: const TextStyle(fontSize: CustomSize.appBarTitleSize), + Container( + width: MediaQuery.of(context).size.width / 2, + alignment: Alignment.center, + child: Text( + widget.title ?? AppLocale.chatAnywhere.getString(context), + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: + const TextStyle(fontSize: CustomSize.appBarTitleSize), + ), ), - // BlocBuilder( - // buildWhen: (previous, current) => current is RoomLoaded, - // builder: (context, state) { - // if (state is RoomLoaded) { - // return BlocBuilder( - // buildWhen: (previous, current) => - // current is FreeCountLoadedState, - // builder: (context, freeState) { - // if (freeState is FreeCountLoadedState) { - // final matched = freeState.model(state.room.model); - // if (matched != null && - // matched.leftCount > 0 && - // matched.maxCount > 0) { - // return Text( - // '今日剩余免费 ${matched.leftCount} 次', - // style: TextStyle( - // color: customColors.weakTextColor, - // fontSize: 12, - // ), - // ); - // } - // } - // return const SizedBox(); - // }, - // ); - // } - // return const SizedBox(); - // }, - // ), - // if (state.chatHistory != null && - // state.chatHistory!.model != null) - // Text( - // state.chatHistory!.model ?? '', - // style: const TextStyle(fontSize: 12), - // ), + if (state.chatHistory?.model != null) + Text( + supportModels + .where((e) => e.id == state.chatHistory!.model!) + .firstOrNull + ?.shortName ?? + '', + style: TextStyle( + color: customColors.weakTextColor, + fontSize: 10, + ), + ) ], ); } @@ -220,15 +217,6 @@ class _ChatAnywhereScreenState extends State { return const SizedBox(); }, ), - - // actions: [ - // buildChatMoreMenu( - // context, - // chatAnywhereRoomId, - // useLocalContext: false, - // withSetting: false, - // ), - // ], flexibleSpace: SizedBox( width: double.infinity, child: ShaderMask( @@ -288,6 +276,10 @@ class _ChatAnywhereScreenState extends State { _inputEnabled.value = false; }); } else if (!state.processing && !_inputEnabled.value) { + // 更新免费使用次数 + context.read().add(FreeCountReloadEvent( + model: widget.model ?? room.room.model)); + // 聊天回复完成时,取消输入框的禁止编辑状态 setState(() { _inputEnabled.value = true; @@ -332,6 +324,7 @@ class _ChatAnywhereScreenState extends State { hintText += '(今日还可免费畅享${matched.leftCount}次)'; } } + return SafeArea( child: ChatInput( enableNotifier: _inputEnabled, @@ -388,6 +381,17 @@ class _ChatAnywhereScreenState extends State { } final messages = loadedMessages.map((e) { + if (loadedState.chatHistory != null && + loadedState.chatHistory!.model != null) { + final mod = supportModels + .where((e) => e.id == loadedState.chatHistory!.model!) + .firstOrNull; + if (mod != null) { + e.senderName = mod.shortName; + e.avatarUrl = mod.avatarUrl; + } + } + final stateMessage = room.states[widget.stateManager.getKey(e.roomId ?? 0, e.id ?? 0)] ?? MessageState(); @@ -401,7 +405,9 @@ class _ChatAnywhereScreenState extends State { scrollController: _scrollController, controller: _chatPreviewController, stateManager: widget.stateManager, - robotAvatar: selectMode ? null : _buildAvatar(room.room), + robotAvatar: selectMode + ? null + : _buildAvatar(room.room, his: loadedState.chatHistory), onDeleteMessage: (id) { handleDeleteMessage(context, id, chatHistoryId: chatId); }, @@ -446,11 +452,18 @@ class _ChatAnywhereScreenState extends State { .add(RoomLoadEvent(chatAnywhereRoomId, cascading: false)); } - Widget _buildAvatar(Room room) { + Widget _buildAvatar(Room room, {ChatHistory? his}) { if (room.avatarUrl != null && room.avatarUrl!.startsWith('http')) { return RemoteAvatar(avatarUrl: room.avatarUrl!, size: 30); } + if (his != null && his.model != null) { + var mod = supportModels.where((e) => e.id == his.model!).firstOrNull; + if (mod != null && mod.avatarUrl != null && mod.avatarUrl != '') { + return RemoteAvatar(avatarUrl: mod.avatarUrl!, size: 30); + } + } + return const LocalAvatar(assetName: 'assets/app.png', size: 30); } } diff --git a/lib/page/chat/home_chat_history.dart b/lib/page/chat/home_chat_history.dart new file mode 100644 index 00000000..e2a3422a --- /dev/null +++ b/lib/page/chat/home_chat_history.dart @@ -0,0 +1,138 @@ +import 'package:askaide/bloc/chat_chat_bloc.dart'; +import 'package:askaide/lang/lang.dart'; +import 'package:askaide/page/chat/home.dart'; +import 'package:askaide/page/component/background_container.dart'; +import 'package:askaide/page/component/loading.dart'; +import 'package:askaide/page/data/chat_history_datasource.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:askaide/repo/chat_message_repo.dart'; +import 'package:askaide/repo/model/chat_history.dart'; +import 'package:askaide/repo/settings_repo.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localization/flutter_localization.dart'; +import 'package:go_router/go_router.dart'; +import 'package:loading_more_list/loading_more_list.dart'; + +class HomeChatHistoryPage extends StatefulWidget { + final SettingRepository setting; + final ChatMessageRepository chatMessageRepo; + + const HomeChatHistoryPage( + {super.key, required this.setting, required this.chatMessageRepo}); + + @override + State createState() => _HomeChatHistoryPageState(); +} + +class _HomeChatHistoryPageState extends State { + late final ChatHistoryDatasource datasource; + + @override + void initState() { + datasource = ChatHistoryDatasource(widget.chatMessageRepo); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final customColors = Theme.of(context).extension()!; + + return Scaffold( + appBar: AppBar( + title: Text( + AppLocale.histories.getString(context), + style: const TextStyle(fontSize: CustomSize.appBarTitleSize), + ), + toolbarHeight: CustomSize.toolbarHeight, + centerTitle: true, + ), + backgroundColor: customColors.backgroundContainerColor, + body: BackgroundContainer( + setting: widget.setting, + enabled: false, + child: SafeArea( + top: false, + left: false, + right: false, + child: RefreshIndicator( + color: customColors.linkColor, + onRefresh: () async { + await datasource.refresh(); + }, + child: BlocListener( + listenWhen: (previous, current) => + current is ChatChatRecentHistoriesLoaded, + listener: (context, state) { + if (state is ChatChatRecentHistoriesLoaded) { + datasource.refresh(); + } + }, + child: RefreshIndicator( + color: customColors.linkColor, + displacement: 20, + onRefresh: () { + return datasource.refresh(); + }, + child: LoadingMoreList( + ListConfig( + itemBuilder: (context, item, index) { + return ChatHistoryItem( + history: item, + customColors: customColors, + onTap: () { + context + .push( + '/chat-anywhere?chat_id=${item.id}&model=${item.model}&title=${item.title}') + .whenComplete(() { + FocusScope.of(context).requestFocus(FocusNode()); + context + .read() + .add(ChatChatLoadRecentHistories()); + }); + }, + ); + }, + sourceList: datasource, + indicatorBuilder: (context, status) { + String msg = ''; + switch (status) { + case IndicatorStatus.noMoreLoad: + msg = '~ 没有更多了 ~'; + break; + case IndicatorStatus.loadingMoreBusying: + msg = '加载中...'; + break; + case IndicatorStatus.error: + msg = '加载失败,请稍后再试'; + break; + case IndicatorStatus.empty: + msg = '暂无数据'; + break; + default: + return const Center(child: LoadingIndicator()); + } + return Container( + padding: const EdgeInsets.all(15), + alignment: Alignment.center, + child: Text( + msg, + style: TextStyle( + color: customColors.weakTextColor, + fontSize: 14, + ), + ), + ); + }, + ), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/page/chat_screen.dart b/lib/page/chat/room_chat.dart similarity index 91% rename from lib/page/chat_screen.dart rename to lib/page/chat/room_chat.dart index 218c1dc8..2df998ee 100644 --- a/lib/page/chat_screen.dart +++ b/lib/page/chat/room_chat.dart @@ -1,11 +1,11 @@ import 'package:askaide/bloc/free_count_bloc.dart'; -import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/audio_player.dart'; import 'package:askaide/page/component/background_container.dart'; +import 'package:askaide/page/component/chat/chat_share.dart'; import 'package:askaide/page/component/chat/empty.dart'; import 'package:askaide/page/component/chat/help_tips.dart'; import 'package:askaide/page/component/chat/message_state_manager.dart'; @@ -13,31 +13,31 @@ import 'package:askaide/page/component/effect/glass.dart'; import 'package:askaide/page/component/enhanced_popup_menu.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/random_avatar.dart'; -import 'package:askaide/page/component/share.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/bloc/chat_message_bloc.dart'; import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/bloc/notify_bloc.dart'; import 'package:askaide/page/component/chat/chat_input.dart'; import 'package:askaide/page/component/chat/chat_preview.dart'; -import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/message.dart'; +import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/model/room.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_initicon/flutter_initicon.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; -import 'dialog.dart'; +import '../component/dialog.dart'; -class ChatScreen extends StatefulWidget { +class RoomChatPage extends StatefulWidget { final int roomId; final MessageStateManager stateManager; final SettingRepository setting; - const ChatScreen({ + const RoomChatPage({ super.key, required this.roomId, required this.stateManager, @@ -45,10 +45,10 @@ class ChatScreen extends StatefulWidget { }); @override - State createState() => _ChatScreenState(); + State createState() => _RoomChatPageState(); } -class _ChatScreenState extends State { +class _RoomChatPageState extends State { final ScrollController _scrollController = ScrollController(); final ValueNotifier _inputEnabled = ValueNotifier(true); final ChatPreviewController _chatPreviewController = ChatPreviewController(); @@ -238,7 +238,7 @@ class _ChatScreenState extends State { if (loadedMessages.isEmpty) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), + padding: const EdgeInsets.only(left: 15, right: 15, top: 10), child: EmptyPreview( examples: room.examples ?? [], onSubmit: _handleSubmit, @@ -247,6 +247,9 @@ class _ChatScreenState extends State { } final messages = loadedMessages.map((e) { + e.avatarUrl = room.room.avatarUrl; + e.senderName = room.room.name; + return MessageWithState( e, room.states[ @@ -378,11 +381,11 @@ class _ChatScreenState extends State { ); } - return RandomAvatar( - id: room.avatar, + return Initicon( + text: room.name.split('、').join(' '), size: 30, - usage: - Ability().supportAPIServer() ? AvatarUsage.room : AvatarUsage.legacy, + backgroundColor: Colors.grey.withAlpha(100), + borderRadius: BorderRadius.circular(8), ); } @@ -583,19 +586,42 @@ Widget buildSelectModeToolbars( context, AppLocale.noMessageSelected.getString(context)); return; } - var shareText = messages.map((e) { - if (e.message.role == Role.sender) { - return e.message.text; - } - - return e.message.text; - }).join('\n\n'); - shareTo( + Navigator.push( context, - content: shareText, - title: AppLocale.chatHistory.getString(context), + MaterialPageRoute( + fullscreenDialog: true, + builder: (context) => ChatShareScreen( + messages: messages + .map((e) => ChatShareMessage( + content: e.message.text, + username: e.message.senderName, + avatarURL: e.message.avatarUrl, + leftSide: e.message.role == Role.receiver, + )) + .toList(), + ), + ), ); + // var messages = chatPreviewController.selectedMessages(); + // if (messages.isEmpty) { + // showErrorMessageEnhanced( + // context, AppLocale.noMessageSelected.getString(context)); + // return; + // } + // var shareText = messages.map((e) { + // if (e.message.role == Role.sender) { + // return '我:\n${e.message.text}'; + // } + + // return '助理:\n${e.message.text}'; + // }).join('\n\n'); + + // shareTo( + // context, + // content: shareText, + // title: AppLocale.chatHistory.getString(context), + // ); }, icon: Icon(Icons.share, color: customColors.linkColor), label: Text( diff --git a/lib/page/chat_room_create.dart b/lib/page/chat/room_create.dart similarity index 93% rename from lib/page/chat_room_create.dart rename to lib/page/chat/room_create.dart index 8f6c66f8..291c9b61 100644 --- a/lib/page/chat_room_create.dart +++ b/lib/page/chat/room_create.dart @@ -18,7 +18,7 @@ import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/room_card.dart'; import 'package:askaide/page/component/weak_text_button.dart'; -import 'package:askaide/page/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/repo/api/room_gallery.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; @@ -29,21 +29,21 @@ import 'package:flutter_localization/flutter_localization.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/model.dart'; import 'package:askaide/page/component/model_item.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/model/model.dart' as mm; import 'package:go_router/go_router.dart'; /// 创建聊天室对话框 -class ChatRoomCreateScreen extends StatefulWidget { +class RoomCreatePage extends StatefulWidget { final SettingRepository setting; - const ChatRoomCreateScreen({super.key, required this.setting}); + const RoomCreatePage({super.key, required this.setting}); @override - State createState() => _ChatRoomCreateScreenState(); + State createState() => _RoomCreatePageState(); } -class _ChatRoomCreateScreenState extends State { +class _RoomCreatePageState extends State { final _nameController = TextEditingController(text: ''); final _promptController = TextEditingController(text: ''); final _initMessageController = TextEditingController(text: ''); @@ -55,13 +55,13 @@ class _ChatRoomCreateScreenState extends State { List avatarPresets = []; - int maxContext = 5; + int maxContext = 3; List validMemories = [ ChatMemory('无记忆', 1, description: '每次对话都是独立的,常用于一次性问答'), - ChatMemory('基础', 5, description: '记住最近的 5 次对话'), - ChatMemory('中等', 10, description: '记住最近的 10 次对话'), - ChatMemory('深度', 20, description: '记住最近的 20 次对话'), + ChatMemory('基础', 3, description: '记住最近的 3 次对话'), + ChatMemory('中等', 6, description: '记住最近的 6 次对话'), + ChatMemory('深度', 10, description: '记住最近的 10 次对话'), ]; bool showAdvancedOptions = false; @@ -75,7 +75,7 @@ class _ChatRoomCreateScreenState extends State { void initState() { super.initState(); - if (Ability().supportAPIServer()) { + if (Ability().enableAPIServer()) { APIServer().avatars().then((value) { avatarPresets = value; }); @@ -111,7 +111,7 @@ class _ChatRoomCreateScreenState extends State { maxWidth: 0, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10), - child: Ability().supportAPIServer() + child: Ability().enableAPIServer() ? SafeArea( top: false, child: DefaultTabController( @@ -134,6 +134,8 @@ class _ChatRoomCreateScreenState extends State { isScrollable: true, labelColor: customColors.linkColor, indicator: const BoxDecoration(), + labelPadding: + const EdgeInsets.only(right: 5, left: 10), overlayColor: MaterialStateProperty.all(Colors.transparent), ), @@ -202,7 +204,7 @@ class _ChatRoomCreateScreenState extends State { child: Row( children: [ WeakTextButton( - title: '取消', + title: AppLocale.cancel.getString(context), onPressed: () { selectedSuggestions.clear(); setState(() {}); @@ -211,7 +213,7 @@ class _ChatRoomCreateScreenState extends State { const SizedBox(width: 20), Expanded( child: EnhancedButton( - title: '添加为专属伙伴', + title: AppLocale.ok.getString(context), onPressed: () { context.read().add(GalleryRoomCopyEvent( selectedSuggestions.map((e) => e.id).toList())); @@ -280,16 +282,16 @@ class _ChatRoomCreateScreenState extends State { maxLength: 50, maxLines: 1, showCounter: false, - labelText: AppLocale.room.getString(context) + - AppLocale.roomName.getString(context), + labelText: AppLocale.roomName.getString(context), labelPosition: LabelPosition.left, hintText: AppLocale.required.getString(context), + textDirection: TextDirection.rtl, ), - if (Ability().supportAPIServer()) + if (Ability().enableAPIServer()) EnhancedInput( padding: const EdgeInsets.only(top: 10, bottom: 5), title: Text( - '数字人头像', + '头像', style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, @@ -458,9 +460,10 @@ class _ChatRoomCreateScreenState extends State { ), value: Text( validMemories - .where((element) => element.number == maxContext) - .first - .name, + .where((element) => element.number == maxContext) + .firstOrNull + ?.name ?? + '', ), onPressed: () { openListSelectDialog( @@ -496,7 +499,9 @@ class _ChatRoomCreateScreenState extends State { return true; }, heightFactor: 0.5, - value: maxContext, + value: validMemories + .where((element) => element.number == maxContext) + .firstOrNull, ); }, ), @@ -605,6 +610,7 @@ void openSelectModelDialog( BuildContext context, Function(mm.Model selected) onSelected, { String? initValue, + List? reservedModels, }) { openModalBottomSheet( context, @@ -621,7 +627,11 @@ void openSelectModelDialog( } return ModelItem( - models: snapshot.data!, + models: snapshot.data! + .where((e) => + !e.disabled || + (reservedModels != null && reservedModels.contains(e.id))) + .toList(), onSelected: (selected) { onSelected(selected); context.pop(); diff --git a/lib/page/chat_room_setting.dart b/lib/page/chat/room_edit.dart similarity index 92% rename from lib/page/chat_room_setting.dart rename to lib/page/chat/room_edit.dart index 64e7c40d..98ea4362 100644 --- a/lib/page/chat_room_setting.dart +++ b/lib/page/chat/room_edit.dart @@ -5,7 +5,7 @@ import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/model.dart'; import 'package:askaide/helper/upload.dart'; import 'package:askaide/lang/lang.dart'; -import 'package:askaide/page/chat_room_create.dart'; +import 'package:askaide/page/chat/room_create.dart'; import 'package:askaide/page/component/avatar_selector.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; @@ -16,10 +16,10 @@ import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/random_avatar.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/bloc/room_bloc.dart'; -import 'package:askaide/page/dialog.dart'; +import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/model.dart' as mm; import 'package:askaide/repo/settings_repo.dart'; @@ -29,17 +29,16 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; -class ChatRoomSettingScreen extends StatefulWidget { +class RoomEditPage extends StatefulWidget { final int roomId; final SettingRepository setting; - const ChatRoomSettingScreen( - {super.key, required this.roomId, required this.setting}); + const RoomEditPage({super.key, required this.roomId, required this.setting}); @override - State createState() => _ChatRoomSettingScreenState(); + State createState() => _RoomEditPageState(); } -class _ChatRoomSettingScreenState extends State { +class _RoomEditPageState extends State { final _nameController = TextEditingController(); final _promptController = TextEditingController(text: ''); final _initMessageController = TextEditingController(text: ''); @@ -58,14 +57,15 @@ class _ChatRoomSettingScreenState extends State { List validMemories = [ ChatMemory('无记忆', 1, description: '每次对话都是独立的,常用于一次性问答'), - ChatMemory('基础', 5, description: '记住最近的 5 次对话'), - ChatMemory('中等', 10, description: '记住最近的 10 次对话'), - ChatMemory('深度', 20, description: '记住最近的 20 次对话'), + ChatMemory('基础', 3, description: '记住最近的 3 次对话'), + ChatMemory('中等', 6, description: '记住最近的 6 次对话'), + ChatMemory('深度', 10, description: '记住最近的 10 次对话'), ]; bool showAdvancedOptions = false; mm.Model? _selectedModel; + String? reservedModel; @override void initState() { @@ -75,7 +75,7 @@ class _ChatRoomSettingScreenState extends State { .add(RoomLoadEvent(widget.roomId, cascading: false)); // 获取预设头像 - if (Ability().supportAPIServer()) { + if (Ability().enableAPIServer()) { APIServer().avatars().then((value) { avatarPresets = value; }); @@ -117,6 +117,7 @@ class _ChatRoomSettingScreenState extends State { ModelAggregate.model(state.room.model).then((value) { setState(() { _selectedModel = value; + reservedModel = value.id; }); }); @@ -167,17 +168,17 @@ class _ChatRoomSettingScreenState extends State { maxLength: 50, maxLines: 1, showCounter: false, - labelText: AppLocale.room.getString(context) + - AppLocale.roomName.getString(context), + labelText: AppLocale.roomName.getString(context), labelPosition: LabelPosition.left, hintText: AppLocale.required.getString(context), + textDirection: TextDirection.rtl, ), - if (Ability().supportAPIServer()) + if (Ability().enableAPIServer()) EnhancedInput( padding: const EdgeInsets.only(top: 10, bottom: 5), title: Text( - '数字人头像', + '头像', style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, @@ -264,7 +265,7 @@ class _ChatRoomSettingScreenState extends State { // 模型 EnhancedInputSimple( title: AppLocale.model.getString(context), - padding: const EdgeInsets.only(top: 10, bottom: 10), + padding: const EdgeInsets.only(top: 10, bottom: 0), onPressed: () { openSelectModelDialog( context, @@ -274,6 +275,9 @@ class _ChatRoomSettingScreenState extends State { }); }, initValue: _selectedModel?.uid(), + reservedModels: reservedModel != null + ? [reservedModel!] + : [], ); }, value: _selectedModel != null @@ -348,10 +352,11 @@ class _ChatRoomSettingScreenState extends State { ), value: Text( validMemories - .where((element) => - element.number == maxContext) - .first - .name, + .where((element) => + element.number == maxContext) + .firstOrNull + ?.name ?? + '', ), onPressed: () { openListSelectDialog( @@ -388,7 +393,10 @@ class _ChatRoomSettingScreenState extends State { return true; }, heightFactor: 0.5, - value: maxContext, + value: validMemories + .where((element) => + element.number == maxContext) + .firstOrNull, ); }, ), diff --git a/lib/page/home_screen.dart b/lib/page/chat/rooms.dart similarity index 84% rename from lib/page/home_screen.dart rename to lib/page/chat/rooms.dart index 97ae5ccc..16813362 100644 --- a/lib/page/home_screen.dart +++ b/lib/page/chat/rooms.dart @@ -1,17 +1,19 @@ +import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/event.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_error.dart'; +import 'package:askaide/page/component/enhanced_popup_menu.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/room_card.dart'; import 'package:askaide/page/component/sliver_component.dart'; import 'package:askaide/page/component/weak_text_button.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/rooms.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/chat/component/room_item.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/room_gallery.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; @@ -19,15 +21,15 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; -class CharactersScreen extends StatefulWidget { +class RoomsPage extends StatefulWidget { final SettingRepository setting; - const CharactersScreen({Key? key, required this.setting}) : super(key: key); + const RoomsPage({Key? key, required this.setting}) : super(key: key); @override - State createState() => _CharactersScreenState(); + State createState() => _RoomsPageState(); } -class _CharactersScreenState extends State { +class _RoomsPageState extends State { @override void initState() { context.read().add(RoomsLoadEvent()); @@ -68,14 +70,34 @@ class _CharactersScreenState extends State { return SliverComponent( actions: [ + // 数字人创建按钮 if (selectedSuggestions.isEmpty) - IconButton( - onPressed: () { - context.push('/create-room').whenComplete(() { - context.read().add(RoomsLoadEvent()); - }); - }, - icon: const Icon(Icons.add_circle_outline), + EnhancedPopupMenu( + items: [ + EnhancedPopupMenuItem( + title: '创建数字人', + icon: Icons.person_add_alt_outlined, + onTap: (p0) { + context.push('/create-room').whenComplete(() { + context.read().add(RoomsLoadEvent()); + }); + }, + ), + if (Ability().enableAPIServer() && + !Ability().enableLocalOpenAI()) + EnhancedPopupMenuItem( + title: '发起群聊', + icon: Icons.chat_bubble_outline, + onTap: (p0) { + context + .push('/group-chat-create') + .whenComplete(() { + context.read().add(RoomsLoadEvent()); + }); + }, + ) + ], + icon: Icons.add_circle_outline, ), ], centerTitle: state.suggests.isEmpty, @@ -162,7 +184,7 @@ class _CharactersScreenState extends State { const SizedBox(width: 20), Expanded( child: EnhancedButton( - title: '添加为专属伙伴', + title: AppLocale.ok.getString(context), onPressed: () { context.read().add(GalleryRoomCopyEvent( selectedSuggestions diff --git a/lib/page/component/account_quota_card.dart b/lib/page/component/account_quota_card.dart index 121f9bf7..120b328d 100644 --- a/lib/page/component/account_quota_card.dart +++ b/lib/page/component/account_quota_card.dart @@ -3,7 +3,7 @@ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/coin.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/image.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/user.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; @@ -66,7 +66,7 @@ class AccountQuotaCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( AppLocale.usage.getString(context), diff --git a/lib/page/component/audio_player.dart b/lib/page/component/audio_player.dart index 434987a1..ad2bdd72 100644 --- a/lib/page/component/audio_player.dart +++ b/lib/page/component/audio_player.dart @@ -1,6 +1,6 @@ import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/material.dart'; diff --git a/lib/page/component/avatar_selector.dart b/lib/page/component/avatar_selector.dart index f2bc6771..5528b276 100644 --- a/lib/page/component/avatar_selector.dart +++ b/lib/page/component/avatar_selector.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/random_avatar.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -73,17 +73,49 @@ class _AvatarSelectorState extends State { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ if (_avatarUrl != null) - SizedBox( - width: 100, - height: 100, - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: _avatarUrl!.startsWith('http') - ? CachedNetworkImageEnhanced( - imageUrl: _avatarUrl!, - ) - : Image.file(File(_avatarUrl!)), - ), + Stack( + children: [ + SizedBox( + width: 100, + height: 100, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: _avatarUrl!.startsWith('http') + ? CachedNetworkImageEnhanced( + imageUrl: _avatarUrl!, + ) + : Image.file(File(_avatarUrl!)), + ), + ), + Positioned( + right: 5, + top: 5, + child: InkWell( + onTap: () { + setState(() { + _avatarUrl = null; + _avatarId = null; + }); + widget.onSelected(Avatar( + type: AvatarType.network, + url: _avatarUrl, + )); + }, + child: Container( + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: customColors.chatRoomBackground, + ), + child: Icon( + Icons.close, + size: 10, + color: customColors.weakTextColor, + ), + ), + ), + ), + ], ), if (_avatarId != null) RandomAvatar(id: _avatarId ?? 0, size: 80, usage: widget.usage), diff --git a/lib/page/component/background_container.dart b/lib/page/component/background_container.dart index 5e3ac678..9c9a0b74 100644 --- a/lib/page/component/background_container.dart +++ b/lib/page/component/background_container.dart @@ -2,8 +2,8 @@ import 'dart:ui'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/page/component/image.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; diff --git a/lib/page/component/bottom_sheet_box.dart b/lib/page/component/bottom_sheet_box.dart index 0a466345..ddc4c446 100644 --- a/lib/page/component/bottom_sheet_box.dart +++ b/lib/page/component/bottom_sheet_box.dart @@ -1,4 +1,4 @@ -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class BottomSheetBox extends StatelessWidget { diff --git a/lib/page/component/chat/chat_input.dart b/lib/page/component/chat/chat_input.dart index b3f2280d..58233c69 100644 --- a/lib/page/component/chat/chat_input.dart +++ b/lib/page/component/chat/chat_input.dart @@ -4,8 +4,8 @@ import 'package:askaide/helper/platform.dart'; import 'package:askaide/helper/upload.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/chat/voice_record.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:file_picker/file_picker.dart'; @@ -15,7 +15,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; class ChatInput extends StatefulWidget { final Function(String value) onSubmit; @@ -25,6 +25,7 @@ class ChatInput extends StatefulWidget { final Function()? onNewChat; final String hintText; final Function()? onVoiceRecordTappedEvent; + final List Function()? leftSideToolsBuilder; const ChatInput({ super.key, @@ -35,6 +36,7 @@ class ChatInput extends StatefulWidget { this.onNewChat, this.hintText = '', this.onVoiceRecordTappedEvent, + this.leftSideToolsBuilder, }); @override @@ -48,7 +50,7 @@ class _ChatInputState extends State { late final FocusNode _focusNode = FocusNode( onKey: (node, event) { if (!event.isShiftPressed && event.logicalKey.keyLabel == 'Enter') { - if (event is RawKeyDownEvent) { + if (event is RawKeyDownEvent && widget.enableNotifier.value) { _handleSubmited(_textController.text.trim()); } @@ -92,126 +94,100 @@ class _ChatInputState extends State { padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), decoration: BoxDecoration( color: customColors.backgroundColor, - // borderRadius: const BorderRadius.only( - // topLeft: Radius.circular(10), - // topRight: Radius.circular(10), - // ), - // boxShadow: const [ - // BoxShadow( - // color: Color.fromARGB(31, 161, 161, 161), - // blurRadius: 5, - // spreadRadius: 0, - // offset: Offset(0, -5), - // ), - // ], ), child: Builder(builder: (context) { final setting = context.read(); - if (widget.enableNotifier.value) { - return Column( - children: [ - // 工具栏 - if (widget.toolbar != null) - Row( - children: [ - Expanded(child: widget.toolbar!), - Text( - "${_textController.text.length}/$maxLength", - textScaleFactor: 0.8, - style: TextStyle( - color: customColors.chatInputPanelText, - ), + return Column( + children: [ + // 工具栏 + if (widget.toolbar != null) + Row( + children: [ + Expanded(child: widget.toolbar!), + Text( + "${_textController.text.length}/$maxLength", + textScaleFactor: 0.8, + style: TextStyle( + color: customColors.chatInputPanelText, ), - ], - ), - // if (widget.toolbar != null) - const SizedBox(height: 8), - // 聊天内容输入栏 - SingleChildScrollView( - child: Slidable( - startActionPane: widget.onNewChat != null - ? ActionPane( - extentRatio: 0.3, - motion: const ScrollMotion(), - children: [ - SlidableAction( - autoClose: true, - label: AppLocale.newChat.getString(context), - backgroundColor: Colors.blue, - borderRadius: - const BorderRadius.all(Radius.circular(20)), - onPressed: (_) { - widget.onNewChat!(); - }, - ), - const SizedBox(width: 10), - ], - ) - : null, - child: Row( - children: [ - // 聊天功能按钮 - Row( + ), + ], + ), + // if (widget.toolbar != null) + const SizedBox(height: 8), + // 聊天内容输入栏 + SingleChildScrollView( + child: Slidable( + startActionPane: widget.onNewChat != null + ? ActionPane( + extentRatio: 0.3, + motion: const ScrollMotion(), children: [ - if (widget.enableImageUpload && - Ability().supportImageUploader()) - _buildImageUploadButton( - context, setting, customColors), - ], - ), - // 聊天输入框 - Expanded( - child: Container( - decoration: BoxDecoration( - color: customColors.chatInputAreaBackground, - borderRadius: BorderRadius.circular(20), + SlidableAction( + autoClose: true, + label: AppLocale.newChat.getString(context), + backgroundColor: Colors.blue, + borderRadius: + const BorderRadius.all(Radius.circular(20)), + onPressed: (_) { + widget.onNewChat!(); + }, ), - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Row( - children: [ - Expanded( - child: TextFormField( - enabled: widget.enableNotifier.value, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.newline, - maxLines: 5, - minLines: 1, - maxLength: maxLength, - focusNode: _focusNode, - controller: _textController, - // onSubmitted: _handleSubmited, - decoration: InputDecoration( - hintText: widget.hintText, - hintStyle: const TextStyle( - fontSize: CustomSize.defaultHintTextSize, - ), - border: InputBorder.none, - counterText: '', + const SizedBox(width: 10), + ], + ) + : null, + child: Row( + children: [ + // 聊天功能按钮 + Row( + children: [ + if (widget.enableImageUpload && + Ability().supportImageUploader()) + _buildImageUploadButton( + context, setting, customColors), + if (widget.leftSideToolsBuilder != null) + ...widget.leftSideToolsBuilder!(), + ], + ), + // 聊天输入框 + Expanded( + child: Container( + decoration: BoxDecoration( + color: customColors.chatInputAreaBackground, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + children: [ + Expanded( + child: TextFormField( + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.newline, + maxLines: 5, + minLines: 1, + maxLength: maxLength, + focusNode: _focusNode, + controller: _textController, + decoration: InputDecoration( + hintText: widget.hintText, + hintStyle: const TextStyle( + fontSize: CustomSize.defaultHintTextSize, ), + border: InputBorder.none, + counterText: '', ), ), - // 聊天发送按钮 - _buildSendOrVoiceButton(context, customColors), - ], - ), + ), + // 聊天发送按钮 + _buildSendOrVoiceButton(context, customColors), + ], ), ), - ], - ), + ), + ], ), ), - ], - ); - } - - /// 回复时加载中效果 - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - LoadingAnimationWidget.flickr( - leftDotColor: const Color.fromARGB(255, 0, 214, 187), - rightDotColor: const Color.fromARGB(255, 243, 133, 0), - size: 40, ), ], ); @@ -224,6 +200,13 @@ class _ChatInputState extends State { BuildContext context, CustomColors customColors, ) { + if (!widget.enableNotifier.value) { + return LoadingAnimationWidget.beat( + color: customColors.linkColor!, + size: 20, + ); + } + return _textController.text == '' ? InkWell( onTap: () { diff --git a/lib/page/component/chat/chat_preview.dart b/lib/page/component/chat/chat_preview.dart index e91731cf..6b4dca4d 100644 --- a/lib/page/component/chat/chat_preview.dart +++ b/lib/page/component/chat/chat_preview.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:askaide/bloc/chat_message_bloc.dart'; import 'package:askaide/bloc/room_bloc.dart'; @@ -7,19 +8,20 @@ import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/attached_button_panel.dart'; +import 'package:askaide/page/component/chat/chat_share.dart'; import 'package:askaide/page/component/chat/message_state_manager.dart'; -import 'package:askaide/page/component/share.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:bot_toast/bot_toast.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/chat/markdown.dart'; import 'package:askaide/repo/model/message.dart'; import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; +import 'package:loading_animation_widget/loading_animation_widget.dart'; class ChatPreview extends StatefulWidget { final List messages; @@ -29,6 +31,9 @@ class ChatPreview extends StatefulWidget { final MessageStateManager stateManager; final List? helpWidgets; final Widget? robotAvatar; + final Widget? Function(Message message)? avatarBuilder; + final Widget? Function(Message message)? senderNameBuilder; + final bool supportBloc; final void Function(Message message)? onSpeakEvent; final void Function(Message message)? onResentEvent; @@ -40,9 +45,12 @@ class ChatPreview extends StatefulWidget { required this.controller, required this.stateManager, this.robotAvatar, + this.avatarBuilder, + this.senderNameBuilder, this.helpWidgets, this.onSpeakEvent, this.onResentEvent, + this.supportBloc = true, }); @override @@ -103,25 +111,38 @@ class _ChatPreviewState extends State { // 消息主体部分 Expanded( - child: BlocBuilder( - buildWhen: (previous, current) => - (current is ChatMessageUpdated && - current.message.id == message.message.id), - builder: (context, state) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 10, - ), - child: _buildMessageBox( - context, - customColors, - _resolveMessage(state, message), - message.state, + child: widget.supportBloc + ? BlocBuilder( + buildWhen: (previous, current) => + (current is ChatMessageUpdated && + current.message.id == message.message.id), + builder: (context, state) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 10, + ), + child: _buildMessageBox( + context, + customColors, + _resolveMessage(state, message), + message.state, + ), + ); + }, + ) + : Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 10, + ), + child: _buildMessageBox( + context, + customColors, + message.message, + message.state, + ), ), - ); - }, - ), ), ], ), @@ -202,8 +223,9 @@ class _ChatPreviewState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.robotAvatar != null && message.role == Role.receiver) - widget.robotAvatar!, + // 消息头像 + buildAvatar(message), + // 消息内容部分 ConstrainedBox( constraints: BoxConstraints( maxWidth: _chatBoxMaxWidth(context) - 80, @@ -212,12 +234,18 @@ class _ChatPreviewState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + // 发送人名称 + if (message.role == Role.receiver && + widget.senderNameBuilder != null) + widget.senderNameBuilder!(message) ?? const SizedBox(), Wrap( crossAxisAlignment: WrapCrossAlignment.end, children: [ + // 错误指示器 if (message.role == Role.sender && message.statusIsFailed()) buildErrorIndicator(message, state, context), + // 消息主体 GestureDetector( // 选择模式下,单击切换选择与否 // 非选择模式下,单击隐藏键盘 @@ -261,6 +289,15 @@ class _ChatPreviewState extends State { ), child: Builder( builder: (context) { + if ((message.statusPending() || + !message.isReady) && + message.text.isEmpty) { + return LoadingAnimationWidget.waveDots( + color: customColors.weakLinkColor!, + size: 25, + ); + } + var text = message.text; if (!message.isReady && text != '') { text += ' ▌'; @@ -374,12 +411,29 @@ class _ChatPreviewState extends State { HapticFeedbackHelper.mediumImpact(); + var confirmMessage = ''; + if (message.extra != null && message.extra!.isNotEmpty) { + try { + final extra = jsonDecode(message.extra!); + if (extra['error'] != null && extra['error'] != '') { + var e1 = extra['error']; + try { + e1 = (e1 as String).getString(context); + // ignore: empty_catches + } catch (ignored) {} + confirmMessage = e1; + } + // ignore: empty_catches + } catch (ignored) {} + } + openConfirmDialog( context, - AppLocale.robotHasSomeError.getString(context), + confirmMessage, () { widget.onResentEvent!(message); }, + title: Text(AppLocale.robotHasSomeError.getString(context)), confirmText: '重新发送', ); }, @@ -388,6 +442,21 @@ class _ChatPreviewState extends State { ); } + Widget buildAvatar(Message message) { + if (widget.avatarBuilder != null) { + final avatar = widget.avatarBuilder!(message); + if (avatar != null) { + return avatar; + } + } + + if (widget.robotAvatar != null && message.role == Role.receiver) { + return widget.robotAvatar!; + } + + return const SizedBox(); + } + /// 点击消息后控制操作弹窗菜单 void _handleMessageTapControl( BuildContext context, @@ -537,49 +606,72 @@ class _ChatPreviewState extends State { ) ], )), - if (message.role == Role.sender && widget.onResentEvent != null) - TextButton.icon( - onPressed: () { - widget.onResentEvent!(message); + TextButton.icon( + onPressed: () async { cancel(); + var messages = []; + + if (message.role == Role.receiver) { + final questions = widget.messages + .where((e) => e.message.id == message.refId) + .toList(); + if (questions.isNotEmpty) { + var q = questions.first; + messages.add(ChatShareMessage( + content: q.message.text, + leftSide: false, + )); + } + } + + messages.add(ChatShareMessage( + content: message.text, + leftSide: message.role == Role.receiver, + avatarURL: message.avatarUrl, + username: message.senderName, + )); + + if (message.role == Role.sender) { + final answers = widget.messages + .where((e) => e.message.refId == message.id) + .toList(); + if (answers.isNotEmpty) { + for (var a in answers) { + messages.add(ChatShareMessage( + content: a.message.text, + leftSide: true, + avatarURL: a.message.avatarUrl, + username: a.message.senderName, + )); + } + } + } + + Navigator.push( + context, + MaterialPageRoute( + fullscreenDialog: true, + builder: (context) => ChatShareScreen(messages: messages), + ), + ); + + // await shareTo(context, content: message.text, title: '聊天记录'); }, label: const Text(''), - icon: const Column( + icon: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.restore, + const Icon( + Icons.share, color: Color.fromARGB(255, 255, 255, 255), size: 14, ), Text( - '重发', - style: TextStyle(fontSize: 12, color: Colors.white), - ), + AppLocale.share.getString(context), + style: const TextStyle(fontSize: 12, color: Colors.white), + ) ], - ), - ) - else - TextButton.icon( - onPressed: () async { - cancel(); - await shareTo(context, content: message.text, title: '聊天记录'); - }, - label: const Text(''), - icon: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.share, - color: Color.fromARGB(255, 255, 255, 255), - size: 14, - ), - Text( - AppLocale.share.getString(context), - style: const TextStyle(fontSize: 12, color: Colors.white), - ) - ], - )), + )), TextButton.icon( onPressed: () { widget.controller.enterSelectMode(); @@ -624,25 +716,48 @@ class _ChatPreviewState extends State { ), if (Ability().supportSpeak() && widget.onSpeakEvent != null) TextButton.icon( - onPressed: () { - cancel(); - widget.onSpeakEvent!(message); - }, - label: const Text(''), - icon: const Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.record_voice_over, - color: Color.fromARGB(255, 255, 255, 255), - size: 14, - ), - Text( - '朗读', - style: TextStyle(fontSize: 12, color: Colors.white), - ) - ], - )), + onPressed: () { + cancel(); + widget.onSpeakEvent!(message); + }, + label: const Text(''), + icon: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.record_voice_over, + color: Color.fromARGB(255, 255, 255, 255), + size: 14, + ), + Text( + '朗读', + style: TextStyle(fontSize: 12, color: Colors.white), + ) + ], + ), + ), + if (message.role == Role.sender && widget.onResentEvent != null) + TextButton.icon( + onPressed: () { + widget.onResentEvent!(message); + cancel(); + }, + label: const Text(''), + icon: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.restore, + color: Color.fromARGB(255, 255, 255, 255), + size: 14, + ), + Text( + '重发', + style: TextStyle(fontSize: 12, color: Colors.white), + ), + ], + ), + ), ], ), ); diff --git a/lib/page/component/chat/chat_share.dart b/lib/page/component/chat/chat_share.dart new file mode 100644 index 00000000..cf8d3276 --- /dev/null +++ b/lib/page/component/chat/chat_share.dart @@ -0,0 +1,434 @@ +import 'package:askaide/helper/ability.dart'; +import 'package:askaide/helper/constant.dart'; +import 'package:askaide/helper/helper.dart'; +import 'package:askaide/helper/image.dart'; +import 'package:askaide/helper/platform.dart'; +import 'package:askaide/lang/lang.dart'; +import 'package:askaide/page/component/chat/markdown.dart'; +import 'package:askaide/page/component/enhanced_popup_menu.dart'; +import 'package:askaide/page/component/image.dart'; +import 'package:askaide/page/component/loading.dart'; +import 'package:askaide/page/component/random_avatar.dart'; +import 'package:askaide/page/component/share.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/model/misc.dart'; +import 'package:bot_toast/bot_toast.dart'; +import 'package:file_saver/file_saver.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localization/flutter_localization.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:widgets_to_image/widgets_to_image.dart'; + +class ChatShareMessage { + final String? username; + final String content; + final String? avatarURL; + final bool leftSide; + + const ChatShareMessage({ + this.username, + required this.content, + this.avatarURL, + this.leftSide = true, + }); +} + +class ChatShareScreen extends StatefulWidget { + final List messages; + const ChatShareScreen({ + super.key, + required this.messages, + }); + + @override + State createState() => _ChatShareScreenState(); +} + +class _ChatShareScreenState extends State { + final WidgetsToImageController controller = WidgetsToImageController(); + + bool showQRCode = true; + bool usingChatStyle = true; + + @override + Widget build(BuildContext context) { + final customColors = Theme.of(context).extension()!; + return Scaffold( + appBar: AppBar( + toolbarHeight: CustomSize.toolbarHeight, + actions: [ + if (!PlatformTool.isWeb()) + TextButton( + onPressed: () async { + final cancel = BotToast.showCustomLoading( + toastBuilder: (cancel) { + return LoadingIndicator( + message: AppLocale.processingWait.getString(context), + ); + }, + allowClick: false, + duration: const Duration(seconds: 15), + ); + + try { + final data = await controller.capture(); + if (data != null) { + final file = await writeTempFile('share-image.png', data); + cancel(); + // ignore: use_build_context_synchronously + await shareTo( + context, + content: 'images', + images: [ + file.path, + ], + ); + } + } finally { + cancel(); + } + }, + child: Row( + children: [ + Icon(Icons.share, + size: 14, color: customColors.weakLinkColor), + const SizedBox(width: 5), + Text( + '分享', + style: TextStyle( + color: customColors.weakLinkColor, fontSize: 14), + ), + ], + ), + ), + EnhancedPopupMenu( + items: [ + EnhancedPopupMenuItem( + title: '保存到本地', + icon: Icons.save, + onTap: (ctx) async { + final cancel = BotToast.showCustomLoading( + toastBuilder: (cancel) { + return LoadingIndicator( + message: AppLocale.processingWait.getString(context), + ); + }, + allowClick: false, + duration: const Duration(seconds: 15), + ); + + try { + final data = await controller.capture(); + if (data != null) { + cancel(); + // ignore: use_build_context_synchronously + + if (PlatformTool.isIOS() || PlatformTool.isAndroid()) { + await ImageGallerySaver.saveImage(data, quality: 100); + + showSuccessMessage('图片保存成功'); + } else { + FileSaver.instance + .saveFile( + name: randomId(), + bytes: data, + ext: 'png', + mimeType: MimeType.png, + ) + .then((value) { + showSuccessMessage('文件保存成功'); + }); + } + } + } finally { + cancel(); + } + }, + ), + EnhancedPopupMenuItem( + title: showQRCode ? '不显示邀请信息' : '显示邀请信息', + icon: showQRCode ? Icons.visibility_off : Icons.visibility, + onTap: (ctx) { + setState(() { + showQRCode = !showQRCode; + }); + }, + ), + EnhancedPopupMenuItem( + title: usingChatStyle ? '使用列表风格' : '使用聊天风格', + icon: usingChatStyle ? Icons.list : Icons.chat, + onTap: (ctx) { + setState(() { + usingChatStyle = !usingChatStyle; + }); + }, + ), + ], + ), + ], + ), + backgroundColor: customColors.backgroundContainerColor, + body: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: CustomSize.smallWindowSize, + ), + child: SafeArea( + child: SingleChildScrollView( + child: FutureBuilder( + future: APIServer().shareInfo(), + builder: (context, snapshot) { + if (snapshot.hasError) { + return Center( + child: Text(resolveError(context, snapshot.error!)), + ); + } + + if (snapshot.hasData) { + return buildShareWindow(customColors, context, snapshot); + } + + return const Center( + child: Text('Loading ...'), + ); + }), + ), + ), + ), + ), + ); + } + + Widget buildShareWindow(CustomColors customColors, BuildContext context, + AsyncSnapshot snapshot) { + return Column( + children: [ + WidgetsToImage( + controller: controller, + child: Container( + color: customColors.backgroundContainerColor, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + usingChatStyle + ? buildChatPreview(context, customColors) + : buildListPreview(context, customColors), + if (showQRCode) buildQRCodePanel(customColors, snapshot), + ], + ), + ), + ), + ], + ); + } + + Widget buildQRCodePanel( + CustomColors customColors, AsyncSnapshot snapshot) { + return Container( + color: customColors.backgroundColor, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 20, + ), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImageEnhanced( + imageUrl: snapshot.data!.qrCode, + width: 100, + height: 100, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + snapshot.data!.message, + ), + ), + ], + ), + ), + ); + } + + Widget buildListPreview(BuildContext context, CustomColors customColors) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 10, + ), + child: Column( + children: widget.messages.map((message) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 10, + ), + child: Align( + alignment: Alignment.topLeft, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (message.avatarURL != null && message.leftSide) + _buildAvatar(avatarUrl: message.avatarURL), + if (message.username != null && message.leftSide) + Container( + margin: const EdgeInsets.fromLTRB(0, 0, 10, 7), + padding: const EdgeInsets.symmetric(horizontal: 13), + child: Text( + message.username!, + style: TextStyle( + color: customColors.weakTextColor, + fontSize: 12, + ), + ), + ), + ], + ), + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: _chatBoxMaxWidth(context), + ), + child: Container( + margin: const EdgeInsets.fromLTRB(0, 10, 10, 7), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: message.leftSide + ? customColors.chatRoomReplyBackground + : customColors.chatRoomSenderBackground, + ), + padding: const EdgeInsets.symmetric( + horizontal: 13, + vertical: 13, + ), + child: Builder( + builder: (context) { + return Markdown(data: message.content); + }, + ), + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ); + } + + Widget buildChatPreview(BuildContext context, CustomColors customColors) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 10, + ), + child: Column( + children: widget.messages.map((message) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 10, + ), + child: Align( + alignment: + message.leftSide ? Alignment.topLeft : Alignment.topRight, + child: ConstrainedBox( + constraints: + BoxConstraints(maxWidth: _chatBoxMaxWidth(context)), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (message.avatarURL != null && message.leftSide) + _buildAvatar(avatarUrl: message.avatarURL), + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: _chatBoxMaxWidth(context) - 80, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (message.username != null && message.leftSide) + Container( + margin: const EdgeInsets.fromLTRB(0, 0, 10, 7), + padding: + const EdgeInsets.symmetric(horizontal: 13), + child: Text( + message.username!, + style: TextStyle( + color: customColors.weakTextColor, + fontSize: 12, + ), + ), + ), + Container( + margin: message.leftSide + ? const EdgeInsets.fromLTRB(10, 0, 0, 7) + : const EdgeInsets.fromLTRB(0, 0, 10, 7), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: message.leftSide + ? customColors.chatRoomReplyBackground + : customColors.chatRoomSenderBackground, + ), + padding: const EdgeInsets.symmetric( + horizontal: 13, + vertical: 13, + ), + child: Builder( + builder: (context) { + return Markdown(data: message.content); + }, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + }).toList(), + ), + ); + } + + /// 获取聊天框的最大宽度 + double _chatBoxMaxWidth(BuildContext context) { + var screenWidth = MediaQuery.of(context).size.width; + if (screenWidth >= CustomSize.maxWindowSize) { + return CustomSize.maxWindowSize; + } + + return screenWidth; + } + + Widget _buildAvatar({String? avatarUrl, int? id, int size = 30}) { + if (avatarUrl != null && avatarUrl.startsWith('http')) { + return RemoteAvatar( + avatarUrl: imageURL(avatarUrl, qiniuImageTypeAvatar), + size: size, + ); + } + + return RandomAvatar( + id: id ?? 0, + size: size, + usage: + Ability().enableAPIServer() ? AvatarUsage.room : AvatarUsage.legacy, + ); + } +} diff --git a/lib/page/component/chat/empty.dart b/lib/page/component/chat/empty.dart index 22124810..1628fa1b 100644 --- a/lib/page/component/chat/empty.dart +++ b/lib/page/component/chat/empty.dart @@ -1,5 +1,5 @@ -import 'package:askaide/page/theme/custom_theme.dart'; -import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:askaide/repo/model/misc.dart'; import 'package:flutter/material.dart'; class EmptyPreview extends StatefulWidget { diff --git a/lib/page/component/chat/help_tips.dart b/lib/page/component/chat/help_tips.dart index 1bbbbdcb..f7f51728 100644 --- a/lib/page/component/chat/help_tips.dart +++ b/lib/page/component/chat/help_tips.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:askaide/lang/lang.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; diff --git a/lib/page/component/chat/markdown.dart b/lib/page/component/chat/markdown.dart index 722667d1..859e3848 100644 --- a/lib/page/component/chat/markdown.dart +++ b/lib/page/component/chat/markdown.dart @@ -1,5 +1,5 @@ import 'package:askaide/page/component/image_preview.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_markdown/flutter_markdown.dart' as md; diff --git a/lib/page/component/chat/voice_record.dart b/lib/page/component/chat/voice_record.dart index 30e5bb5a..12c38807 100644 --- a/lib/page/component/chat/voice_record.dart +++ b/lib/page/component/chat/voice_record.dart @@ -5,8 +5,8 @@ import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/model_resolver.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/loading.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; diff --git a/lib/page/component/chat_tools_button.dart b/lib/page/component/chat_tools_button.dart index 85a39c38..1dd352a5 100644 --- a/lib/page/component/chat_tools_button.dart +++ b/lib/page/component/chat_tools_button.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; class ChatToolsButton extends StatefulWidget { final String text; diff --git a/lib/page/component/column_block.dart b/lib/page/component/column_block.dart index 267361f2..ca43f717 100644 --- a/lib/page/component/column_block.dart +++ b/lib/page/component/column_block.dart @@ -1,4 +1,4 @@ -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class ColumnBlock extends StatelessWidget { diff --git a/lib/page/dialog.dart b/lib/page/component/dialog.dart similarity index 97% rename from lib/page/dialog.dart rename to lib/page/component/dialog.dart index 06c3d2f0..08ccb4cd 100644 --- a/lib/page/dialog.dart +++ b/lib/page/component/dialog.dart @@ -8,7 +8,7 @@ import 'package:askaide/page/component/bottom_sheet_box.dart'; import 'package:askaide/page/component/button.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/item_selector_search.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; @@ -230,14 +230,18 @@ openConfirmDialog( children: [ buildBottomSheetTopBar(customColors), const SizedBox(height: 10), - Text( - message, - style: TextStyle( - color: customColors.dialogDefaultTextColor, - fontSize: 16, + if (title != null) title, + if (title != null && message != '') const SizedBox(height: 10), + if (message != '') + Text( + message, + style: TextStyle( + color: customColors.dialogDefaultTextColor, + fontSize: title == null ? 16 : 12, + ), + textAlign: TextAlign.center, + maxLines: title == null ? 4 : 2, ), - textAlign: TextAlign.center, - ), const SizedBox(height: 20), Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/page/component/enhanced_button.dart b/lib/page/component/enhanced_button.dart index d6ffd5a5..7966a9d6 100644 --- a/lib/page/component/enhanced_button.dart +++ b/lib/page/component/enhanced_button.dart @@ -1,4 +1,4 @@ -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class EnhancedButton extends StatelessWidget { diff --git a/lib/page/component/enhanced_input.dart b/lib/page/component/enhanced_input.dart index cf9dff01..624939b9 100644 --- a/lib/page/component/enhanced_input.dart +++ b/lib/page/component/enhanced_input.dart @@ -1,4 +1,4 @@ -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -38,7 +38,7 @@ class EnhancedInputSimple extends StatelessWidget { overflow: TextOverflow.ellipsis, style: TextStyle( color: customColors.textfieldValueColor, - fontSize: 14, + fontSize: 15, ), ) : null, diff --git a/lib/page/component/enhanced_popup_menu.dart b/lib/page/component/enhanced_popup_menu.dart index a52f2ad5..a2943f37 100644 --- a/lib/page/component/enhanced_popup_menu.dart +++ b/lib/page/component/enhanced_popup_menu.dart @@ -16,12 +16,13 @@ class EnhancedPopupMenuItem { class EnhancedPopupMenu extends StatelessWidget { final List items; - const EnhancedPopupMenu({super.key, required this.items}); + final IconData? icon; + const EnhancedPopupMenu({super.key, required this.items, this.icon}); @override Widget build(BuildContext context) { return PopupMenuButton( - icon: const Icon(Icons.more_horiz), + icon: Icon(icon ?? Icons.more_horiz), splashRadius: 20, elevation: 0, shape: RoundedRectangleBorder( diff --git a/lib/page/component/enhanced_textfield.dart b/lib/page/component/enhanced_textfield.dart index 5ca1db0f..3759777c 100644 --- a/lib/page/component/enhanced_textfield.dart +++ b/lib/page/component/enhanced_textfield.dart @@ -1,5 +1,5 @@ -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/page/component/gallery_item_share.dart b/lib/page/component/gallery_item_share.dart index 008f7eb7..3bd0596e 100644 --- a/lib/page/component/gallery_item_share.dart +++ b/lib/page/component/gallery_item_share.dart @@ -9,10 +9,10 @@ import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/image_preview.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/share.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/gallery/gallery_item.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/creative_island/gallery/gallery_item.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:file_saver/file_saver.dart'; @@ -49,48 +49,50 @@ class _GalleryItemShareScreenState extends State { appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, actions: [ - TextButton( - onPressed: () async { - final cancel = BotToast.showCustomLoading( - toastBuilder: (cancel) { - return LoadingIndicator( - message: AppLocale.processingWait.getString(context), - ); - }, - allowClick: false, - duration: const Duration(seconds: 15), - ); + if (!PlatformTool.isWeb()) + TextButton( + onPressed: () async { + final cancel = BotToast.showCustomLoading( + toastBuilder: (cancel) { + return LoadingIndicator( + message: AppLocale.processingWait.getString(context), + ); + }, + allowClick: false, + duration: const Duration(seconds: 15), + ); - try { - final data = await controller.capture(); - if (data != null) { - final file = await writeTempFile('share-image.png', data); + try { + final data = await controller.capture(); + if (data != null) { + final file = await writeTempFile('share-image.png', data); + cancel(); + // ignore: use_build_context_synchronously + await shareTo( + context, + content: 'images', + images: [ + file.path, + ], + ); + } + } finally { cancel(); - // ignore: use_build_context_synchronously - await shareTo( - context, - content: 'images', - images: [ - file.path, - ], - ); } - } finally { - cancel(); - } - }, - child: Row( - children: [ - Icon(Icons.share, size: 14, color: customColors.weakLinkColor), - const SizedBox(width: 5), - Text( - '分享', - style: TextStyle( - color: customColors.weakLinkColor, fontSize: 14), - ), - ], + }, + child: Row( + children: [ + Icon(Icons.share, + size: 14, color: customColors.weakLinkColor), + const SizedBox(width: 5), + Text( + '分享', + style: TextStyle( + color: customColors.weakLinkColor, fontSize: 14), + ), + ], + ), ), - ), EnhancedPopupMenu( items: [ EnhancedPopupMenuItem( diff --git a/lib/page/component/image_preview.dart b/lib/page/component/image_preview.dart index 5c9ef10b..6f60b77a 100644 --- a/lib/page/component/image_preview.dart +++ b/lib/page/component/image_preview.dart @@ -7,9 +7,9 @@ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/gallery_item_share.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/loading.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:before_after/before_after.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:file_saver/file_saver.dart'; diff --git a/lib/page/component/item_selector_search.dart b/lib/page/component/item_selector_search.dart index 1d1dccc7..002d81fb 100644 --- a/lib/page/component/item_selector_search.dart +++ b/lib/page/component/item_selector_search.dart @@ -1,5 +1,5 @@ import 'package:askaide/lang/lang.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; diff --git a/lib/page/component/loading.dart b/lib/page/component/loading.dart index 0f2c4833..518453e4 100644 --- a/lib/page/component/loading.dart +++ b/lib/page/component/loading.dart @@ -1,4 +1,4 @@ -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart'; diff --git a/lib/page/component/model_indicator.dart b/lib/page/component/model_indicator.dart index 7a6fffa2..b6f6465c 100644 --- a/lib/page/component/model_indicator.dart +++ b/lib/page/component/model_indicator.dart @@ -1,12 +1,12 @@ -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class ModelIndicatorInfo { - final IconData icon; - final Color activeColor; - final String modelId; - final String modelName; - final String description; + IconData icon; + Color activeColor; + String modelId; + String modelName; + String description; ModelIndicatorInfo({ required this.modelName, @@ -20,11 +20,13 @@ class ModelIndicatorInfo { class ModelIndicator extends StatelessWidget { final ModelIndicatorInfo model; final bool selected; + final bool showDescription; const ModelIndicator({ super.key, required this.model, this.selected = false, + this.showDescription = true, }); @override @@ -61,16 +63,17 @@ class ModelIndicator extends StatelessWidget { fontWeight: FontWeight.w600, ), ), - Text( - model.description, - style: TextStyle( - fontSize: 10, - color: selected - ? Colors.white - : customColors.weakTextColor, - overflow: TextOverflow.ellipsis, + if (showDescription) + Text( + model.description, + style: TextStyle( + fontSize: 10, + color: selected + ? Colors.white + : customColors.weakTextColor, + overflow: TextOverflow.ellipsis, + ), ), - ), ], ), const SizedBox(width: 15), diff --git a/lib/page/component/model_item.dart b/lib/page/component/model_item.dart index 51066497..84a41a17 100644 --- a/lib/page/component/model_item.dart +++ b/lib/page/component/model_item.dart @@ -1,6 +1,8 @@ +import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/helper/image.dart'; +import 'package:askaide/page/component/random_avatar.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/model/model.dart'; import 'package:flutter/material.dart'; @@ -19,46 +21,79 @@ class ModelItem extends StatelessWidget { @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; - - Map> modelGroups = {}; - for (var model in models) { - if (modelGroups.containsKey(model.category)) { - modelGroups[model.category]!.add(model); - } else { - modelGroups[model.category] = [model]; - } - } - return models.isNotEmpty - ? ListView( - children: [ - for (var group in modelGroups.entries) - ExpansionTile( - title: Row(children: [ - Text( - group.key.toUpperCase(), - style: TextStyle( - color: customColors.weakLinkColor, - ), - ), - if (group.value.where((e) => !e.disabled).isEmpty) - Text( - '(敬请期待)', - style: TextStyle( - color: customColors.weakTextColor, + ? Padding( + padding: const EdgeInsets.only(top: 15), + child: ListView.separated( + itemCount: models.length, + itemBuilder: (context, i) { + var item = models[i]; + return ListTile( + title: Container( + alignment: Alignment.center, + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + _buildAvatar(avatarUrl: item.avatarUrl, size: 40), + const SizedBox(width: 20), + Expanded( + child: Container( + alignment: Alignment.centerLeft, + child: Row(children: [ + Text( + item.name, + overflow: TextOverflow.ellipsis, + ), + if (item.tag != null && item.tag!.isNotEmpty) + Container( + decoration: BoxDecoration( + color: customColors.tagsBackgroundHover, + borderRadius: BorderRadius.circular(8), + ), + margin: const EdgeInsets.only(left: 5), + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 2, + ), + child: Text( + item.tag!, + style: TextStyle( + fontSize: 10, + color: customColors.tagsText, + ), + ), + ), + ]), + ), + ), + SizedBox( + width: 10, + child: Icon( + Icons.check, + color: + initValue == item.uid() || initValue == item.id + ? customColors.linkColor + : Colors.transparent, + ), ), - textScaleFactor: 0.7, - ), - ]), - iconColor: customColors.weakLinkColor, - childrenPadding: const EdgeInsets.only(bottom: 10), - initiallyExpanded: group.key == modelTypeOpenAI, - children: [ - for (var model in group.value) - _buildListTile(model, customColors, context), - ], - ), - ], + ], + ), + ), + onTap: () { + onSelected(item); + }, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider( + height: 1, + color: customColors.columnBlockDividerColor, + ); + }, + ), ) : const Center( child: Text( @@ -68,106 +103,19 @@ class ModelItem extends StatelessWidget { ); } - ListTile _buildListTile( - Model model, - CustomColors customColors, - BuildContext context, - ) { - return ListTile( - title: Stack( - children: [ - Container( - alignment: Alignment.center, - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 10, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: initValue == model.uid() - ? customColors.linkColor - : (model.disabled - ? customColors.tagsBackground - : customColors.tagsBackgroundHover), - boxShadow: [ - BoxShadow( - color: customColors.boxShadowColor!, - blurRadius: 3, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - children: [ - Text( - // 模型 ID - model.name, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: initValue == model.uid() - ? Colors.white - : (model.disabled - ? customColors.weakTextColor!.withAlpha(150) - : customColors.chatExampleItemText), - ), - ), - // 模型描述 - if (model.description != null) const SizedBox(height: 5), - if (model.description != null) - Text( - model.description!, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: initValue == model.uid() - ? Colors.white - : (model.disabled - ? customColors.weakTextColor!.withAlpha(150) - : customColors.chatExampleItemText), - ), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 3, - ), - ], - ), - ), - if (model.disabled) - _buildBadge(const Color.fromARGB(150, 122, 122, 122), '敬请期待'), - if (!model.disabled && model.tag != null) - _buildBadge(const Color.fromARGB(150, 122, 122, 122), model.tag!), - ], - ), - onTap: () { - if (model.disabled) { - showImportantMessage(context, '该模型即将推出,敬请期待!'); - return; - } - onSelected(model); - }, - ); - } + Widget _buildAvatar({String? avatarUrl, int? id, int size = 30}) { + if (avatarUrl != null && avatarUrl.startsWith('http')) { + return RemoteAvatar( + avatarUrl: imageURL(avatarUrl, qiniuImageTypeAvatar), + size: size, + ); + } - Widget _buildBadge(Color color, String text) { - return Positioned( - top: 0, - left: 0, - child: Container( - padding: const EdgeInsets.all(5), - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(10), - bottomRight: Radius.circular(10), - ), - color: color, - ), - child: Text( - text, - style: const TextStyle( - fontSize: 10, - color: Colors.white, - ), - ), - ), + return RandomAvatar( + id: id ?? 0, + size: size, + usage: + Ability().enableAPIServer() ? AvatarUsage.room : AvatarUsage.legacy, ); } } diff --git a/lib/page/component/multi_item_selector.dart b/lib/page/component/multi_item_selector.dart new file mode 100644 index 00000000..fe866068 --- /dev/null +++ b/lib/page/component/multi_item_selector.dart @@ -0,0 +1,130 @@ +import 'package:askaide/lang/lang.dart'; +import 'package:askaide/page/component/enhanced_button.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localization/flutter_localization.dart'; +import 'package:go_router/go_router.dart'; + +class MultiItemSelector extends StatefulWidget { + final Widget Function(T item)? itemAvatarBuilder; + final Widget Function(T item) itemBuilder; + final List items; + final Function(List selected)? onSubmit; + final Function(List selected)? onChanged; + final List? selectedItems; + + const MultiItemSelector({ + super.key, + required this.items, + required this.itemBuilder, + this.selectedItems, + this.onSubmit, + this.onChanged, + this.itemAvatarBuilder, + }); + + @override + State> createState() => _MultiItemSelectorState(); +} + +class _MultiItemSelectorState extends State> { + var selectedItems = []; + + @override + void initState() { + selectedItems = widget.selectedItems ?? []; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final customColors = Theme.of(context).extension()!; + + return Container( + margin: const EdgeInsets.only(top: 15), + child: Column( + children: [ + if (widget.onSubmit != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + EnhancedButton( + width: 100, + height: 40, + backgroundColor: customColors.weakTextColor, + title: AppLocale.cancel.getString(context), + onPressed: () { + context.pop(); + }, + ), + EnhancedButton( + width: 100, + height: 40, + title: AppLocale.ok.getString(context), + onPressed: () { + widget.onSubmit!(selectedItems); + }, + ), + ], + ), + Expanded( + child: ListView.separated( + itemCount: widget.items.length, + itemBuilder: (context, i) { + var item = widget.items[i]; + return CheckboxListTile( + controlAffinity: ListTileControlAffinity.leading, + checkboxShape: const CircleBorder(), + activeColor: customColors.linkColor, + side: BorderSide( + color: customColors.weakTextColor!.withAlpha(100), + ), + title: Container( + alignment: Alignment.center, + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.itemAvatarBuilder != null) + widget.itemAvatarBuilder!(item), + const SizedBox(width: 20), + Expanded( + child: Container( + alignment: Alignment.centerLeft, + child: widget.itemBuilder(item), + ), + ), + ], + ), + ), + onChanged: (selected) { + setState(() { + if (selectedItems.contains(item)) { + selectedItems.remove(item); + } else { + selectedItems.add(item); + } + + if (widget.onChanged != null) { + widget.onChanged!(selectedItems); + } + }); + }, + value: selectedItems.contains(item), + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider( + height: 1, + color: customColors.columnBlockDividerColor, + ); + }, + ), + ) + ], + ), + ); + } +} diff --git a/lib/page/component/password_field.dart b/lib/page/component/password_field.dart index 79b6caa2..22314679 100644 --- a/lib/page/component/password_field.dart +++ b/lib/page/component/password_field.dart @@ -1,4 +1,4 @@ -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/page/component/prompt_tags_selector.dart b/lib/page/component/prompt_tags_selector.dart index d9217aa0..8ef75f60 100644 --- a/lib/page/component/prompt_tags_selector.dart +++ b/lib/page/component/prompt_tags_selector.dart @@ -1,7 +1,8 @@ import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/weak_text_button.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/model/misc.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; diff --git a/lib/page/component/room_card.dart b/lib/page/component/room_card.dart index f25f70b8..f39c0159 100644 --- a/lib/page/component/room_card.dart +++ b/lib/page/component/room_card.dart @@ -3,8 +3,8 @@ import 'package:askaide/helper/image.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/weak_text_button.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/room_gallery.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; diff --git a/lib/page/component/share.dart b/lib/page/component/share.dart index aab9ad42..7638dd90 100644 --- a/lib/page/component/share.dart +++ b/lib/page/component/share.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:askaide/helper/platform.dart'; -import 'package:askaide/page/dialog.dart'; +import 'package:askaide/page/component/dialog.dart'; import 'package:flutter/material.dart'; import 'package:fluwx/fluwx.dart'; import 'package:go_router/go_router.dart'; @@ -13,7 +13,15 @@ Future shareTo( String? title, List? images, }) async { - final box = context.findRenderObject() as RenderBox?; + Rect? sharePositionOrigin; + + try { + final box = context.findRenderObject() as RenderBox?; + Rect? pos = box!.localToGlobal(Offset.zero) & box.size; + sharePositionOrigin = pos; + // ignore: empty_catches + } catch (ignored) {} + if ((PlatformTool.isIOS() || PlatformTool.isAndroid()) && await isWeChatInstalled) { // ignore: use_build_context_synchronously @@ -93,15 +101,13 @@ Future shareTo( Share.shareXFiles( [XFile(images.first)], subject: title, - sharePositionOrigin: - box!.localToGlobal(Offset.zero) & box.size, + sharePositionOrigin: sharePositionOrigin, ).whenComplete(() => context.pop()); } else { Share.share( content, subject: title, - sharePositionOrigin: - box!.localToGlobal(Offset.zero) & box.size, + sharePositionOrigin: sharePositionOrigin, ).whenComplete(() => context.pop()); } }, @@ -128,13 +134,13 @@ Future shareTo( Share.shareXFiles( [XFile(images.first)], subject: title, - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + sharePositionOrigin: sharePositionOrigin, ); } else { Share.share( content, subject: title, - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + sharePositionOrigin: sharePositionOrigin, ); } } diff --git a/lib/page/component/sliver_component.dart b/lib/page/component/sliver_component.dart index 0ee61855..31d59977 100644 --- a/lib/page/component/sliver_component.dart +++ b/lib/page/component/sliver_component.dart @@ -1,6 +1,6 @@ import 'package:askaide/page/component/image.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class SliverSingleComponent extends StatelessWidget { @@ -34,7 +34,9 @@ class SliverSingleComponent extends StatelessWidget { pinned: true, snap: false, primary: true, - actions: actions, + actions: (actions ?? []).isEmpty + ? null + : [...actions!, const SizedBox(width: 8)], backgroundColor: customColors.backgroundContainerColor, flexibleSpace: FlexibleSpaceBar( title: title, @@ -94,7 +96,9 @@ class SliverComponent extends StatelessWidget { pinned: true, snap: false, primary: true, - actions: actions, + actions: (actions ?? []).isEmpty + ? null + : [...actions!, const SizedBox(width: 8)], backgroundColor: customColors.backgroundContainerColor, flexibleSpace: FlexibleSpaceBar( title: title, diff --git a/lib/page/theme/custom_size.dart b/lib/page/component/theme/custom_size.dart similarity index 100% rename from lib/page/theme/custom_size.dart rename to lib/page/component/theme/custom_size.dart diff --git a/lib/page/theme/custom_theme.dart b/lib/page/component/theme/custom_theme.dart similarity index 100% rename from lib/page/theme/custom_theme.dart rename to lib/page/component/theme/custom_theme.dart diff --git a/lib/page/theme/theme.dart b/lib/page/component/theme/theme.dart similarity index 100% rename from lib/page/theme/theme.dart rename to lib/page/component/theme/theme.dart diff --git a/lib/page/component/verify_code_input.dart b/lib/page/component/verify_code_input.dart index c952d229..ab96b4d7 100644 --- a/lib/page/component/verify_code_input.dart +++ b/lib/page/component/verify_code_input.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localization/flutter_localization.dart'; diff --git a/lib/page/component/weak_text_button.dart b/lib/page/component/weak_text_button.dart index 9fc52300..bf241fa7 100644 --- a/lib/page/component/weak_text_button.dart +++ b/lib/page/component/weak_text_button.dart @@ -1,4 +1,4 @@ -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class WeakTextButton extends StatelessWidget { diff --git a/lib/page/creative_island/creative_island.dart b/lib/page/creative_island/creative_island.dart deleted file mode 100644 index 9daab211..00000000 --- a/lib/page/creative_island/creative_island.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'package:animated_button_bar/animated_button_bar.dart'; -import 'package:animated_text_kit/animated_text_kit.dart'; -import 'package:askaide/bloc/creative_island_bloc.dart'; -import 'package:askaide/helper/ability.dart'; -import 'package:askaide/lang/lang.dart'; -import 'package:askaide/page/component/background_container.dart'; -import 'package:askaide/page/component/enhanced_error.dart'; -import 'package:askaide/page/component/sliver_component.dart'; -import 'package:askaide/page/component/weak_text_button.dart'; -import 'package:askaide/page/creative_island/box.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; -import 'package:askaide/repo/settings_repo.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_localization/flutter_localization.dart'; -import 'package:go_router/go_router.dart'; - -enum CreativeIslandMode { - /// 创作岛 - creativeIsland, - - /// 绘图 - imageDraw; - - String getString() { - switch (this) { - case CreativeIslandMode.creativeIsland: - return 'creative-island'; - case CreativeIslandMode.imageDraw: - return 'image-draw'; - } - } -} - -/// 创作岛 -class CreativeIsland extends StatefulWidget { - final SettingRepository setting; - const CreativeIsland({super.key, required this.setting}); - - @override - State createState() => _CreativeIslandState(); -} - -class _CreativeIslandState extends State { - String? selectedCategory; - AnimatedButtonController controller = AnimatedButtonController(); - - @override - void initState() { - if (Ability().supportAPIServer()) { - context.read().add(CreativeIslandListLoadEvent( - mode: CreativeIslandMode.creativeIsland.getString())); - } - super.initState(); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final customColors = Theme.of(context).extension()!; - return BackgroundContainer( - setting: widget.setting, - child: Scaffold( - backgroundColor: Colors.transparent, - body: Ability().supportAPIServer() - ? _buildIslandItems(customColors) - : Center( - child: WeakTextButton( - onPressed: () { - context.push('/login'); - }, - title: AppLocale.creativeIslandNeedSignIn.getString(context), - icon: Icons.account_circle, - ), - ), - ), - ); - } - - /// 创作岛列表 - Widget _buildIslandItems( - CustomColors customColors, - ) { - return BlocBuilder( - buildWhen: (previous, current) => current is CreativeIslandListLoaded, - builder: (context, state) { - if (state is CreativeIslandListLoaded) { - if (state.error != null) { - return EnhancedErrorWidget(error: state.error); - } - - return SliverTabComponent( - tabBarTitles: state.categories, - title: AnimatedTextKit( - isRepeatingAnimation: false, - animatedTexts: [ - TypewriterAnimatedText( - AppLocale.creativeIsland.getString(context), - textStyle: - const TextStyle(fontSize: CustomSize.appBarTitleSize), - speed: const Duration(milliseconds: 150), - ), - ], - ), - crossAxisCount: _calCrossAxisCount(context), - itemsBuilder: (context, tabName) { - return state.items - .where((e) => e.categories.contains(tabName)) - .map((e) => CreativeIslandBox(item: e)) - .toList(); - }, - backgroundImageUrl: state.backgroundImage, - childAspectRatio: 1, - actions: [ - IconButton( - onPressed: () { - context.push( - '/creative-island/history?mode=${CreativeIslandMode.creativeIsland.getString()}'); - }, - icon: const Icon(Icons.list), - ), - ], - ); - } - - return Center( - child: CircularProgressIndicator( - color: customColors.chatInputPanelText, - ), - ); - }, - ); - } - - int _calCrossAxisCount(BuildContext context) { - return (MediaQuery.of(context).size.width / 200).round(); - } -} diff --git a/lib/page/creative_island/creative_island_create_page.dart b/lib/page/creative_island/creative_island_create_page.dart deleted file mode 100644 index 5927fc64..00000000 --- a/lib/page/creative_island/creative_island_create_page.dart +++ /dev/null @@ -1,1007 +0,0 @@ -import 'dart:math'; - -import 'package:askaide/bloc/creative_island_bloc.dart'; -import 'package:askaide/helper/constant.dart'; -import 'package:askaide/helper/haptic_feedback.dart'; -import 'package:askaide/helper/helper.dart'; -import 'package:askaide/helper/upload.dart'; -import 'package:askaide/lang/lang.dart'; -import 'package:askaide/page/component/background_container.dart'; -import 'package:askaide/page/component/column_block.dart'; -import 'package:askaide/page/component/enhanced_button.dart'; -import 'package:askaide/page/component/enhanced_input.dart'; -import 'package:askaide/page/component/enhanced_textfield.dart'; -import 'package:askaide/page/component/image.dart'; -import 'package:askaide/page/component/item_selector_search.dart'; -import 'package:askaide/page/component/loading.dart'; -import 'package:askaide/page/creative_island/content_preview.dart'; -import 'package:askaide/page/creative_island/creative_island_result.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; -import 'package:askaide/repo/api/creative.dart'; -import 'package:askaide/repo/api_server.dart'; -import 'package:askaide/repo/creative_island_repo.dart'; -import 'package:askaide/repo/settings_repo.dart'; -import 'package:bot_toast/bot_toast.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_localization/flutter_localization.dart'; -import 'package:go_router/go_router.dart'; -import 'package:quickalert/models/quickalert_type.dart'; - -class CreativeIslandCreatePage extends StatefulWidget { - final String id; - final CreativeIslandRepository repo; - final SettingRepository setting; - const CreativeIslandCreatePage({ - super.key, - required this.id, - required this.repo, - required this.setting, - }); - - @override - State createState() => - _CreativeIslandCreatePageState(); -} - -class ImageSize { - final String name; - final int width; - final int height; - - const ImageSize(this.name, this.width, this.height); -} - -class StabilityAIImageStyle { - final String name; - final String value; - - const StabilityAIImageStyle(this.name, this.value); -} - -class _CreativeIslandCreatePageState extends State - with SingleTickerProviderStateMixin { - final TextEditingController _contentController = TextEditingController(); - final TextEditingController _negativeTextController = TextEditingController(); - // 字数限制 - final TextEditingController _wordCountController = TextEditingController(); - - var _generationImageCount = 1; - - bool selectDialogOpened = false; - String? selectedImagePath; - // 是否启用 AI 改写 - bool _enableAIRewrite = false; - // 是否显示高级选项 - bool _showAdvancedOptions = false; - - CreativeIslandItemExtSize? _selectedImageSize; - ModelStyle _imageStyle = ModelStyle(id: '', name: ''); - - @override - void initState() { - super.initState(); - - context - .read() - .add(CreativeIslandItemLoadEvent(widget.id)); - } - - @override - void dispose() { - _contentController.dispose(); - _negativeTextController.dispose(); - _wordCountController.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final customColors = Theme.of(context).extension()!; - return BlocConsumer( - listener: (context, state) { - if (state is CreativeIslandItemLoaded) { - setState(() { - _enableAIRewrite = state.item.aiRewriteDefaultValue; - if (!state.item.showAdvanceButton) { - _showAdvancedOptions = true; - } - }); - } - }, - listenWhen: (previous, current) => current is CreativeIslandItemLoaded, - buildWhen: (previous, current) => current is CreativeIslandItemLoaded, - builder: (context, state) { - if (state is CreativeIslandItemLoaded) { - return _buildIslandEditArea( - state, - state.item, - context, - customColors, - ); - } - - return Scaffold( - appBar: AppBar( - title: Text( - AppLocale.creativeIsland.getString(context), - style: const TextStyle(fontSize: CustomSize.appBarTitleSize), - ), - centerTitle: true, - ), - body: const Center( - child: Text('Loading ...'), - ), - ); - }, - ); - } - - /// 构建创意岛编辑区域 - Widget _buildIslandEditArea( - CreativeIslandItemLoaded state, - CreativeIslandItem item, - BuildContext context, - CustomColors customColors, - ) { - return Scaffold( - appBar: AppBar( - title: Text( - item.title, - style: const TextStyle(fontSize: CustomSize.appBarTitleSize), - ), - centerTitle: true, - leading: IconButton( - onPressed: () { - context.pop(); - }, - icon: const Icon(Icons.arrow_back_ios), - ), - flexibleSpace: SizedBox( - width: double.infinity, - child: ShaderMask( - shaderCallback: (rect) { - return const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Colors.black, Colors.transparent], - ).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height)); - }, - blendMode: BlendMode.dstIn, - child: state.item.bgImage != null - ? Image( - image: CachedNetworkImageProviderEnhanced( - state.item.bgImage!, - ), - fit: BoxFit.cover, - ) - : null, - ), - ), - actions: [ - IconButton( - onPressed: () { - HapticFeedbackHelper.mediumImpact(); - context.push('/creative-island/${widget.id}/history'); - }, - icon: const Icon(Icons.article_outlined), - ), - ], - ), - body: _buildEditPanel(customColors, state, context), - ); - } - - /// 构建编辑面板 - Widget _buildEditPanel( - CustomColors customColors, - CreativeIslandItemLoaded state, - BuildContext context, - ) { - return BackgroundContainer( - setting: widget.setting, - enabled: false, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10), - height: double.infinity, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 上传图片(图生图) - if (state.item.modelType == creativeIslandModelTypeImageToImage) - ColumnBlock( - innerPanding: 10, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '原图', - style: TextStyle( - fontSize: 16, - color: customColors.textfieldLabelColor, - ), - ), - const SizedBox(height: 10), - Material( - borderRadius: BorderRadius.circular(8), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () async { - if (selectDialogOpened) return; - - selectDialogOpened = true; - HapticFeedbackHelper.mediumImpact(); - FilePickerResult? result = await FilePicker - .platform - .pickFiles(type: FileType.image) - .whenComplete( - () => selectDialogOpened = false); - if (result != null && result.files.isNotEmpty) { - setState(() { - selectedImagePath = result.files.first.path!; - }); - } - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Stack( - children: [ - Container( - decoration: selectedImagePath != null && - selectedImagePath != null - ? BoxDecoration( - image: selectedImagePath != - null && - selectedImagePath != '' - ? DecorationImage( - image: resolveImageProvider( - selectedImagePath!), - fit: BoxFit.cover, - ) - : null, - color: customColors - .backgroundContainerColor - ?.withAlpha(100), - borderRadius: - BorderRadius.circular(8), - ) - : null, - child: const SizedBox( - width: double.infinity, - height: 200, - ), - ), - selectedImagePath == null || - selectedImagePath == '' - ? SizedBox( - height: 200, - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon( - Icons.camera_alt, - size: 30, - color: customColors - .chatInputPanelText, - ), - const SizedBox(width: 10), - Text( - AppLocale.selectImage - .getString(context), - style: TextStyle( - fontSize: 14, - fontWeight: - FontWeight.w500, - color: customColors - .chatInputPanelText - ?.withOpacity(0.8), - ), - ), - ], - ), - ) - : Positioned( - bottom: 0, - left: 0, - right: 0, - child: Container( - color: const Color.fromARGB( - 80, 255, 255, 255), - height: 50, - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: const [ - Icon( - Icons.camera_alt, - size: 30, - color: Color.fromARGB( - 147, 255, 255, 255), - ), - SizedBox(width: 10), - Text( - '点击此处更换图片', - style: TextStyle( - fontSize: 14, - fontWeight: - FontWeight.w500, - color: Color.fromARGB( - 147, 255, 255, 255), - ), - ), - ], - ), - ), - ) - ], - ), - )), - ), - ), - ], - ), - ], - ), - - if (!state.item.noPrompt) - ColumnBlock( - innerPanding: 10, - children: [ - // 生成内容 - EnhancedTextField( - labelPosition: LabelPosition.top, - labelText: state.item.promptInputTitle ?? - (state.item.modelType == creativeIslandModelTypeText - ? AppLocale.writeYourIdeas.getString(context) - : AppLocale.describeYourImages - .getString(context)), - customColors: customColors, - controller: _contentController, - textAlignVertical: TextAlignVertical.top, - hintText: state.item.hint ?? - AppLocale.required.getString(context), - maxLines: 10, - minLines: 5, - maxLength: (state.item.wordCount ?? 0) > 0 - ? state.item.wordCount - : 1000, - showCounter: false, - bottomButton: state.item.modelType == - creativeIslandModelTypeImage - ? Row( - children: [ - Icon( - Icons.shuffle, - size: 13, - color: customColors.linkColor?.withAlpha(150), - ), - const SizedBox(width: 5), - Text( - AppLocale.random.getString(context), - style: TextStyle( - color: - customColors.linkColor?.withAlpha(150), - fontSize: 13, - ), - ), - ], - ) - : null, - bottomButtonOnPressed: () async { - final examples = await APIServer() - .exampleByTag(state.item.modelType); - if (examples.isEmpty) { - return; - } - - // 随机选取一个例子 - final example = - examples[Random().nextInt(examples.length)]; - _contentController.text = example.text; - }, - ), - // 文本类生成选项 - if (state.item.modelType == creativeIslandModelTypeText) - _buildTextGenerationToolbar( - state.item, - customColors, - ), - ], - ), - - if (_showAdvancedOptions) - ColumnBlock( - children: [ - // 排除关键词 - if (state.item.isShowNegativeText) - EnhancedTextField( - labelPosition: LabelPosition.top, - labelText: AppLocale.excludeContents.getString(context), - customColors: customColors, - controller: _negativeTextController, - textAlignVertical: TextAlignVertical.top, - hintText: 'text, blurry, low quality', - maxLength: 500, - showCounter: false, - ), - - if (state.item.isShowAIRewrite) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('AI 优化'), - Switch( - inactiveThumbColor: Colors.white, - inactiveTrackColor: Colors.grey.withOpacity(0.5), - activeColor: customColors.linkColor, - value: _enableAIRewrite, - onChanged: (value) { - setState(() { - _enableAIRewrite = value; - }); - }, - ), - ], - ), - ], - ), - - // 图片风格 - if (_showAdvancedOptions && - (state.item.modelType == - creativeIslandModelTypeImageToImage || - state.item.modelType == creativeIslandModelTypeImage) && - state.item.showImageStyleSelector) - ColumnBlock( - children: [ - _buildImageStyleField( - context, customColors, state.item.vendor), - ], - ), - if (_showAdvancedOptions && - state.item.modelType == creativeIslandModelTypeImage) - ColumnBlock( - children: _buildImageGenerationToolbar( - customColors, - state.item, - ), - ), - - // 生成按钮 - const SizedBox(height: 20), - Row( - children: [ - if (state.item.showAdvanceButton) - EnhancedButton( - title: '高级选项', - width: 100, - backgroundColor: Colors.transparent, - color: customColors.weakLinkColor, - fontSize: 15, - icon: Icon( - _showAdvancedOptions - ? Icons.unfold_less - : Icons.unfold_more, - color: customColors.weakLinkColor, - size: 15, - ), - onPressed: () { - setState(() { - _showAdvancedOptions = !_showAdvancedOptions; - }); - }, - ), - if (state.item.showAdvanceButton) const SizedBox(width: 10), - Expanded( - flex: 1, - child: EnhancedButton( - title: state.item.submitBtnText ?? - AppLocale.generate.getString(context), - onPressed: () { - onGenerate( - context, - state.item.modelType, - state.item.vendor, - state.item.waitSeconds, - state.item.noPrompt, - customColors, - ); - }, - ), - ), - ], - ), - const SizedBox(height: 20), - ], - ), - ), - ), - ); - } - - Widget _buildImageStyleItemPreview(ModelStyle style, {double? size}) { - return Container( - width: size, - height: size, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - image: style.preview != null - ? DecorationImage( - image: CachedNetworkImageProviderEnhanced(style.preview!), - fit: BoxFit.cover, - ) - : null, - ), - child: style.preview == null - ? const Center( - child: Icon( - Icons.interests, - color: Colors.grey, - size: 40, - ), - ) - : null); - } - - Widget _buildImageStyleField( - BuildContext context, - CustomColors customColors, - String vendor, - ) { - return EnhancedInput( - title: Text( - AppLocale.style.getString(context), - style: TextStyle( - color: customColors.textfieldLabelColor, - fontSize: 16, - ), - ), - value: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Text(_imageStyle.name == '' - ? AppLocale.auto.getString(context) - : _imageStyle.name), - const SizedBox(width: 10), - _buildImageStyleItemPreview(_imageStyle, size: 50), - ], - ), - onPressed: () { - openModalBottomSheet( - context, - (context) { - return FutureBuilder( - future: APIServer().modelStyles(vendor), - builder: (context, snapshot) { - if (snapshot.hasError) { - showErrorMessage(resolveError(context, snapshot.error!)); - return Text( - resolveError(context, snapshot.error!), - style: const TextStyle(color: Colors.red), - ); - } - - if (!snapshot.hasData) { - return const SizedBox.shrink(); - } - - var data = snapshot.data ?? []; - data.insert( - 0, - ModelStyle( - id: '', - name: AppLocale.auto.getString(context), - ), - ); - - return GridView.count( - crossAxisCount: 3, - crossAxisSpacing: 20, - mainAxisSpacing: 20, - padding: const EdgeInsets.only(top: 20), - children: [ - for (var item in data) - InkWell( - onTap: () { - setState(() { - _imageStyle = item; - }); - - Navigator.pop(context); - }, - child: Column( - children: [ - Expanded( - child: AspectRatio( - aspectRatio: 1, - child: _buildImageStyleItemPreview(item), - ), - ), - const SizedBox(height: 10), - Text( - item.name, - style: const TextStyle(fontSize: 12), - ), - ], - ), - ), - ], - ); - }, - ); - }, - heightFactor: 0.8, - ); - }, - ); - } - - /// 构建文本生成工具栏 - List _buildImageGenerationToolbar( - CustomColors customColors, - CreativeIslandItem item, - ) { - return [ - // 图片数量 - if (item.vendor == modelTypeStabilityAI || item.vendor == modelTypeLeapAI) - EnhancedInput( - title: Text( - AppLocale.imageCount.getString(context), - style: TextStyle( - color: customColors.textfieldLabelColor, - fontSize: 16, - ), - ), - value: Text(_generationImageCount.toString()), - onPressed: () { - openListSelectDialog( - context, - [ - SelectorItem(const Text('1', textAlign: TextAlign.center), 1), - SelectorItem(const Text('2', textAlign: TextAlign.center), 2), - SelectorItem(const Text('3', textAlign: TextAlign.center), 3), - SelectorItem(const Text('4', textAlign: TextAlign.center), 4), - ], - (value) { - setState(() { - _generationImageCount = value.value; - }); - return true; - }, - // title: AppLocale.imageCount.getString(context), - heightFactor: 0.4, - value: _generationImageCount, - ); - }, - ), - - //图片尺寸 - if (item.imageAllowSizes.isNotEmpty) - EnhancedInput( - title: Text( - AppLocale.imageSize.getString(context), - style: TextStyle( - color: customColors.textfieldLabelColor, - fontSize: 16, - ), - ), - value: _selectedImageSize == null - ? const Text('自动') - : Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - _buildSizeImage(_selectedImageSize!, customColors), - ], - ), - onPressed: () { - openListSelectDialog( - context, - item.imageAllowSizes - .map( - (e) => SelectorItem( - _buildSizeImage(e, customColors), - e.aspectRatio, - ), - ) - .toList(), - (value) { - setState(() { - _selectedImageSize = item.imageAllowSizes - .firstWhere((e) => e.aspectRatio == value.value); - }); - return true; - }, - // title: AppLocale.imageSize.getString(context), - value: _selectedImageSize?.aspectRatio, - heightFactor: 0.25, - horizontal: true, - horizontalCount: 4, - ); - }, - ), - ]; - } - - Widget _buildSizeImage( - CreativeIslandItemExtSize e, - CustomColors customColors, - ) { - final width = e.width > e.height ? 40 : 40 * e.width / e.height; - final height = e.width > e.height ? 40 * e.height / e.width : 40; - return Container( - width: width.toDouble(), - height: height.toDouble(), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(2), - color: customColors.backgroundContainerColor, - ), - alignment: Alignment.center, - child: Text( - e.aspectRatio, - style: const TextStyle(fontSize: 12, color: Colors.white), - textAlign: TextAlign.center, - ), - ); - } - - /// 构建文本生成工具栏 - Widget _buildTextGenerationToolbar( - CreativeIslandItem item, - CustomColors customColors, - ) { - return EnhancedTextField( - labelText: AppLocale.wordCount.getString(context), - labelPosition: LabelPosition.left, - customColors: customColors, - controller: _wordCountController, - hintText: '≤ 1000', - showCounter: false, - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - textDirection: TextDirection.rtl, - fieldWidth: 50, - ); - } - - /// 是否停止周期性查询任务执行状态 - var stopPeriodQuery = false; - - /// 生成事件处理 - void onGenerate( - BuildContext context, - String modelType, - String modelCategory, - int waitSeconds, - bool noPrompt, - CustomColors customColors, - ) async { - FocusScope.of(context).requestFocus(FocusNode()); - HapticFeedbackHelper.mediumImpact(); - - final content = _contentController.text.trim(); - if (!noPrompt && content.isEmpty) { - showErrorMessage(AppLocale.contentIsRequired.getString(context)); - return; - } - - var params = {}; - - if (modelType == creativeIslandModelTypeText) { - final wordCount = int.parse( - _wordCountController.text == '' ? '500' : _wordCountController.text); - if (wordCount < 0 || wordCount > 1000) { - showErrorMessage(AppLocale.wordCountInvalid.getString(context)); - return; - } - - params = { - "word_count": wordCount, - "prompt": content, - }; - } else if (modelType == creativeIslandModelTypeImage) { - params = { - "prompt": content, - "width": _selectedImageSize?.width, - "height": _selectedImageSize?.height, - "image_count": _generationImageCount, - "negative_prompt": _negativeTextController.text, - 'style_preset': _imageStyle.id, - 'ai_rewrite': _enableAIRewrite, - }; - } - // 图生图,先上传图片 - else if (modelType == creativeIslandModelTypeImageToImage) { - if (selectedImagePath == null || selectedImagePath == '') { - showErrorMessage('请选择图片'); - return; - } - - params = { - "prompt": content, - "negative_prompt": _negativeTextController.text, - 'style_preset': _imageStyle.id, - "width": _selectedImageSize?.width, - "height": _selectedImageSize?.height, - 'ai_rewrite': _enableAIRewrite, - 'image': - 'https://${selectedImagePath ?? 'demo'}', // 仅用于测试消耗量,正式上传后会被替换为 URL - }; - } - - final cancel = BotToast.showCustomLoading( - toastBuilder: (cancel) { - return const LoadingIndicator( - message: '思考中,请稍候...', - ); - }, - allowClick: false, - duration: const Duration(seconds: 15), - ); - - request() async { - try { - cancel(); - - if (modelType == creativeIslandModelTypeImageToImage) { - final cancel = BotToast.showCustomLoading( - toastBuilder: (cancel) { - return const LoadingIndicator( - message: '正在上传图片,请稍后...', - ); - }, - allowClick: false, - ); - - final uploadRes = await ImageUploader(widget.setting) - .upload(selectedImagePath!) - .whenComplete(() => cancel()); - params['image'] = uploadRes.url; - } - - final taskId = await APIServer().creativeIslandCompletionsAsync( - widget.id, - params, - ); - - stopPeriodQuery = false; - - // ignore: use_build_context_synchronously - Navigator.push( - context, - MaterialPageRoute( - fullscreenDialog: true, - builder: (context) => CreativeIslandResultDialog( - future: _generateResult( - modelType, - taskId, - waitSeconds > 30 ? 5 : 3, - params: params, - ), - waitDuration: waitSeconds, - ), - ), - ).whenComplete(() { - stopPeriodQuery = true; - context.read().add( - CreativeIslandHistoriesLoadEvent(widget.id, forceRefresh: true)); - }); - } catch (e) { - stopPeriodQuery = true; - cancel(); - // ignore: use_build_context_synchronously - showErrorMessage(resolveError(context, e)); - } - } - - try { - final res = await APIServer() - .creativeIslandCompletionsEvaluate(widget.id, params); - if (!res.enough) { - if (context.mounted) { - showBeautyDialog( - context, - type: QuickAlertType.warning, - text: AppLocale.quotaExceeded.getString(context), - confirmBtnText: '立即购买', - showCancelBtn: true, - onConfirmBtnTap: () { - context.pop(); - context.push('/payment'); - }, - ); - } - return; - } - if (res.cost > 0) { - cancel(); - // ignore: use_build_context_synchronously - openConfirmDialog( - context, - '【测试专用】\n本次请求预计消耗 ${res.cost} 个智慧果,是否继续操作?', - () => request(), - ); - } else { - request(); - } - } catch (e) { - cancel(); - showErrorMessageEnhanced(context, e); - } - } - - Future _generateResult( - String modelType, - String taskId, - int delaySeconds, { - Map? params, - }) async { - return await Future.delayed(Duration(seconds: delaySeconds), () async { - return await _queryCompletionTaskStatus( - taskId, - modelType, - 0, - delaySeconds, - params: params, - ); - }); - } - - Future _queryCompletionTaskStatus( - String taskId, - String modelType, - int retryTimes, - int delaySeconds, { - Map? params, - }) async { - if (retryTimes > 60) { - return Future.error(AppLocale.generateTimeout.getString(context)); - } - - final resp = await APIServer().asyncTaskStatus(taskId); - - if (resp.status == 'success') { - if (modelType == creativeIslandModelTypeImage || - modelType == creativeIslandModelTypeImageToImage) { - return IslandResult( - result: resp.resources ?? const [], - params: params, - ); - } - - return IslandResult( - result: [resp.resources!.join("\n\n")], - params: params, - ); - } else if (resp.status == 'failed') { - return Future.error(resp.errors!.join(";")); - } else { - if (stopPeriodQuery) { - // ignore: use_build_context_synchronously - return Future.error(AppLocale.generateTimeout.getString(context)); - } - - return await Future.delayed(Duration(seconds: delaySeconds), () async { - return await _queryCompletionTaskStatus( - taskId, - modelType, - retryTimes + 1, - delaySeconds, - params: params, - ); - }); - } - } -} diff --git a/lib/page/creative_island/creative_island_gallery.dart b/lib/page/creative_island/creative_island_gallery.dart deleted file mode 100644 index 747a8445..00000000 --- a/lib/page/creative_island/creative_island_gallery.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'package:askaide/bloc/creative_island_bloc.dart'; -import 'package:askaide/page/component/background_container.dart'; -import 'package:askaide/page/component/image.dart'; -import 'package:askaide/page/component/image_preview.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; -import 'package:askaide/repo/settings_repo.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sizer/sizer.dart'; - -class CreativeIslandGalleryScreen extends StatefulWidget { - final SettingRepository setting; - - const CreativeIslandGalleryScreen({super.key, required this.setting}); - - @override - State createState() => - _CreativeIslandGalleryScreenState(); -} - -class _CreativeIslandGalleryScreenState - extends State { - @override - void initState() { - context - .read() - .add(CreativeIslandGalleryLoadEvent(mode: "all")); - super.initState(); - } - - @override - Widget build(BuildContext context) { - final customColors = Theme.of(context).extension()!; - - return Scaffold( - appBar: AppBar( - title: const Text( - '创作岛 Gallery', - style: TextStyle(fontSize: CustomSize.appBarTitleSize), - ), - centerTitle: true, - ), - backgroundColor: customColors.chatInputPanelBackground, - body: BackgroundContainer( - setting: widget.setting, - enabled: false, - child: RefreshIndicator( - color: customColors.linkColor, - onRefresh: () async { - context - .read() - .add(CreativeIslandGalleryLoadEvent( - forceRefresh: true, - mode: "all", - )); - }, - child: BlocConsumer( - listenWhen: (previous, current) => - current is CreativeIslandGalleryLoaded, - buildWhen: (previous, current) => - current is CreativeIslandGalleryLoaded, - listener: (context, state) { - if (state is CreativeIslandHistoriesAllLoaded) { - if (state.error != null) { - showErrorMessageEnhanced(context, state.error); - } - } - }, - builder: (context, state) { - if (state is CreativeIslandGalleryLoaded) { - return GridView.count( - padding: const EdgeInsets.all(10), - crossAxisCount: _calCrossAxisCount(), - crossAxisSpacing: 10, - mainAxisSpacing: 10, - children: state.items.where((e) => e.images.isNotEmpty).map( - (e) { - if (e.userId != null && e.userId! > 0) { - return GestureDetector( - onTap: () { - context.push( - '/creative-island/${e.islandId}/history/${e.id}'); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: Colors.amber, - width: 2, - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: CachedNetworkImageEnhanced( - imageUrl: e.firstImagePreview, - fit: BoxFit.cover, - ), - ), - ), - ); - } - - return ClipRRect( - borderRadius: BorderRadius.circular(10), - child: NetworkImagePreviewer( - url: e.firstImagePreview, - hidePreviewButton: true, - ), - ); - }, - ).toList(), - ); - } - return const Center(child: CircularProgressIndicator()); - }, - ), - ), - ), - ); - } - - int _calCrossAxisCount() { - if (SizerUtil.deviceType == DeviceType.tablet) { - if (SizerUtil.orientation == Orientation.landscape) { - return 6; - } - return 4; - } - - if (SizerUtil.orientation == Orientation.landscape) { - return 6; - } - return 3; - } -} diff --git a/lib/page/creative_island/creative_island_history.dart b/lib/page/creative_island/creative_island_history.dart deleted file mode 100644 index db895461..00000000 --- a/lib/page/creative_island/creative_island_history.dart +++ /dev/null @@ -1,337 +0,0 @@ -import 'package:askaide/bloc/creative_island_bloc.dart'; -import 'package:askaide/helper/constant.dart'; -import 'package:askaide/helper/helper.dart'; -import 'package:askaide/lang/lang.dart'; -import 'package:askaide/page/component/background_container.dart'; -import 'package:askaide/page/component/image.dart'; -import 'package:askaide/page/creative_island/creative_island.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; -import 'package:askaide/repo/api/creative.dart'; -import 'package:askaide/repo/creative_island_repo.dart'; -import 'package:askaide/repo/settings_repo.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_localization/flutter_localization.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:go_router/go_router.dart'; - -class CreativeIslandHistoryPage extends StatefulWidget { - final String id; - final CreativeIslandRepository repo; - final SettingRepository setting; - const CreativeIslandHistoryPage({ - super.key, - required this.id, - required this.repo, - required this.setting, - }); - - @override - State createState() => - _CreativeIslandHistoryPageState(); -} - -class _CreativeIslandHistoryPageState extends State { - @override - void initState() { - super.initState(); - context - .read() - .add(CreativeIslandHistoriesLoadEvent(widget.id)); - } - - @override - Widget build(BuildContext context) { - final customColors = Theme.of(context).extension()!; - - return BlocConsumer( - listener: (context, state) { - if (state is CreativeIslandHistoriesLoaded) { - if (state.error != null) { - showErrorMessage(state.error); - } - } - }, - listenWhen: (previous, current) { - return current is CreativeIslandHistoriesLoaded; - }, - buildWhen: (previous, current) { - return current is CreativeIslandHistoriesLoaded; - }, - builder: (context, state) { - if (state is CreativeIslandHistoriesLoaded) { - return Scaffold( - appBar: _buildAppBar(context, state, customColors), - // backgroundColor: customColors.chatInputPanelBackground, - backgroundColor: customColors.backgroundContainerColor, - body: BackgroundContainer( - setting: widget.setting, - enabled: false, - child: RefreshIndicator( - color: customColors.linkColor, - onRefresh: () async { - context - .read() - .add(CreativeIslandHistoriesLoadEvent( - widget.id, - forceRefresh: true, - )); - }, - child: state.histories.isNotEmpty - ? _buildHistoryItems(state, customColors) - : Center( - child: Text(AppLocale.noRecords.getString(context))), - ), - ), - ); - } - - return Scaffold( - appBar: _buildAppBar(context, null, customColors), - backgroundColor: customColors.chatInputPanelBackground, - body: const Center(child: CircularProgressIndicator()), - ); - }, - ); - } - - /// 构建历史项目列表 - Widget _buildHistoryItems( - CreativeIslandHistoriesLoaded state, - CustomColors customColors, - ) { - return ListView.builder( - itemCount: state.histories.length, - itemBuilder: (context, index) { - return Container( - padding: EdgeInsets.only(top: index == 0 ? 15 : 10), - child: Container( - margin: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 0, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - ), - child: Slidable( - endActionPane: ActionPane( - motion: const ScrollMotion(), - children: [ - const SizedBox(width: 10), - SlidableAction( - label: AppLocale.delete.getString(context), - borderRadius: const BorderRadius.all(Radius.circular(10)), - backgroundColor: Colors.red, - icon: Icons.delete, - onPressed: (_) { - openConfirmDialog( - context, AppLocale.confirmDelete.getString(context), - () { - context - .read() - .add(CreativeIslandDeleteEvent( - widget.id, - state.histories[index].id, - mode: (state.island.modelType == - creativeIslandModelTypeImage || - state.island.modelType == - creativeIslandModelTypeImageToImage) - ? CreativeIslandMode.imageDraw.getString() - : CreativeIslandMode.creativeIsland - .getString(), - )); - }); - }, - ), - ], - ), - child: Material( - // color: customColors.chatExampleItemBackground, - borderRadius: const BorderRadius.all( - Radius.circular(10), - ), - // color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(10), - onTap: () { - _openHistoryItemDialog(context, state, index, customColors); - }, - child: ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 15, - vertical: 8, - ), - leading: _buildAnswerImagePreview( - context, state.histories[index]), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (state.histories[index].isTextType || - !state.histories[index].isSuccessful) - _buildPrefixIcon(state.histories[index]), - if (state.histories[index].isTextType || - !state.histories[index].isSuccessful) - const SizedBox(width: 10), - Expanded( - child: Text( - state.histories[index].prompt! - .replaceAll('\n', ' '), - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 15), - ), - ), - Text( - humanTime(state.histories[index].createdAt), - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - _buildAnswerTextPreview( - context, state.histories[index]), - ], - ), - ), - ), - ), - ), - ), - ); - }, - ); - } - - Widget? _buildAnswerImagePreview( - BuildContext context, - CreativeItemInServer item, - ) { - if (item.isImageType && item.images.isNotEmpty) { - return SizedBox( - height: 50, - width: 50, - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: CachedNetworkImageEnhanced( - imageUrl: item.firstImagePreview, - fit: BoxFit.cover, - ), - ), - ); - } - - return null; - } - - Widget _buildAnswerTextPreview( - BuildContext context, - CreativeItemInServer item, - ) { - if (item.isFailed) { - return Text( - '创作失败', - style: Theme.of(context).textTheme.bodySmall, - ); - } - - if (item.isProcessing) { - return Text( - '创作中', - style: Theme.of(context).textTheme.bodySmall, - ); - } - - if (item.isImageType && item.images.isNotEmpty) { - return const SizedBox(); - } - - return Text( - (item.answer ?? '').replaceAll('\n', ' '), - style: Theme.of(context).textTheme.bodySmall, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ); - } - - /// 打开历史项目详情对话框 - _openHistoryItemDialog( - BuildContext context, - CreativeIslandHistoriesLoaded state, - int index, - CustomColors customColors, - ) { - context.push( - '/creative-island/${widget.id}/history/${state.histories[index].id}'); - } - - AppBar _buildAppBar( - BuildContext context, - CreativeIslandHistoriesLoaded? state, - CustomColors customColors, - ) { - return AppBar( - title: Text( - AppLocale.histories.getString(context), - style: const TextStyle(fontSize: CustomSize.appBarTitleSize), - ), - centerTitle: true, - flexibleSpace: state != null - ? SizedBox( - width: double.infinity, - child: ShaderMask( - shaderCallback: (rect) { - return const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Colors.black, Colors.transparent], - ).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height)); - }, - blendMode: BlendMode.dstIn, - child: Image( - image: CachedNetworkImageProviderEnhanced( - state.island.bgImage!, - ), - fit: BoxFit.cover, - ), - ), - ) - : null, - ); - } - - Widget _buildPrefixIcon(CreativeItemInServer his) { - if (his.isFailed) { - return const Icon( - Icons.error_outline, - size: 18, - color: Colors.red, - ); - } - - if (his.isSuccessful) { - return const Icon(Icons.tag, size: 18, color: Colors.green); - } - - if (his.isProcessing) { - return const Icon( - Icons.hourglass_top, - size: 18, - color: Colors.blue, - ); - } - - return const SizedBox(); - } -} diff --git a/lib/page/creative_island/box.dart b/lib/page/creative_island/draw/components/box.dart similarity index 100% rename from lib/page/creative_island/box.dart rename to lib/page/creative_island/draw/components/box.dart diff --git a/lib/page/creative_island/content_preview.dart b/lib/page/creative_island/draw/components/content_preview.dart similarity index 98% rename from lib/page/creative_island/content_preview.dart rename to lib/page/creative_island/draw/components/content_preview.dart index 1aedf0ce..e70e9178 100644 --- a/lib/page/creative_island/content_preview.dart +++ b/lib/page/creative_island/draw/components/content_preview.dart @@ -1,7 +1,7 @@ import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/page/component/image_preview.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; diff --git a/lib/page/draw/components/creative_item.dart b/lib/page/creative_island/draw/components/creative_item.dart similarity index 96% rename from lib/page/draw/components/creative_item.dart rename to lib/page/creative_island/draw/components/creative_item.dart index b592cbf4..6c45ecfe 100644 --- a/lib/page/draw/components/creative_item.dart +++ b/lib/page/creative_island/draw/components/creative_item.dart @@ -1,6 +1,6 @@ import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/prompt_tags_selector.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class CreativeItem extends StatelessWidget { diff --git a/lib/page/draw/components/image_selector.dart b/lib/page/creative_island/draw/components/image_selector.dart similarity index 99% rename from lib/page/draw/components/image_selector.dart rename to lib/page/creative_island/draw/components/image_selector.dart index 471fb2d6..aae89e75 100644 --- a/lib/page/draw/components/image_selector.dart +++ b/lib/page/creative_island/draw/components/image_selector.dart @@ -4,7 +4,7 @@ import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/image.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; diff --git a/lib/page/draw/components/image_size.dart b/lib/page/creative_island/draw/components/image_size.dart similarity index 94% rename from lib/page/draw/components/image_size.dart rename to lib/page/creative_island/draw/components/image_size.dart index 306cb8a7..fdc27cac 100644 --- a/lib/page/draw/components/image_size.dart +++ b/lib/page/creative_island/draw/components/image_size.dart @@ -1,4 +1,4 @@ -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class ImageSize extends StatelessWidget { diff --git a/lib/page/draw/components/image_style_selector.dart b/lib/page/creative_island/draw/components/image_style_selector.dart similarity index 97% rename from lib/page/draw/components/image_style_selector.dart rename to lib/page/creative_island/draw/components/image_style_selector.dart index 4b2f0b30..c17e6e4f 100644 --- a/lib/page/draw/components/image_style_selector.dart +++ b/lib/page/creative_island/draw/components/image_style_selector.dart @@ -1,8 +1,8 @@ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/image.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; diff --git a/lib/page/draw/data/draw_history_datasource.dart b/lib/page/creative_island/draw/data/draw_history_datasource.dart similarity index 100% rename from lib/page/draw/data/draw_history_datasource.dart rename to lib/page/creative_island/draw/data/draw_history_datasource.dart diff --git a/lib/page/draw/draw_create.dart b/lib/page/creative_island/draw/draw_create.dart similarity index 97% rename from lib/page/draw/draw_create.dart rename to lib/page/creative_island/draw/draw_create.dart index 0e17318d..384e6fbb 100644 --- a/lib/page/draw/draw_create.dart +++ b/lib/page/creative_island/draw/draw_create.dart @@ -12,16 +12,17 @@ import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/prompt_tags_selector.dart'; -import 'package:askaide/page/creative_island/content_preview.dart'; -import 'package:askaide/page/creative_island/creative_island_result.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/draw/components/image_selector.dart'; -import 'package:askaide/page/draw/components/image_size.dart'; -import 'package:askaide/page/draw/components/image_style_selector.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/creative_island/draw/components/content_preview.dart'; +import 'package:askaide/page/creative_island/draw/draw_result.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/creative_island/draw/components/image_selector.dart'; +import 'package:askaide/page/creative_island/draw/components/image_size.dart'; +import 'package:askaide/page/creative_island/draw/components/image_style_selector.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/cupertino.dart'; @@ -59,7 +60,7 @@ class _DrawCreateScreenState extends State { String selectedImageSize = '1:1'; bool showAdvancedOptions = false; CreativeIslandImageFilter? selectedStyle; - double? imageStrength = 0.5; + double? imageStrength = 0.65; /// 是否停止周期性查询任务执行状态 var stopPeriodQuery = false; @@ -92,7 +93,8 @@ class _DrawCreateScreenState extends State { if (widget.galleryCopyId != null && widget.galleryCopyId! > 0) { APIServer() .creativeGalleryItem(id: widget.galleryCopyId!) - .then((gallery) { + .then((response) { + final gallery = response.item; if (gallery.prompt != null && gallery.prompt!.isNotEmpty) { promptController.text = gallery.prompt!; } @@ -375,7 +377,7 @@ class _DrawCreateScreenState extends State { const SizedBox(width: 10), Expanded( child: Slider( - value: imageStrength ?? 0, + value: imageStrength ?? 0.65, min: 0, max: 1, divisions: 20, @@ -856,7 +858,7 @@ class _DrawCreateScreenState extends State { context, MaterialPageRoute( fullscreenDialog: true, - builder: (context) => CreativeIslandResultDialog( + builder: (context) => DrawResultPage( future: Future.delayed(const Duration(seconds: 10), () async { return await queryCompletionTaskStatus( taskId: taskId, @@ -910,20 +912,21 @@ class _DrawCreateScreenState extends State { } } catch (e) { cancel(); + // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); } } String imageStrengthText() { - if (imageStrength == 0 || imageStrength == null) { + if (imageStrength == 0) { return '自动'; } - if (imageStrength! >= 0.4 && imageStrength! <= 0.6) { - return '适中(推荐)'; + if (imageStrength! >= 0.4 && imageStrength! <= 0.67) { + return '适中'; } - if (imageStrength! > 0.6 && imageStrength! < 0.9) { + if (imageStrength! > 0.65 && imageStrength! < 0.9) { return '更有创造力'; } diff --git a/lib/page/draw/draw.dart b/lib/page/creative_island/draw/draw_list.dart similarity index 88% rename from lib/page/draw/draw.dart rename to lib/page/creative_island/draw/draw_list.dart index f4dd5677..d9c68466 100644 --- a/lib/page/draw/draw.dart +++ b/lib/page/creative_island/draw/draw_list.dart @@ -4,27 +4,27 @@ import 'package:askaide/helper/color.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/sliver_component.dart'; -import 'package:askaide/page/draw/components/creative_item.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/creative_island/draw/components/creative_item.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; -class DrawScreen extends StatefulWidget { +class DrawListScreen extends StatefulWidget { final SettingRepository setting; - const DrawScreen({super.key, required this.setting}); + const DrawListScreen({super.key, required this.setting}); @override - State createState() => _DrawScreenState(); + State createState() => _DrawListScreenState(); } -class _DrawScreenState extends State { +class _DrawListScreenState extends State { @override void initState() { - if (Ability().supportAPIServer()) { + if (Ability().enableAPIServer()) { userSignedIn = true; } diff --git a/lib/page/creative_island/creative_island_result.dart b/lib/page/creative_island/draw/draw_result.dart similarity index 93% rename from lib/page/creative_island/creative_island_result.dart rename to lib/page/creative_island/draw/draw_result.dart index 119cca18..8e415bf0 100644 --- a/lib/page/creative_island/creative_island_result.dart +++ b/lib/page/creative_island/draw/draw_result.dart @@ -1,33 +1,31 @@ import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; -import 'package:askaide/page/creative_island/content_preview.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/creative_island/draw/components/content_preview.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:circular_countdown_timer/circular_countdown_timer.dart'; -class CreativeIslandResultDialog extends StatefulWidget { +class DrawResultPage extends StatefulWidget { final Future future; final int waitDuration; - const CreativeIslandResultDialog({ + const DrawResultPage({ super.key, required this.future, this.waitDuration = 30, }); @override - State createState() => - _CreativeIslandResultDialogState(); + State createState() => _DrawResultPageState(); } const defaultCounterRestartValue = 15; -class _CreativeIslandResultDialogState - extends State { +class _DrawResultPageState extends State { var loading = true; var restartCounterValue = defaultCounterRestartValue; diff --git a/lib/page/draw/image_edit_direct.dart b/lib/page/creative_island/draw/image_edit_direct.dart similarity index 94% rename from lib/page/draw/image_edit_direct.dart rename to lib/page/creative_island/draw/image_edit_direct.dart index c1a38873..f0609225 100644 --- a/lib/page/draw/image_edit_direct.dart +++ b/lib/page/creative_island/draw/image_edit_direct.dart @@ -6,12 +6,12 @@ import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/message_box.dart'; -import 'package:askaide/page/creative_island/content_preview.dart'; -import 'package:askaide/page/creative_island/creative_island_result.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/draw/components/image_selector.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/creative_island/draw/components/content_preview.dart'; +import 'package:askaide/page/creative_island/draw/draw_result.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/creative_island/draw/components/image_selector.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; @@ -193,7 +193,7 @@ class _ImageEditDirectScreenState extends State { context, MaterialPageRoute( fullscreenDialog: true, - builder: (context) => CreativeIslandResultDialog( + builder: (context) => DrawResultPage( future: Future.delayed(const Duration(seconds: 10), () async { return await queryCompletionTaskStatus( taskId: taskId, diff --git a/lib/page/gallery/components/image_card.dart b/lib/page/creative_island/gallery/components/image_card.dart similarity index 98% rename from lib/page/gallery/components/image_card.dart rename to lib/page/creative_island/gallery/components/image_card.dart index 30fc9013..da4a6669 100644 --- a/lib/page/gallery/components/image_card.dart +++ b/lib/page/creative_island/gallery/components/image_card.dart @@ -2,7 +2,7 @@ import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/random_avatar.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class ImageCard extends StatelessWidget { diff --git a/lib/page/gallery/data/gallery_datasource.dart b/lib/page/creative_island/gallery/data/gallery_datasource.dart similarity index 100% rename from lib/page/gallery/data/gallery_datasource.dart rename to lib/page/creative_island/gallery/data/gallery_datasource.dart diff --git a/lib/page/gallery/gallery.dart b/lib/page/creative_island/gallery/gallery.dart similarity index 94% rename from lib/page/gallery/gallery.dart rename to lib/page/creative_island/gallery/gallery.dart index a69f2994..9c3717be 100644 --- a/lib/page/gallery/gallery.dart +++ b/lib/page/creative_island/gallery/gallery.dart @@ -2,10 +2,10 @@ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/sliver_component.dart'; -import 'package:askaide/page/gallery/components/image_card.dart'; -import 'package:askaide/page/gallery/data/gallery_datasource.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/creative_island/gallery/components/image_card.dart'; +import 'package:askaide/page/creative_island/gallery/data/gallery_datasource.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; diff --git a/lib/page/gallery/gallery_item.dart b/lib/page/creative_island/gallery/gallery_item.dart similarity index 90% rename from lib/page/gallery/gallery_item.dart rename to lib/page/creative_island/gallery/gallery_item.dart index fd89f614..284da456 100644 --- a/lib/page/gallery/gallery_item.dart +++ b/lib/page/creative_island/gallery/gallery_item.dart @@ -4,6 +4,7 @@ import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/image.dart'; +import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/attached_button_panel.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; @@ -11,15 +12,16 @@ import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/gallery_item_share.dart'; import 'package:askaide/page/component/image_preview.dart'; import 'package:askaide/page/component/random_avatar.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class GalleryItemScreen extends StatefulWidget { @@ -54,6 +56,44 @@ class _GalleryItemScreenState extends State { ), backgroundColor: customColors.backgroundContainerColor?.withAlpha(200), toolbarHeight: CustomSize.toolbarHeight, + actions: [ + BlocBuilder( + buildWhen: (previous, current) => current is GalleryItemLoaded, + builder: (context, state) { + if (state is GalleryItemLoaded && + state.isInternalUser && + state.item.status == 1) { + return TextButton( + onPressed: () { + openConfirmDialog( + context, + '确认取消?', + () => APIServer() + .cancelShareCreativeHistoryToGallery( + historyId: state.item.creativeHistoryId!) + .then((value) { + showSuccessMessage( + AppLocale.operateSuccess.getString(context)); + + context.read().add(GalleryItemLoadEvent( + id: widget.galleryId, forceRefresh: true)); + }), + ); + }, + child: Text( + '取消共享', + style: TextStyle( + color: customColors.weakLinkColor, + fontSize: 12, + ), + ), + ); + } + + return const SizedBox(); + }, + ), + ], ), extendBodyBehindAppBar: true, backgroundColor: customColors.backgroundContainerColor, @@ -197,7 +237,7 @@ class _GalleryItemScreenState extends State { child: EnhancedButton( title: '制作同款', onPressed: () { - if (Ability().supportAPIServer()) { + if (Ability().enableAPIServer()) { context.push( '/creative-draw/create?mode=text-to-image&id=${state.item.creativeId}&gallery_copy_id=${state.item.id}'); } else { diff --git a/lib/page/creative_island/list.dart b/lib/page/creative_island/list.dart deleted file mode 100644 index 9d8249ca..00000000 --- a/lib/page/creative_island/list.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:askaide/page/creative_island/box.dart'; -import 'package:askaide/repo/api/creative.dart'; -import 'package:flutter/material.dart'; -import 'package:sizer/sizer.dart'; - -/// 创作岛列表 -class CreativeIslandList extends StatelessWidget { - final List items; - final Color? color; - const CreativeIslandList({super.key, required this.items, this.color}); - - @override - Widget build(BuildContext context) { - return GridView.count( - crossAxisCount: _calCrossAxisCount(), - childAspectRatio: 1, - children: items - .map((e) => CreativeIslandBox(item: e, backgroundColor: color)) - .toList(), - ); - } - - int _calCrossAxisCount() { - if (SizerUtil.deviceType == DeviceType.tablet) { - if (SizerUtil.orientation == Orientation.landscape) { - return 4; - } - return 3; - } - - if (SizerUtil.orientation == Orientation.landscape) { - return 3; - } - return 2; - } -} diff --git a/lib/page/creative_island/creative_island_history_all.dart b/lib/page/creative_island/my_creation.dart similarity index 96% rename from lib/page/creative_island/creative_island_history_all.dart rename to lib/page/creative_island/my_creation.dart index 43b92e9e..19432b3d 100644 --- a/lib/page/creative_island/creative_island_history_all.dart +++ b/lib/page/creative_island/my_creation.dart @@ -7,10 +7,10 @@ import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/button.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/loading.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/draw/data/draw_history_datasource.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/creative_island/draw/data/draw_history_datasource.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; @@ -20,19 +20,17 @@ import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:loading_more_list/loading_more_list.dart'; -class CreativeIslandHistoriesAllScreen extends StatefulWidget { +class MyCreationScreen extends StatefulWidget { final SettingRepository setting; final String mode; - const CreativeIslandHistoriesAllScreen( + const MyCreationScreen( {super.key, required this.setting, required this.mode}); @override - State createState() => - _CreativeIslandHistoriesAllScreenState(); + State createState() => _MyCreationScreenState(); } -class _CreativeIslandHistoriesAllScreenState - extends State { +class _MyCreationScreenState extends State { final DrawHistoryDatasource datasource = DrawHistoryDatasource(); @override diff --git a/lib/page/creative_island/creative_island_history_preview.dart b/lib/page/creative_island/my_creation_item.dart similarity index 96% rename from lib/page/creative_island/creative_island_history_preview.dart rename to lib/page/creative_island/my_creation_item.dart index f2bbc9ee..997d199c 100644 --- a/lib/page/creative_island/creative_island_history_preview.dart +++ b/lib/page/creative_island/my_creation_item.dart @@ -2,10 +2,10 @@ import 'package:askaide/bloc/creative_island_bloc.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; -import 'package:askaide/page/creative_island/content_preview.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/creative_island/draw/components/content_preview.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; @@ -13,13 +13,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; -class CreativeIslandHistoryPreview extends StatefulWidget { +class MyCreationItemPage extends StatefulWidget { final String islandId; final int itemId; final SettingRepository setting; final bool showErrorMessage; - const CreativeIslandHistoryPreview({ + const MyCreationItemPage({ super.key, required this.setting, required this.islandId, @@ -28,12 +28,10 @@ class CreativeIslandHistoryPreview extends StatefulWidget { }); @override - State createState() => - _CreativeIslandHistoryPreviewState(); + State createState() => _MyCreationItemPageState(); } -class _CreativeIslandHistoryPreviewState - extends State +class _MyCreationItemPageState extends State with SingleTickerProviderStateMixin { late final TabController _tabController; diff --git a/lib/page/data/chat_history_datasource.dart b/lib/page/data/chat_history_datasource.dart new file mode 100644 index 00000000..40a16414 --- /dev/null +++ b/lib/page/data/chat_history_datasource.dart @@ -0,0 +1,60 @@ +import 'package:askaide/helper/constant.dart'; +import 'package:askaide/helper/logger.dart'; +import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/chat_message_repo.dart'; +import 'package:askaide/repo/model/chat_history.dart'; +import 'package:loading_more_list/loading_more_list.dart'; + +class ChatHistoryDatasource extends LoadingMoreBase { + int pageindex = 1; + bool _hasMore = true; + bool forceRefresh = false; + + final ChatMessageRepository repo; + ChatHistoryDatasource(this.repo); + + @override + bool get hasMore => (_hasMore && length < 300) || forceRefresh; + + @override + Future loadData([bool isloadMoreAction = false]) async { + try { + final histories = await repo.recentChatHistories( + chatAnywhereRoomId, + 30, + offset: 30 * (pageindex - 1), + userId: APIServer().localUserID(), + ); + + if (pageindex == 1) { + clear(); + } + + for (var element in histories) { + add(element); + } + + if (histories.isEmpty) { + _hasMore = false; + } + + pageindex = pageindex + 1; + return true; + } catch (e) { + Logger.instance.e(e); + return false; + } + } + + @override + Future refresh([bool notifyStateChanged = false]) async { + _hasMore = true; + pageindex = 1; + //force to refresh list when you don't want clear list before request + //for the case, if your list already has 20 items. + forceRefresh = !notifyStateChanged; + var result = await super.refresh(notifyStateChanged); + forceRefresh = false; + return result; + } +} diff --git a/lib/page/data/group_message_datasource.dart b/lib/page/data/group_message_datasource.dart new file mode 100644 index 00000000..8a92e7ed --- /dev/null +++ b/lib/page/data/group_message_datasource.dart @@ -0,0 +1,55 @@ +import 'package:askaide/helper/logger.dart'; +import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/model/group.dart'; +import 'package:loading_more_list/loading_more_list.dart'; + +class GroupMessageDatasource extends LoadingMoreBase { + int startId = 0; + bool _hasMore = true; + bool forceRefresh = false; + + final int groupId; + + GroupMessageDatasource({required this.groupId}); + + @override + bool get hasMore => _hasMore || forceRefresh; + + @override + Future loadData([bool isloadMoreAction = false]) async { + try { + final messages = await APIServer() + .chatGroupMessages(groupId, startId: startId, cache: false); + + if (startId == 0) { + clear(); + } + + for (var element in messages.data) { + add(element); + } + + if (messages.data.isEmpty) { + _hasMore = false; + } + + startId = messages.lastId; + return true; + } catch (e) { + Logger.instance.e(e); + return false; + } + } + + @override + Future refresh([bool notifyStateChanged = false]) async { + _hasMore = true; + startId = 1; + //force to refresh list when you don't want clear list before request + //for the case, if your list already has 20 items. + forceRefresh = !notifyStateChanged; + var result = await super.refresh(notifyStateChanged); + forceRefresh = false; + return result; + } +} diff --git a/lib/page/avatar_selector.dart b/lib/page/lab/avatar_selector.dart similarity index 100% rename from lib/page/avatar_selector.dart rename to lib/page/lab/avatar_selector.dart diff --git a/lib/page/lab/creative_models.dart b/lib/page/lab/creative_models.dart index 1cd6aa65..a099a347 100644 --- a/lib/page/lab/creative_models.dart +++ b/lib/page/lab/creative_models.dart @@ -1,7 +1,5 @@ import 'package:askaide/bloc/creative_island_bloc.dart'; import 'package:askaide/helper/constant.dart'; -import 'package:askaide/helper/helper.dart'; -import 'package:askaide/helper/image.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; @@ -9,9 +7,9 @@ import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/image_preview.dart'; import 'package:askaide/page/component/item_selector_search.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/image_model.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; @@ -20,7 +18,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; -import 'package:sizer/sizer.dart'; class CreativeModelScreen extends StatefulWidget { final SettingRepository setting; diff --git a/lib/page/lab/draw_board.dart b/lib/page/lab/draw_board.dart index 55043471..b325dc39 100644 --- a/lib/page/lab/draw_board.dart +++ b/lib/page/lab/draw_board.dart @@ -5,9 +5,9 @@ import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/column_block.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/draw/components/image_selector.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/creative_island/draw/components/image_selector.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:file_saver/file_saver.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; diff --git a/lib/page/prompt.dart b/lib/page/lab/prompt.dart similarity index 97% rename from lib/page/prompt.dart rename to lib/page/lab/prompt.dart index 01e6aeb8..ddd8c545 100644 --- a/lib/page/prompt.dart +++ b/lib/page/lab/prompt.dart @@ -4,8 +4,8 @@ import 'package:askaide/page/component/effect/glass.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/item_selector_search.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; diff --git a/lib/page/lab/user_center.dart b/lib/page/lab/user_center.dart index 19658162..5edff271 100644 --- a/lib/page/lab/user_center.dart +++ b/lib/page/lab/user_center.dart @@ -4,8 +4,8 @@ import 'package:askaide/page/component/account_quota_card.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/invite_card.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/page/quota_usage_statistics.dart b/lib/page/quota_usage_statistics.dart deleted file mode 100644 index 758a3c02..00000000 --- a/lib/page/quota_usage_statistics.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'package:askaide/helper/helper.dart'; -import 'package:askaide/page/component/background_container.dart'; -import 'package:askaide/page/component/message_box.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; -import 'package:askaide/repo/api_server.dart'; -import 'package:askaide/repo/settings_repo.dart'; -import 'package:flutter/material.dart'; - -class QuotaUsageStatisticsScreen extends StatefulWidget { - final SettingRepository setting; - const QuotaUsageStatisticsScreen({super.key, required this.setting}); - - @override - State createState() => - _QuotaUsageStatisticsScreenState(); -} - -class _QuotaUsageStatisticsScreenState - extends State { - @override - Widget build(BuildContext context) { - var customColors = Theme.of(context).extension()!; - - return Scaffold( - appBar: AppBar( - toolbarHeight: CustomSize.toolbarHeight, - title: const Text( - '使用明细', - style: TextStyle(fontSize: CustomSize.appBarTitleSize), - ), - centerTitle: true, - elevation: 0, - ), - backgroundColor: customColors.backgroundContainerColor, - body: BackgroundContainer( - setting: widget.setting, - enabled: false, - child: Container( - padding: const EdgeInsets.all(16), - child: FutureBuilder( - future: APIServer().quotaUsedStatistics(), - builder: (context, snapshot) { - if (snapshot.hasError) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.error_outline, - size: 50, - color: Colors.red, - ), - const SizedBox(height: 10), - Text( - resolveError(context, snapshot.error!), - style: const TextStyle(color: Colors.red), - ), - ], - ), - ); - } - - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - return Column( - children: [ - const MessageBox( - message: '使用明细将在次日更新,显示近 30 天的使用量。', - type: MessageBoxType.info, - ), - const SizedBox(height: 10), - Expanded( - child: _buildQuotaUsagePage( - context, snapshot.data!, customColors), - ), - ], - ); - }, - ), - ), - ), - ); - } - - Widget _buildQuotaUsagePage( - BuildContext context, - List usages, - CustomColors customColors, - ) { - final usageGt0 = usages.where((e) => e.used > 0).toList(); - if (usageGt0.isEmpty) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: const [ - Icon( - Icons.error_outline, - size: 50, - ), - SizedBox(height: 10), - Text( - '暂无使用记录', - ), - ], - ), - ); - } - - return Column( - children: [ - Expanded( - child: ListView( - shrinkWrap: true, - children: [ - for (var item in usageGt0) - Container( - margin: const EdgeInsets.symmetric(vertical: 6), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: customColors.paymentItemBackgroundColor, - borderRadius: BorderRadius.circular(10), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(item.date), - Row( - children: [ - if (item.used > 0) const Text('-'), - Text( - '${item.used}', - style: TextStyle( - fontWeight: - item.used > 0 ? FontWeight.bold : null, - ), - ), - ], - ), - ], - ), - ) - ], - ), - ), - ], - ); - } -} diff --git a/lib/page/rooms.dart b/lib/page/rooms.dart deleted file mode 100644 index e0c1dea6..00000000 --- a/lib/page/rooms.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'package:askaide/bloc/room_bloc.dart'; -import 'package:askaide/helper/ability.dart'; -import 'package:askaide/helper/constant.dart'; -import 'package:askaide/helper/haptic_feedback.dart'; -import 'package:askaide/helper/helper.dart'; -import 'package:askaide/helper/image.dart'; -import 'package:askaide/lang/lang.dart'; -import 'package:askaide/page/component/image.dart'; -import 'package:askaide/page/component/random_avatar.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; -import 'package:askaide/repo/model/room.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_localization/flutter_localization.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:go_router/go_router.dart'; - -class RoomItem extends StatelessWidget { - final Room room; - const RoomItem({super.key, required this.room}); - - @override - Widget build(BuildContext context) { - final customColors = Theme.of(context).extension()!; - return Container( - margin: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 5, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(customColors.borderRadius ?? 8), - ), - child: Slidable( - endActionPane: ActionPane( - motion: const ScrollMotion(), - children: [ - const SizedBox(width: 10), - SlidableAction( - label: '编辑', - backgroundColor: Colors.green, - borderRadius: room.category == 'system' - ? BorderRadius.all( - Radius.circular(customColors.borderRadius ?? 8)) - : BorderRadius.only( - topLeft: Radius.circular(customColors.borderRadius ?? 8), - bottomLeft: - Radius.circular(customColors.borderRadius ?? 8), - ), - icon: Icons.edit, - onPressed: (_) { - final chatRoomBloc = context.read(); - context.push('/room/${room.id}/setting').then((value) { - chatRoomBloc.add(RoomsLoadEvent()); - }); - }, - ), - if (room.category != 'system') - SlidableAction( - label: AppLocale.delete.getString(context), - borderRadius: BorderRadius.only( - topRight: Radius.circular(customColors.borderRadius ?? 8), - bottomRight: Radius.circular(customColors.borderRadius ?? 8), - ), - backgroundColor: Colors.red, - icon: Icons.delete, - onPressed: (_) { - openConfirmDialog( - context, - AppLocale.confirmToDeleteRoom.getString(context), - () => - context.read().add(RoomDeleteEvent(room.id!)), - danger: true, - ); - }, - ), - ], - ), - child: Material( - borderRadius: - BorderRadius.all(Radius.circular(customColors.borderRadius ?? 8)), - color: customColors.columnBlockBackgroundColor, - child: InkWell( - borderRadius: BorderRadius.all( - Radius.circular(customColors.borderRadius ?? 8)), - onTap: () { - HapticFeedbackHelper.lightImpact(); - final chatRoomBloc = context.read(); - context.push('/room/${room.id}/chat').then((value) { - chatRoomBloc.add(RoomsLoadEvent()); - }); - }, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildAvatar(room), - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - room.name, - overflow: TextOverflow.ellipsis, - ), - ), - Text( - humanTime(room.lastActiveTime), - style: TextStyle( - color: - customColors.weakLinkColor?.withAlpha(65), - fontSize: 12, - ), - ), - ], - ), - const SizedBox(height: 5), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (room.systemPrompt != null && - room.systemPrompt != '') - Text( - room.systemPrompt!, - style: TextStyle( - color: customColors.weakLinkColor - ?.withAlpha(150), - fontSize: 13, - ), - overflow: TextOverflow.ellipsis, - ), - // if (room.description != null) - // Text( - // room.description!, - // style: - // Theme.of(context).textTheme.bodySmall, - // ), - if (room.systemPrompt == null || - room.systemPrompt == '') - Text( - room - .modelName() - .toUpperCase() - .replaceAll('-TURBO', ''), - style: TextStyle( - color: customColors.weakLinkColor - ?.withAlpha(150), - fontSize: 13, - ), - ), - ], - ), - ], - ), - ), - ), - ], - ), - ), - ), - ), - ); - } - - Widget _buildAvatar(Room room) { - if (room.avatarUrl != null && room.avatarUrl!.startsWith('http')) { - return SizedBox( - width: 70, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - bottomLeft: Radius.circular(8), - ), - child: CachedNetworkImageEnhanced( - imageUrl: imageURL(room.avatarUrl!, qiniuImageTypeAvatar), - fit: BoxFit.fill, - ), - ), - ); - } - - return RandomAvatar( - id: room.avatar, - size: 70, - usage: - Ability().supportAPIServer() ? AvatarUsage.room : AvatarUsage.legacy, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - bottomLeft: Radius.circular(8), - ), - ); - } -} diff --git a/lib/page/account_security.dart b/lib/page/setting/account_security.dart similarity index 97% rename from lib/page/account_security.dart rename to lib/page/setting/account_security.dart index b0dc83d9..900e692e 100644 --- a/lib/page/account_security.dart +++ b/lib/page/setting/account_security.dart @@ -3,9 +3,9 @@ import 'package:askaide/helper/logger.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/enhanced_popup_menu.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; diff --git a/lib/page/background_selector.dart b/lib/page/setting/background_selector.dart similarity index 99% rename from lib/page/background_selector.dart rename to lib/page/setting/background_selector.dart index af440631..77f9ac79 100644 --- a/lib/page/background_selector.dart +++ b/lib/page/setting/background_selector.dart @@ -7,8 +7,8 @@ import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/image.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; diff --git a/lib/page/bind_phone_page.dart b/lib/page/setting/bind_phone_page.dart similarity index 96% rename from lib/page/bind_phone_page.dart rename to lib/page/setting/bind_phone_page.dart index 65a7e338..53a9354d 100644 --- a/lib/page/bind_phone_page.dart +++ b/lib/page/setting/bind_phone_page.dart @@ -1,15 +1,16 @@ import 'dart:convert'; import 'package:askaide/bloc/account_bloc.dart'; +import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/verify_code_input.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; @@ -86,7 +87,8 @@ class _BindPhoneScreenState extends State { leading: IconButton( onPressed: () { if (widget.isSignIn) { - context.go('/chat-chat?show_initial_dialog=false&reward=0'); + context.go( + '${Ability().homeRoute}?show_initial_dialog=false&reward=0'); } else { context.pop(); } @@ -292,7 +294,7 @@ class _BindPhoneScreenState extends State { if (widget.isSignIn) { if (context.mounted) { context.go( - '/chat-chat?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); + '${Ability().homeRoute}?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); } } else { if (context.mounted) { diff --git a/lib/page/change_password.dart b/lib/page/setting/change_password.dart similarity index 96% rename from lib/page/change_password.dart rename to lib/page/setting/change_password.dart index a69af574..5c99532d 100644 --- a/lib/page/change_password.dart +++ b/lib/page/setting/change_password.dart @@ -5,9 +5,9 @@ import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/password_field.dart'; import 'package:askaide/page/component/verify_code_input.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; diff --git a/lib/page/setting/custom_home_models.dart b/lib/page/setting/custom_home_models.dart new file mode 100644 index 00000000..11e45554 --- /dev/null +++ b/lib/page/setting/custom_home_models.dart @@ -0,0 +1,210 @@ +import 'package:askaide/helper/ability.dart'; +import 'package:askaide/helper/color.dart'; +import 'package:askaide/lang/lang.dart'; +import 'package:askaide/page/chat/room_create.dart'; +import 'package:askaide/page/component/background_container.dart'; +import 'package:askaide/page/component/column_block.dart'; +import 'package:askaide/page/component/enhanced_button.dart'; +import 'package:askaide/page/component/loading.dart'; +import 'package:askaide/page/component/message_box.dart'; +import 'package:askaide/page/component/model_indicator.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/settings_repo.dart'; +import 'package:bot_toast/bot_toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localization/flutter_localization.dart'; + +class CustomHomeModelsPage extends StatefulWidget { + final SettingRepository setting; + const CustomHomeModelsPage({super.key, required this.setting}); + + @override + State createState() => _CustomHomeModelsPageState(); +} + +class _CustomHomeModelsPageState extends State { + List models = [ + ModelIndicatorInfo( + modelId: "openai:gpt-3.5-turbo", + modelName: 'GPT-3.5', + description: '速度快,成本低', + icon: Icons.bolt, + activeColor: Colors.green, + ), + ModelIndicatorInfo( + modelId: "openai:gpt-4", + modelName: 'GPT-4', + description: '能力强,更精准', + icon: Icons.auto_awesome, + activeColor: const Color.fromARGB(255, 120, 73, 223), + ), + ]; + + @override + void initState() { + if (Ability().homeModels.isNotEmpty) { + models = Ability() + .homeModels + .map((e) => ModelIndicatorInfo( + modelId: e.modelId, + modelName: e.name, + description: e.desc, + icon: e.powerful ? Icons.auto_awesome : Icons.bolt, + activeColor: stringToColor(e.color), + )) + .toList(); + } + + APIServer().capabilities(cache: false).then((cap) { + Ability().updateCapabilities(cap); + + if (cap.homeModels.isNotEmpty) { + models = cap.homeModels + .map((e) => ModelIndicatorInfo( + modelId: e.modelId, + modelName: e.name, + description: e.desc, + icon: e.powerful ? Icons.auto_awesome : Icons.bolt, + activeColor: stringToColor(e.color), + )) + .toList(); + + if (mounted) { + setState(() {}); + } + } + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + var customColors = Theme.of(context).extension()!; + var reservedModels = models.map((e) => e.modelId).toList(); + + return Scaffold( + appBar: AppBar( + toolbarHeight: CustomSize.toolbarHeight, + title: Text( + AppLocale.customHomeModels.getString(context), + style: const TextStyle(fontSize: CustomSize.appBarTitleSize), + ), + centerTitle: true, + elevation: 0, + ), + backgroundColor: customColors.backgroundContainerColor, + body: BackgroundContainer( + setting: widget.setting, + enabled: false, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + const MessageBox( + message: '用于设置聊一聊中的常用模型。', + type: MessageBoxType.info, + ), + const SizedBox(height: 10), + ColumnBlock( + innerPanding: 5, + padding: + const EdgeInsets.symmetric(horizontal: 15, vertical: 15), + children: [ + for (var i = 0; i < models.length; i++) + GestureDetector( + onTap: () { + openSelectModelDialog( + context, + (selected) { + models[i].modelId = selected.id; + models[i].modelName = + selected.shortName ?? selected.name; + setState(() {}); + }, + initValue: models[i].modelId, + reservedModels: reservedModels, + ); + }, + child: Stack( + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 15), + width: double.infinity, + decoration: BoxDecoration( + color: models[i].activeColor, + borderRadius: BorderRadius.circular(10), + ), + child: ModelIndicator( + model: models[i], + selected: true, + showDescription: false, + ), + ), + Positioned( + right: 0, + top: 0, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + child: Text( + '模型 ${i + 1}', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 10), + EnhancedButton( + title: AppLocale.save.getString(context), + onPressed: () async { + final cancelLoading = BotToast.showCustomLoading( + toastBuilder: (cancel) { + return LoadingIndicator( + message: AppLocale.processingWait.getString(context), + ); + }, + allowClick: false, + duration: const Duration(seconds: 120), + ); + + try { + final selectedModels = + models.map((e) => e.modelId).toList(); + await APIServer() + .updateCustomHomeModels(models: selectedModels); + + APIServer() + .capabilities(cache: false) + .then((value) => Ability().updateCapabilities(value)); + + showSuccessMessage( + // ignore: use_build_context_synchronously + AppLocale.operateSuccess.getString(context)); + } catch (e) { + // ignore: use_build_context_synchronously + showErrorMessageEnhanced(context, e); + } finally { + cancelLoading(); + } + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/page/destroy_account.dart b/lib/page/setting/destroy_account.dart similarity index 96% rename from lib/page/destroy_account.dart rename to lib/page/setting/destroy_account.dart index f916ff4b..87c0f033 100644 --- a/lib/page/destroy_account.dart +++ b/lib/page/setting/destroy_account.dart @@ -6,9 +6,9 @@ import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/message_box.dart'; import 'package:askaide/page/component/verify_code_input.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; diff --git a/lib/page/diagnosis.dart b/lib/page/setting/diagnosis.dart similarity index 95% rename from lib/page/diagnosis.dart rename to lib/page/setting/diagnosis.dart index 90c55ecf..3fad44f6 100644 --- a/lib/page/diagnosis.dart +++ b/lib/page/setting/diagnosis.dart @@ -3,9 +3,9 @@ import 'dart:io'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; diff --git a/lib/page/openai_setting.dart b/lib/page/setting/openai_setting.dart similarity index 97% rename from lib/page/openai_setting.dart rename to lib/page/setting/openai_setting.dart index c245504f..5fdd9cbb 100644 --- a/lib/page/openai_setting.dart +++ b/lib/page/setting/openai_setting.dart @@ -1,3 +1,4 @@ +import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; @@ -6,9 +7,9 @@ import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/message_box.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:dio/dio.dart'; @@ -78,7 +79,7 @@ class _OpenAISettingScreenState extends State { if (widget.source != 'setting') TextButton( onPressed: () { - context.go('/chat-chat'); + context.go(Ability().homeRoute); }, child: Text( '暂不设置', @@ -227,7 +228,7 @@ class _OpenAISettingScreenState extends State { } else { await widget.settings.set(settingOpenAISelfHosted, 'true'); if (context.mounted) { - context.go('/chat-chat'); + context.go(Ability().homeRoute); } } } diff --git a/lib/page/retrieve_password_screen.dart b/lib/page/setting/retrieve_password_screen.dart similarity index 97% rename from lib/page/retrieve_password_screen.dart rename to lib/page/setting/retrieve_password_screen.dart index b14fd5e6..723a2e8b 100644 --- a/lib/page/retrieve_password_screen.dart +++ b/lib/page/setting/retrieve_password_screen.dart @@ -4,9 +4,9 @@ import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/password_field.dart'; import 'package:askaide/page/component/verify_code_input.dart'; -import 'package:askaide/page/dialog.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; +import 'package:askaide/page/component/dialog.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; diff --git a/lib/page/setting_screen.dart b/lib/page/setting/setting_screen.dart similarity index 94% rename from lib/page/setting_screen.dart rename to lib/page/setting/setting_screen.dart index ee171e1f..524695bd 100644 --- a/lib/page/setting_screen.dart +++ b/lib/page/setting/setting_screen.dart @@ -3,23 +3,24 @@ import 'dart:io'; import 'package:askaide/bloc/account_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/cache.dart'; +import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/http.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; -import 'package:askaide/page/account_security.dart'; +import 'package:askaide/page/setting/account_security.dart'; import 'package:askaide/page/component/account_quota_card.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/invite_card.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/sliver_component.dart'; import 'package:askaide/page/component/social_icon.dart'; -import 'package:askaide/page/theme/custom_size.dart'; -import 'package:askaide/page/theme/custom_theme.dart'; -import 'package:askaide/page/theme/theme.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:askaide/page/component/theme/theme.dart'; import 'package:askaide/helper/constant.dart'; -import 'package:askaide/page/dialog.dart'; +import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/cupertino.dart'; @@ -90,6 +91,9 @@ class _SettingScreenState extends State { _buildCommonThemeSetting(customColors), // 语言设置 _buildCommonLanguageSetting(), + // 常用模型 + if (Ability().enableAPIServer()) + _buildCustomHomeModelsSetting(customColors), // OpenAI 自定义配置 if (Ability().enableOpenAI) _buildOpenAISelfHostedSetting(customColors), @@ -125,17 +129,17 @@ class _SettingScreenState extends State { }, ), // 诊断 - SettingsTile( - title: Text(AppLocale.diagnostic.getString(context)), - trailing: Icon( - CupertinoIcons.chevron_forward, - size: MediaQuery.of(context).textScaleFactor * 18, - color: Colors.grey, - ), - onPressed: (context) { - context.push('/diagnosis'); - }, - ), + // SettingsTile( + // title: Text(AppLocale.diagnostic.getString(context)), + // trailing: Icon( + // CupertinoIcons.chevron_forward, + // size: MediaQuery.of(context).textScaleFactor * 18, + // color: Colors.grey, + // ), + // onPressed: (context) { + // context.push('/diagnosis'); + // }, + // ), // 检查更新 if (!PlatformTool.isIOS()) SettingsTile( @@ -249,6 +253,7 @@ class _SettingScreenState extends State { context.push('/lab/draw-board'); }, ), + // SettingsTile( // title: const Text('用户中心'), // trailing: Icon( @@ -557,7 +562,7 @@ class _SettingScreenState extends State { ? AppLocale.enable.getString(context) : AppLocale.disable.getString(context), style: TextStyle( - color: customColors.weakTextColor, + color: customColors.weakTextColor?.withAlpha(200), fontSize: 13, ), ), @@ -567,6 +572,16 @@ class _SettingScreenState extends State { ); } + /// 常用模型 + SettingsTile _buildCustomHomeModelsSetting(CustomColors customColors) { + return SettingsTile.navigation( + title: Text(AppLocale.customHomeModels.getString(context)), + onPressed: (context) { + context.push('/setting/custom-home-models'); + }, + ); + } + SettingsTile _buildServerSelfHostedSetting(CustomColors customColors) { return SettingsTile( title: const Text('自定义服务器'), diff --git a/lib/repo/api/creative.dart b/lib/repo/api/creative.dart index 10d53d93..f6577e75 100644 --- a/lib/repo/api/creative.dart +++ b/lib/repo/api/creative.dart @@ -2,6 +2,25 @@ import 'dart:convert'; import 'package:intl/intl.dart'; +class CreativeGalleryItemResponse { + CreativeGallery item; + bool isInternalUser; + + CreativeGalleryItemResponse(this.item, this.isInternalUser); + + toJson() => { + 'data': item.toJson(), + 'is_internal_user': isInternalUser, + }; + + static CreativeGalleryItemResponse fromJson(Map json) { + return CreativeGalleryItemResponse( + CreativeGallery.fromJson(json['data']), + json['is_internal_user'] ?? false, + ); + } +} + class CreativeGallery { int id; int? userId; @@ -16,6 +35,7 @@ class CreativeGallery { int refCount; int starLevel; int hotValue; + int status; DateTime? createdAt; DateTime? updatedAt; @@ -33,6 +53,7 @@ class CreativeGallery { this.refCount = 0, this.starLevel = 0, this.hotValue = 0, + this.status = 0, this.createdAt, this.updatedAt, }); @@ -72,6 +93,7 @@ class CreativeGallery { 'ref_count': refCount, 'star_level': starLevel, 'hot_value': hotValue, + 'status': status, 'created_at': createdAt?.toIso8601String(), 'updated_at': updatedAt?.toIso8601String(), }; @@ -91,6 +113,7 @@ class CreativeGallery { refCount: json['ref_count'] ?? 0, starLevel: json['star_level'] ?? 0, hotValue: json['hot_value'] ?? 0, + status: json['status'] ?? 0, createdAt: json['created_at'] != null ? DateTime.parse(json['created_at']) : null, diff --git a/lib/repo/api/info.dart b/lib/repo/api/info.dart index bb89d46b..33aba414 100644 --- a/lib/repo/api/info.dart +++ b/lib/repo/api/info.dart @@ -18,6 +18,27 @@ class Capabilities { /// 首页显示的模型信息 final List homeModels; + /// 是否显示首页模型描述 + final bool showHomeModelDescription; + + /// 首页路由 + final String homeRoute; + + /// 是否支持 Websocket + final bool supportWebsocket; + + /// 是否显示绘玩 + final bool disableGallery; + + /// 是否支持创作岛 + final bool disableCreationIsland; + + /// 是否禁用数字人 + final bool disableDigitalHuman; + + /// 是否禁用聊天 + final bool disableChat; + Capabilities({ required this.applePayEnabled, required this.alipayEnabled, @@ -25,6 +46,13 @@ class Capabilities { required this.mailEnabled, required this.openaiEnabled, required this.homeModels, + this.homeRoute = '/chat-chat', + this.showHomeModelDescription = true, + this.supportWebsocket = false, + this.disableGallery = false, + this.disableCreationIsland = false, + this.disableDigitalHuman = false, + this.disableChat = false, }); factory Capabilities.fromJson(Map json) { @@ -37,6 +65,13 @@ class Capabilities { homeModels: ((json['home_models'] ?? []) as List) .map((e) => HomeModel.fromJson(e)) .toList(), + homeRoute: json['home_route'] ?? '/chat-chat', + showHomeModelDescription: json['show_home_model_description'] ?? true, + supportWebsocket: json['support_websocket'] ?? false, + disableGallery: json['disable_gallery'] ?? false, + disableCreationIsland: json['disable_creation_island'] ?? false, + disableDigitalHuman: json['disable_digital_human'] ?? false, + disableChat: json['disable_chat'] ?? false, ); } @@ -48,6 +83,13 @@ class Capabilities { 'mail_enabled': mailEnabled, 'openai_enabled': openaiEnabled, 'home_models': homeModels.map((e) => e.toJson()).toList(), + 'home_route': homeRoute, + 'show_home_model_description': showHomeModelDescription, + 'support_websocket': supportWebsocket, + 'disable_gallery': disableGallery, + 'disable_creation_island': disableCreationIsland, + 'disable_digital_human': disableDigitalHuman, + 'disable_chat': disableChat, }; } } diff --git a/lib/repo/api/page.dart b/lib/repo/api/page.dart index 4ebbf57d..e90d3290 100644 --- a/lib/repo/api/page.dart +++ b/lib/repo/api/page.dart @@ -13,3 +13,17 @@ class PagedData { required this.data, }); } + +class OffsetPageData { + int startId; + int lastId; + int perPage; + List data; + + OffsetPageData({ + required this.startId, + required this.lastId, + required this.perPage, + required this.data, + }); +} diff --git a/lib/repo/api/payment.dart b/lib/repo/api/payment.dart index 65706dd2..fb57fce5 100644 --- a/lib/repo/api/payment.dart +++ b/lib/repo/api/payment.dart @@ -1,18 +1,21 @@ class AlipayCreatedReponse { String params; String paymentId; + bool sandbox; - AlipayCreatedReponse(this.params, this.paymentId); + AlipayCreatedReponse(this.params, this.paymentId, {this.sandbox = false}); toJson() => { 'params': params, 'payment_id': paymentId, + 'sandbox': sandbox, }; static AlipayCreatedReponse fromJson(Map json) { return AlipayCreatedReponse( json['params'], json['payment_id'], + sandbox: json['sandbox'] ?? false, ); } } diff --git a/lib/repo/api_server.dart b/lib/repo/api_server.dart index 92d0f2c1..489fe680 100644 --- a/lib/repo/api_server.dart +++ b/lib/repo/api_server.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:askaide/helper/constant.dart'; +import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/error.dart'; import 'package:askaide/helper/http.dart'; import 'package:askaide/helper/logger.dart'; @@ -13,6 +14,8 @@ import 'package:askaide/repo/api/payment.dart'; import 'package:askaide/repo/api/quota.dart'; import 'package:askaide/repo/api/room_gallery.dart'; import 'package:askaide/repo/api/user.dart'; +import 'package:askaide/repo/model/group.dart'; +import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; @@ -57,14 +60,16 @@ class APIServer { ]; /// 异常处理 - Object _exceptionHandle(Object e) { - Logger.instance.e(e); + Object _exceptionHandle(Object e, Object? stackTrace) { + Logger.instance.e(e, stackTrace: stackTrace as StackTrace?); if (e is DioError) { if (e.response != null) { final resp = e.response!; - if (resp.data is Map && resp.data['error'] != null) { + if (resp.data is Map && + resp.data['error'] != null && + resp.statusCode != 402) { return resp.data['error'] ?? e.toString(); } @@ -187,6 +192,25 @@ class APIServer { ); } + Future sendPostJSONRequest( + String endpoint, + T Function(dynamic) parser, { + Map? queryParameters, + Map? data, + VoidCallback? finallyCallback, + }) async { + return request( + HttpClient.postJSON( + '$url$endpoint', + queryParameters: queryParameters, + data: data, + options: _buildRequestOptions(), + ), + parser, + finallyCallback: finallyCallback, + ); + } + Future sendPutRequest( String endpoint, T Function(dynamic) parser, { @@ -209,6 +233,28 @@ class APIServer { ); } + Future sendPutJSONRequest( + String endpoint, + T Function(dynamic) parser, { + String? subKey, + Duration duration = const Duration(days: 1), + Map? queryParameters, + Map? data, + bool forceRefresh = false, + VoidCallback? finallyCallback, + }) async { + return request( + HttpClient.putJSON( + '$url$endpoint', + queryParameters: queryParameters, + data: data, + options: _buildRequestOptions(), + ), + parser, + finallyCallback: finallyCallback, + ); + } + Future sendDeleteRequest( String endpoint, T Function(dynamic) parser, { @@ -245,8 +291,8 @@ class APIServer { // Logger.instance.d("API Response: ${resp.data}"); return parser(resp); - } catch (e) { - return Future.error(_exceptionHandle(e)); + } catch (e, stackTrace) { + return Future.error(_exceptionHandle(e, stackTrace)); } finally { finallyCallback?.call(); } @@ -1017,6 +1063,55 @@ class APIServer { ); } + /// 创建群聊房间 + Future createGroupRoom({ + required String name, + String? description, + String? avatarUrl, + List? members, + }) async { + return sendPostJSONRequest( + '/v1/group-chat', + (resp) => resp.data["group_id"], + data: { + 'name': name, + 'avatar_url': avatarUrl, + 'members': members?.map((e) => e.toJson()).toList(), + }, + finallyCallback: () { + HttpClient.cacheManager + .deleteByPrimaryKey('$url/v2/rooms', requestMethod: 'GET'); + }, + ); + } + + /// 更新群聊房间 + Future updateGroupRoom({ + required int groupId, + required String name, + String? description, + String? avatarUrl, + List? members, + }) async { + return sendPutJSONRequest( + '/v1/group-chat/$groupId', + (resp) {}, + data: { + 'name': name, + 'avatar_url': avatarUrl, + 'members': members?.map((e) => e.toJson()).toList(), + }, + finallyCallback: () { + HttpClient.cacheManager + .deleteByPrimaryKey('$url/v2/rooms', requestMethod: 'GET'); + + HttpClient.cacheManager.deleteByPrimaryKey( + '$url/v1/group-chat/$groupId', + requestMethod: 'GET'); + }, + ); + } + /// 创建房间 Future createRoom({ required String name, @@ -1279,6 +1374,22 @@ class APIServer { ); } + /// 获取用户智慧果消耗历史记录详情 + Future> quotaUsedDetails( + {required String date}) async { + return sendGetRequest( + '/v1/users/quota/usage-stat/$date', + (resp) { + var res = []; + for (var item in resp.data['data']) { + res.add(QuotaUsageDetailInDay.fromJson(item)); + } + + return res; + }, + ); + } + Future> creativeGallery({ bool cache = true, int page = 1, @@ -1309,13 +1420,13 @@ class APIServer { ); } - Future creativeGalleryItem({ + Future creativeGalleryItem({ required int id, bool cache = true, }) async { return sendCachedGetRequest( '/v1/creatives/gallery/$id', - (resp) => CreativeGallery.fromJson(resp.data), + (resp) => CreativeGalleryItemResponse.fromJson(resp.data), forceRefresh: !cache, duration: const Duration(minutes: 30), ); @@ -1444,11 +1555,11 @@ class APIServer { } /// 服务器支持的能力 - Future capabilities() async { - return sendGetRequest( + Future capabilities({bool cache = true}) async { + return sendCachedGetRequest( '/public/info/capabilities', (resp) => Capabilities.fromJson(resp.data), - requestTimeout: 5000, + forceRefresh: !cache, ); } @@ -1499,635 +1610,130 @@ class APIServer { forceRefresh: !cache, ); } -} - -enum PromotionEventClickButtonType { - none, - url, - inAppRoute; - - static PromotionEventClickButtonType fromName(String typeName) { - switch (typeName) { - case 'url': - return PromotionEventClickButtonType.url; - case 'in_app_route': - return PromotionEventClickButtonType.inAppRoute; - default: - return PromotionEventClickButtonType.none; - } - } - String toName() { - switch (this) { - case PromotionEventClickButtonType.url: - return 'url'; - case PromotionEventClickButtonType.inAppRoute: - return 'in_app_route'; - default: - return 'none'; - } - } -} - -class PromotionEvent { - String? title; - String content; - PromotionEventClickButtonType clickButtonType; - String? clickValue; - String? clickButtonColor; - String? backgroundImage; - String? textColor; - bool closeable; - int? maxCloseDurationInDays; - - PromotionEvent({ - this.title, - required this.content, - required this.clickButtonType, - this.clickValue, - this.clickButtonColor, - this.backgroundImage, - this.textColor, - required this.closeable, - this.maxCloseDurationInDays, - }); - - toJson() => { - 'title': title, - 'content': content, - 'click_button_type': clickButtonType.toName(), - 'click_value': clickValue, - 'click_button_color': clickButtonColor, - 'background_image': backgroundImage, - 'text_color': textColor, - 'closeable': closeable, - 'max_close_duration_in_days': maxCloseDurationInDays, - }; - - static PromotionEvent fromJson(Map json) { - return PromotionEvent( - title: json['title'], - content: json['content'], - clickButtonType: PromotionEventClickButtonType.fromName( - json['click_button_type'] ?? ''), - clickValue: json['click_value'], - clickButtonColor: json['click_button_color'], - backgroundImage: json['background_image'], - textColor: json['text_color'], - closeable: json['closeable'] ?? false, - maxCloseDurationInDays: json['max_close_duration_in_days'], + /// 更新自定义模型 + Future updateCustomHomeModels({required List models}) async { + return sendPostRequest( + '/v1/users/custom/home-models', + (value) => {}, + formData: { + 'models': models.join(','), + }, ); } -} -class ShareInfo { - String qrCode; - String message; - String? inviteCode; + /// 群聊 //////////////////////////////////////////////////////////////////// - ShareInfo({ - required this.qrCode, - required this.message, - this.inviteCode, - }); - - toJson() => { - 'qr_code': qrCode, - 'message': message, - 'invite_code': inviteCode, - }; + /// 群组列表 + Future> chatGroups({bool cache = true}) async { + return sendCachedGetRequest( + '/v1/group-chat', + (value) { + var res = []; + for (var item in value.data['data']) { + res.add(RoomInServer.fromJson(item)); + } - static ShareInfo fromJson(Map json) { - return ShareInfo( - qrCode: json['qr_code'], - message: json['message'], - inviteCode: json['invite_code'], + return res; + }, + forceRefresh: !cache, ); } -} - -class QuotaUsageInDay { - String date; - int used; - QuotaUsageInDay({ - required this.date, - required this.used, - }); - - toJson() => { - 'date': date, - 'used': used, - }; - - static QuotaUsageInDay fromJson(Map json) { - return QuotaUsageInDay( - date: json['date'], - used: json['used'], + /// 群组详情 + Future chatGroup(int groupId, {bool cache = true}) async { + return sendCachedGetRequest( + '/v1/group-chat/$groupId', + (value) => ChatGroup.fromJson(value.data), + forceRefresh: !cache, ); } -} - -class RoomsResponse { - List rooms; - List? suggests; - - RoomsResponse({ - required this.rooms, - this.suggests, - }); - - toJson() => { - 'rooms': rooms, - 'suggests': suggests, - }; - - static RoomsResponse fromJson(Map json) { - var rooms = []; - for (var item in json['data'] ?? []) { - rooms.add(RoomInServer.fromJson(item)); - } - var suggests = []; - for (var item in json['suggests'] ?? []) { - suggests.add(RoomGallery.fromJson(item)); - } + /// 群组聊天消息列表 + Future> chatGroupMessages( + int groupId, { + int startId = 0, + int? perPage, + bool cache = true, + }) async { + return sendCachedGetRequest( + '/v1/group-chat/$groupId/messages', + (resp) { + var res = []; + for (var item in resp.data['data']) { + res.add(GroupMessage.fromJson(item)); + } - return RoomsResponse( - rooms: rooms, - suggests: suggests, + return OffsetPageData( + data: res, + lastId: resp.data['last_id'], + startId: resp.data['start_id'], + perPage: resp.data['per_page'], + ); + }, + queryParameters: { + 'start_id': startId, + 'per_page': perPage, + }, + forceRefresh: !cache, ); } -} -class RoomInServer { - int id; - int userId; - int avatarId; - String? avatarUrl; - String name; - String? description; - int? priority; - String model; - String vendor; - String? systemPrompt; - String? initMessage; - int maxContext; - int? maxTokens; - DateTime? lastActiveTime; - DateTime? createdAt; - DateTime? updatedAt; - - RoomInServer({ - required this.id, - required this.userId, - required this.avatarId, - required this.name, - required this.maxContext, - this.avatarUrl, - this.description, - this.priority, - required this.model, - required this.vendor, - this.systemPrompt, - this.initMessage, - this.lastActiveTime, - this.createdAt, - this.updatedAt, - this.maxTokens, - }); - - toJson() => { - 'id': id, - 'user_id': userId, - 'avatar_id': avatarId, - 'avatar_url': avatarUrl, - 'name': name, - 'description': description, - 'priority': priority, - 'model': model, - 'vendor': vendor, - 'init_message': initMessage, - 'max_context': maxContext, - 'max_tokens': maxTokens, - 'system_prompt': systemPrompt, - 'last_active_time': lastActiveTime?.toIso8601String(), - 'created_at': createdAt?.toIso8601String(), - 'updated_at': updatedAt?.toIso8601String(), - }; - - static RoomInServer fromJson(Map json) { - return RoomInServer( - id: json['id'], - userId: json['user_id'], - avatarId: json['avatar_id'] ?? 0, - avatarUrl: json['avatar_url'], - name: json['name'], - description: json['description'], - priority: json['priority'], - model: json['model'], - vendor: json['vendor'], - systemPrompt: json['system_prompt'], - initMessage: json['init_message'], - maxContext: json['max_context'] ?? 10, - maxTokens: json['max_tokens'], - lastActiveTime: json['last_active_time'] != null - ? DateTime.parse(json['last_active_time']) - : null, - createdAt: - json['CreatedAt'] != null ? DateTime.parse(json['CreatedAt']) : null, - updatedAt: - json['UpdatedAt'] != null ? DateTime.parse(json['UpdatedAt']) : null, + /// 发起群聊消息 + Future chatGroupSendMessage( + int groupId, GroupChatSendRequest req) async { + return sendPostJSONRequest( + '/v1/group-chat/$groupId/chat', + (resp) { + return GroupChatSendResponse.fromJson(resp.data); + }, + data: req.toJson(), ); } -} -class VersionCheckResp { - bool hasUpdate; - String serverVersion; - bool forceUpdate; - String url; - String message; - - VersionCheckResp({ - required this.hasUpdate, - required this.serverVersion, - required this.forceUpdate, - required this.url, - required this.message, - }); - - toJson() => { - 'has_update': hasUpdate, - 'server_version': serverVersion, - 'force_update': forceUpdate, - 'url': url, + /// 群聊发送系统消息 + Future chatGroupSendSystemMessage( + int groupId, { + required String messageType, + String? message, + }) async { + return sendPostRequest( + '/v1/group-chat/$groupId/chat-system', + (resp) => GroupMessage.fromJson(resp['data']), + formData: { + 'message_type': messageType, 'message': message, - }; - - static VersionCheckResp fromJson(Map json) { - return VersionCheckResp( - hasUpdate: json['has_update'] ?? false, - serverVersion: json['server_version'], - forceUpdate: json['force_update'] ?? false, - url: json['url'], - message: json['message'], - ); - } -} - -class SignInResp { - int id; - String name; - String? email; - String? phone; - String token; - bool isNewUser; - int reward; - - SignInResp({ - required this.id, - required this.name, - this.email, - required this.token, - this.phone, - this.isNewUser = false, - this.reward = 0, - }); - - toJson() => { - 'id': id, - 'name': name, - 'email': email, - 'phone': phone, - 'token': token, - 'is_new_user': isNewUser, - 'reward': reward, - }; - - bool get needBindPhone => phone == null || phone!.isEmpty; - - static SignInResp fromJson(Map json) { - return SignInResp( - id: json['id'], - name: json['name'], - email: json['email'], - phone: json['phone'], - token: json['token'], - isNewUser: json['is_new_user'] ?? false, - reward: json['reward'] ?? 0, - ); - } -} - -class AsyncTaskResp { - String status; - List? errors; - List? resources; - String? originImage; - - AsyncTaskResp(this.status, {this.errors, this.resources, this.originImage}); - - toJson() => { - 'status': status, - 'errors': errors, - 'resources': resources, - 'origin_image': originImage, - }; - - static AsyncTaskResp fromJson(Map json) { - return AsyncTaskResp( - json['status'], - errors: json['errors'] != null - ? (json['errors'] as List).map((e) => e.toString()).toList() - : null, - resources: json['resources'] != null - ? (json['resources'] as List) - .map((e) => e.toString()) - .toList() - : null, - originImage: json['origin_image'], - ); - } -} - -class Prompt { - String title; - String content; - - Prompt(this.title, this.content); - - toJson() { - return { - 'title': title, - 'content': content, - }; - } - - fromJson(Map json) { - title = json['title']; - content = json['content']; - } -} - -class ChatExample { - String title; - String? content; - List models; - List tags; - - ChatExample( - this.title, { - this.content, - this.models = const [], - this.tags = const [], - }); - - get text => content ?? title; - - toJson() => { - 'title': title, - 'content': content, - 'models': models, - 'tags': tags, - }; - - fromJson(Map json) { - title = json['title']; - content = json['content']; - models = json['models']; - tags = json['tags']; - } -} - -class TranslateText { - String? result; - String? speakUrl; - - TranslateText(this.result, this.speakUrl); - - toJson() => { - 'result': result, - 'speak_url': speakUrl, - }; - - static fromJson(Map json) { - return TranslateText(json['result'], json['speak_url']); - } -} - -class UploadInitResponse { - String bucket; - String key; - String token; - String url; - - UploadInitResponse(this.key, this.bucket, this.token, this.url); - - toJson() => { - 'bucket': bucket, - 'key': key, - 'token': token, - 'url': url, - }; - - static fromJson(Map json) { - return UploadInitResponse( - json['key'], - json['bucket'], - json['token'], - json['url'], - ); - } -} - -class ModelStyle { - String id; - String name; - String? preview; - - ModelStyle({required this.id, required this.name, this.preview}); - - toJson() => { - 'id': id, - 'name': name, - 'preview': preview, - }; - - static ModelStyle fromJson(Map json) { - return ModelStyle( - id: json['id'], - name: json['name'], - preview: json['preview'], - ); - } -} - -class Model { - String id; - String name; - String? description; - String category; - bool isChat; - bool isImage; - bool disabled; - String? tag; - - Model({ - required this.id, - required this.name, - required this.category, - required this.isChat, - required this.isImage, - this.description, - this.disabled = false, - this.tag, - }); - - toJson() => { - 'id': id, - 'name': name, - 'description': description, - 'category': category, - 'is_chat': isChat, - 'is_image': isImage, - 'disabled': disabled, - 'tag': tag, - }; - - static Model fromJson(Map json) { - return Model( - id: json['id'], - name: json['name'], - description: json['description'], - category: json['category'], - isChat: json['is_chat'], - isImage: json['is_image'], - disabled: json['disabled'] ?? false, - tag: json['tag'], - ); - } -} - -class BackgroundImage { - String url; - String preview; - - BackgroundImage(this.url, this.preview); - - toJson() => { - 'url': url, - 'preview': preview, - }; - - static BackgroundImage fromJson(Map json) { - return BackgroundImage( - json['url'], - json['preview'], - ); - } -} - -class UserExistenceResp { - bool exist; - String signInMethod; - - UserExistenceResp(this.exist, this.signInMethod); - - toJson() => { - 'exist': exist, - 'sign_in_method': signInMethod, - }; - - static UserExistenceResp fromJson(Map json) { - return UserExistenceResp( - json['exist'], - json['sign_in_method'], + }, ); } -} - -class PromptCategory { - String name; - List children; - List tags; - PromptCategory(this.name, this.children, this.tags); - - toJson() => { - 'name': name, - 'children': children, - 'tags': tags, - }; - - static PromptCategory fromJson(Map json) { - var children = []; - for (var item in json['children'] ?? []) { - children.add(PromptCategory.fromJson(item)); - } - - var tags = []; - for (var item in json['tags'] ?? []) { - tags.add(PromptTag.fromJson(item)); - } + /// 群组聊天消息状态 + Future> chatGroupMessageStatus( + int groupId, List messageIds) async { + return sendGetRequest( + '/v1/group-chat/$groupId/chat-messages', + (resp) { + var res = []; + for (var item in resp.data['data']) { + res.add(GroupMessage.fromJson(item)); + } - return PromptCategory( - json['name'], - children, - tags, + return res; + }, + queryParameters: { + "message_ids": messageIds.join(','), + }, ); } -} - -class PromptTag { - String name; - String value; - - PromptTag(this.name, this.value); - - toJson() => { - 'name': name, - 'value': value, - }; - static PromptTag fromJson(Map json) { - return PromptTag( - json['name'], - json['value'], - ); + /// 清空群组聊天消息 + Future chatGroupDeleteAllMessages(int groupId) async { + return sendDeleteRequest('/v1/group-chat/$groupId/all-chat', (resp) {}); } -} -class FreeModelCount { - String model; - String name; - int leftCount; - int maxCount; - String? info; - - FreeModelCount({ - required this.model, - required this.name, - required this.leftCount, - required this.maxCount, - this.info, - }); - - toJson() => { - 'model': model, - 'name': name, - 'left_count': leftCount, - 'max_count': maxCount, - 'info': info, - }; - - static FreeModelCount fromJson(Map json) { - return FreeModelCount( - model: json['model'], - name: json['name'] ?? json['model'], - leftCount: json['left_count'] ?? 0, - maxCount: json['max_count'] ?? 0, - info: json['info'], - ); + /// 删除群组聊天消息 + Future chatGroupDeleteMessage(int groupId, int messageId) async { + return sendDeleteRequest( + '/v1/group-chat/$groupId/chat/$messageId', (resp) {}); } } diff --git a/lib/repo/chat_message_repo.dart b/lib/repo/chat_message_repo.dart index 50ae13ae..8adbd9a6 100644 --- a/lib/repo/chat_message_repo.dart +++ b/lib/repo/chat_message_repo.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'package:askaide/helper/constant.dart'; -import 'package:askaide/lang/lang.dart'; import 'package:askaide/repo/data/chat_history.dart'; import 'package:askaide/repo/data/room_data.dart'; import 'package:askaide/repo/model/chat_history.dart'; @@ -150,11 +149,13 @@ class ChatMessageRepository { int roomId, int count, { int? userId, + int? offset, }) async { return await _chatHistoryProvider.getChatHistories( roomId, count, userId: userId, + offset: offset, ); } diff --git a/lib/repo/data/chat_history.dart b/lib/repo/data/chat_history.dart index adb48ab3..053fd2fa 100644 --- a/lib/repo/data/chat_history.dart +++ b/lib/repo/data/chat_history.dart @@ -6,7 +6,7 @@ class ChatHistoryProvider { ChatHistoryProvider(this.conn); Future> getChatHistories(int roomId, int count, - {int? userId}) async { + {int? userId, int? offset}) async { final userConditon = userId == null ? ' AND user_id IS NULL' : ' AND user_id = $userId'; @@ -16,6 +16,7 @@ class ChatHistoryProvider { whereArgs: [roomId], orderBy: 'updated_at DESC', limit: count, + offset: offset, ); return histories.map((e) => ChatHistory.fromMap(e)).toList(); diff --git a/lib/repo/data/room_data.dart b/lib/repo/data/room_data.dart index a9b10fed..7d90cd45 100644 --- a/lib/repo/data/room_data.dart +++ b/lib/repo/data/room_data.dart @@ -41,6 +41,7 @@ class RoomDataProvider { maxContext: maxContext ?? 10, createdAt: DateTime.now(), lastActiveTime: DateTime.now(), + iconData: '57683,MaterialIcons', ); room.id = await conn.insert('chat_room', room.toJson()); diff --git a/lib/repo/deepai_repo.dart b/lib/repo/deepai_repo.dart index 0af3d6c2..8439e9fa 100644 --- a/lib/repo/deepai_repo.dart +++ b/lib/repo/deepai_repo.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:askaide/helper/constant.dart'; +import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/repo/data/settings_data.dart'; import 'package:http/http.dart' as http; diff --git a/lib/repo/model/group.dart b/lib/repo/model/group.dart new file mode 100644 index 00000000..4ac6b7b7 --- /dev/null +++ b/lib/repo/model/group.dart @@ -0,0 +1,241 @@ +import 'package:askaide/repo/model/misc.dart'; + +const groupMessageStatusWaiting = 0; +const groupMessageStatusSuccess = 1; +const groupMessageStatusFailed = 2; + +class ChatGroup { + final RoomInServer group; + final List members; + + GroupMember? findMember(int memberId) { + return members.where((member) => member.id == memberId).firstOrNull; + } + + ChatGroup({ + required this.group, + required this.members, + }); + + factory ChatGroup.fromJson(Map json) { + return ChatGroup( + group: RoomInServer.fromJson(json['group']), + members: (json['members'] as List) + .map((member) => GroupMember.fromJson(member)) + .toList(), + ); + } + + Map toJson() { + return { + 'group': group.toJson(), + 'members': members.map((member) => member.toJson()).toList(), + }; + } +} + +class GroupMember { + final int? id; + final String modelId; + final String modelName; + final String? avatarUrl; + final int? status; + + GroupMember({ + this.id, + required this.modelId, + required this.modelName, + this.avatarUrl, + this.status, + }); + + factory GroupMember.fromJson(Map json) { + return GroupMember( + id: json['id'], + modelId: json['model_id'], + modelName: json['model_name'], + avatarUrl: json['avatar_url'], + status: json['status'], + ); + } + + Map toJson() { + return { + 'id': id, + 'model_id': modelId, + 'model_name': modelName, + 'avatar_url': avatarUrl, + 'status': status, + }; + } +} + +class GroupMessage { + final int id; + final String message; + final String role; + final String type; + final int groupId; + final int? tokenConsumed; + final int? quotaConsumed; + final int? pid; + final int? memberId; + final int status; + DateTime? createdAt; + DateTime? updatedAt; + + GroupMessage({ + required this.id, + required this.message, + required this.role, + required this.status, + required this.type, + required this.groupId, + this.tokenConsumed, + this.quotaConsumed, + this.pid, + this.memberId, + this.createdAt, + this.updatedAt, + }); + + factory GroupMessage.fromJson(Map json) { + return GroupMessage( + id: json['id'], + message: json['message'] ?? '', + role: json['role'] == 1 ? 'user' : 'assistant', + type: json['type'] ?? 'text', + groupId: json['group_id'], + tokenConsumed: json['token_consumed'], + quotaConsumed: json['quota_consumed'], + pid: json['pid'], + memberId: json['member_id'], + status: json['status'] ?? 0, + createdAt: DateTime.tryParse(json['CreatedAt']), + updatedAt: DateTime.tryParse(json['UpdatedAt']), + ); + } + + Map toJson() { + return { + 'id': id, + 'message': message, + 'role': role, + 'type': type, + 'group_id': groupId, + 'token_consumed': tokenConsumed, + 'quota_consumed': quotaConsumed, + 'pid': pid, + 'member_id': memberId, + 'status': status, + 'CreatedAt': createdAt?.toIso8601String(), + 'UpdatedAt': updatedAt?.toIso8601String(), + }; + } +} + +class GroupChatSendRequestMessage { + final String role; + final String content; + final int? memberId; + + GroupChatSendRequestMessage({ + required this.role, + required this.content, + this.memberId, + }); + + Map toJson() { + return { + 'role': role, + 'content': content, + 'member_id': memberId, + }; + } + + factory GroupChatSendRequestMessage.fromJson(Map json) { + return GroupChatSendRequestMessage( + role: json['role'], + content: json['content'], + memberId: json['member_id'], + ); + } +} + +class GroupChatSendRequest { + final String message; + final List memberIds; + + GroupChatSendRequest({ + required this.message, + required this.memberIds, + }); + + Map toJson() { + return { + 'message': message, + 'member_ids': memberIds, + }; + } + + factory GroupChatSendRequest.fromJson(Map json) { + return GroupChatSendRequest( + message: json['message'], + memberIds: (json['member_ids'] as List).map((e) => e as int).toList(), + ); + } +} + +class GroupChatSendResponseTask { + final int memberId; + final String taskId; + final int answerId; + + GroupChatSendResponseTask({ + required this.memberId, + required this.taskId, + required this.answerId, + }); + + factory GroupChatSendResponseTask.fromJson(Map json) { + return GroupChatSendResponseTask( + memberId: json['member_id'], + taskId: json['task_id'], + answerId: json['answer_id'], + ); + } + + Map toJson() { + return { + 'member_id': memberId, + 'task_id': taskId, + 'answer_id': answerId, + }; + } +} + +class GroupChatSendResponse { + final int questionId; + final List tasks; + + GroupChatSendResponse({ + required this.questionId, + required this.tasks, + }); + + factory GroupChatSendResponse.fromJson(Map json) { + return GroupChatSendResponse( + questionId: json['question_id'], + tasks: (json['tasks'] as List) + .map((task) => GroupChatSendResponseTask.fromJson(task)) + .toList(), + ); + } + + Map toJson() { + return { + 'question_id': questionId, + 'tasks': tasks.map((task) => task.toJson()).toList(), + }; + } +} diff --git a/lib/repo/model/message.dart b/lib/repo/model/message.dart index 00f8fb87..7b5d175e 100644 --- a/lib/repo/model/message.dart +++ b/lib/repo/model/message.dart @@ -55,6 +55,12 @@ class Message { /// 是否当前消息已就绪,不需要持久化 bool isReady = true; + /// 消息发送者的头像,不需要持久化 + String? avatarUrl; + + /// 消息发送者的名称,不需要持久化 + String? senderName; + Message( this.role, this.text, { @@ -72,6 +78,8 @@ class Message { this.status = 1, this.quotaConsumed, this.tokenConsumed, + this.avatarUrl, + this.senderName, }); /// 获取消息附加信息 @@ -175,8 +183,12 @@ enum Role { switch (value) { case 'receiver': return Role.receiver; + case 'assistant': + return Role.receiver; case 'sender': return Role.sender; + case 'user': + return Role.sender; default: return Role.receiver; } diff --git a/lib/repo/model/misc.dart b/lib/repo/model/misc.dart new file mode 100644 index 00000000..7ac5dae9 --- /dev/null +++ b/lib/repo/model/misc.dart @@ -0,0 +1,682 @@ +import 'package:askaide/repo/api/room_gallery.dart'; + +enum PromotionEventClickButtonType { + none, + url, + inAppRoute; + + static PromotionEventClickButtonType fromName(String typeName) { + switch (typeName) { + case 'url': + return PromotionEventClickButtonType.url; + case 'in_app_route': + return PromotionEventClickButtonType.inAppRoute; + default: + return PromotionEventClickButtonType.none; + } + } + + String toName() { + switch (this) { + case PromotionEventClickButtonType.url: + return 'url'; + case PromotionEventClickButtonType.inAppRoute: + return 'in_app_route'; + default: + return 'none'; + } + } +} + +class PromotionEvent { + String? title; + String content; + PromotionEventClickButtonType clickButtonType; + String? clickValue; + String? clickButtonColor; + String? backgroundImage; + String? textColor; + bool closeable; + int? maxCloseDurationInDays; + + PromotionEvent({ + this.title, + required this.content, + required this.clickButtonType, + this.clickValue, + this.clickButtonColor, + this.backgroundImage, + this.textColor, + required this.closeable, + this.maxCloseDurationInDays, + }); + + toJson() => { + 'title': title, + 'content': content, + 'click_button_type': clickButtonType.toName(), + 'click_value': clickValue, + 'click_button_color': clickButtonColor, + 'background_image': backgroundImage, + 'text_color': textColor, + 'closeable': closeable, + 'max_close_duration_in_days': maxCloseDurationInDays, + }; + + static PromotionEvent fromJson(Map json) { + return PromotionEvent( + title: json['title'], + content: json['content'], + clickButtonType: PromotionEventClickButtonType.fromName( + json['click_button_type'] ?? ''), + clickValue: json['click_value'], + clickButtonColor: json['click_button_color'], + backgroundImage: json['background_image'], + textColor: json['text_color'], + closeable: json['closeable'] ?? false, + maxCloseDurationInDays: json['max_close_duration_in_days'], + ); + } +} + +class ShareInfo { + String qrCode; + String message; + String? inviteCode; + + ShareInfo({ + required this.qrCode, + required this.message, + this.inviteCode, + }); + + toJson() => { + 'qr_code': qrCode, + 'message': message, + 'invite_code': inviteCode, + }; + + static ShareInfo fromJson(Map json) { + return ShareInfo( + qrCode: json['qr_code'], + message: json['message'], + inviteCode: json['invite_code'], + ); + } +} + +class QuotaUsageInDay { + String date; + int used; + + QuotaUsageInDay({ + required this.date, + required this.used, + }); + + toJson() => { + 'date': date, + 'used': used, + }; + + static QuotaUsageInDay fromJson(Map json) { + return QuotaUsageInDay( + date: json['date'], + used: json['used'], + ); + } +} + +class QuotaUsageDetailInDay { + int used; + String type; + String createdAt; + + QuotaUsageDetailInDay({ + required this.used, + required this.type, + required this.createdAt, + }); + + toJson() => { + 'used': used, + 'type': type, + 'created_at': createdAt, + }; + + static QuotaUsageDetailInDay fromJson(Map json) { + return QuotaUsageDetailInDay( + used: json['used'], + type: json['type'], + createdAt: json['created_at'], + ); + } +} + +class RoomsResponse { + List rooms; + List? suggests; + + RoomsResponse({ + required this.rooms, + this.suggests, + }); + + toJson() => { + 'rooms': rooms, + 'suggests': suggests, + }; + + static RoomsResponse fromJson(Map json) { + var rooms = []; + for (var item in json['data'] ?? []) { + rooms.add(RoomInServer.fromJson(item)); + } + + var suggests = []; + for (var item in json['suggests'] ?? []) { + suggests.add(RoomGallery.fromJson(item)); + } + + return RoomsResponse( + rooms: rooms, + suggests: suggests, + ); + } +} + +class RoomInServer { + int id; + int userId; + int avatarId; + String? avatarUrl; + String name; + String? description; + int? priority; + String model; + String vendor; + String? systemPrompt; + String? initMessage; + int maxContext; + int? maxTokens; + int? roomType; + DateTime? lastActiveTime; + DateTime? createdAt; + DateTime? updatedAt; + + List members; + + RoomInServer({ + required this.id, + required this.userId, + required this.avatarId, + required this.name, + required this.maxContext, + this.roomType, + this.avatarUrl, + this.description, + this.priority, + required this.model, + required this.vendor, + this.systemPrompt, + this.initMessage, + this.lastActiveTime, + this.createdAt, + this.updatedAt, + this.maxTokens, + this.members = const [], + }); + + toJson() => { + 'id': id, + 'user_id': userId, + 'avatar_id': avatarId, + 'avatar_url': avatarUrl, + 'name': name, + 'description': description, + 'priority': priority, + 'model': model, + 'vendor': vendor, + 'init_message': initMessage, + 'max_context': maxContext, + 'room_type': roomType, + 'max_tokens': maxTokens, + 'system_prompt': systemPrompt, + 'last_active_time': lastActiveTime?.toIso8601String(), + 'created_at': createdAt?.toIso8601String(), + 'updated_at': updatedAt?.toIso8601String(), + 'members': members, + }; + + static RoomInServer fromJson(Map json) { + return RoomInServer( + id: json['id'], + userId: json['user_id'], + avatarId: json['avatar_id'] ?? 0, + avatarUrl: json['avatar_url'], + name: json['name'], + description: json['description'], + priority: json['priority'], + model: json['model'] ?? '', + vendor: json['vendor'] ?? '', + systemPrompt: json['system_prompt'], + initMessage: json['init_message'], + maxContext: json['max_context'] ?? 10, + maxTokens: json['max_tokens'], + roomType: json['room_type'], + lastActiveTime: json['last_active_time'] != null + ? DateTime.parse(json['last_active_time']) + : null, + createdAt: + json['CreatedAt'] != null ? DateTime.parse(json['CreatedAt']) : null, + updatedAt: + json['UpdatedAt'] != null ? DateTime.parse(json['UpdatedAt']) : null, + members: (json['members'] as List?) + ?.map((e) => e.toString()) + .toList() ?? + [], + ); + } +} + +class VersionCheckResp { + bool hasUpdate; + String serverVersion; + bool forceUpdate; + String url; + String message; + + VersionCheckResp({ + required this.hasUpdate, + required this.serverVersion, + required this.forceUpdate, + required this.url, + required this.message, + }); + + toJson() => { + 'has_update': hasUpdate, + 'server_version': serverVersion, + 'force_update': forceUpdate, + 'url': url, + 'message': message, + }; + + static VersionCheckResp fromJson(Map json) { + return VersionCheckResp( + hasUpdate: json['has_update'] ?? false, + serverVersion: json['server_version'], + forceUpdate: json['force_update'] ?? false, + url: json['url'], + message: json['message'], + ); + } +} + +class SignInResp { + int id; + String name; + String? email; + String? phone; + String token; + bool isNewUser; + int reward; + + SignInResp({ + required this.id, + required this.name, + this.email, + required this.token, + this.phone, + this.isNewUser = false, + this.reward = 0, + }); + + toJson() => { + 'id': id, + 'name': name, + 'email': email, + 'phone': phone, + 'token': token, + 'is_new_user': isNewUser, + 'reward': reward, + }; + + bool get needBindPhone => phone == null || phone!.isEmpty; + + static SignInResp fromJson(Map json) { + return SignInResp( + id: json['id'], + name: json['name'], + email: json['email'], + phone: json['phone'], + token: json['token'], + isNewUser: json['is_new_user'] ?? false, + reward: json['reward'] ?? 0, + ); + } +} + +class AsyncTaskResp { + String status; + List? errors; + List? resources; + String? originImage; + + AsyncTaskResp(this.status, {this.errors, this.resources, this.originImage}); + + toJson() => { + 'status': status, + 'errors': errors, + 'resources': resources, + 'origin_image': originImage, + }; + + static AsyncTaskResp fromJson(Map json) { + return AsyncTaskResp( + json['status'], + errors: json['errors'] != null + ? (json['errors'] as List).map((e) => e.toString()).toList() + : null, + resources: json['resources'] != null + ? (json['resources'] as List) + .map((e) => e.toString()) + .toList() + : null, + originImage: json['origin_image'], + ); + } +} + +class Prompt { + String title; + String content; + + Prompt(this.title, this.content); + + toJson() { + return { + 'title': title, + 'content': content, + }; + } + + fromJson(Map json) { + title = json['title']; + content = json['content']; + } +} + +class ChatExample { + String title; + String? content; + List models; + List tags; + + ChatExample( + this.title, { + this.content, + this.models = const [], + this.tags = const [], + }); + + get text => content ?? title; + + toJson() => { + 'title': title, + 'content': content, + 'models': models, + 'tags': tags, + }; + + fromJson(Map json) { + title = json['title']; + content = json['content']; + models = json['models']; + tags = json['tags']; + } +} + +class TranslateText { + String? result; + String? speakUrl; + + TranslateText(this.result, this.speakUrl); + + toJson() => { + 'result': result, + 'speak_url': speakUrl, + }; + + static fromJson(Map json) { + return TranslateText(json['result'], json['speak_url']); + } +} + +class UploadInitResponse { + String bucket; + String key; + String token; + String url; + + UploadInitResponse(this.key, this.bucket, this.token, this.url); + + toJson() => { + 'bucket': bucket, + 'key': key, + 'token': token, + 'url': url, + }; + + static fromJson(Map json) { + return UploadInitResponse( + json['key'], + json['bucket'], + json['token'], + json['url'], + ); + } +} + +class ModelStyle { + String id; + String name; + String? preview; + + ModelStyle({required this.id, required this.name, this.preview}); + + toJson() => { + 'id': id, + 'name': name, + 'preview': preview, + }; + + static ModelStyle fromJson(Map json) { + return ModelStyle( + id: json['id'], + name: json['name'], + preview: json['preview'], + ); + } +} + +class Model { + String id; + String name; + String shortName; + String? description; + String category; + bool isChat; + bool isImage; + bool disabled; + String? tag; + String? avatarUrl; + + String get realModelId { + return id.split(':').last; + } + + Model({ + required this.id, + required this.name, + required this.shortName, + required this.category, + required this.isChat, + required this.isImage, + this.description, + this.disabled = false, + this.tag, + this.avatarUrl, + }); + + toJson() => { + 'id': id, + 'name': name, + 'short_name': shortName, + 'description': description, + 'category': category, + 'is_chat': isChat, + 'is_image': isImage, + 'disabled': disabled, + 'tag': tag, + 'avatar_url': avatarUrl, + }; + + static Model fromJson(Map json) { + return Model( + id: json['id'], + name: json['name'], + shortName: json['short_name'] ?? json['name'], + description: json['description'], + category: json['category'], + isChat: json['is_chat'], + isImage: json['is_image'], + disabled: json['disabled'] ?? false, + tag: json['tag'], + avatarUrl: json['avatar_url'], + ); + } +} + +class BackgroundImage { + String url; + String preview; + + BackgroundImage(this.url, this.preview); + + toJson() => { + 'url': url, + 'preview': preview, + }; + + static BackgroundImage fromJson(Map json) { + return BackgroundImage( + json['url'], + json['preview'], + ); + } +} + +class UserExistenceResp { + bool exist; + String signInMethod; + + UserExistenceResp(this.exist, this.signInMethod); + + toJson() => { + 'exist': exist, + 'sign_in_method': signInMethod, + }; + + static UserExistenceResp fromJson(Map json) { + return UserExistenceResp( + json['exist'], + json['sign_in_method'], + ); + } +} + +class PromptCategory { + String name; + List children; + List tags; + + PromptCategory(this.name, this.children, this.tags); + + toJson() => { + 'name': name, + 'children': children, + 'tags': tags, + }; + + static PromptCategory fromJson(Map json) { + var children = []; + for (var item in json['children'] ?? []) { + children.add(PromptCategory.fromJson(item)); + } + + var tags = []; + for (var item in json['tags'] ?? []) { + tags.add(PromptTag.fromJson(item)); + } + + return PromptCategory( + json['name'], + children, + tags, + ); + } +} + +class PromptTag { + String name; + String value; + + PromptTag(this.name, this.value); + + toJson() => { + 'name': name, + 'value': value, + }; + + static PromptTag fromJson(Map json) { + return PromptTag( + json['name'], + json['value'], + ); + } +} + +class FreeModelCount { + String model; + String name; + int leftCount; + int maxCount; + String? info; + + FreeModelCount({ + required this.model, + required this.name, + required this.leftCount, + required this.maxCount, + this.info, + }); + + toJson() => { + 'model': model, + 'name': name, + 'left_count': leftCount, + 'max_count': maxCount, + 'info': info, + }; + + static FreeModelCount fromJson(Map json) { + return FreeModelCount( + model: json['model'], + name: json['name'] ?? json['model'], + leftCount: json['left_count'] ?? 0, + maxCount: json['max_count'] ?? 0, + info: json['info'], + ); + } +} diff --git a/lib/repo/model/model.dart b/lib/repo/model/model.dart index 12d52225..190f8243 100644 --- a/lib/repo/model/model.dart +++ b/lib/repo/model/model.dart @@ -1,22 +1,26 @@ class Model { final String id; final String name; + final String? shortName; final String ownedBy; String? description; String category; bool isChatModel = false; bool disabled; String? tag; + String? avatarUrl; Model( this.id, this.name, this.ownedBy, { + this.shortName, required this.category, this.description, this.isChatModel = false, this.disabled = false, this.tag, + this.avatarUrl, }); String uid() { diff --git a/lib/repo/model/room.dart b/lib/repo/model/room.dart index a1973925..b6ecfbce 100644 --- a/lib/repo/model/room.dart +++ b/lib/repo/model/room.dart @@ -38,6 +38,9 @@ class Room { /// room 类型:local or remote bool? localRoom; + /// 聊天室类型 + int? roomType; + bool get isLocalRoom => localRoom ?? false; /// 聊天室头像 标识 @@ -81,23 +84,31 @@ class Room { /// 聊天室最后活跃时间 DateTime? lastActiveTime; - Room(this.name, this.category, - {this.description, - this.id, - this.userId, - this.avatarId, - this.avatarUrl, - this.createdAt, - this.lastActiveTime, - this.iconData, - this.systemPrompt, - this.priority = 0, - this.color, - this.initMessage, - this.localRoom, - this.maxContext = 10, - this.maxTokens, - this.model = defaultChatModel}); + /// 聊天室成员头像列表 + List members; + + Room( + this.name, + this.category, { + this.description, + this.id, + this.userId, + this.avatarId, + this.avatarUrl, + this.createdAt, + this.lastActiveTime, + this.iconData, + this.systemPrompt, + this.priority = 0, + this.color, + this.roomType, + this.initMessage, + this.localRoom, + this.maxContext = 10, + this.maxTokens, + this.model = defaultChatModel, + this.members = const [], + }); Map toJson() { return { @@ -124,16 +135,21 @@ class Room { avatarId = map['avatar_id'] as int?, avatarUrl = map['avatar_url'] as String?, name = map['name'] as String, - category = map['category'] as String, - priority = map['priority'] as int, - model = map['model'] as String, + category = (map['category'] ?? '') as String, + priority = (map['priority'] ?? 0) as int, + model = (map['model'] ?? '') as String, iconData = map['icon_data'] as String?, color = map['color'] as String?, + roomType = map['room_type'] as int?, systemPrompt = map['system_prompt'] as String?, description = map['description'] as String?, initMessage = map['init_message'] as String?, maxContext = map['max_context'] as int? ?? 10, maxTokens = map['max_tokens'] as int?, + members = (map['members'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], createdAt = DateTime.fromMillisecondsSinceEpoch(map['created_at'] as int? ?? 0), lastActiveTime = DateTime.fromMillisecondsSinceEpoch( diff --git a/lib/repo/openai_repo.dart b/lib/repo/openai_repo.dart index 0b59bad5..169a09cb 100644 --- a/lib/repo/openai_repo.dart +++ b/lib/repo/openai_repo.dart @@ -1,11 +1,14 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; +import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/repo/model/model.dart' as mm; import 'package:dart_openai/openai.dart'; import 'package:askaide/repo/data/settings_data.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; class OpenAIRepository { final SettingDataProvider settings; @@ -80,36 +83,48 @@ class OpenAIRepository { static final supportForChat = { 'gpt-3.5-turbo': mm.Model( 'gpt-3.5-turbo', - 'gpt-3.5-turbo', + 'GPT-3.5 Turbo', 'openai', category: modelTypeOpenAI, isChatModel: true, - description: '能力最强的 GPT-3.5 模型,成本低', + description: '速度快,成本低', + shortName: 'GPT-3.5 Turbo', + tag: 'local', + avatarUrl: 'https://ssl.aicode.cc/ai-server/assets/avatar/gpt35.png', ), 'gpt-3.5-turbo-16k': mm.Model( 'gpt-3.5-turbo-16k', - 'gpt-3.5-turbo-16k', + 'GPT-3.5 Turbo 16k', 'openai', category: modelTypeOpenAI, isChatModel: true, - description: '能力最强的 GPT-3.5 模型,成本为 gpt-3.5-turbo 的两倍,但是支持 4K 上下文', + description: '3.5 升级版,支持 16K 长文本', + shortName: 'GPT-3.5 Turbo 16K', + tag: 'local', + avatarUrl: 'https://ssl.aicode.cc/ai-server/assets/avatar/gpt35.png', ), 'gpt-4': mm.Model( 'gpt-4', - 'gpt-4', + 'GPT-4', 'openai', category: modelTypeOpenAI, isChatModel: true, - description: '比GPT-3.5模型更强,能够执行复杂任务,并优化用于聊天', + description: '能力强,更精准', + shortName: 'GPT-4', + tag: 'local', + avatarUrl: 'https://ssl.aicode.cc/ai-server/assets/avatar/gpt4.png', ), 'gpt-4-32k': mm.Model( 'gpt-4-32k', - 'gpt-4-32k', + 'GPT-4 32k', 'openai', category: modelTypeOpenAI, isChatModel: true, description: '基于 GPT-4,但是支持4倍的内容长度', + shortName: 'GPT-4 32K', + tag: 'local', + avatarUrl: 'https://ssl.aicode.cc/ai-server/assets/avatar/gpt4.png', ), // 'gpt-4-0314': Model( @@ -233,41 +248,124 @@ class OpenAIRepository { void Function(ChatStreamRespData data) onData, { double temperature = 1.0, user = 'user', - model = defaultChatModel, + String model = defaultChatModel, int? roomId, int? maxTokens, }) async { var completer = Completer(); try { - var chatStream = OpenAI.instance.chat.createStream( - model: model, - messages: messages, - temperature: temperature, - user: user, - maxTokens: maxTokens, - n: Ability().supportLocalOpenAI() - ? null - : roomId, // n 参数暂时用不到,复用作为 roomId - ); - - chatStream.listen( - (event) { - for (var element in event.choices) { - if (element.delta.content != null) { + bool canUseWebsocket = true; + if (Ability().enableLocalOpenAI()) { + if (supportForChat.containsKey(model) || model.startsWith('openai:')) { + canUseWebsocket = false; + } + } + + if (!Ability().enableAPIServer()) { + canUseWebsocket = false; + } + + if (Ability().supportWebSocket() && canUseWebsocket) { + final serverURL = settings.getDefault(settingServerURL, apiServerURL); + final wsURL = serverURL.startsWith('https://') + ? serverURL.replaceFirst('https://', 'wss://') + : serverURL.replaceFirst('http://', 'ws://'); + + final apiToken = settings.getDefault(settingAPIServerToken, ''); + + final wsUri = Uri.parse('$wsURL/v1/chat/completions'); + var channel = WebSocketChannel.connect(Uri( + scheme: wsUri.scheme, + host: wsUri.host, + port: wsUri.port, + path: wsUri.path, + queryParameters: { + 'ws': 'true', + 'authorization': apiToken, + 'client-version': clientVersion, + 'platform-version': PlatformTool.operatingSystemVersion(), + 'platform': PlatformTool.operatingSystem(), + 'language': language, + }, + )); + + channel.stream.listen( + (event) { + final evt = jsonDecode(event); + if (evt['code'] != null && evt['code'] > 0) { onData(ChatStreamRespData( - content: element.delta.content!, - role: element.delta.role, + content: evt['error'], + code: evt['code'], + error: evt['error'], )); + + return; } - } - }, - onDone: () => completer.complete(), - onError: (e) => completer.completeError(e), - cancelOnError: true, - ).onError((e) { - completer.completeError(e); - }); + + final res = OpenAIStreamChatCompletionModel.fromMap(evt); + for (var element in res.choices) { + if (element.delta.content != null) { + onData(ChatStreamRespData( + content: element.delta.content!, + role: element.delta.role, + )); + } + } + }, + onDone: () { + channel.sink.close(); + completer.complete(); + }, + onError: (e) { + channel.sink.close(); + completer.completeError(e); + }, + cancelOnError: true, + ).onError((e) { + completer.completeError(e); + }); + + channel.sink.add(jsonEncode({ + 'model': model, + 'messages': messages.map((e) => e.toMap()).toList(), + 'temperature': temperature, + 'user': user, + 'max_tokens': maxTokens, + 'n': Ability().enableLocalOpenAI() + ? null + : roomId, // n 参数暂时用不到,复用作为 roomId + })); + } else { + var chatStream = OpenAI.instance.chat.createStream( + model: model, + messages: messages, + temperature: temperature, + user: user, + maxTokens: maxTokens, + n: Ability().enableLocalOpenAI() + ? null + : roomId, // n 参数暂时用不到,复用作为 roomId + ); + + chatStream.listen( + (event) { + for (var element in event.choices) { + if (element.delta.content != null) { + onData(ChatStreamRespData( + content: element.delta.content!, + role: element.delta.role, + )); + } + } + }, + onDone: () => completer.complete(), + onError: (e) => completer.completeError(e), + cancelOnError: true, + ).onError((e) { + completer.completeError(e); + }); + } } catch (e) { completer.completeError(e); } @@ -305,9 +403,13 @@ class ChatReplyMessage { class ChatStreamRespData { final String? role; final String content; + final int? code; + final String? error; ChatStreamRespData({ this.role, required this.content, + this.code, + this.error, }); } diff --git a/lib/repo/stabilityai_repo.dart b/lib/repo/stabilityai_repo.dart index 6fe2aa0d..dc637569 100644 --- a/lib/repo/stabilityai_repo.dart +++ b/lib/repo/stabilityai_repo.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:askaide/helper/constant.dart'; +import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/repo/data/settings_data.dart'; import 'package:http/http.dart' as http; diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 00000000..33687536 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,15 @@ +server { + listen 80; + + gzip on; + gzip_static on; + gzip_vary on; + gzip_types text/plain application/x-javascript text/css application/xml text/xml application/javascript; + + root /data/webroot; + + location / { + index index.html; + try_files $uri $uri/ =404; + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 371a23f4..bf76dd26 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -452,13 +452,13 @@ packages: source: hosted version: "1.3.1" fetch_api: - dependency: transitive + dependency: "direct main" description: name: fetch_api - sha256: b88977a8f369344cbaa13a53752629185ecae0d56c6683f0a2060b652a0cf437 + sha256: "7896632eda5af40c4459d673ad601df21d4c3ae6a45997e300a92ca63ec9fe4c" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.1" fetch_client: dependency: transitive description: @@ -585,6 +585,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.3" + flutter_initicon: + dependency: "direct main" + description: + name: flutter_initicon + sha256: "5aeda6b16150cb54a34a048a85f9cfddbf36a92135f769ea998d1394dcb54a0f" + url: "https://pub.dev" + source: hosted + version: "3.0.0+1" flutter_launcher_icons: dependency: "direct dev" description: @@ -1768,7 +1776,7 @@ packages: source: hosted version: "0.1.4-beta" web_socket_channel: - dependency: transitive + dependency: "direct main" description: name: web_socket_channel sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b diff --git a/pubspec.yaml b/pubspec.yaml index e8d30c6a..66ebaa4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # 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. # 应用正式发布时,需要同步修改 lib/helper/constant.dart 中的 VERSION 值 -version: 1.0.5+1 +version: 1.0.7+1 environment: sdk: '>=3.0.0 <4.0.0' @@ -113,6 +113,9 @@ dependencies: sqflite_common_ffi_web: ^0.4.0 flutter_markdown: ^0.6.17+3 markdown: ^7.1.1 + fetch_api: 1.0.1 + web_socket_channel: ^2.4.0 + flutter_initicon: ^3.0.0+1 dev_dependencies: flutter_test: