Merge pull request 'docker' (#6) from dev into master

Reviewed-on: #6
master
DebuggerX 3 years ago
commit 24ab0e593a

@ -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 ./ ./
# Install dependencies, pre-build
RUN pub get
RUN dart compile exe bin/prod.dart -o /server
ENTRYPOINT ["dart", "bin/migrate.dart", "up"]
# 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"]

@ -1,10 +1,12 @@
import 'package:angel3_migration/angel3_migration.dart';
import 'package:angel3_orm_postgres/angel3_orm_postgres.dart';
import 'package:dde_gesture_manager_api/src/config/plugins/orm.dart';
import 'package:dde_gesture_manager_api/models.dart';
import 'package:angel3_configuration/angel3_configuration.dart';
import 'package:angel3_migration/angel3_migration.dart';
import 'package:angel3_migration_runner/angel3_migration_runner.dart';
import 'package:angel3_migration_runner/postgres.dart';
import 'package:angel3_orm_postgres/angel3_orm_postgres.dart';
import 'package:dde_gesture_manager_api/models.dart';
import 'package:dde_gesture_manager_api/src/config/plugins/orm.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:file/local.dart';
import 'package:logging/logging.dart';
@ -28,6 +30,9 @@ void main(List<String> args) async {
var migrationRunner = PostgresMigrationRunner(connection, migrations: [
UserMigration(),
UserSeed(),
SchemeMigration(),
DownloadHistoryMigration(),
LikeRecordMigration(),
]);
await runMigrations(migrationRunner, args);
}

@ -2,12 +2,17 @@
host: 127.0.0.1
port: 3000
postgres:
host: localhost
host: db
port: 5432
database_name: appdb
username: appuser
password: App1970#
useSSL: false
time_zone: UTC
database_name: gesture_manager
username: postgres
password: gesture_manager_secret
use_ssl: false
time_zone: Asia/Shanghai
redis:
host: kv
port: 6379
jwt_secret: "OvA9SBLnncot8gFHvt8Gh1qkQ1ptGIQW"
password_salt: "test"
password_salt: "Z5b84rrgsKmfNFNRExAC4BCJe5aZPdJq"

@ -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

@ -7,6 +7,7 @@ class Apis {
static final system = SystemApis();
static final auth = AuthApis();
static final scheme = SchemeApis();
}
class AuthApis {
@ -15,6 +16,8 @@ class AuthApis {
String get loginOrSignup => [path, 'login_or_signup'].joinPath();
String confirmSignup({required StringParam accessKey}) => [path, 'confirm_sign_up', accessKey].joinPath();
String get status => [path, 'status'].joinPath();
}
class SystemApis {
@ -23,7 +26,26 @@ class SystemApis {
String get appVersion => [path, 'app-version'].joinPath();
}
class SchemeApis {
static final String path = '/scheme';
String get upload => [path, 'upload'].joinPath();
String markAsShared({required StringParam schemeId}) => [path, 'mark_as_shared', schemeId].joinPath();
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 = {
'BoolParam': BoolParam.nameOnRoute,
'IntParam': IntParam.nameOnRoute,
'DoubleParam': DoubleParam.nameOnRoute,
'StringParam': StringParam.nameOnRoute,
@ -47,6 +69,18 @@ extension RouteUrl on Function {
}
}
class BoolParam {
final bool val;
String? name;
BoolParam(this.val);
BoolParam.nameOnRoute(this.name) : val = true;
@override
String toString() => name == null ? val.toString() : 'bool:$name';
}
class IntParam {
final int val;
String? name;
@ -83,6 +117,10 @@ class StringParam {
String toString() => name == null ? val.toString() : ':$name';
}
extension BoolParamExt on bool {
BoolParam get param => BoolParam(this);
}
extension IntParamExt on int {
IntParam get param => IntParam(this);
}

@ -1,3 +1,4 @@
export 'src/models/user.dart';
export 'src/models/app_version.dart';
export 'src/models/login_success.dart';
export 'src/models/scheme.dart';

@ -9,7 +9,7 @@ Future<void> configureServer(Angel app) async {
allowCookie: false,
deserializer: (p) async => (UserQuery()..where!.id.equals(int.parse(p)))
.getOne(app.container!.make<orm.QueryExecutor>())
.then((value) => value.value),
.then((value) => value.isNotEmpty ? value.value : User(email: '')),
serializer: (p) => p.id ?? '',
);
await auth.configureServer(app);

@ -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,
);

@ -20,5 +20,9 @@ abstract class _User extends BaseModel {
@SerializableField(isNullable: true, exclude: true)
String? get password;
@Column(isNullable: false)
@SerializableField(defaultValue: false)
bool? get blocked;
String secret(String salt) => base64.encode(Hmac(sha256, salt.codeUnits).convert((password ?? '').codeUnits).bytes);
}

@ -5,6 +5,7 @@ import 'package:angel3_auth/angel3_auth.dart';
import 'package:angel3_framework/angel3_framework.dart';
import 'package:dde_gesture_manager_api/apis.dart';
import 'package:dde_gesture_manager_api/models.dart';
import 'package:dde_gesture_manager_api/src/routes/controllers/middlewares.dart';
import 'package:mailer/mailer.dart';
import 'package:mailer/smtp_server.dart';
import 'package:uuid/uuid.dart';
@ -48,6 +49,8 @@ Future configureServer(Angel app) async {
return res.notFound();
} else if (user.value.password != userParams.password) {
return res.unauthorized();
} else if (user.value.blocked == true) {
return res.forbidden();
} else {
var angelAuth = req.container!.make<AngelAuth>();
await angelAuth.loginById(user.value.id!, req, res);
@ -75,4 +78,14 @@ Future configureServer(Angel app) async {
}
return res.render('sign_up_result.html', {'success': false});
});
app.get(
Apis.auth.status,
chain(
[
jwtMiddleware(),
(req, res) => req.user!.blocked == false ? res.noContent() : res.forbidden(),
],
),
);
}

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel3_orm/angel3_orm.dart' as orm;
import 'package:dde_gesture_manager_api/models.dart';
import 'package:dde_gesture_manager_api/src/config/plugins/redis_cache.dart';
import 'package:neat_cache/neat_cache.dart';
@ -20,6 +21,16 @@ extension ResponseNoContent on ResponseContext {
statusCode = HttpStatus.unauthorized;
return close();
}
forbidden() {
statusCode = HttpStatus.forbidden;
return close();
}
unProcessableEntity() {
statusCode = HttpStatus.unprocessableEntity;
return close();
}
}
extension QueryWhereId on orm.Query {
@ -32,6 +43,16 @@ extension QueryExecutor on RequestContext {
orm.QueryExecutor get queryExecutor => container!.make<orm.QueryExecutor>();
}
extension RedisExecutor on RequestContext {
extension RedisClient on RequestContext {
Cache get cache => container!.make<RedisCache>().cache;
}
extension JWTUserInstance on RequestContext {
User? get user {
try {
return container!.make<User>();
} catch (_) {
return null;
}
}
}

@ -2,16 +2,16 @@ import 'package:angel3_auth/angel3_auth.dart';
import 'package:angel3_framework/angel3_framework.dart';
import 'package:dde_gesture_manager_api/models.dart';
import '../controllers/controller_extensions.dart';
RequestHandler jwtMiddleware() {
RequestHandler jwtMiddleware({ignoreError = false}) {
return (RequestContext req, ResponseContext res, {bool throwError = true}) async {
bool _reject(ResponseContext res) {
bool _reject(ResponseContext res, [ignoreError = false]) {
if (ignoreError) return true;
if (throwError) {
res.statusCode = 403;
throw AngelHttpException.forbidden();
} else {
return false;
res.forbidden();
}
return false;
}
if (req.container != null) {
@ -19,17 +19,22 @@ RequestHandler jwtMiddleware() {
if (reqContainer.has<User>() || req.method == 'OPTIONS') {
return true;
} else if (reqContainer.has<Future<User>>()) {
try {
User user = await reqContainer.makeAsync<User>();
var authToken = req.container!.make<AuthToken>();
if (user.secret(req.app!.configuration['password_salt']) != authToken.payload[UserFields.password]) {
return _reject(res);
return _reject(res, ignoreError);
}
} catch (e) {
if (ignoreError) return true;
rethrow;
}
return true;
} else {
return _reject(res);
return _reject(res, ignoreError);
}
} else {
return _reject(res);
return _reject(res, ignoreError);
}
};
}

@ -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()),
],
),
);
}

@ -2,6 +2,7 @@ import 'package:angel3_framework/angel3_framework.dart';
import 'package:file/file.dart';
import 'controllers/auth_controllers.dart' as auth_controllers;
import 'controllers/system_controllers.dart' as system_controllers;
import 'controllers/scheme_controllers.dart' as scheme_controllers;
/// Put your app routes here!
///
@ -22,6 +23,7 @@ AngelConfigurer configureServer(FileSystem fileSystem) {
// Typically, you want to mount controllers first, after any global middleware.
await app.configure(system_controllers.configureServerWithFileSystem(fileSystem));
await app.configure(auth_controllers.configureServer);
await app.configure(scheme_controllers.configureServer);
// Throw a 404 if no route matched the request.
app.fallback((req, res) => throw AngelHttpException.notFound());

@ -8,13 +8,11 @@ dependencies:
angel3_auth: ^4.0.0
angel3_configuration: ^4.1.0
angel3_framework: ^4.2.0
angel3_migration: ^4.0.0
angel3_orm: ^4.0.0
angel3_orm_postgres: ^3.0.0
angel3_migration: ^4.0.3
angel3_orm: ^4.0.5
angel3_orm_postgres: ^3.3.0
angel3_serialize: ^4.1.0
angel3_production: ^3.1.0
angel3_static: ^4.1.0
angel3_validate: ^4.0.0
angel3_production: ^3.1.2
belatuk_pretty_logging: ^4.0.0
optional: ^6.0.0
logging: ^1.0.0
@ -24,16 +22,13 @@ dependencies:
neat_cache:
path: 3rd_party/neat_cache
dev_dependencies:
angel3_hot: ^4.2.0
angel3_hot: ^4.3.0
angel3_jinja: ^2.0.1
angel3_migration_runner: ^4.0.0
angel3_orm_generator: ^4.1.0
angel3_serialize_generator: ^4.2.0
angel3_migration_runner: ^4.1.1
angel3_orm_generator: ^4.3.0
angel3_serialize_generator: ^4.3.0
angel3_test: ^4.0.0
build_runner: ^2.0.3
io: ^1.0.0
test: ^1.17.5
lints: ^1.0.0

@ -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

@ -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);

@ -3,8 +3,10 @@ import 'dart:io';
import 'package:dde_gesture_manager/constants/sp_keys.dart';
import 'package:dde_gesture_manager/extensions.dart';
import 'package:dde_gesture_manager/models/scheme.dart' as AppScheme;
import 'package:dde_gesture_manager/utils/helper.dart';
import 'package:dde_gesture_manager/utils/notificator.dart';
import 'package:dde_gesture_manager/widgets/me.dart';
import 'package:dde_gesture_manager_api/apis.dart';
import 'package:dde_gesture_manager_api/models.dart';
import 'package:http/http.dart' as http;
@ -13,7 +15,12 @@ typedef T BeanBuilder<T>(Map res);
typedef T HandleRespBuild<T>(http.Response resp);
getStatusCodeFunc<int>(Map resp) => resp["statusCode"];
typedef int GetStatusCodeFunc(Map resp);
int getStatusCodeFunc(Map resp) => resp["statusCode"] as int;
BeanBuilder<List<T>> listRespBuilderWrap<T>(BeanBuilder<T> builder) =>
(Map resp) => (resp['list'] as List).map<T>((e) => builder(e)).toList();
class HttpErrorCode extends Error {
int statusCode;
@ -39,18 +46,20 @@ class Api {
}
}
static HandleRespBuild<T> _handleRespBuild<T>(BeanBuilder<T> builder) => (http.Response resp) {
if (builder == getStatusCodeFunc) return builder({"statusCode": resp.statusCode});
T res;
static HandleRespBuild<T?> _handleRespBuild<T>(BeanBuilder<T> builder) => (http.Response resp) {
if (builder is GetStatusCodeFunc) return builder({"statusCode": resp.statusCode});
T? res;
try {
res = builder(json.decode(resp.body));
var decodeBody = json.decode(utf8.decode(resp.bodyBytes));
res = decodeBody is Map ? builder(decodeBody) : builder({'list': decodeBody});
} catch (e) {
e.sout();
throw HttpErrorCode(resp.statusCode, message: resp.body);
}
return res;
};
static Future<T> _get<T>(
static Future<T?> _get<T>(
String path,
BeanBuilder<T> builder, {
Map<String, dynamic>? queryParams,
@ -67,21 +76,21 @@ class Api {
path: path,
),
headers: <String, String>{
HttpHeaders.contentTypeHeader: ContentType.json.value,
HttpHeaders.contentTypeHeader: ContentType.json.toString(),
}..addAll(
ignoreToken ? {} : {HttpHeaders.authorizationHeader: 'Bearer ${H().sp.getString(SPKeys.accessToken)}'}),
)
.then(
.then<T?>(
_handleRespBuild<T>(builder),
onError: (e) {
if (ignoreErrorHandle)
throw e;
else
return _handleHttpError(e);
_handleHttpError(e);
},
);
static Future<T> _post<T>(
static Future<T?> _post<T>(
String path,
BeanBuilder<T> builder, {
Map<String, dynamic>? body,
@ -98,17 +107,17 @@ class Api {
),
body: jsonEncode(body),
headers: <String, String>{
HttpHeaders.contentTypeHeader: ContentType.json.value,
HttpHeaders.contentTypeHeader: ContentType.json.toString(),
}..addAll(
ignoreToken ? {} : {HttpHeaders.authorizationHeader: 'Bearer ${H().sp.getString(SPKeys.accessToken)}'}),
)
.then(
.then<T?>(
_handleRespBuild<T>(builder),
onError: (e) {
if (ignoreErrorHandle)
throw e;
else
return _handleHttpError(e);
_handleHttpError(e);
},
);
@ -132,4 +141,36 @@ class Api {
ignoreToken: true,
ignoreErrorHandle: ignoreErrorHandle,
);
static Future<bool> checkAuthStatus() => _get<int>(Apis.auth.status, getStatusCodeFunc, ignoreErrorHandle: true)
.then((value) => value == HttpStatus.noContent);
static Future<bool> uploadScheme({required AppScheme.Scheme scheme, required bool share}) => _post(
Apis.scheme.upload,
getStatusCodeFunc,
body: SchemeSerializer.toMap(
Scheme(
name: scheme.name,
uuid: scheme.id,
description: scheme.description,
gestures: scheme.gestures,
shared: share,
),
),
).then((value) => value == HttpStatus.noContent);
static Future<List<SimpleSchemeTransMetaData>?> userSchemes({required SchemeListType type}) =>
_get(Apis.scheme.user(type: type.name.param), listRespBuilderWrap(SimpleSchemeTransMetaDataSerializer.fromMap));
static Future<bool> likeScheme({required String schemeId, required bool isLike}) => _get(
Apis.scheme.like(schemeId: schemeId.param, isLike: StringParam(isLike ? 'like' : 'unlike')),
getStatusCodeFunc)
.then((value) {
return value == HttpStatus.noContent;
});
static Future<SchemeForDownload?> downloadScheme({required String schemeId}) => _get(
Apis.scheme.download(schemeId: schemeId.param),
SchemeForDownloadSerializer.fromMap,
);
}

@ -3,6 +3,7 @@ import 'package:dde_gesture_manager/constants/sp_keys.dart';
import 'package:dde_gesture_manager/constants/supported_locales.dart';
import 'package:dde_gesture_manager/extensions.dart';
import 'package:dde_gesture_manager/generated/codegen_loader.g.dart';
import 'package:dde_gesture_manager/http/api.dart';
import 'package:dde_gesture_manager/models/configs.dart';
import 'package:dde_gesture_manager/models/configs.provider.dart';
import 'package:dde_gesture_manager/models/settings.provider.dart';
@ -10,6 +11,7 @@ import 'package:dde_gesture_manager/themes/dark.dart';
import 'package:dde_gesture_manager/themes/light.dart';
import 'package:dde_gesture_manager/utils/helper.dart';
import 'package:dde_gesture_manager/utils/init.dart';
import 'package:dde_gesture_manager/utils/simple_throttle.dart';
import 'package:flutter/material.dart';
import 'pages/home.dart';
@ -68,7 +70,10 @@ class MyApp extends StatelessWidget {
],
),
firstChild: Builder(builder: (context) {
Future.microtask(() => initEvents(context));
Future.microtask(() {
initEvents(context);
SimpleThrottle.throttledFunc(_checkAuthStatus, timeout: const Duration(minutes: 5))?.call(context);
});
return Container();
}),
secondChild: HomePage(),
@ -79,3 +84,15 @@ class MyApp extends StatelessWidget {
);
}
}
void _checkAuthStatus(BuildContext context) {
if (H().lastCheckAuthStatusTime != null &&
H().lastCheckAuthStatusTime!.difference(DateTime.now()) < Duration(minutes: 10)) return;
if (context.read<ConfigsProvider>().accessToken.notNull) {
Api.checkAuthStatus().then((value) {
if (!value) context.read<ConfigsProvider>().setProps(email: '', accessToken: '');
});
} else {
H().lastCheckAuthStatusTime = DateTime.now();
}
}

@ -1,4 +1,7 @@
import 'package:dde_gesture_manager/builder/provider_annotation.dart';
import 'package:dde_gesture_manager/constants/sp_keys.dart';
import 'package:dde_gesture_manager/utils/helper.dart';
import 'package:dde_gesture_manager/extensions.dart';
@ProviderModel()
class ContentLayout {
@ -9,7 +12,7 @@ class ContentLayout {
bool? marketOrMeOpened;
@ProviderModelProp()
bool? currentIsMarket = true;
bool? currentIsMarket = H().sp.getString(SPKeys.accessToken).isNull;
bool get isMarket => currentIsMarket ?? true;
}

@ -131,7 +131,7 @@ class Scheme {
@ProviderModelProp()
List<GestureProp>? gestures;
bool get readOnly => uploaded == true || fromMarket == true || id == Uuid.NAMESPACE_NIL;
bool get readOnly => fromMarket == true || id == Uuid.NAMESPACE_NIL;
Scheme.parse(scheme) {
if (scheme is String) scheme = json.decode(scheme);

@ -1,6 +1,8 @@
import 'package:adaptive_scrollbar/adaptive_scrollbar.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/content_layout.provider.dart';
import 'package:dde_gesture_manager/models/local_schemes_provider.dart';
import 'package:dde_gesture_manager/models/scheme.dart';
@ -18,6 +20,7 @@ import 'package:dde_gesture_manager/widgets/table_cell_shortcut_listener.dart';
import 'package:dde_gesture_manager/widgets/table_cell_text_field.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_platform_alert/flutter_platform_alert.dart';
import 'package:uuid/uuid.dart';
const double _headingRowHeight = 56;
@ -292,6 +295,52 @@ class GestureEditor extends StatelessWidget {
},
),
),
Padding(
padding: const EdgeInsets.only(left: 10),
child: DButton.upload(
enabled: schemeProvider.readOnly == false,
onTap: () async {
if (context.read<ConfigsProvider>().accessToken.isNull) {
return Notificator.showAlert(
title: LocaleKeys.info_login_for_upload_title.tr(),
description: LocaleKeys.info_login_for_upload_description.tr(),
).then((value) {
if (value == CustomButton.positiveButton) {
context
.read<ContentLayoutProvider>()
.setProps(marketOrMeOpened: true, currentIsMarket: false);
}
});
}
Notificator.showConfirm(
title: LocaleKeys.info_upload_and_share_title.tr(),
description: LocaleKeys.info_upload_and_share_description.tr(),
positiveButtonTitle: LocaleKeys.str_share.tr(),
negativeButtonTitle: LocaleKeys.str_cancel.tr(),
).then((value) {
bool? _share;
if (value == CustomButton.positiveButton)
_share = true;
else if (value == CustomButton.negativeButton) _share = false;
if (_share != null) {
Api.uploadScheme(scheme: schemeProvider, share: _share).then((value) {
if (value) {
Notificator.success(context, title: LocaleKeys.info_upload_success.tr());
var localSchemesProvider = context.read<LocalSchemesProvider>();
var localSchemeEntry = localSchemesProvider.schemes!
.firstWhere((ele) => ele.scheme.id == schemeProvider.id);
localSchemeEntry.scheme.uploaded = true;
localSchemeEntry.save(localSchemesProvider);
} else {
Notificator.error(context, title: LocaleKeys.info_upload_failed.tr());
}
});
}
});
},
),
),
],
),
Divider(),

@ -184,8 +184,8 @@ class _LocalManagerState extends State<LocalManager> {
),
),
),
Container(height: 5),
Container(
Padding(
padding: const EdgeInsets.only(top: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [

@ -1,10 +1,11 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:dde_gesture_manager/constants/constants.dart';
import 'package:dde_gesture_manager/extensions.dart';
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<ContentLayoutProvider>();
bool isOpen = layoutProvider.marketOrMeOpened == true;
bool isMarket = layoutProvider.isMarket;
bool showLogin = context.watch<ConfigsProvider>().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,
@ -82,36 +82,8 @@ class MarketOrMe extends StatelessWidget {
Widget buildMeContent(BuildContext context) {
var accessToken = context.watch<ConfigsProvider>().accessToken;
if (accessToken.isNull) return LoginWidget();
return Padding(
padding: EdgeInsets.symmetric(vertical: 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: ''),
),
],
),
],
),
);
return Expanded(child: MeWidget());
}
Widget buildMarketContent(BuildContext context) {
return Container();
}
Widget buildMarketContent(BuildContext context) => Expanded(child: MarketWidget());
}

@ -34,6 +34,8 @@ class H {
BuildContext get topContext => _topContext;
DateTime? lastCheckAuthStatusTime;
initTopContext(BuildContext context) {
_topContext = context;
}

@ -49,9 +49,9 @@ Future<void> initEvents(BuildContext context) async {
}
}
if (!_updateChecked)
Api.checkAppVersion(ignoreErrorHandle: true).then((value) async {
if (!_updateChecked) {
_updateChecked = true;
Api.checkAppVersion(ignoreErrorHandle: true).then((value) async {
var info = await PackageInfo.fromPlatform();
var _buildNumber = int.parse(info.buildNumber);
var _newVersionCode = value?.versionCode ?? 0;
@ -75,6 +75,7 @@ Future<void> initEvents(BuildContext context) async {
}
});
}
}
Future<void> initConfigs() async {
await H().initSharedPreference();

@ -15,7 +15,7 @@ class Notificator {
return AlertImpl().showAlert(
windowTitle: title,
text: description,
positiveButtonTitle: positiveButtonTitle,
positiveButtonTitle: positiveButtonTitle ?? LocaleKeys.str_ok.tr(),
);
}

@ -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);
}

@ -124,6 +124,74 @@ class DButton extends StatefulWidget {
message: LocaleKeys.operation_logout.tr(),
));
factory DButton.upload({
Key? key,
required enabled,
GestureTapCallback? onTap,
height = defaultButtonHeight,
width = defaultButtonHeight,
}) =>
DButton(
key: key,
width: width,
height: height,
onTap: enabled ? onTap : null,
child: Tooltip(
child: Opacity(opacity: enabled ? 1 : 0.4, child: const Icon(Icons.cloud_upload, size: 20)),
message: LocaleKeys.operation_upload.tr(),
));
factory DButton.download({
Key? key,
required enabled,
GestureTapCallback? onTap,
height = defaultButtonHeight * .7,
width = defaultButtonHeight * .7,
}) =>
DButton(
key: key,
width: width,
height: height,
onTap: enabled ? onTap : null,
child: Tooltip(
child: Opacity(opacity: enabled ? 1 : 0.4, child: const Icon(Icons.file_download, size: 18)),
message: LocaleKeys.operation_download.tr(),
));
factory DButton.share({
Key? key,
required enabled,
GestureTapCallback? onTap,
height = defaultButtonHeight * .7,
width = defaultButtonHeight * .7,
}) =>
DButton(
key: key,
width: width,
height: height,
onTap: enabled ? onTap : null,
child: Tooltip(
child: Opacity(opacity: enabled ? 1 : 0.4, child: const Icon(Icons.share, size: 18)),
message: LocaleKeys.operation_share.tr(),
));
factory DButton.like({
Key? key,
required enabled,
GestureTapCallback? onTap,
height = defaultButtonHeight * .7,
width = defaultButtonHeight * .7,
}) =>
DButton(
key: key,
width: width,
height: height,
onTap: enabled ? onTap : null,
child: Tooltip(
child: Opacity(opacity: enabled ? 1 : 0.4, child: const Icon(Icons.thumb_up, size: 16)),
message: LocaleKeys.operation_like.tr(),
));
factory DButton.dropdown({
Key? key,
width = 60.0,

@ -54,6 +54,12 @@ class _LoginWidgetState extends State<LoginWidget> {
title: LocaleKeys.info_sign_up_hint_title.tr(),
description: LocaleKeys.info_sign_up_hint_description.tr(),
);
else if (code == HttpStatus.forbidden)
Notificator.info(
context,
title: LocaleKeys.info_user_blocked_hint_title.tr(),
description: LocaleKeys.info_user_blocked_hint_description.tr(),
);
else
throw e;
}

@ -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(),
),
),
],
),
);
}
}

@ -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,9 @@ 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
# following page: https://dart.dev/tools/pub/pubspec

@ -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",
@ -66,19 +72,26 @@
}
},
"operation": {
"add": "Add",
"add": "add",
"delete": "delete",
"duplicate": "duplicate",
"apply": "apply",
"paste": "paste",
"logout": "sign out"
"logout": "sign out",
"upload": "upload",
"download": "download",
"share": "share to market",
"like": "like"
},
"str": {
"null": "Null",
"new_scheme": "New gesture scheme",
"copy": "copy",
"yes": "Yes",
"no": "No"
"no": "No",
"ok": "OK",
"share": "Share",
"cancel": "Cancel"
},
"built_in_commands": {
"ShowWorkspace": "ShowWorkspace",
@ -126,6 +139,27 @@
"description_for_startup": "Click [{YES}] to view, click [{NO}] ignore this update",
"title_already_latest": "Already the latest version ~",
"description_for_manual": "Visit the official website to see more?"
},
"login_for_upload": {
"title": "please login",
"description": "You need to login first to perform upload operations"
},
"upload_and_share": {
"title": "Share the scheme at the same time?",
"description": "If you select [Share], other users can see this scheme and download it;\nIf you select [Cancel], you can still find this scheme in the [My Upload] list and share."
},
"user_blocked_hint": {
"title": "The account has been blocked!",
"description": "If you have any questions, please contact me using email."
},
"upload": {
"success": "Upload success ~",
"failed": "Upload failed.."
},
"share": {
"title": "Are you sure to sharing?",
"description": "Other users can see this scheme and download it after share",
"success": "Share success"
}
},
"me": {
@ -135,6 +169,11 @@
"email_hint": "Please enter email",
"password_hint": "Please enter 8-16-bit password",
"email_error_hint": "Please enter your vaild email"
},
"scheme_types": {
"uploaded": "Uploaded",
"downloaded": "Downloaded",
"liked": "Liked"
}
}
}

@ -20,7 +20,13 @@
"tip": "显示帮助文档"
},
"market": {
"title": "方案市场"
"title": "方案市场",
"sort_types": {
"recommend": "推荐",
"updated": "最近更新",
"likes": "点赞数",
"downloads": "下载量"
}
},
"local_manager": {
"title": "本地方案管理",
@ -71,14 +77,21 @@
"duplicate": "复制",
"apply": "应用",
"paste": "粘贴",
"logout": "退出登录"
"logout": "退出登录",
"upload": "上传",
"download": "下载",
"share": "分享到市场",
"like": "点赞"
},
"str": {
"null": "无",
"new_scheme": "新建手势方案",
"copy": "副本",
"yes": "是",
"no": "否"
"no": "否",
"ok": "好的",
"share": "分享",
"cancel": "放弃"
},
"built_in_commands": {
"ShowWorkspace": "显示工作区",
@ -126,6 +139,27 @@
"description_for_startup": "点击[{yes}]查看,点击[{no}]忽略本次更新",
"title_already_latest": "已经是最新版本~",
"description_for_manual": "是否前去官网查看?"
},
"login_for_upload": {
"title": "请登录",
"description": "您需要先登录才能进行上传操作"
},
"upload_and_share": {
"title": "是否同时分享到方案市场?",
"description": "如果选择[分享],其他用户可以看到本方案并下载使用;\n如果选择[放弃],您仍可以稍后在[我的上传]列表中找到本方案进行操作。"
},
"user_blocked_hint": {
"title": "该账号已被封禁!",
"description": "如有疑问请通过发送邮件联系"
},
"upload": {
"success": "上传成功~",
"failed": "上传失败。。"
},
"share": {
"title": "确定分享?",
"description": "分享后其他用户可以看到本方案并下载使用",
"success": "分享成功"
}
},
"me": {
@ -135,6 +169,11 @@
"email_hint": "请输入邮箱",
"password_hint": "请输入8-16位密码",
"email_error_hint": "请输入正确的邮箱"
},
"scheme_types": {
"uploaded": "我的上传",
"downloaded": "我的下载",
"liked": "我的点赞"
}
}
}
Loading…
Cancel
Save