diff --git a/docker-build.sh b/docker-build.sh index 3b553c5e..f8eb7b16 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash -VERSION=1.0.6 -VERSION_DATE=202310091100 +VERSION=1.0.9 rm -fr build/web @@ -10,11 +9,5 @@ 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 +docker buildx build --platform=linux/amd64,linux/arm64 -t mylxsw/aidea-web:$VERSION . --push diff --git a/lib/main.dart b/lib/main.dart index e19264df..bd319ab6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,6 +21,7 @@ import 'package:askaide/page/creative_island/draw/artistic_text.dart'; import 'package:askaide/page/setting/account_security.dart'; import 'package:askaide/page/app_scaffold.dart'; import 'package:askaide/page/lab/avatar_selector.dart'; +import 'package:askaide/page/setting/article.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'; @@ -46,6 +47,7 @@ import 'package:askaide/page/creative_island/draw/image_edit_direct.dart'; import 'package:askaide/page/lab/draw_board.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/notification.dart'; import 'package:askaide/page/setting/openai_setting.dart'; import 'package:askaide/page/balance/payment.dart'; import 'package:askaide/page/lab/prompt.dart'; @@ -925,6 +927,29 @@ class MyApp extends StatefulWidget { ); }, ), + GoRoute( + name: 'notifications', + path: '/notifications', + parentNavigatorKey: _shellNavigatorKey, + pageBuilder: (context, state) { + return transitionResolver( + NotificationScreen(setting: settingRepo), + ); + }, + ), + GoRoute( + name: 'articles', + path: '/article', + parentNavigatorKey: _shellNavigatorKey, + pageBuilder: (context, state) { + return transitionResolver( + ArticleScreen( + settings: settingRepo, + id: int.tryParse(state.queryParameters['id'] ?? '') ?? 0, + ), + ); + }, + ), ], ) ], diff --git a/lib/page/chat/room_create.dart b/lib/page/chat/room_create.dart index 291c9b61..2d275368 100644 --- a/lib/page/chat/room_create.dart +++ b/lib/page/chat/room_create.dart @@ -138,6 +138,7 @@ class _RoomCreatePageState extends State { const EdgeInsets.only(right: 5, left: 10), overlayColor: MaterialStateProperty.all(Colors.transparent), + tabAlignment: TabAlignment.center, ), ), Expanded( diff --git a/lib/page/component/model_indicator.dart b/lib/page/component/model_indicator.dart index b5dcaed7..e821416d 100644 --- a/lib/page/component/model_indicator.dart +++ b/lib/page/component/model_indicator.dart @@ -58,7 +58,7 @@ class ModelIndicator extends StatelessWidget { Text( model.modelName, style: TextStyle( - fontSize: 16, + fontSize: 15, color: selected ? Colors.white : customColors.weakLinkColor, diff --git a/lib/page/component/sliver_component.dart b/lib/page/component/sliver_component.dart index 31d59977..ebf6677e 100644 --- a/lib/page/component/sliver_component.dart +++ b/lib/page/component/sliver_component.dart @@ -29,6 +29,7 @@ class SliverSingleComponent extends StatelessWidget { return CustomScrollView( slivers: [ SliverAppBar( + automaticallyImplyLeading: false, expandedHeight: expendedHeight, floating: false, pinned: true, @@ -90,6 +91,7 @@ class SliverComponent extends StatelessWidget { headerSliverBuilder: (context, innerBoxIsScrolled) { return [ SliverAppBar( + automaticallyImplyLeading: false, toolbarHeight: CustomSize.toolbarHeight, expandedHeight: expendedHeight, floating: false, diff --git a/lib/page/data/notification_datasource.dart b/lib/page/data/notification_datasource.dart new file mode 100644 index 00000000..f3f179a4 --- /dev/null +++ b/lib/page/data/notification_datasource.dart @@ -0,0 +1,53 @@ +import 'package:askaide/helper/logger.dart'; +import 'package:askaide/repo/api/notification.dart'; +import 'package:askaide/repo/api_server.dart'; +import 'package:loading_more_list/loading_more_list.dart'; + +class NotificationDatasource extends LoadingMoreBase { + int startId = 0; + bool _hasMore = true; + bool forceRefresh = false; + + NotificationDatasource(); + + @override + bool get hasMore => _hasMore || forceRefresh; + + @override + Future loadData([bool isloadMoreAction = false]) async { + try { + final messages = + await APIServer().notifications(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 = 0; + //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/setting/article.dart b/lib/page/setting/article.dart new file mode 100644 index 00000000..6c46e237 --- /dev/null +++ b/lib/page/setting/article.dart @@ -0,0 +1,126 @@ +import 'package:askaide/helper/ability.dart'; +import 'package:askaide/page/component/background_container.dart'; +import 'package:askaide/page/component/chat/markdown.dart'; +import 'package:askaide/page/component/column_block.dart'; +import 'package:askaide/page/component/theme/custom_size.dart'; +import 'package:askaide/page/component/theme/custom_theme.dart'; +import 'package:askaide/repo/api/article.dart'; +import 'package:askaide/repo/api_server.dart'; +import 'package:askaide/repo/settings_repo.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class ArticleScreen extends StatefulWidget { + final SettingRepository settings; + final int id; + const ArticleScreen({super.key, required this.settings, required this.id}); + + @override + State createState() => _ArticleScreenState(); +} + +class _ArticleScreenState extends State { + Article article = Article( + id: 0, + title: '标题', + content: '内容', + ); + + @override + void initState() { + APIServer().article(id: widget.id).then((value) { + setState(() { + article = value; + }); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final customColors = Theme.of(context).extension()!; + + return Scaffold( + appBar: AppBar( + title: Text( + article.title, + style: const TextStyle(fontSize: CustomSize.appBarTitleSize), + ), + toolbarHeight: CustomSize.toolbarHeight, + centerTitle: true, + leading: IconButton( + icon: Icon( + Icons.close, + color: customColors.weakLinkColor, + ), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go(Ability().homeRoute); + } + }, + ), + ), + backgroundColor: customColors.backgroundContainerColor, + body: BackgroundContainer( + setting: widget.settings, + child: SafeArea( + top: false, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: SingleChildScrollView( + child: ColumnBlock( + padding: + const EdgeInsets.symmetric(horizontal: 15, vertical: 15), + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '作者:${article.author ?? '管理员'}', + style: TextStyle( + fontSize: 12, + color: customColors.weakTextColor, + ), + ), + if (article.createdAt != null) + Text( + DateFormat('yyyy/MM/dd HH:mm') + .format(article.createdAt!.toLocal()), + style: TextStyle( + fontSize: 12, + color: + customColors.weakTextColor?.withAlpha(100), + ), + ), + ], + ), + const SizedBox(height: 10), + Markdown( + data: article.content, + onUrlTap: (value) { + if (value.startsWith("aidea-app://")) { + var route = value.substring('aidea-app://'.length); + context.push(route); + } else { + launchUrlString(value); + } + }, + ), + ], + ) + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/page/setting/notification.dart b/lib/page/setting/notification.dart new file mode 100644 index 00000000..3cfe246d --- /dev/null +++ b/lib/page/setting/notification.dart @@ -0,0 +1,211 @@ +import 'package:askaide/helper/haptic_feedback.dart'; +import 'package:askaide/helper/helper.dart'; +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/page/data/notification_datasource.dart'; +import 'package:askaide/repo/api/notification.dart'; +import 'package:askaide/repo/settings_repo.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:go_router/go_router.dart'; +import 'package:loading_more_list/loading_more_list.dart'; + +class NotificationScreen extends StatefulWidget { + final SettingRepository setting; + const NotificationScreen({super.key, required this.setting}); + + @override + State createState() => _NotificationScreenState(); +} + +class _NotificationScreenState extends State { + final NotificationDatasource datasource = NotificationDatasource(); + + @override + void dispose() { + datasource.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var customColors = Theme.of(context).extension()!; + + return Scaffold( + appBar: AppBar( + title: const Text( + '消息', + style: 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, + displacement: 20, + onRefresh: () { + return datasource.refresh(); + }, + child: LoadingMoreList( + ListConfig( + itemBuilder: (context, item, index) { + return NotifyMessageItem( + message: item, + customColors: customColors, + onTap: () { + context.push(Uri(path: '/article', queryParameters: { + 'id': item.articleId.toString() + }).toString()); + }, + ); + }, + 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, + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + } +} + +class NotifyMessageItem extends StatelessWidget { + const NotifyMessageItem({ + super.key, + required this.message, + required this.customColors, + required this.onTap, + }); + + final NotifyMessage message; + final CustomColors customColors; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 5, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + ), + child: Slidable( + endActionPane: ActionPane( + motion: const ScrollMotion(), + children: [ + const SizedBox(width: 10), + SlidableAction( + label: '详情', + borderRadius: BorderRadius.circular(10), + backgroundColor: Colors.green, + icon: Icons.info_outline, + onPressed: (_) { + HapticFeedbackHelper.lightImpact(); + onTap(); + }, + ), + ], + ), + child: Material( + color: customColors.backgroundColor?.withAlpha(200), + borderRadius: BorderRadius.all( + Radius.circular(customColors.borderRadius ?? 8), + ), + child: InkWell( + child: ListTile( + contentPadding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(customColors.borderRadius ?? 8), + ), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + message.title.trim(), + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: customColors.weakTextColor, + fontSize: 15, + ), + maxLines: 1, + ), + ), + Text( + humanTime(message.createdAt), + style: TextStyle( + color: customColors.weakTextColor?.withAlpha(65), + fontSize: 12, + ), + ), + ], + ), + dense: true, + subtitle: Padding( + padding: const EdgeInsets.only(top: 5), + child: Text( + message.content.trim().replaceAll("\n", " "), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: customColors.weakTextColor?.withAlpha(150), + fontSize: 12, + overflow: TextOverflow.ellipsis, + ), + ), + ), + onTap: () { + HapticFeedbackHelper.lightImpact(); + onTap(); + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/page/setting/setting_screen.dart b/lib/page/setting/setting_screen.dart index 212451f8..eff61571 100644 --- a/lib/page/setting/setting_screen.dart +++ b/lib/page/setting/setting_screen.dart @@ -66,6 +66,14 @@ class _SettingScreenState extends State { color: customColors.backgroundInvertedColor, ), ), + actions: [ + IconButton( + onPressed: () { + context.push('/notifications'); + }, + icon: const Icon(Icons.notifications_outlined), + ), + ], child: BlocBuilder( builder: (_, state) { return buildSettingsList([ diff --git a/lib/repo/api/article.dart b/lib/repo/api/article.dart new file mode 100644 index 00000000..356faa2c --- /dev/null +++ b/lib/repo/api/article.dart @@ -0,0 +1,41 @@ +class Article { + int id; + String title; + String content; + String? author; + String? type; + DateTime? createdAt; + + Article({ + required this.id, + required this.title, + required this.content, + this.author, + this.type, + this.createdAt, + }); + + factory Article.fromJson(Map json) { + return Article( + id: json['id'], + title: json['title'], + content: json['content'], + author: json['author'], + type: json['type'], + createdAt: json['created_at'] != null + ? DateTime.parse(json['created_at']) + : null, + ); + } + + Map toJson() { + return { + 'id': id, + 'title': title, + 'content': content, + 'author': author, + 'type': type, + 'created_at': createdAt?.toIso8601String(), + }; + } +} diff --git a/lib/repo/api/notification.dart b/lib/repo/api/notification.dart new file mode 100644 index 00000000..e7427327 --- /dev/null +++ b/lib/repo/api/notification.dart @@ -0,0 +1,41 @@ +class NotifyMessage { + int id; + int articleId; + String title; + String content; + String? type; + DateTime? createdAt; + + NotifyMessage({ + required this.id, + required this.title, + required this.content, + required this.articleId, + this.type, + this.createdAt, + }); + + factory NotifyMessage.fromJson(Map json) { + return NotifyMessage( + id: json['id'], + articleId: json['article_id'] ?? json['id'], + title: json['title'], + content: json['content'], + type: json['type'], + createdAt: json['created_at'] != null + ? DateTime.parse(json['created_at']) + : null, + ); + } + + Map toJson() { + return { + 'id': id, + 'article_id': articleId, + 'title': title, + 'content': content, + 'type': type, + 'created_at': createdAt?.toIso8601String(), + }; + } +} diff --git a/lib/repo/api_server.dart b/lib/repo/api_server.dart index e84b6ef9..5c2e14a4 100644 --- a/lib/repo/api_server.dart +++ b/lib/repo/api_server.dart @@ -6,10 +6,12 @@ import 'package:askaide/helper/error.dart'; import 'package:askaide/helper/http.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; +import 'package:askaide/repo/api/article.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api/image_model.dart'; import 'package:askaide/repo/api/info.dart'; import 'package:askaide/repo/api/keys.dart'; +import 'package:askaide/repo/api/notification.dart'; import 'package:askaide/repo/api/page.dart'; import 'package:askaide/repo/api/payment.dart'; import 'package:askaide/repo/api/quota.dart'; @@ -1780,4 +1782,49 @@ class APIServer { Future deleteAPIKey({required int id}) async { return sendDeleteRequest('/v1/api-keys/$id', (data) {}); } + + /// 消息通知 //////////////////////////////////////////////////////////////////// + /// 消息通知列表 + Future> notifications({ + int startId = 0, + int? perPage = 20, + bool cache = true, + }) async { + return sendCachedGetRequest( + '/v1/notifications', + (resp) { + var res = []; + for (var item in resp.data['data']) { + res.add(NotifyMessage.fromJson(item)); + } + + 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, + ); + } + + /// 文章 //////////////////////////////////////////////////////////////////// + /// 文章详情 + Future
article({ + required int id, + bool cache = true, + }) async { + return sendCachedGetRequest( + '/v1/articles/$id', + (resp) { + return Article.fromJson(resp.data['data']); + }, + forceRefresh: !cache, + ); + } } diff --git a/lib/repo/openai_repo.dart b/lib/repo/openai_repo.dart index 55d1d836..88c778c1 100644 --- a/lib/repo/openai_repo.dart +++ b/lib/repo/openai_repo.dart @@ -268,14 +268,19 @@ class OpenAIRepository { } if (Ability().supportWebSocket && canUseWebsocket) { - final serverURL = settings.getDefault(settingServerURL, apiServerURL); + var serverURL = settings.getDefault(settingServerURL, apiServerURL); + if (PlatformTool.isWeb() && (serverURL == '' || serverURL == '/')) { + serverURL = + '${Uri.base.scheme}://${Uri.base.host}${Uri.base.hasPort ? ':${Uri.base.port}' : ''}'; + } + final wsURL = serverURL.startsWith('https://') ? serverURL.replaceFirst('https://', 'wss://') : serverURL.replaceFirst('http://', 'ws://'); + final wsUri = Uri.parse('$wsURL/v1/chat/completions'); final apiToken = settings.getDefault(settingAPIServerToken, ''); - final wsUri = Uri.parse('$wsURL/v1/chat/completions'); var channel = WebSocketChannel.connect(Uri( scheme: wsUri.scheme, host: wsUri.host, diff --git a/pubspec.lock b/pubspec.lock index a2db2a69..9c4e9964 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -310,10 +310,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" convert: dependency: transitive description: @@ -997,10 +997,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -1539,18 +1539,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1587,26 +1587,26 @@ packages: dependency: transitive description: name: test - sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f url: "https://pub.dev" source: hosted - version: "1.24.3" + version: "1.24.9" test_api: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" test_core: dependency: transitive description: name: test_core - sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.9" tiktoken: dependency: "direct main" description: @@ -1811,10 +1811,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: "direct main" description: @@ -1872,5 +1872,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0 <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=3.13.0"