diff --git a/api/.gitignore b/api/.gitignore index 2bb6e81..342a80c 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -91,7 +91,7 @@ server_log.txt .metals/ -/config/production.yaml.bak +/config/production.yaml /config/development.yaml *.g.dart diff --git a/api/Dockerfile b/api/Dockerfile index c84bc87..d7f0050 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,14 +1,17 @@ -FROM google/dart:latest - +FROM dart:stable AS build-env +LABEL stage=dart_builder +ENV PUB_HOSTED_URL="https://pub.flutter-io.cn" +ENV ANGEL_ENV=production COPY ./ ./ - -# Install dependencies, pre-build RUN pub get +RUN dart compile exe bin/prod.dart -o /server -# Optionally build generaed sources. -# RUN pub run build_runner build - -# Set environment, start server +FROM scratch +WORKDIR /app ENV ANGEL_ENV=production +ADD ./views ./views +ADD ./config ./config +COPY --from=build-env /runtime/ / +COPY --from=build-env /server /app EXPOSE 3000 -CMD dart bin/prod.dart +ENTRYPOINT ["./server", "-a", "0.0.0.0", "--port", "3000"] \ No newline at end of file diff --git a/api/docker-compose.yml b/api/docker-compose.yml new file mode 100644 index 0000000..2dbb198 --- /dev/null +++ b/api/docker-compose.yml @@ -0,0 +1,34 @@ +version: "2.1" + +services: + mysql: + image: postgres:latest + restart: always + container_name: dgm_postgres + command: --default-authentication-plugin=mysql_native_password + environment: + MYSQL_ROOT_PASSWORD: Dx@8917312 + MYSQL_DATABASE: piwigo + MYSQL_USER: debuggerx + MYSQL_PASSWORD: dx8917312 + volumes: + - /mnt/hd500/db:/var/lib/mysql + ports: + - "5432:5432" + networks: + - mynet + + api: + build: . + container_name: dgm_api + links: + - dgm_postgres + ports: + - 8888:8888 + networks: + - mynet + restart: unless-stopped + +networks: + mynet: + driver: bridge \ No newline at end of file diff --git a/api/lib/apis.dart b/api/lib/apis.dart index ada4277..292ce4c 100644 --- a/api/lib/apis.dart +++ b/api/lib/apis.dart @@ -35,9 +35,13 @@ class SchemeApis { String user({required StringParam type}) => [path, 'user', type].joinPath(); + String market({required StringParam type, required IntParam page, required IntParam pageSize}) => [path, 'market', type, page, pageSize].joinPath(); + String download({required StringParam schemeId}) => [path, 'download', schemeId].joinPath(); String like({required StringParam schemeId, required StringParam isLike}) => [path, 'like', schemeId, isLike].joinPath(); + + String get userLikes => [path, 'user-likes'].joinPath(); } final _paramsMap = { diff --git a/api/lib/src/models/like_record.dart b/api/lib/src/models/like_record.dart index c794cfe..3847c6d 100644 --- a/api/lib/src/models/like_record.dart +++ b/api/lib/src/models/like_record.dart @@ -20,4 +20,21 @@ abstract class _LikeRecord extends BaseModel { @Column(isNullable: false, indexType: IndexType.standardIndex) @SerializableField(isNullable: false) bool? get liked; +} + + +@serializable +@Orm(tableName: 'like_records', generateMigrations: false) +abstract class _UserLikes { + @Column(isNullable: false) + @SerializableField(isNullable: false) + int? id; + + @Column(isNullable: false) + @SerializableField(exclude: true) + int? get uid; + + @Column(isNullable: false) + @SerializableField(exclude: true) + bool? get liked; } \ No newline at end of file diff --git a/api/lib/src/models/scheme.dart b/api/lib/src/models/scheme.dart index ff665c3..c0c1cba 100644 --- a/api/lib/src/models/scheme.dart +++ b/api/lib/src/models/scheme.dart @@ -71,6 +71,9 @@ abstract class _SimpleScheme { @serializable abstract class _SimpleSchemeTransMetaData { @SerializableField(isNullable: false) + int? id; + + @SerializableField(isNullable: false) String? get uuid; @SerializableField(isNullable: false) @@ -90,6 +93,7 @@ abstract class _SimpleSchemeTransMetaData { } SimpleSchemeTransMetaData transSimpleSchemeMetaData(SimpleScheme scheme) => SimpleSchemeTransMetaData( + id: scheme.id, description: scheme.description, uuid: scheme.uuid, name: scheme.name, @@ -121,3 +125,57 @@ SchemeForDownload transSchemeForDownload(Scheme scheme) => SchemeForDownload( description: scheme.description, gestures: scheme.gestures, ); + +@serializable +@Orm(tableName: 'schemes', generateMigrations: false) +abstract class _MarketScheme { + @Column() + int? id; + + @Column(isNullable: false, indexType: IndexType.unique) + @SerializableField(isNullable: false) + String? get uuid; + + @Column(isNullable: false) + @SerializableField(isNullable: false) + String? get name; + + @Column(type: ColumnType.text) + String? description; + + @Column(isNullable: false) + @SerializableField(exclude: true) + bool? get shared; + + @SerializableField(isNullable: true) + @Column(type: ColumnType.json) + Map? get metadata; +} + +@serializable +abstract class _MarketSchemeTransMetaData { + @SerializableField(isNullable: false) + int? id; + + @SerializableField(isNullable: false) + String? get uuid; + + @SerializableField(isNullable: false) + String? get name; + + @SerializableField(isNullable: false) + String? description; + + int? get downloads; + + int? get likes; +} + +MarketSchemeTransMetaData transMarketSchemeMetaData(MarketScheme scheme) => MarketSchemeTransMetaData( + id: scheme.id, + description: scheme.description, + uuid: scheme.uuid, + name: scheme.name, + likes: scheme.metadata?['likes'] ?? 0, + downloads: scheme.metadata?['downloads'] ?? 0, +); \ No newline at end of file diff --git a/api/lib/src/routes/controllers/scheme_controllers.dart b/api/lib/src/routes/controllers/scheme_controllers.dart index 03f9306..cdf22ff 100644 --- a/api/lib/src/routes/controllers/scheme_controllers.dart +++ b/api/lib/src/routes/controllers/scheme_controllers.dart @@ -62,41 +62,6 @@ Future configureServer(Angel app) async { ); app.get( - Apis.scheme.user.route, - chain( - [ - jwtMiddleware(), - (req, res) async { - var schemeQuery = SimpleSchemeQuery(); - var type = req.params['type']; - var likeRecordTableName = LikeRecordQuery().tableName; - schemeQuery.leftJoin(likeRecordTableName, SchemeFields.id, LikeRecordFields.schemeId, alias: 'lr'); - - switch (type) { - case 'uploaded': - schemeQuery.where!.uid.equals(req.user!.idAsInt); - break; - case 'downloaded': - var downloadHistoryTableName = DownloadHistoryQuery().tableName; - schemeQuery.leftJoin(downloadHistoryTableName, SchemeFields.id, DownloadHistoryFields.schemeId, - alias: 'dh'); - schemeQuery.where!.raw('dh.${DownloadHistoryFields.uid} = ${req.user!.idAsInt}'); - break; - case 'liked': - schemeQuery.where!.raw('lr.${LikeRecordFields.uid} = ${req.user!.idAsInt}'); - schemeQuery.where!.raw('lr.${LikeRecordFields.liked} = true'); - break; - default: - return res.unProcessableEntity(); - } - schemeQuery.orderBy('${schemeQuery.tableName}.${SchemeFields.updatedAt}', descending: true); - return schemeQuery.get(req.queryExecutor).then((value) => value.map(transSimpleSchemeMetaData).toList()); - }, - ], - ), - ); - - app.get( Apis.scheme.download.route, chain( [ @@ -185,4 +150,100 @@ Future configureServer(Angel app) async { ], ), ); + + app.get( + Apis.scheme.user.route, + chain( + [ + jwtMiddleware(), + (req, res) async { + var schemeQuery = SimpleSchemeQuery(); + var type = req.params['type']; + var likeRecordTableName = LikeRecordQuery().tableName; + schemeQuery.leftJoin(likeRecordTableName, SchemeFields.id, LikeRecordFields.schemeId, alias: 'lr'); + + switch (type) { + case 'uploaded': + schemeQuery.where!.uid.equals(req.user!.idAsInt); + break; + case 'downloaded': + var downloadHistoryTableName = DownloadHistoryQuery().tableName; + schemeQuery.leftJoin(downloadHistoryTableName, SchemeFields.id, DownloadHistoryFields.schemeId, + alias: 'dh'); + schemeQuery.where!.raw('dh.${DownloadHistoryFields.uid} = ${req.user!.idAsInt}'); + break; + case 'liked': + schemeQuery.where!.raw('lr.${LikeRecordFields.uid} = ${req.user!.idAsInt}'); + schemeQuery.where!.raw('lr.${LikeRecordFields.liked} = true'); + break; + default: + return res.unProcessableEntity(); + } + schemeQuery.orderBy('${schemeQuery.tableName}.${SchemeFields.updatedAt}', descending: true); + return schemeQuery.get(req.queryExecutor).then((value) => value.map(transSimpleSchemeMetaData).toList()); + }, + ], + ), + ); + + const recommend = "(metadata->'recommends') is null ,(metadata->'recommends')::int"; + const updated = "updated_at"; + const likes = "(metadata->'likes') is null ,(metadata->'likes')::int"; + const downloads = "(metadata->'downloads') is null ,(metadata->'downloads')::int"; + + app.get(Apis.scheme.market.route, (req, res) async { + var schemeQuery = MarketSchemeQuery(); + var type = req.params['type']; + var page = req.params['page'] as int; + var pageSize = req.params['pageSize'] as int; + schemeQuery.where?.shared.equals(true); + late List orders; + + switch (type) { + case 'recommend': + // orders = [recommend, likes, downloads, SchemeFields.id]; + orders = [recommend]; + break; + case 'updated': + orders = [updated]; + break; + case 'likes': + // orders = [likes, recommend, downloads, SchemeFields.id]; + orders = [likes]; + break; + case 'downloads': + // orders = [downloads, recommend, likes, SchemeFields.id]; + orders = [downloads]; + break; + default: + return res.unProcessableEntity(); + } + for (var order in orders) { + schemeQuery.orderBy(order, descending: true); + } + schemeQuery.offset(page * pageSize); + schemeQuery.limit(pageSize + 1); + return schemeQuery.get(req.queryExecutor).then((value) { + var hasMore = value.length > pageSize; + if (hasMore) value.removeLast(); + return { + 'hasMore': hasMore, + 'items': value.map(transMarketSchemeMetaData).toList(), + }; + }); + }); + + app.get( + Apis.scheme.userLikes, + chain( + [ + jwtMiddleware(), + (req, res) async => (UserLikesQuery() + ..where?.uid.equals(req.user!.idAsInt) + ..where?.liked.equals(true)) + .get(req.queryExecutor) + .then((value) => value.map((e) => e.id).toList()), + ], + ), + ); } diff --git a/api/pubspec.yaml b/api/pubspec.yaml index 579e527..2b58d05 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -13,7 +13,6 @@ dependencies: angel3_orm_postgres: ^3.3.0 angel3_serialize: ^4.1.0 angel3_production: ^3.1.2 - angel3_static: ^4.1.0 belatuk_pretty_logging: ^4.0.0 optional: ^6.0.0 logging: ^1.0.0 diff --git a/api/start.sh b/api/start.sh new file mode 100644 index 0000000..89f09ef --- /dev/null +++ b/api/start.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +docker build -t dgm_api_image . +docker image prune -f --filter label=stage=dart_builder +docker rm -f dgm_api +docker run -d --restart=always --name dgm_api -p 3000:3000 dgm_api_image +docker image prune -f diff --git a/app/lib/constants/constants.dart b/app/lib/constants/constants.dart index 0a4ab78..f4214a8 100644 --- a/app/lib/constants/constants.dart +++ b/app/lib/constants/constants.dart @@ -3,7 +3,7 @@ import 'package:flutter/cupertino.dart'; /// [UOS设计指南](https://docs.uniontech.com/zh/content/t_dbG3kBK9iDf9B963ok) const double localManagerPanelWidth = 260; -const double marketOrMePanelWidth = 300; +const double marketOrMePanelWidth = 450; const shortDuration = const Duration(milliseconds: 100); diff --git a/app/lib/pages/market_or_me.dart b/app/lib/pages/market_or_me.dart index af2bf2a..447c963 100644 --- a/app/lib/pages/market_or_me.dart +++ b/app/lib/pages/market_or_me.dart @@ -4,6 +4,7 @@ import 'package:dde_gesture_manager/models/configs.provider.dart'; import 'package:dde_gesture_manager/models/content_layout.provider.dart'; import 'package:dde_gesture_manager/widgets/dde_button.dart'; import 'package:dde_gesture_manager/widgets/login.dart'; +import 'package:dde_gesture_manager/widgets/market.dart'; import 'package:dde_gesture_manager/widgets/me.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -18,15 +19,14 @@ class MarketOrMe extends StatelessWidget { var layoutProvider = context.watch(); bool isOpen = layoutProvider.marketOrMeOpened == true; bool isMarket = layoutProvider.isMarket; - bool showLogin = context.watch().accessToken.isNull && !isMarket; return AnimatedContainer( duration: mediumDuration, curve: Curves.easeInOut, - width: isOpen ? marketOrMePanelWidth * (showLogin ? 1.5 : 1) : 0, + width: isOpen ? marketOrMePanelWidth * 1 : 0, child: OverflowBox( alignment: Alignment.centerLeft, - maxWidth: marketOrMePanelWidth * (showLogin ? 1.5 : 1), - minWidth: marketOrMePanelWidth * (showLogin ? 1.5 : 1), + maxWidth: marketOrMePanelWidth, + minWidth: marketOrMePanelWidth, child: Material( color: context.t.backgroundColor, elevation: isOpen ? 10 : 0, @@ -85,7 +85,5 @@ class MarketOrMe extends StatelessWidget { return Expanded(child: MeWidget()); } - Widget buildMarketContent(BuildContext context) { - return Container(); - } + Widget buildMarketContent(BuildContext context) => Expanded(child: MarketWidget()); } diff --git a/app/lib/widgets/market.dart b/app/lib/widgets/market.dart new file mode 100644 index 0000000..33886a7 --- /dev/null +++ b/app/lib/widgets/market.dart @@ -0,0 +1,102 @@ +import 'package:dde_gesture_manager/constants/constants.dart'; +import 'package:dde_gesture_manager/widgets/dde_button.dart'; +import 'package:flutter/material.dart'; +import 'package:dde_gesture_manager/extensions.dart'; + +enum MarketSortType { + recommend, + updated, + likes, + downloads, +} + +class MarketWidget extends StatefulWidget { + const MarketWidget({Key? key}) : super(key: key); + + @override + _MarketWidgetState createState() => _MarketWidgetState(); +} + +class _MarketWidgetState extends State { + MarketSortType _type = MarketSortType.recommend; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(top: 15, bottom: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MarketSortType.recommend, + MarketSortType.updated, + MarketSortType.likes, + MarketSortType.downloads, + ] + .map( + (e) => Flexible( + flex: 1, + fit: FlexFit.tight, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + setState(() { + _type = e; + }); + }, + child: Center( + child: Text( + '${LocaleKeys.market_sort_types}.${e.name}'.tr(), + style: _type == e ? TextStyle(fontWeight: FontWeight.bold, fontSize: 15) : null, + ), + ), + ), + ), + ), + ) + .toList(), + ), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + border: Border.all( + width: .3, + color: context.t.dividerColor, + ), + borderRadius: BorderRadius.circular(defaultBorderRadius), + ), + child: Column( + children: [Text('asd')], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DButton.like( + enabled: true, + onTap: () {}, + ), + DButton.download( + enabled: true, + onTap: () {}, + ), + ] + .map((e) => Padding( + padding: const EdgeInsets.only(right: 4), + child: e, + )) + .toList(), + ), + ), + ], + ); + } +} diff --git a/app/lib/widgets/me.dart b/app/lib/widgets/me.dart index 978c239..c8d00d5 100644 --- a/app/lib/widgets/me.dart +++ b/app/lib/widgets/me.dart @@ -1,4 +1,5 @@ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:collection/collection.dart'; import 'package:dde_gesture_manager/constants/constants.dart'; import 'package:dde_gesture_manager/extensions.dart'; @@ -9,6 +10,8 @@ import 'package:dde_gesture_manager/utils/notificator.dart'; import 'package:dde_gesture_manager_api/models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_alert/flutter_platform_alert.dart'; +import 'package:markdown_editor_ot/markdown_editor.dart'; +import 'package:numeral/fun.dart'; import 'dde_button.dart'; @@ -137,60 +140,102 @@ class _MeWidgetState extends State { ), borderRadius: BorderRadius.circular(defaultBorderRadius), ), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 1, vertical: 2), - child: ListView.builder( - itemBuilder: (context, index) => GestureDetector( - onTap: () { - setState(() { - _selected = _schemes[index].uuid; - }); - }, - child: MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) { - setState(() { - _hovering = _schemes[index].uuid; - }); - }, - child: Container( - color: _getItemBackgroundColor(index, _schemes[index].uuid), - child: Padding( - padding: const EdgeInsets.only(left: 6, right: 12.0), - child: DefaultTextStyle( - style: context.t.textTheme.bodyText2!.copyWith( - color: _selected == _schemes[index].uuid ? Colors.white : null, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(_schemes[index].name ?? ''), - Row( - children: [ - Text('${_schemes[index].downloads ?? 0}'.padLeft(4)), - Icon( - Icons.file_download, - size: 18, - ), - Text('${_schemes[index].likes ?? 0}'.padLeft(4)), - Icon(_schemes[index].liked == true ? Icons.thumb_up : Icons.thumb_up_off_alt, - size: 17), - ] - .map((e) => Padding( - padding: const EdgeInsets.only(right: 3), - child: e, - )) - .toList(), + child: Column( + children: [ + Flexible( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 1, vertical: 2), + child: ListView.builder( + itemBuilder: (context, index) => GestureDetector( + onTap: () { + setState(() { + _selected = _schemes[index].uuid; + }); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) { + setState(() { + _hovering = _schemes[index].uuid; + }); + }, + child: Container( + color: _getItemBackgroundColor(index, _schemes[index].uuid), + child: Padding( + padding: const EdgeInsets.only(left: 6, right: 12.0), + child: DefaultTextStyle( + style: context.t.textTheme.bodyText2!.copyWith( + color: _selected == _schemes[index].uuid ? Colors.white : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(_schemes[index].name ?? ''), + Row( + children: [ + SizedBox( + width: 50, + child: Align( + alignment: Alignment.centerRight, + child: AutoSizeText( + '${numeral(_schemes[index].downloads ?? 0, fractionDigits: 1)}' + .padLeft(5), + ), + ), + ), + Icon( + Icons.file_download, + size: 18, + ), + SizedBox( + width: 50, + child: Align( + alignment: Alignment.centerRight, + child: AutoSizeText( + '${numeral(_schemes[index].likes ?? 0, fractionDigits: 1)}'.padLeft(5), + ), + ), + ), + Icon(_schemes[index].liked == true ? Icons.thumb_up : Icons.thumb_up_off_alt, + size: 17), + ] + .map((e) => Padding( + padding: const EdgeInsets.only(right: 3), + child: e, + )) + .toList(), + ), + ], + ), ), - ], + ), ), ), ), + itemCount: _schemes.length, ), ), ), - itemCount: _schemes.length, - ), + Divider(thickness: .5), + Flexible( + child: Padding( + padding: const EdgeInsets.only(left: 8), + child: MdPreview( + text: _schemes.firstWhereOrNull((e) => e.uuid == _selected)?.description ?? '', + widgetImage: (imageUrl) => CachedNetworkImage( + imageUrl: imageUrl, + placeholder: (context, url) => const SizedBox( + width: double.infinity, + height: 300, + child: Center(child: CircularProgressIndicator()), + ), + errorWidget: (context, url, error) => const Icon(Icons.error), + ), + onCodeCopied: () {}, + ), + ), + ) + ], ), ), ), diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 410e658..09381ad 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -23,25 +23,26 @@ environment: dependencies: flutter: sdk: flutter - cupertino_icons: ^1.0.2 - window_manager: ^0.0.5 + cupertino_icons: ^1.0.4 + window_manager: ^0.1.3 localstorage: ^4.0.0+1 - shared_preferences: ^2.0.7 - xdg_directories: 0.2.0 - gsettings: 0.2.0 - provider: ^6.0.0 - package_info_plus: ^1.0.6 + shared_preferences: ^2.0.12 + xdg_directories: ^0.2.0 + gsettings: 0.2.3 + provider: ^6.0.2 + package_info_plus: ^1.3.0 easy_localization: ^3.0.0 glass_kit: ^2.0.1 rect_getter: ^1.0.0 - path_provider: ^2.0.5 + path_provider: ^2.0.8 uuid: ^3.0.5 adaptive_scrollbar: ^2.1.0 flutter_platform_alert: ^0.2.1 cached_network_image: ^3.2.0 - url_launcher: ^6.0.17 + url_launcher: ^6.0.18 flutter_login: ^3.1.0 auto_size_text: ^3.0.0 + numeral: ^1.2.5 markdown_editor_ot: path: 3rd_party/markdown_editor_ot cherry_toast: @@ -54,8 +55,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: 2.1.2 - source_gen: 1.1.0 + build_runner: 2.1.7 + source_gen: 1.2.1 # For information on the generic Dart part of this file, see the diff --git a/app/resources/langs/en.json b/app/resources/langs/en.json index e6ae3be..dabc42f 100644 --- a/app/resources/langs/en.json +++ b/app/resources/langs/en.json @@ -20,7 +20,13 @@ "tip": "Display help documentation" }, "market": { - "title": "Scheme market" + "title": "Scheme market", + "sort_types": { + "recommend": "Recommend", + "updated": "updated", + "likes": "likes", + "downloads": "downloads" + } }, "local_manager": { "title": "Local scheme management", diff --git a/app/resources/langs/zh-CN.json b/app/resources/langs/zh-CN.json index 467bd79..d842369 100644 --- a/app/resources/langs/zh-CN.json +++ b/app/resources/langs/zh-CN.json @@ -20,7 +20,13 @@ "tip": "显示帮助文档" }, "market": { - "title": "方案市场" + "title": "方案市场", + "sort_types": { + "recommend": "推荐", + "updated": "最近更新", + "likes": "点赞数", + "downloads": "下载量" + } }, "local_manager": { "title": "本地方案管理",