@ -1,14 +1,18 @@
|
|||||||
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 ./ ./
|
COPY ./ ./
|
||||||
|
|
||||||
# Install dependencies, pre-build
|
|
||||||
RUN pub get
|
RUN pub get
|
||||||
|
RUN dart compile exe bin/prod.dart -o /server
|
||||||
|
ENTRYPOINT ["dart", "bin/migrate.dart", "up"]
|
||||||
|
|
||||||
# Optionally build generaed sources.
|
FROM scratch
|
||||||
# RUN pub run build_runner build
|
WORKDIR /app
|
||||||
|
|
||||||
# Set environment, start server
|
|
||||||
ENV ANGEL_ENV=production
|
ENV ANGEL_ENV=production
|
||||||
|
ADD ./views ./views
|
||||||
|
ADD ./config ./config
|
||||||
|
COPY --from=build-env /runtime/ /
|
||||||
|
COPY --from=build-env /server /app
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD dart bin/prod.dart
|
ENTRYPOINT ["./server", "-a", "0.0.0.0", "--port", "3000"]
|
@ -0,0 +1,45 @@
|
|||||||
|
version: "2.4"
|
||||||
|
|
||||||
|
services:
|
||||||
|
kv:
|
||||||
|
image: redis:alpine
|
||||||
|
restart: always
|
||||||
|
container_name: dgm_redis
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
networks:
|
||||||
|
- dgm_api_default
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:alpine
|
||||||
|
restart: always
|
||||||
|
container_name: dgm_postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: gesture_manager
|
||||||
|
POSTGRES_PASSWORD: gesture_manager_secret
|
||||||
|
volumes:
|
||||||
|
- ../db_data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
networks:
|
||||||
|
- dgm_api_default
|
||||||
|
|
||||||
|
api:
|
||||||
|
build: .
|
||||||
|
image: dgm_api_image
|
||||||
|
container_name: dgm_api
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- kv
|
||||||
|
links:
|
||||||
|
- db:db
|
||||||
|
- kv:kv
|
||||||
|
networks:
|
||||||
|
- dgm_api_default
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dgm_api_default:
|
||||||
|
name: dgm_api_default
|
@ -1,3 +1,4 @@
|
|||||||
export 'src/models/user.dart';
|
export 'src/models/user.dart';
|
||||||
export 'src/models/app_version.dart';
|
export 'src/models/app_version.dart';
|
||||||
export 'src/models/login_success.dart';
|
export 'src/models/login_success.dart';
|
||||||
|
export 'src/models/scheme.dart';
|
@ -0,0 +1,19 @@
|
|||||||
|
import 'package:angel3_orm/angel3_orm.dart';
|
||||||
|
import 'package:angel3_serialize/angel3_serialize.dart';
|
||||||
|
import 'package:dde_gesture_manager_api/src/models/base_model.dart';
|
||||||
|
import 'package:angel3_migration/angel3_migration.dart';
|
||||||
|
import 'package:optional/optional.dart';
|
||||||
|
|
||||||
|
part 'download_history.g.dart';
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
@orm
|
||||||
|
abstract class _DownloadHistory extends BaseModel {
|
||||||
|
@Column(isNullable: false, indexType: IndexType.standardIndex)
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
int? get uid;
|
||||||
|
|
||||||
|
@Column(isNullable: false, indexType: IndexType.standardIndex)
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
int? get schemeId;
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
import 'package:angel3_orm/angel3_orm.dart';
|
||||||
|
import 'package:angel3_serialize/angel3_serialize.dart';
|
||||||
|
import 'package:dde_gesture_manager_api/src/models/base_model.dart';
|
||||||
|
import 'package:angel3_migration/angel3_migration.dart';
|
||||||
|
import 'package:optional/optional.dart';
|
||||||
|
|
||||||
|
part 'like_record.g.dart';
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
@orm
|
||||||
|
abstract class _LikeRecord extends BaseModel {
|
||||||
|
@Column(isNullable: false, indexType: IndexType.standardIndex)
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
int? get uid;
|
||||||
|
|
||||||
|
@Column(isNullable: false, indexType: IndexType.standardIndex)
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
int? get schemeId;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
@ -0,0 +1,181 @@
|
|||||||
|
import 'package:angel3_orm/angel3_orm.dart';
|
||||||
|
import 'package:angel3_serialize/angel3_serialize.dart';
|
||||||
|
import 'package:dde_gesture_manager_api/src/models/base_model.dart';
|
||||||
|
import 'package:angel3_migration/angel3_migration.dart';
|
||||||
|
import 'package:optional/optional.dart';
|
||||||
|
|
||||||
|
part 'scheme.g.dart';
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
@orm
|
||||||
|
abstract class _Scheme extends BaseModel {
|
||||||
|
@Column(isNullable: false, indexType: IndexType.unique)
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
String? get uuid;
|
||||||
|
|
||||||
|
@Column(isNullable: false)
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
String? get name;
|
||||||
|
|
||||||
|
@Column(isNullable: false, indexType: IndexType.standardIndex)
|
||||||
|
@SerializableField(isNullable: true, exclude: true)
|
||||||
|
int? uid;
|
||||||
|
|
||||||
|
@Column(type: ColumnType.text)
|
||||||
|
String? description;
|
||||||
|
|
||||||
|
@Column(isNullable: false, indexType: IndexType.standardIndex)
|
||||||
|
@SerializableField(defaultValue: false, isNullable: false)
|
||||||
|
bool? get shared;
|
||||||
|
|
||||||
|
@Column(type: ColumnType.jsonb)
|
||||||
|
@SerializableField()
|
||||||
|
@DefaultsTo([])
|
||||||
|
List? get gestures;
|
||||||
|
}
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
@Orm(tableName: 'schemes', generateMigrations: false)
|
||||||
|
abstract class _SimpleScheme {
|
||||||
|
@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(isNullable: false, indexType: IndexType.standardIndex)
|
||||||
|
@SerializableField(isNullable: true, exclude: true)
|
||||||
|
int? uid;
|
||||||
|
|
||||||
|
@Column(type: ColumnType.text)
|
||||||
|
String? description;
|
||||||
|
|
||||||
|
@Column(isNullable: false, indexType: IndexType.standardIndex)
|
||||||
|
@SerializableField(defaultValue: false, isNullable: false)
|
||||||
|
bool? get shared;
|
||||||
|
|
||||||
|
@SerializableField(isNullable: true)
|
||||||
|
@Column(type: ColumnType.json)
|
||||||
|
Map<String, dynamic>? get metadata;
|
||||||
|
|
||||||
|
@SerializableField(isNullable: true)
|
||||||
|
@Column(expression: 'lr.liked')
|
||||||
|
bool? get liked;
|
||||||
|
}
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
abstract class _SimpleSchemeTransMetaData {
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
int? id;
|
||||||
|
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
String? get uuid;
|
||||||
|
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
String? get name;
|
||||||
|
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
String? description;
|
||||||
|
|
||||||
|
@SerializableField(defaultValue: false, isNullable: false)
|
||||||
|
bool? get shared;
|
||||||
|
|
||||||
|
int? get downloads;
|
||||||
|
|
||||||
|
int? get likes;
|
||||||
|
|
||||||
|
bool? get liked;
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleSchemeTransMetaData transSimpleSchemeMetaData(SimpleScheme scheme) => SimpleSchemeTransMetaData(
|
||||||
|
id: scheme.id,
|
||||||
|
description: scheme.description,
|
||||||
|
uuid: scheme.uuid,
|
||||||
|
name: scheme.name,
|
||||||
|
shared: scheme.shared,
|
||||||
|
liked: scheme.liked,
|
||||||
|
likes: scheme.metadata?['likes'] ?? 0,
|
||||||
|
downloads: scheme.metadata?['downloads'] ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
@serializable
|
||||||
|
abstract class _SchemeForDownload {
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
String? get uuid;
|
||||||
|
|
||||||
|
@SerializableField(isNullable: false)
|
||||||
|
String? get name;
|
||||||
|
|
||||||
|
@Column(type: ColumnType.text)
|
||||||
|
String? description;
|
||||||
|
|
||||||
|
@SerializableField()
|
||||||
|
@DefaultsTo([])
|
||||||
|
List? get gestures;
|
||||||
|
}
|
||||||
|
|
||||||
|
SchemeForDownload transSchemeForDownload(Scheme scheme) => SchemeForDownload(
|
||||||
|
uuid: scheme.uuid,
|
||||||
|
name: scheme.name,
|
||||||
|
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<String, dynamic>? 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,
|
||||||
|
);
|
@ -0,0 +1,250 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:angel3_framework/angel3_framework.dart';
|
||||||
|
import 'package:dde_gesture_manager_api/apis.dart';
|
||||||
|
import 'package:dde_gesture_manager_api/src/models/download_history.dart';
|
||||||
|
import 'package:dde_gesture_manager_api/src/models/like_record.dart';
|
||||||
|
import 'package:dde_gesture_manager_api/src/models/scheme.dart';
|
||||||
|
import 'package:dde_gesture_manager_api/src/routes/controllers/middlewares.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'controller_extensions.dart';
|
||||||
|
|
||||||
|
Future configureServer(Angel app) async {
|
||||||
|
final _log = Logger('scheme_controller');
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
Apis.scheme.upload,
|
||||||
|
chain(
|
||||||
|
[
|
||||||
|
jwtMiddleware(),
|
||||||
|
(req, res) async {
|
||||||
|
try {
|
||||||
|
var scheme = SchemeSerializer.fromMap(req.bodyAsMap);
|
||||||
|
var schemeQuery = SchemeQuery();
|
||||||
|
schemeQuery.where!.uuid.equals(scheme.uuid!);
|
||||||
|
req.queryExecutor.transaction((tx) async {
|
||||||
|
var one = await schemeQuery.getOne(tx);
|
||||||
|
schemeQuery = SchemeQuery();
|
||||||
|
schemeQuery.values.copyFrom(scheme);
|
||||||
|
schemeQuery.values.uid = req.user!.idAsInt;
|
||||||
|
if (one.isEmpty) {
|
||||||
|
return await schemeQuery.insert(tx);
|
||||||
|
} else {
|
||||||
|
schemeQuery.whereId = one.value.idAsInt;
|
||||||
|
return await schemeQuery.updateOne(tx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
_log.severe(e);
|
||||||
|
return res.unProcessableEntity();
|
||||||
|
}
|
||||||
|
return res.noContent();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
Apis.scheme.markAsShared.route,
|
||||||
|
chain(
|
||||||
|
[
|
||||||
|
jwtMiddleware(),
|
||||||
|
(req, res) async {
|
||||||
|
var schemeId = req.params['schemeId'];
|
||||||
|
var schemeQuery = SchemeQuery();
|
||||||
|
schemeQuery.where!.uuid.equals(schemeId);
|
||||||
|
schemeQuery.values.shared = true;
|
||||||
|
await schemeQuery.updateOne(req.queryExecutor);
|
||||||
|
return res.noContent();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
Apis.scheme.download.route,
|
||||||
|
chain(
|
||||||
|
[
|
||||||
|
jwtMiddleware(ignoreError: true),
|
||||||
|
(req, res) async {
|
||||||
|
var schemeQuery = SchemeQuery();
|
||||||
|
schemeQuery.where?.uuid.equals(req.params['schemeId']);
|
||||||
|
var optionalScheme = await schemeQuery.getOne(req.queryExecutor);
|
||||||
|
if (optionalScheme.isNotEmpty) {
|
||||||
|
var scheme = optionalScheme.value;
|
||||||
|
if (req.user != null) {
|
||||||
|
/// 增加用户下载记录
|
||||||
|
var downloadHistoryQuery = DownloadHistoryQuery();
|
||||||
|
downloadHistoryQuery.where?.uid.equals(req.user!.idAsInt);
|
||||||
|
downloadHistoryQuery.where?.schemeId.equals(scheme.idAsInt);
|
||||||
|
var notExist = (await downloadHistoryQuery.getOne(req.queryExecutor)).isEmpty;
|
||||||
|
if (notExist) {
|
||||||
|
downloadHistoryQuery = DownloadHistoryQuery();
|
||||||
|
downloadHistoryQuery.values.copyFrom(DownloadHistory(uid: req.user!.idAsInt, schemeId: scheme.idAsInt));
|
||||||
|
await downloadHistoryQuery.insert(req.queryExecutor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 增加方案的下载数量
|
||||||
|
schemeQuery = SchemeQuery();
|
||||||
|
schemeQuery.whereId = scheme.idAsInt;
|
||||||
|
Map<String, dynamic> metadata = Map.from(scheme.metadata!);
|
||||||
|
metadata.update('downloads', (value) => ++value, ifAbsent: () => 1);
|
||||||
|
schemeQuery.values.metadata = metadata;
|
||||||
|
schemeQuery.updateOne(req.queryExecutor);
|
||||||
|
|
||||||
|
return res.json(transSchemeForDownload(scheme));
|
||||||
|
}
|
||||||
|
return res.notFound();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
Apis.scheme.like.route,
|
||||||
|
chain(
|
||||||
|
[
|
||||||
|
jwtMiddleware(),
|
||||||
|
(req, res) async {
|
||||||
|
bool isLike = req.params['isLike'] == 'like';
|
||||||
|
bool needUpdate = true;
|
||||||
|
var schemeQuery = SchemeQuery();
|
||||||
|
schemeQuery.where?.uuid.equals(req.params['schemeId']);
|
||||||
|
var optionalScheme = await schemeQuery.getOne(req.queryExecutor);
|
||||||
|
if (optionalScheme.isNotEmpty) {
|
||||||
|
var scheme = optionalScheme.value;
|
||||||
|
if (req.user != null) {
|
||||||
|
/// 增加用户点赞记录
|
||||||
|
var likeRecordQuery = LikeRecordQuery();
|
||||||
|
likeRecordQuery.where?.uid.equals(req.user!.idAsInt);
|
||||||
|
likeRecordQuery.where?.schemeId.equals(scheme.idAsInt);
|
||||||
|
var likeRecordCheck = await likeRecordQuery.getOne(req.queryExecutor);
|
||||||
|
likeRecordQuery = LikeRecordQuery();
|
||||||
|
likeRecordQuery.values
|
||||||
|
.copyFrom(LikeRecord(uid: req.user!.idAsInt, schemeId: scheme.idAsInt, liked: isLike));
|
||||||
|
if (likeRecordCheck.isEmpty) {
|
||||||
|
likeRecordQuery.insert(req.queryExecutor);
|
||||||
|
} else if (likeRecordCheck.value.liked != isLike) {
|
||||||
|
likeRecordQuery.whereId = likeRecordCheck.value.idAsInt;
|
||||||
|
likeRecordQuery.updateOne(req.queryExecutor);
|
||||||
|
} else {
|
||||||
|
needUpdate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needUpdate) {
|
||||||
|
/// 增加/减少方案的点赞数量
|
||||||
|
schemeQuery = SchemeQuery();
|
||||||
|
schemeQuery.whereId = scheme.idAsInt;
|
||||||
|
Map<String, dynamic> metadata = Map.from(scheme.metadata!);
|
||||||
|
metadata.update('likes', (value) => isLike ? ++value : --value, ifAbsent: () => 1);
|
||||||
|
schemeQuery.values.metadata = metadata;
|
||||||
|
schemeQuery.updateOne(req.queryExecutor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.noContent();
|
||||||
|
}
|
||||||
|
return res.notFound();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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<String> 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)
|
||||||
|
..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()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
docker-compose build
|
||||||
|
docker-compose up -d
|
||||||
|
MIGRATION_IMAGE="$(docker image ls --filter label=stage=dart_builder -q)"
|
||||||
|
docker run --name=dgm_api_migrate --network dgm_api_default "$MIGRATION_IMAGE"
|
||||||
|
docker rm "$(docker ps -a --filter name=dgm_api_migrate -q)"
|
||||||
|
docker image prune -f --filter label=stage=dart_builder
|
@ -0,0 +1,45 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
class _SimpleThrottleNode {
|
||||||
|
int funcHashCode;
|
||||||
|
int timestamp;
|
||||||
|
|
||||||
|
_SimpleThrottleNode(this.funcHashCode, this.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef void _VoidFunc();
|
||||||
|
|
||||||
|
final _simpleThrottleQueue = Queue();
|
||||||
|
|
||||||
|
/// Usage: If you have a function : test(int n) => n;
|
||||||
|
/// you can use SimpleThrottle.throttledFunc(test)?.call(1) to make it throttled
|
||||||
|
/// this will return function's return value if last call time over the timeout
|
||||||
|
/// otherwise this will return null.
|
||||||
|
/// If your function is a 'void function()', you can use SimpleThrottle.invoke(func) to call it throttled,
|
||||||
|
/// and you can get a throttled function by SimpleThrottle.bind(func) if you do not call it immediately.
|
||||||
|
class SimpleThrottle {
|
||||||
|
static T? throttledFunc<T extends Function>(T func,
|
||||||
|
{String? funcKey, Duration timeout = const Duration(seconds: 1)}) {
|
||||||
|
var node = _simpleThrottleQueue.firstWhereOrNull((element) => element.funcHashCode == (funcKey ?? func).hashCode);
|
||||||
|
if (node != null) {
|
||||||
|
if (DateTime.now().millisecondsSinceEpoch - node.timestamp < timeout.inMilliseconds)
|
||||||
|
return null;
|
||||||
|
else
|
||||||
|
node.timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
} else {
|
||||||
|
_simpleThrottleQueue.add(_SimpleThrottleNode((funcKey ?? func).hashCode, DateTime.now().millisecondsSinceEpoch));
|
||||||
|
while (_simpleThrottleQueue.length > 16) {
|
||||||
|
_simpleThrottleQueue.removeFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return func;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void invoke(_VoidFunc func, {String? funcKey, Duration timeout = const Duration(seconds: 1)}) =>
|
||||||
|
throttledFunc(func, timeout: timeout, funcKey: funcKey)?.call();
|
||||||
|
|
||||||
|
static _VoidFunc bind(_VoidFunc func, {String? funcKey, Duration timeout = const Duration(seconds: 1)}) =>
|
||||||
|
() => invoke(func, timeout: timeout, funcKey: funcKey);
|
||||||
|
}
|
@ -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<MarketWidget> {
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,293 @@
|
|||||||
|
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';
|
||||||
|
import 'package:dde_gesture_manager/http/api.dart';
|
||||||
|
import 'package:dde_gesture_manager/models/configs.provider.dart';
|
||||||
|
import 'package:dde_gesture_manager/models/settings.provider.dart';
|
||||||
|
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';
|
||||||
|
|
||||||
|
enum SchemeListType {
|
||||||
|
uploaded,
|
||||||
|
downloaded,
|
||||||
|
liked,
|
||||||
|
}
|
||||||
|
|
||||||
|
class MeWidget extends StatefulWidget {
|
||||||
|
const MeWidget({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_MeWidgetState createState() => _MeWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MeWidgetState extends State<MeWidget> {
|
||||||
|
List<SimpleSchemeTransMetaData> _schemes = [];
|
||||||
|
SchemeListType _type = SchemeListType.uploaded;
|
||||||
|
String? _selected;
|
||||||
|
String? _hovering;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
Api.userSchemes(type: _type).then((value) {
|
||||||
|
if (mounted && value != null)
|
||||||
|
setState(() {
|
||||||
|
_schemes = value;
|
||||||
|
_selected = value.isNotEmpty ? value.first.uuid : null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getItemBackgroundColor(int index, String? schemeId) {
|
||||||
|
Color _color = index % 2 == 0 ? context.t.scaffoldBackgroundColor : context.t.backgroundColor;
|
||||||
|
if (schemeId == _hovering) _color = context.t.dialogBackgroundColor;
|
||||||
|
if (schemeId == _selected) _color = context.read<SettingsProvider>().currentActiveColor;
|
||||||
|
return _color;
|
||||||
|
}
|
||||||
|
|
||||||
|
_refreshList() {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
Api.userSchemes(type: _type).then((value) {
|
||||||
|
if (mounted && value != null)
|
||||||
|
setState(() {
|
||||||
|
_schemes = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var currentSelectedScheme = _schemes.firstWhereOrNull((e) => e.uuid == _selected);
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(top: 10),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.person, size: defaultButtonHeight),
|
||||||
|
Flexible(
|
||||||
|
child: AutoSizeText(
|
||||||
|
context.watch<ConfigsProvider>().email ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DButton.logout(
|
||||||
|
enabled: true,
|
||||||
|
onTap: () => context.read<ConfigsProvider>().setProps(accessToken: '', email: ''),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 3, bottom: 2),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
SchemeListType.uploaded,
|
||||||
|
SchemeListType.downloaded,
|
||||||
|
SchemeListType.liked,
|
||||||
|
]
|
||||||
|
.map(
|
||||||
|
(e) => Flexible(
|
||||||
|
flex: 1,
|
||||||
|
fit: FlexFit.tight,
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_type = e;
|
||||||
|
});
|
||||||
|
Api.userSchemes(type: e).then((value) {
|
||||||
|
if (mounted && value != null)
|
||||||
|
setState(() {
|
||||||
|
_schemes = value;
|
||||||
|
_selected = value.first.uuid;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'${LocaleKeys.me_scheme_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: [
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 5),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (_type == SchemeListType.uploaded)
|
||||||
|
DButton.share(
|
||||||
|
enabled: currentSelectedScheme?.shared == false,
|
||||||
|
onTap: () {
|
||||||
|
Notificator.showConfirm(
|
||||||
|
title: LocaleKeys.info_share_title.tr(),
|
||||||
|
description: LocaleKeys.info_share_description.tr())
|
||||||
|
.then((value) {
|
||||||
|
if (value == CustomButton.positiveButton) {
|
||||||
|
Notificator.success(context, title: LocaleKeys.info_share_success.tr());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DButton.like(
|
||||||
|
enabled: true,
|
||||||
|
onTap: () {
|
||||||
|
Api.likeScheme(schemeId: currentSelectedScheme!.uuid!, isLike: !currentSelectedScheme.liked!)
|
||||||
|
.then((value) {
|
||||||
|
if (value) {
|
||||||
|
_refreshList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DButton.download(
|
||||||
|
enabled: true,
|
||||||
|
onTap: () {
|
||||||
|
Api.downloadScheme(schemeId: currentSelectedScheme!.uuid!).then((value) {
|
||||||
|
value.sout();
|
||||||
|
_refreshList();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.map((e) => Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 4),
|
||||||
|
child: e,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue