docker #6
+16
-12
@@ -1,14 +1,18 @@
|
||||
FROM google/dart:latest
|
||||
|
||||
COPY ./ ./
|
||||
|
||||
# Install dependencies, pre-build
|
||||
RUN pub get
|
||||
|
||||
# Optionally build generaed sources.
|
||||
# RUN pub run build_runner build
|
||||
|
||||
# Set environment, start server
|
||||
FROM dart:stable AS build-env
|
||||
LABEL stage=dart_builder
|
||||
ENV PUB_HOSTED_URL="https://pub.flutter-io.cn"
|
||||
ENV ANGEL_ENV=production
|
||||
COPY ./ ./
|
||||
RUN pub get
|
||||
RUN dart compile exe bin/prod.dart -o /server
|
||||
ENTRYPOINT ["dart", "bin/migrate.dart", "up"]
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
+12
-7
@@ -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);
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
export 'src/models/user.dart';
|
||||
export 'src/models/app_version.dart';
|
||||
export 'src/models/login_success.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>>()) {
|
||||
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);
|
||||
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, 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,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);
|
||||
|
||||
|
||||
+54
-13
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
+18
-1
@@ -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)
|
||||
if (!_updateChecked) {
|
||||
_updateChecked = true;
|
||||
Api.checkAppVersion(ignoreErrorHandle: true).then((value) async {
|
||||
_updateChecked = true;
|
||||
var info = await PackageInfo.fromPlatform();
|
||||
var _buildNumber = int.parse(info.buildNumber);
|
||||
var _newVersionCode = value?.versionCode ?? 0;
|
||||
@@ -74,6 +74,7 @@ Future<void> initEvents(BuildContext context) async {
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initConfigs() async {
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+13
-11
@@ -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": "我的点赞"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user