From 85a7d36fdae65fbf22a931940bbd5021df9e3818 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Fri, 7 Jan 2022 18:04:59 +0800 Subject: [PATCH] wip: me panel. --- api/3rd_party/angel3_orm/AUTHORS.md | 12 + api/3rd_party/angel3_orm/CHANGELOG.md | 230 +++++++ api/3rd_party/angel3_orm/LICENSE | 30 + api/3rd_party/angel3_orm/README.md | 15 + api/3rd_party/angel3_orm/analysis_options.yaml | 1 + api/3rd_party/angel3_orm/lib/angel3_orm.dart | 15 + api/3rd_party/angel3_orm/lib/src/annotations.dart | 31 + api/3rd_party/angel3_orm/lib/src/builder.dart | 675 +++++++++++++++++++++ api/3rd_party/angel3_orm/lib/src/join_builder.dart | 74 +++ api/3rd_party/angel3_orm/lib/src/join_on.dart | 8 + .../angel3_orm/lib/src/map_query_values.dart | 9 + api/3rd_party/angel3_orm/lib/src/migration.dart | 135 +++++ api/3rd_party/angel3_orm/lib/src/order_by.dart | 8 + api/3rd_party/angel3_orm/lib/src/query.dart | 425 +++++++++++++ api/3rd_party/angel3_orm/lib/src/query_base.dart | 81 +++ .../angel3_orm/lib/src/query_executor.dart | 23 + api/3rd_party/angel3_orm/lib/src/query_values.dart | 89 +++ api/3rd_party/angel3_orm/lib/src/query_where.dart | 69 +++ api/3rd_party/angel3_orm/lib/src/relations.dart | 91 +++ api/3rd_party/angel3_orm/lib/src/union.dart | 38 ++ api/3rd_party/angel3_orm/lib/src/util.dart | 3 + api/3rd_party/angel3_orm/mono_pkg.yaml | 0 api/3rd_party/angel3_orm/pubspec.yaml | 21 + api/bin/migrate.dart | 4 + api/lib/apis.dart | 25 +- api/lib/src/models/download_history.dart | 19 + api/lib/src/models/like_record.dart | 23 + api/lib/src/models/scheme.dart | 88 +++ .../src/routes/controllers/auth_controllers.dart | 2 +- .../routes/controllers/controller_extensions.dart | 8 +- api/lib/src/routes/controllers/middlewares.dart | 22 +- .../src/routes/controllers/scheme_controllers.dart | 164 ++++- api/pubspec.yaml | 12 +- app/lib/http/api.dart | 22 +- app/lib/main.dart | 23 +- app/lib/models/content_layout.dart | 5 +- app/lib/pages/local_manager.dart | 4 +- app/lib/pages/market_or_me.dart | 2 +- app/lib/utils/init_linux.dart | 5 +- app/lib/utils/simple_throttle.dart | 45 ++ app/lib/widgets/dde_button.dart | 51 ++ app/lib/widgets/me.dart | 200 +++++- app/pubspec.yaml | 4 + app/resources/langs/en.json | 15 +- app/resources/langs/zh-CN.json | 15 +- 45 files changed, 2775 insertions(+), 66 deletions(-) create mode 100644 api/3rd_party/angel3_orm/AUTHORS.md create mode 100644 api/3rd_party/angel3_orm/CHANGELOG.md create mode 100644 api/3rd_party/angel3_orm/LICENSE create mode 100644 api/3rd_party/angel3_orm/README.md create mode 100644 api/3rd_party/angel3_orm/analysis_options.yaml create mode 100644 api/3rd_party/angel3_orm/lib/angel3_orm.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/annotations.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/builder.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/join_builder.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/join_on.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/map_query_values.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/migration.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/order_by.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/query.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/query_base.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/query_executor.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/query_values.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/query_where.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/relations.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/union.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/util.dart create mode 100644 api/3rd_party/angel3_orm/mono_pkg.yaml create mode 100644 api/3rd_party/angel3_orm/pubspec.yaml create mode 100644 api/lib/src/models/download_history.dart create mode 100644 api/lib/src/models/like_record.dart create mode 100644 app/lib/utils/simple_throttle.dart diff --git a/api/3rd_party/angel3_orm/AUTHORS.md b/api/3rd_party/angel3_orm/AUTHORS.md new file mode 100644 index 0000000..ac95ab5 --- /dev/null +++ b/api/3rd_party/angel3_orm/AUTHORS.md @@ -0,0 +1,12 @@ +Primary Authors +=============== + +* __[Thomas Hii](dukefirehawk.apps@gmail.com)__ + + Thomas is the current maintainer of the code base. He has refactored and migrated the + code base to support NNBD. + +* __[Tobe O](thosakwe@gmail.com)__ + + Tobe has written much of the original code prior to NNBD migration. He has moved on and + is no longer involved with the project. diff --git a/api/3rd_party/angel3_orm/CHANGELOG.md b/api/3rd_party/angel3_orm/CHANGELOG.md new file mode 100644 index 0000000..4dbc079 --- /dev/null +++ b/api/3rd_party/angel3_orm/CHANGELOG.md @@ -0,0 +1,230 @@ +# Change Log + +## 4.0.4 + +* Changed default varchar size to 255 +* Changed default primary key to serial + +## 4.0.3 + +* Removed debugging messages + +## 4.0.2 + +* Updated linter to `package:lints` +* Set `createdAt` and `updatedAt` to current datetime as default + +## 4.0.1 + +* Fixed expressions parsing error +* Fixed json data type error +* Added debug logging to sql query execution + +## 4.0.0 + +* Updated `Optional` package + +## 4.0.0-beta.4 + +* Added `hasSize` to `ColumnType` + +## 4.0.0-beta.3 + +* Updated README +* Fixed NNBD issues + +## 4.0.0-beta.2 + +* Fixed static analysis warning + +## 4.0.0-beta.1 + +* Migrated to support Dart SDK 2.12.x NNBD + +## 3.0.0 + +* Migrated to work with Dart SDK 2.12.x Non NNBD + +## 2.1.0-beta.3 + +* Remove parentheses from `AS` when renaming raw `expressions`. + +## 2.1.0-beta.2 + +* Add `expressions` to `Query`, to support custom SQL expressions that are +read as normal fields. + +## 2.1.0-beta.1 + +* Calls to `leftJoin`, etc. alias all fields in a child query, to prevent +`ambiguous column a0.id` errors. + +## 2.1.0-beta + +* Split the formerly 600+ line `src/query.dart` up into +separate files. +* **BREAKING**: Add a required `QueryExecutor` argument to `transaction` +callbacks. +* Make `JoinBuilder` take `to` as a `String Function()`. This will allow +ORM queries to reference their joined subqueries. +* Removed deprecated `Join`, `toSql`, `sanitizeExpression`, `isAscii`. +* Always put `ORDER BY` before `LIMIT`. +* `and`, `or`, `not` in `QueryWhere` include parentheses. +* Add `joinType` to `Relationship` class. + +## 2.0.2 + +* Place `LIMIT` and `OFFSET` after `ORDER BY`. + +## 2.0.1 + +* Apply `package:pedantic` fixes. +* `@PrimaryKey()` no longer defaults to `serial`, allowing its type to be +inferenced. + +## 2.0.0 + +* Add `isNull`, `isNotNull` getters to builders. + +## 2.0.0-dev.24 + +* Fix a bug that caused syntax errors on `ORDER BY`. +* Add `pattern` to `like` on string builder. `sanitize` is optional. +* Add `RawSql`. + +## 2.0.0-dev.23 + +* Add `@ManyToMany` annotation, which builds many-to-many relations. + +## 2.0.0-dev.22 + +* `compileInsert` will explicitly never emit a key not belonging to the +associated query. + +## 2.0.0-dev.21 + +* Add tableName to query + +## 2.0.0-dev.20 + +* Join updates. + +## 2.0.0-dev.19 + +* Implement cast-based `double` support. +* Finish `ListSqlExpressionBuilder`. + +## 2.0.0-dev.18 + +* Add `ListSqlExpressionBuilder` (still in development). + +## 2.0.0-dev.17 + +* Add `EnumSqlExpressionBuilder`. + +## 2.0.0-dev.16 + +* Add `MapSqlExpressionBuilder` for JSON/JSONB support. + +## 2.0.0-dev.15 + +* Remove `Column.defaultValue`. +* Deprecate `toSql` and `sanitizeExpression`. +* Refactor builders so that strings are passed through + +## 2.0.0-dev.14 + +* Remove obsolete `@belongsToMany`. + +## 2.0.0-dev.13 + +* Push for consistency with orm_gen @ `2.0.0-dev`. + +## 2.0.0-dev.12 + +* Always apply `toSql` escapes. + +## 2.0.0-dev.11 + +* Remove `limit(1)` except on `getOne` + +## 2.0.0-dev.10 + +* Add `withFields` to `compile()` + +## 2.0.0-dev.9 + +* Permanent preamble fix + +## 2.0.0-dev.8 + +* Escapes + +## 2.0.0-dev.7 + +* Update `toSql` +* Add `isTrue` and `isFalse` + +## 2.0.0-dev.6 + +* Add `delete`, `insert` and `update` methods to `Query`. + +## 2.0.0-dev.4 + +* Add more querying methods. +* Add preamble to `Query.compile`. + +## 2.0.0-dev.3 + +* Brought back old-style query builder. +* Strong-mode updates, revised `Join`. + +## 2.0.0-dev.2 + +* Renamed `ORM` to `Orm`. +* `Orm` now requires a database type. + +## 2.0.0-dev.1 + +* Restored all old PostgreSQL-specific annotations. Rather than a smart runtime, +having a codegen capable of building ORM's for multiple databases can potentially +provide a very fast ORM for everyone. + +## 2.0.0-dev + +* Removed PostgreSQL-specific functionality, so that the ORM can ultimately +target all services. +* Created a better `Join` model. +* Created a far better `Query` model. +* Removed `lib/server.dart` + +## 1.0.0-alpha+10 + +* Split into `angel_orm.dart` and `server.dart`. Prevents DDC failures. + +## 1.0.0-alpha+7 + +* Added a `@belongsToMany` annotation class. +* Resolved [##20](https://github.com/angel-dart/orm/issues/20). The +`PostgreSQLConnectionPool` keeps track of which connections have been opened now. + +## 1.0.0-alpha+6 + +* `DateTimeSqlExpressionBuilder` will no longer automatically +insert quotation marks around names. + +## 1.0.0-alpha+5 + +* Corrected a typo that was causing the aforementioned test failures. +`==` becomes `=`. + +## 1.0.0-alpha+4 + +* Added a null-check in `lib/src/query.dart##L24` to (hopefully) prevent it from +crashing on Travis. + +## 1.0.0-alpha+3 + +* Added `isIn`, `isNotIn`, `isBetween`, `isNotBetween` to `SqlExpressionBuilder` and its +subclasses. +* Added a dependency on `package:meta`. diff --git a/api/3rd_party/angel3_orm/LICENSE b/api/3rd_party/angel3_orm/LICENSE new file mode 100644 index 0000000..a81d1a8 --- /dev/null +++ b/api/3rd_party/angel3_orm/LICENSE @@ -0,0 +1,30 @@ + +BSD 3-Clause License + +Copyright (c) 2021, dukefirehawk.com +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/api/3rd_party/angel3_orm/README.md b/api/3rd_party/angel3_orm/README.md new file mode 100644 index 0000000..43f12ff --- /dev/null +++ b/api/3rd_party/angel3_orm/README.md @@ -0,0 +1,15 @@ +# Angel3 ORM + +![Pub Version (including pre-releases)](https://img.shields.io/pub/v/angel3_orm?include_prereleases) +[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety) +[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion) +[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/master/packages/orm/angel_orm/LICENSE) + +Runtime support for Angel3 ORM. Includes a clean, database-agnostic query builder and relationship/join support. + +## Supported database + +* PostgreSQL version 10, 11, 12, 13 and 14 +* MySQL 8.0 or later + +For documentation about the ORM, see [Developer Guide](https://angel3-docs.dukefirehawk.com/guides/orm) diff --git a/api/3rd_party/angel3_orm/analysis_options.yaml b/api/3rd_party/angel3_orm/analysis_options.yaml new file mode 100644 index 0000000..ea2c9e9 --- /dev/null +++ b/api/3rd_party/angel3_orm/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml \ No newline at end of file diff --git a/api/3rd_party/angel3_orm/lib/angel3_orm.dart b/api/3rd_party/angel3_orm/lib/angel3_orm.dart new file mode 100644 index 0000000..9195227 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/angel3_orm.dart @@ -0,0 +1,15 @@ +export 'src/annotations.dart'; +export 'src/builder.dart'; +export 'src/join_builder.dart'; +export 'src/join_on.dart'; +export 'src/map_query_values.dart'; +export 'src/migration.dart'; +export 'src/order_by.dart'; +export 'src/query_base.dart'; +export 'src/query_executor.dart'; +export 'src/query_values.dart'; +export 'src/query_where.dart'; +export 'src/query.dart'; +export 'src/relations.dart'; +export 'src/union.dart'; +export 'src/util.dart'; diff --git a/api/3rd_party/angel3_orm/lib/src/annotations.dart b/api/3rd_party/angel3_orm/lib/src/annotations.dart new file mode 100644 index 0000000..891cddc --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/annotations.dart @@ -0,0 +1,31 @@ +/// A raw SQL statement that specifies a date/time default to the +/// current time. +const RawSql currentTimestamp = RawSql('CURRENT_TIMESTAMP'); + +/// Can passed to a [MigrationColumn] to default to a raw SQL expression. +class RawSql { + /// The raw SQL text. + final String value; + + const RawSql(this.value); +} + +/// Canonical instance of [ORM]. Implies all defaults. +const Orm orm = Orm(); + +class Orm { + /// The name of the table to query. + /// + /// Inferred if not present. + final String? tableName; + + /// Whether to generate migrations for this model. + /// + /// Defaults to [:true:]. + final bool generateMigrations; + + const Orm({this.tableName, this.generateMigrations = true}); +} + +/// The various types of join. +enum JoinType { inner, left, right, full, self } diff --git a/api/3rd_party/angel3_orm/lib/src/builder.dart b/api/3rd_party/angel3_orm/lib/src/builder.dart new file mode 100644 index 0000000..313d125 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/builder.dart @@ -0,0 +1,675 @@ +import 'package:intl/intl.dart' show DateFormat; +import 'query.dart'; + +final DateFormat dateYmd = DateFormat('yyyy-MM-dd'); +final DateFormat dateYmdHms = DateFormat('yyyy-MM-dd HH:mm:ss'); + +abstract class SqlExpressionBuilder { + final Query query; + final String columnName; + String? _cast; + bool _isProperty = false; + String? _substitution; + + SqlExpressionBuilder(this.query, this.columnName); + + String get substitution { + var c = _isProperty ? 'prop' : columnName; + return _substitution ??= query.reserveName(c); + } + + bool get hasValue; + + String? compile(); +} + +class NumericSqlExpressionBuilder + extends SqlExpressionBuilder { + bool _hasValue = false; + String _op = '='; + String? _raw; + T? _value; + + NumericSqlExpressionBuilder(Query query, String columnName) + : super(query, columnName); + + @override + bool get hasValue => _hasValue; + + bool _change(String op, T value) { + _raw = null; + _op = op; + _value = value; + return _hasValue = true; + } + + @override + String? compile() { + if (_raw != null) return _raw; + if (_value == null) return null; + var v = _value.toString(); + if (T == double) v = 'CAST ("$v" as decimal)'; + if (_cast != null) v = 'CAST ($v AS $_cast)'; + return '$_op $v'; + } + + bool operator <(T value) => _change('<', value); + + bool operator >(T value) => _change('>', value); + + bool operator <=(T value) => _change('<=', value); + + bool operator >=(T value) => _change('>=', value); + + void get isNull { + _raw = 'IS NULL'; + _hasValue = true; + } + + void get isNotNull { + _raw = 'IS NOT NULL'; + _hasValue = true; + } + + void lessThan(T value) { + _change('<', value); + } + + void lessThanOrEqualTo(T value) { + _change('<=', value); + } + + void greaterThan(T value) { + _change('>', value); + } + + void greaterThanOrEqualTo(T value) { + _change('>=', value); + } + + void equals(T value) { + _change('=', value); + } + + void notEquals(T value) { + _change('!=', value); + } + + void isBetween(T lower, T upper) { + _raw = 'BETWEEN $lower AND $upper'; + _hasValue = true; + } + + void isNotBetween(T lower, T upper) { + _raw = 'NOT BETWEEN $lower AND $upper'; + _hasValue = true; + } + + void isIn(Iterable values) { + _raw = 'IN (' + values.join(', ') + ')'; + _hasValue = true; + } + + void isNotIn(Iterable values) { + _raw = 'NOT IN (' + values.join(', ') + ')'; + _hasValue = true; + } +} + +class EnumSqlExpressionBuilder extends SqlExpressionBuilder { + final int Function(T) _getValue; + bool _hasValue = false; + String _op = '='; + String? _raw; + int? _value; + + EnumSqlExpressionBuilder(Query query, String columnName, this._getValue) + : super(query, columnName); + + @override + bool get hasValue => _hasValue; + + bool _change(String op, T value) { + _raw = null; + _op = op; + _value = _getValue(value); + return _hasValue = true; + } + + UnsupportedError _unsupported() => + UnsupportedError('Enums do not support this operation.'); + + @override + String compile() { + if (_raw != null) { + return _raw!; + } + if (_value == null) { + return ''; + } + return '$_op $_value'; + } + + void get isNull { + _raw = 'IS NULL'; + _hasValue = true; + } + + void get isNotNull { + _raw = 'IS NOT NULL'; + _hasValue = true; + } + + void equals(T value) { + _change('=', value); + } + + void notEquals(T value) { + _change('!=', value); + } + + void isBetween(T lower, T upper) => throw _unsupported(); + + void isNotBetween(T lower, T upper) => throw _unsupported(); + + void isIn(Iterable values) { + _raw = 'IN (' + values.map(_getValue).join(', ') + ')'; + _hasValue = true; + } + + void isNotIn(Iterable values) { + _raw = 'NOT IN (' + values.map(_getValue).join(', ') + ')'; + _hasValue = true; + } +} + +class StringSqlExpressionBuilder extends SqlExpressionBuilder { + bool _hasValue = false; + String? _op = '=', _raw, _value; + + StringSqlExpressionBuilder(Query query, String columnName) + : super(query, columnName); + + @override + bool get hasValue => _hasValue; + + String get lowerName => '${substitution}_lower'; + + String get upperName => '${substitution}_upper'; + + bool _change(String op, String value) { + _raw = null; + _op = op; + _value = value; + query.substitutionValues[substitution] = _value; + return _hasValue = true; + } + + @override + String? compile() { + if (_raw != null) return _raw; + if (_value == null) return null; + return '$_op @$substitution'; + } + + void isEmpty() => equals(''); + + void equals(String value) { + _change('=', value); + } + + void notEquals(String value) { + _change('!=', value); + } + + /// Builds a `LIKE` predicate. + /// + /// To prevent injections, an optional [sanitizer] is called with a name that + /// will be escaped by the underlying [QueryExecutor]. Use this if the [pattern] + /// is not constant, and/or involves user input. + /// + /// Otherwise, you can omit [sanitizer]. + /// + /// Example: + /// ```dart + /// carNameBuilder.like('%Mazda%'); + /// carNameBuilder.like((name) => 'Mazda %$name%'); + /// ``` + void like(String pattern, {String Function(String)? sanitize}) { + sanitize ??= (s) => pattern; + _raw = 'LIKE \'' + sanitize('@$substitution') + '\''; + query.substitutionValues[substitution] = pattern; + _hasValue = true; + _value = null; + } + + void isBetween(String lower, String upper) { + query.substitutionValues[lowerName] = lower; + query.substitutionValues[upperName] = upper; + _raw = 'BETWEEN @$lowerName AND @$upperName'; + _hasValue = true; + } + + void isNotBetween(String lower, String upper) { + query.substitutionValues[lowerName] = lower; + query.substitutionValues[upperName] = upper; + _raw = 'NOT BETWEEN @$lowerName AND @$upperName'; + _hasValue = true; + } + + void get isNull { + _raw = 'IS NULL'; + _hasValue = true; + } + + void get isNotNull { + _raw = 'IS NOT NULL'; + _hasValue = true; + } + + String _in(Iterable values) { + return 'IN (' + + values.map((v) { + var name = query.reserveName('${columnName}_in_value'); + query.substitutionValues[name] = v; + return '@$name'; + }).join(', ') + + ')'; + } + + void isIn(Iterable values) { + _raw = _in(values); + _hasValue = true; + } + + void isNotIn(Iterable values) { + _raw = 'NOT ' + _in(values); + _hasValue = true; + } +} + +class BooleanSqlExpressionBuilder extends SqlExpressionBuilder { + bool _hasValue = false; + String? _op = '=', _raw; + bool? _value; + + BooleanSqlExpressionBuilder(Query query, String columnName) + : super(query, columnName); + + @override + bool get hasValue => _hasValue; + + bool _change(String op, bool value) { + _raw = null; + _op = op; + _value = value; + return _hasValue = true; + } + + @override + String? compile() { + if (_raw != null) return _raw; + if (_value == null) return null; + var v = _value! ? 'TRUE' : 'FALSE'; + if (_cast != null) v = 'CAST ($v AS $_cast)'; + return '$_op $v'; + } + + void get isTrue => equals(true); + + void get isFalse => equals(false); + + void get isNull { + _raw = 'IS NULL'; + _hasValue = true; + } + + void get isNotNull { + _raw = 'IS NOT NULL'; + _hasValue = true; + } + + void equals(bool value) { + _change('=', value); + } + + void notEquals(bool value) { + _change('!=', value); + } +} + +class DateTimeSqlExpressionBuilder extends SqlExpressionBuilder { + NumericSqlExpressionBuilder? _year, + _month, + _day, + _hour, + _minute, + _second; + + String? _raw; + + DateTimeSqlExpressionBuilder(Query query, String columnName) + : super(query, columnName); + + NumericSqlExpressionBuilder get year => + _year ??= NumericSqlExpressionBuilder(query, 'year'); + NumericSqlExpressionBuilder get month => + _month ??= NumericSqlExpressionBuilder(query, 'month'); + NumericSqlExpressionBuilder get day => + _day ??= NumericSqlExpressionBuilder(query, 'day'); + NumericSqlExpressionBuilder get hour => + _hour ??= NumericSqlExpressionBuilder(query, 'hour'); + NumericSqlExpressionBuilder get minute => + _minute ??= NumericSqlExpressionBuilder(query, 'minute'); + NumericSqlExpressionBuilder get second => + _second ??= NumericSqlExpressionBuilder(query, 'second'); + + @override + bool get hasValue => + _raw?.isNotEmpty == true || + _year?.hasValue == true || + _month?.hasValue == true || + _day?.hasValue == true || + _hour?.hasValue == true || + _minute?.hasValue == true || + _second?.hasValue == true; + + bool _change(String _op, DateTime dt, bool time) { + var dateString = time ? dateYmdHms.format(dt) : dateYmd.format(dt); + _raw = '$columnName $_op \'$dateString\''; + return true; + } + + bool operator <(DateTime value) => _change('<', value, true); + + bool operator <=(DateTime value) => _change('<=', value, true); + + bool operator >(DateTime value) => _change('>', value, true); + + bool operator >=(DateTime value) => _change('>=', value, true); + + void equals(DateTime value, {bool includeTime = true}) { + _change('=', value, includeTime != false); + } + + void lessThan(DateTime value, {bool includeTime = true}) { + _change('<', value, includeTime != false); + } + + void lessThanOrEqualTo(DateTime value, {bool includeTime = true}) { + _change('<=', value, includeTime != false); + } + + void greaterThan(DateTime value, {bool includeTime = true}) { + _change('>', value, includeTime != false); + } + + void greaterThanOrEqualTo(DateTime value, {bool includeTime = true}) { + _change('>=', value, includeTime != false); + } + + void isIn(Iterable values) { + _raw = '$columnName IN (' + + values.map(dateYmdHms.format).map((s) => '$s').join(', ') + + ')'; + } + + void isNotIn(Iterable values) { + _raw = '$columnName NOT IN (' + + values.map(dateYmdHms.format).map((s) => '$s').join(', ') + + ')'; + } + + void isBetween(DateTime lower, DateTime upper) { + var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper); + _raw = "$columnName BETWEEN '$l' and '$u'"; + } + + void isNotBetween(DateTime lower, DateTime upper) { + var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper); + _raw = "$columnName NOT BETWEEN '$l' and '$u'"; + } + + void get isNull { + _raw = '$columnName IS NULL'; + } + + void get isNotNull { + _raw = '$columnName IS NOT NULL'; + } + + @override + String? compile() { + if (_raw?.isNotEmpty == true) return _raw; + var parts = []; + if (year.hasValue == true) { + parts.add('YEAR($columnName) ${year.compile()}'); + } + if (month.hasValue == true) { + parts.add('MONTH($columnName) ${month.compile()}'); + } + if (day.hasValue == true) { + parts.add('DAY($columnName) ${day.compile()}'); + } + if (hour.hasValue == true) { + parts.add('HOUR($columnName) ${hour.compile()}'); + } + if (minute.hasValue == true) { + parts.add('MINUTE($columnName) ${minute.compile()}'); + } + if (second.hasValue == true) { + parts.add('SECOND($columnName) ${second.compile()}'); + } + + return parts.isEmpty ? null : parts.join(' AND '); + } +} + +abstract class JsonSqlExpressionBuilder extends SqlExpressionBuilder { + final List _properties = []; + bool _hasValue = false; + T? _value; + String? _op; + String? _raw; + + JsonSqlExpressionBuilder(Query query, String columnName) + : super(query, columnName); + + JsonSqlExpressionBuilderProperty operator [](K name) { + var p = _property(name); + _properties.add(p); + return p; + } + + JsonSqlExpressionBuilderProperty _property(K name); + + bool get hasRaw => _raw != null || _properties.any((p) => p.hasValue); + + @override + bool get hasValue => _hasValue || _properties.any((p) => p.hasValue); + + T? _encodeValue(T? v) => v; + + bool _change(String op, T value) { + _raw = null; + _op = op; + _value = value; + query.substitutionValues[substitution] = _encodeValue(_value); + return _hasValue = true; + } + + void get isNull { + _raw = 'IS NULL'; + _hasValue = true; + } + + void get isNotNull { + _raw = 'IS NOT NULL'; + _hasValue = true; + } + + @override + String compile() { + var s = _compile(); + if (!_properties.any((p) => p.hasValue)) return s; + //s ??= ''; + + for (var p in _properties) { + if (p.hasValue) { + var c = p.compile(); + + if (c != null) { + _hasValue = true; + //s ??= ''; + + if (p.typed is! DateTimeSqlExpressionBuilder) { + s += '${p.typed!.columnName} '; + } + + s += c; + } + } + } + + return s; + } + + String _compile() { + if (_raw != null) { + return _raw!; + } + if (_value == null) { + return ''; + } + return '::jsonb $_op @$substitution::jsonb'; + } + + void contains(T value) { + _change('@>', value); + } + + void equals(T value) { + _change('=', value); + } +} + +class MapSqlExpressionBuilder extends JsonSqlExpressionBuilder { + MapSqlExpressionBuilder(Query query, String columnName) + : super(query, columnName); + + @override + JsonSqlExpressionBuilderProperty _property(String name) { + return JsonSqlExpressionBuilderProperty(this, name, false); + } + + void containsKey(String key) { + this[key].isNotNull; + } + + void containsPair(key, value) { + contains({key: value}); + } +} + +class ListSqlExpressionBuilder extends JsonSqlExpressionBuilder { + ListSqlExpressionBuilder(Query query, String columnName) + : super(query, columnName); + + @override + List? _encodeValue(List? v) => v; //[json.encode(v)]; + + @override + JsonSqlExpressionBuilderProperty _property(int name) { + return JsonSqlExpressionBuilderProperty(this, name.toString(), true); + } +} + +class JsonSqlExpressionBuilderProperty { + final JsonSqlExpressionBuilder builder; + final String name; + final bool isInt; + SqlExpressionBuilder? _typed; + + JsonSqlExpressionBuilderProperty(this.builder, this.name, this.isInt); + + SqlExpressionBuilder? get typed => _typed; + + bool get hasValue => _typed?.hasValue == true; + + String? compile() => _typed?.compile(); + + T? _set(T Function() value) { + if (_typed is T) { + return _typed as T?; + } else if (_typed != null) { + throw StateError( + '$nameString is already typed as $_typed, and cannot be changed.'); + } else { + _typed = value() + ?.._cast = 'text' + .._isProperty = true; + return _typed as T?; + } + } + + String get nameString { + var n = isInt ? name : "'$name'"; + return '${builder.columnName}::jsonb->>$n'; + } + + void get isNotNull { + builder + .._hasValue = true + .._raw ??= ''; + + var r = builder._raw; + if (r != null) { + builder._raw = r + '$nameString IS NOT NULL'; + } else { + builder._raw = '$nameString IS NOT NULL'; + } + } + + void get isNull { + builder + .._hasValue = true + .._raw ??= ''; + + var r = builder._raw; + if (r != null) { + builder._raw = r + '$nameString IS NULL'; + } else { + builder._raw = '$nameString IS NULL'; + } + } + + StringSqlExpressionBuilder? get asString { + return _set(() => StringSqlExpressionBuilder(builder.query, nameString)); + } + + BooleanSqlExpressionBuilder? get asBool { + return _set(() => BooleanSqlExpressionBuilder(builder.query, nameString)); + } + + DateTimeSqlExpressionBuilder? get asDateTime { + return _set(() => DateTimeSqlExpressionBuilder(builder.query, nameString)); + } + + NumericSqlExpressionBuilder? get asDouble { + return _set( + () => NumericSqlExpressionBuilder(builder.query, nameString)); + } + + NumericSqlExpressionBuilder? get asInt { + return _set( + () => NumericSqlExpressionBuilder(builder.query, nameString)); + } + + MapSqlExpressionBuilder? get asMap { + return _set(() => MapSqlExpressionBuilder(builder.query, nameString)); + } + + ListSqlExpressionBuilder? get asList { + return _set(() => ListSqlExpressionBuilder(builder.query, nameString)); + } +} diff --git a/api/3rd_party/angel3_orm/lib/src/join_builder.dart b/api/3rd_party/angel3_orm/lib/src/join_builder.dart new file mode 100644 index 0000000..c342c77 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/join_builder.dart @@ -0,0 +1,74 @@ +import 'annotations.dart'; +import 'query.dart'; + +/// Builds a SQL `JOIN` query. +class JoinBuilder { + final JoinType type; + final Query from; + final String? key, value, op, alias; + final bool aliasAllFields; + + /// A callback to produces the expression to join against, i.e. + /// a table name, or the result of compiling a query. + final String Function() to; + final List additionalFields; + + JoinBuilder(this.type, this.from, this.to, this.key, this.value, + {this.op = '=', + this.alias, + this.additionalFields = const [], + this.aliasAllFields = false}) { + //assert(to != null, + // 'computation of this join threw an error, and returned null.'); + } + + String get fieldName { + var v = value; + if (aliasAllFields) { + v = '${alias}_$v'; + } + var right = '${from.tableName}.$v'; + if (alias != null) right = '$alias.$v'; + return right; + } + + String nameFor(String name) { + if (aliasAllFields) name = '${alias}_$name'; + var right = '${from.tableName}.$name'; + if (alias != null) right = '$alias.$name'; + return right; + } + + String compile(Set? trampoline) { + var compiledTo = to(); + //if (compiledTo == null) return null; + if (compiledTo == '') { + return ''; + } + var b = StringBuffer(); + var left = '${from.tableName}.$key'; + var right = fieldName; + switch (type) { + case JoinType.inner: + b.write(' INNER JOIN'); + break; + case JoinType.left: + b.write(' LEFT JOIN'); + break; + case JoinType.right: + b.write(' RIGHT JOIN'); + break; + case JoinType.full: + b.write(' FULL OUTER JOIN'); + break; + case JoinType.self: + b.write(' SELF JOIN'); + break; + } + + b.write(' $compiledTo'); + if (alias != null) b.write(' $alias'); + b.write(' ON $left$op$right'); + return b.toString(); + } +} diff --git a/api/3rd_party/angel3_orm/lib/src/join_on.dart b/api/3rd_party/angel3_orm/lib/src/join_on.dart new file mode 100644 index 0000000..7bfed40 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/join_on.dart @@ -0,0 +1,8 @@ +import 'builder.dart'; + +class JoinOn { + final SqlExpressionBuilder key; + final SqlExpressionBuilder value; + + JoinOn(this.key, this.value); +} diff --git a/api/3rd_party/angel3_orm/lib/src/map_query_values.dart b/api/3rd_party/angel3_orm/lib/src/map_query_values.dart new file mode 100644 index 0000000..398ba9b --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/map_query_values.dart @@ -0,0 +1,9 @@ +import 'query_values.dart'; + +/// A [QueryValues] implementation that simply writes to a [Map]. +class MapQueryValues extends QueryValues { + final Map values = {}; + + @override + Map toMap() => values; +} diff --git a/api/3rd_party/angel3_orm/lib/src/migration.dart b/api/3rd_party/angel3_orm/lib/src/migration.dart new file mode 100644 index 0000000..9af765e --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/migration.dart @@ -0,0 +1,135 @@ +const List SQL_RESERVED_WORDS = [ + 'SELECT', + 'UPDATE', + 'INSERT', + 'DELETE', + 'FROM', + 'ASC', + 'DESC', + 'VALUES', + 'RETURNING', + 'ORDER', + 'BY', +]; + +/// Applies additional attributes to a database column. +class Column { + /// If `true`, a SQL field will be nullable. + final bool isNullable; + + /// Specifies this column name. + final String name; + + /// Specifies the length of a `VARCHAR`. + final int length; + + /// Explicitly defines a SQL type for this column. + final ColumnType type; + + /// Specifies what kind of index this column is, if any. + final IndexType indexType; + + /// A custom SQL expression to execute, instead of a named column. + final String? expression; + + const Column( + {this.isNullable = true, + this.length = 255, + this.name = "", + this.type = ColumnType.varChar, + this.indexType = IndexType.none, + this.expression}); + + /// Returns `true` if [expression] is not `null`. + bool get hasExpression => expression != null; +} + +class PrimaryKey extends Column { + const PrimaryKey({ColumnType columnType = ColumnType.serial}) + : super(type: columnType, indexType: IndexType.primaryKey); +} + +const Column primaryKey = PrimaryKey(); + +/// Maps to SQL index types. +enum IndexType { + none, + + /// Standard index. + standardIndex, + + /// A primary key. + primaryKey, + + /// A *unique* index. + unique +} + +/// Maps to SQL data types. +/// +/// Features all types from this list: http://www.tutorialspoint.com/sql/sql-data-types.htm +class ColumnType { + /// The name of this data type. + final String name; + final bool hasSize; + + const ColumnType(this.name, [this.hasSize = false]); + + static const ColumnType boolean = ColumnType('boolean'); + + static const ColumnType smallSerial = ColumnType('smallserial'); + static const ColumnType serial = ColumnType('serial'); + static const ColumnType bigSerial = ColumnType('bigserial'); + + // Numbers + static const ColumnType bigInt = ColumnType('bigint'); + static const ColumnType int = ColumnType('int'); + static const ColumnType smallInt = ColumnType('smallint'); + static const ColumnType tinyInt = ColumnType('tinyint'); + static const ColumnType bit = ColumnType('bit'); + static const ColumnType decimal = ColumnType('decimal', true); + static const ColumnType numeric = ColumnType('numeric', true); + static const ColumnType money = ColumnType('money'); + static const ColumnType smallMoney = ColumnType('smallmoney'); + static const ColumnType float = ColumnType('float'); + static const ColumnType real = ColumnType('real'); + + // Dates and times + static const ColumnType dateTime = ColumnType('datetime'); + static const ColumnType smallDateTime = ColumnType('smalldatetime'); + static const ColumnType date = ColumnType('date'); + static const ColumnType time = ColumnType('time'); + static const ColumnType timeStamp = ColumnType('timestamp'); + static const ColumnType timeStampWithTimeZone = + ColumnType('timestamp with time zone'); + + // Strings + static const ColumnType char = ColumnType('char', true); + static const ColumnType varChar = ColumnType('varchar', true); + static const ColumnType varCharMax = ColumnType('varchar(max)'); + static const ColumnType text = ColumnType('text', true); + + // Unicode strings + static const ColumnType nChar = ColumnType('nchar', true); + static const ColumnType nVarChar = ColumnType('nvarchar', true); + static const ColumnType nVarCharMax = ColumnType('nvarchar(max)', true); + static const ColumnType nText = ColumnType('ntext', true); + + // Binary + static const ColumnType binary = ColumnType('binary', true); + static const ColumnType varBinary = ColumnType('varbinary', true); + static const ColumnType varBinaryMax = ColumnType('varbinary(max)', true); + static const ColumnType image = ColumnType('image', true); + + // JSON. + static const ColumnType json = ColumnType('json', true); + static const ColumnType jsonb = ColumnType('jsonb', true); + + // Misc. + static const ColumnType sqlVariant = ColumnType('sql_variant', true); + static const ColumnType uniqueIdentifier = + ColumnType('uniqueidentifier', true); + static const ColumnType xml = ColumnType('xml', true); + static const ColumnType cursor = ColumnType('cursor', true); + static const ColumnType table = ColumnType('table', true); +} diff --git a/api/3rd_party/angel3_orm/lib/src/order_by.dart b/api/3rd_party/angel3_orm/lib/src/order_by.dart new file mode 100644 index 0000000..4501cf4 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/order_by.dart @@ -0,0 +1,8 @@ +class OrderBy { + final String key; + final bool descending; + + const OrderBy(this.key, {this.descending = false}); + + String compile() => descending ? '$key DESC' : '$key ASC'; +} diff --git a/api/3rd_party/angel3_orm/lib/src/query.dart b/api/3rd_party/angel3_orm/lib/src/query.dart new file mode 100644 index 0000000..f718236 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/query.dart @@ -0,0 +1,425 @@ +import 'dart:async'; +import 'package:logging/logging.dart'; + +import 'annotations.dart'; +import 'join_builder.dart'; +import 'order_by.dart'; +import 'query_base.dart'; +import 'query_executor.dart'; +import 'query_values.dart'; +import 'query_where.dart'; +import 'package:optional/optional.dart'; + +/// A SQL `SELECT` query builder. +abstract class Query extends QueryBase { + final _log = Logger('Query'); + + final List _joins = []; + final Map _names = {}; + final List _orderBy = []; + + // An optional "parent query". If provided, [reserveName] will operate in + // the parent's context. + final Query? parent; + + /// A map of field names to explicit SQL expressions. The expressions will be aliased + /// to the given names. + final Map expressions = {}; + + String? _crossJoin, _groupBy; + int? _limit, _offset; + + Query({this.parent}); + + @override + Map get substitutionValues => + parent?.substitutionValues ?? super.substitutionValues; + + /// A reference to an abstract query builder. + /// + /// This is usually a generated class. + Where? get where; + + /// A set of values, for an insertion or update. + /// + /// This is usually a generated class. + QueryValues? get values; + + /// Preprends the [tableName] to the [String], [s]. + String adornWithTableName(String s) { + if (expressions.containsKey(s)) { + //return '${expressions[s]} AS $s'; + return '(${expressions[s]} AS $s)'; + } else { + return '$tableName.$s'; + } + } + + /// Returns a unique version of [name], which will not produce a collision within + /// the context of this [query]. + String reserveName(String name) { + if (parent != null) { + return parent!.reserveName(name); + } + // var n = _names[name] ??= 0; + // _names[name]++; + var n = 0; + var nn = _names[name]; + if (nn != null) { + n = nn; + nn++; + _names[name] = nn; + } else { + _names[name] = 1; + } + return n == 0 ? name : '$name$n'; + } + + /// Makes a [Where] clause. + Where newWhereClause() { + throw UnsupportedError( + 'This instance does not support creating WHERE clauses.'); + } + + /// Determines whether this query can be compiled. + /// + /// Used to prevent ambiguities in joins. + bool canCompile(Set trampoline) => true; + + /// Shorthand for calling [where].or with a [Where] clause. + void andWhere(void Function(Where) f) { + var w = newWhereClause(); + f(w); + where?.and(w); + } + + /// Shorthand for calling [where].or with a [Where] clause. + void notWhere(void Function(Where) f) { + var w = newWhereClause(); + f(w); + where?.not(w); + } + + /// Shorthand for calling [where].or with a [Where] clause. + void orWhere(void Function(Where) f) { + var w = newWhereClause(); + f(w); + where?.or(w); + } + + /// Limit the number of rows to return. + void limit(int n) { + _limit = n; + } + + /// Skip a number of rows in the query. + void offset(int n) { + _offset = n; + } + + /// Groups the results by a given key. + void groupBy(String key) { + _groupBy = key; + } + + /// Sorts the results by a key. + void orderBy(String key, {bool descending = false}) { + _orderBy.add(OrderBy(key, descending: descending)); + } + + /// Execute a `CROSS JOIN` (Cartesian product) against another table. + void crossJoin(String tableName) { + _crossJoin = tableName; + } + + String _joinAlias(Set trampoline) { + var i = _joins.length; + + while (true) { + var a = 'a$i'; + if (trampoline.add(a)) { + return a; + } else { + i++; + } + } + } + + String Function() _compileJoin(tableName, Set trampoline) { + if (tableName is String) { + return () => tableName; + } else if (tableName is Query) { + return () { + var c = tableName.compile(trampoline); + //if (c == null) return c; + if (c == '') { + return c; + } + return '($c)'; + }; + } else { + _log.severe('$tableName must be a String or Query'); + throw ArgumentError.value( + tableName, 'tableName', 'must be a String or Query'); + } + } + + void _makeJoin( + tableName, + Set? trampoline, + String? alias, + JoinType type, + String localKey, + String foreignKey, + String op, + List additionalFields) { + trampoline ??= {}; + + // Pivot tables guard against ambiguous fields by excluding tables + // that have already been queried in this scope. + if (trampoline.contains(tableName) && trampoline.contains(this.tableName)) { + // ex. if we have {roles, role_users}, then don't join "roles" again. + return; + } + + var to = _compileJoin(tableName, trampoline); + alias ??= _joinAlias(trampoline); + if (tableName is Query) { + for (var field in tableName.fields) { + tableName.aliases[field] = '${alias}_$field'; + } + } + _joins.add(JoinBuilder(type, this, to, localKey, foreignKey, + op: op, + alias: alias, + additionalFields: additionalFields, + aliasAllFields: tableName is Query)); + } + + /// Execute an `INNER JOIN` against another table. + void join(tableName, String localKey, String foreignKey, + {String op = '=', + List additionalFields = const [], + Set? trampoline, + String? alias}) { + _makeJoin(tableName, trampoline, alias, JoinType.inner, localKey, foreignKey, op, + additionalFields); + } + + /// Execute a `LEFT JOIN` against another table. + void leftJoin(tableName, String localKey, String foreignKey, + {String op = '=', + List additionalFields = const [], + Set? trampoline, + String? alias}) { + _makeJoin(tableName, trampoline, alias, JoinType.left, localKey, foreignKey, op, + additionalFields); + } + + /// Execute a `RIGHT JOIN` against another table. + void rightJoin(tableName, String localKey, String foreignKey, + {String op = '=', + List additionalFields = const [], + Set? trampoline, + String? alias}) { + _makeJoin(tableName, trampoline, alias, JoinType.right, localKey, foreignKey, op, + additionalFields); + } + + /// Execute a `FULL OUTER JOIN` against another table. + void fullOuterJoin(tableName, String localKey, String foreignKey, + {String op = '=', + List additionalFields = const [], + Set? trampoline, + String? alias}) { + _makeJoin(tableName, trampoline, alias, JoinType.full, localKey, foreignKey, op, + additionalFields); + } + + /// Execute a `SELF JOIN`. + void selfJoin(tableName, String localKey, String foreignKey, + {String op = '=', + List additionalFields = const [], + Set? trampoline, + String? alias}) { + _makeJoin(tableName, trampoline, alias, JoinType.self, localKey, foreignKey, op, + additionalFields); + } + + @override + String compile(Set trampoline, + {bool includeTableName = false, + String? preamble, + bool withFields = true, + String? fromQuery}) { + // One table MAY appear multiple times in a query. + if (!canCompile(trampoline)) { + //return null; + //throw Exception('One table appear multiple times in a query'); + return ''; + } + + includeTableName = includeTableName || _joins.isNotEmpty; + var b = StringBuffer(preamble ?? 'SELECT'); + b.write(' '); + List f; + + var compiledJoins = {}; + + //if (fields == null) { + if (fields.isEmpty) { + f = ['*']; + } else { + f = List.from(fields.map((s) { + String? ss = includeTableName ? '$tableName.$s' : s; + if (expressions.containsKey(s)) { + ss = '( ${expressions[s]} )'; + //ss = expressions[s]; + } + var cast = casts[s]; + if (cast != null) ss = 'CAST ($ss AS $cast)'; + if (aliases.containsKey(s)) { + if (cast != null) { + ss = '($ss) AS ${aliases[s]}'; + } else { + ss = '$ss AS ${aliases[s]}'; + } + if (expressions.containsKey(s)) { + // ss = '($ss)'; + } + } else if (expressions.containsKey(s)) { + if (cast != null) { + ss = '($ss) AS $s'; + // ss = '(($ss) AS $s)'; + } else { + ss = '$ss AS $s'; + // ss = '($ss AS $s)'; + } + } + return ss; + })); + _joins.forEach((j) { + var c = compiledJoins[j] = j.compile(trampoline); + //if (c != null) { + if (c != '') { + var additional = j.additionalFields.map(j.nameFor).toList(); + f.addAll(additional); + } else { + // If compilation failed, fill in NULL placeholders. + for (var i = 0; i < j.additionalFields.length; i++) { + f.add('NULL'); + } + } + }); + } + if (withFields) b.write(f.join(', ')); + fromQuery ??= tableName; + b.write(' FROM $fromQuery'); + + // No joins if it's not a select. + if (preamble == null) { + if (_crossJoin != null) b.write(' CROSS JOIN $_crossJoin'); + for (var join in _joins) { + var c = compiledJoins[join]; + if (c != null) b.write(' $c'); + } + } + + var whereClause = + where?.compile(tableName: includeTableName ? tableName : null); + if (whereClause?.isNotEmpty == true) { + b.write(' WHERE $whereClause'); + } + if (_groupBy != null) b.write(' GROUP BY $_groupBy'); + for (var item in _orderBy) { + b.write(' ORDER BY ${item.compile()}'); + } + if (_limit != null) b.write(' LIMIT $_limit'); + if (_offset != null) b.write(' OFFSET $_offset'); + return b.toString(); + } + + @override + Future> getOne(QueryExecutor executor) { + //limit(1); + return super.getOne(executor); + } + + Future> delete(QueryExecutor executor) { + var sql = compile({}, preamble: 'DELETE', withFields: false); + + //_log.fine("Delete Query = $sql"); + + if (_joins.isEmpty) { + return executor + .query(tableName, sql, substitutionValues, + fields.map(adornWithTableName).toList()) + .then((it) => deserializeList(it)); + } else { + return executor.transaction((tx) async { + // TODO: Can this be done with just *one* query? + var existing = await get(tx); + //var sql = compile(preamble: 'SELECT $tableName.id', withFields: false); + return tx + .query(tableName, sql, substitutionValues) + .then((_) => existing); + }); + } + } + + Future> deleteOne(QueryExecutor executor) { + return delete(executor).then((it) => + it.isEmpty == true ? Optional.empty() : Optional.ofNullable(it.first)); + } + + Future> insert(QueryExecutor executor) { + var insertion = values?.compileInsert(this, tableName); + + if (insertion == '') { + throw StateError('No values have been specified for update.'); + } else { + // TODO: How to do this in a non-Postgres DB? + var returning = fields.map(adornWithTableName).join(', '); + var sql = compile({}); + sql = 'WITH $tableName as ($insertion RETURNING $returning) ' + sql; + + //_log.fine("Insert Query = $sql"); + + return executor.query(tableName, sql, substitutionValues).then((it) { + // Return SQL execution results + return it.isEmpty ? Optional.empty() : deserialize(it.first); + }); + } + } + + Future> update(QueryExecutor executor) async { + var updateSql = StringBuffer('UPDATE $tableName '); + var valuesClause = values?.compileForUpdate(this); + + if (valuesClause == '') { + throw StateError('No values have been specified for update.'); + } else { + updateSql.write(' $valuesClause'); + var whereClause = where?.compile(); + if (whereClause?.isNotEmpty == true) { + updateSql.write(' WHERE $whereClause'); + } + if (_limit != null) updateSql.write(' LIMIT $_limit'); + + var returning = fields.map(adornWithTableName).join(', '); + var sql = compile({}); + sql = 'WITH $tableName as ($updateSql RETURNING $returning) ' + sql; + + //_log.fine("Update Query = $sql"); + + return executor + .query(tableName, sql, substitutionValues) + .then((it) => deserializeList(it)); + } + } + + Future> updateOne(QueryExecutor executor) { + return update(executor).then( + (it) => it.isEmpty ? Optional.empty() : Optional.ofNullable(it.first)); + } +} diff --git a/api/3rd_party/angel3_orm/lib/src/query_base.dart b/api/3rd_party/angel3_orm/lib/src/query_base.dart new file mode 100644 index 0000000..17a6215 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/query_base.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'query_executor.dart'; +import 'union.dart'; +import 'package:optional/optional.dart'; + +/// A base class for objects that compile to SQL queries, typically within an ORM. +abstract class QueryBase { + /// Casts to perform when querying the database. + Map get casts => {}; + + /// `AS` aliases to inject into the query, if any. + Map aliases = {}; + + /// Values to insert into a prepared statement. + final Map substitutionValues = {}; + + /// The table against which to execute this query. + String get tableName; + + /// The list of fields returned by this query. + /// + /// @deprecated If it's `null`, then this query will perform a `SELECT *`. + /// If it's empty, then this query will perform a `SELECT *`. + List get fields; + + /// A String of all [fields], joined by a comma (`,`). + String get fieldSet => fields.map((k) { + var cast = casts[k]; + if (!aliases.containsKey(k)) { + return cast == null ? k : 'CAST ($k AS $cast)'; + } else { + var inner = cast == null ? k : '(CAST ($k AS $cast))'; + return '$inner AS ${aliases[k]}'; + } + }).join(', '); + + String compile(Set trampoline, + {bool includeTableName = false, + String preamble = '', + bool withFields = true}); + + Optional deserialize(List row); + + List deserializeList(List> it) { + var optResult = it.map(deserialize).toList(); + var result = []; + optResult.forEach((element) { + element.ifPresent((item) { + result.add(item); + }); + }); + + return result; + } + + Future> get(QueryExecutor executor) async { + var sql = compile({}); + + //_log.fine('sql = $sql'); + //_log.fine('substitutionValues = $substitutionValues'); + + return executor.query(tableName, sql, substitutionValues).then((it) { + return deserializeList(it); + }); + } + + Future> getOne(QueryExecutor executor) { + //return get(executor).then((it) => it.isEmpty ? : it.first); + return get(executor).then( + (it) => it.isEmpty ? Optional.empty() : Optional.ofNullable(it.first)); + } + + Union union(QueryBase other) { + return Union(this, other); + } + + Union unionAll(QueryBase other) { + return Union(this, other, all: true); + } +} diff --git a/api/3rd_party/angel3_orm/lib/src/query_executor.dart b/api/3rd_party/angel3_orm/lib/src/query_executor.dart new file mode 100644 index 0000000..fac6195 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/query_executor.dart @@ -0,0 +1,23 @@ +import 'dart:async'; + +/// An abstract interface that performs queries. +/// +/// This class should be implemented. +abstract class QueryExecutor { + const QueryExecutor(); + + /// Executes a single query. + Future> query( + String tableName, String query, Map substitutionValues, + [List returningFields = const []]); + + /// Enters a database transaction, performing the actions within, + /// and returning the results of [f]. + /// + /// If [f] fails, the transaction will be rolled back, and the + /// responsible exception will be re-thrown. + /// + /// Whether nested transactions are supported depends on the + /// underlying driver. + Future transaction(FutureOr Function(QueryExecutor) f); +} diff --git a/api/3rd_party/angel3_orm/lib/src/query_values.dart b/api/3rd_party/angel3_orm/lib/src/query_values.dart new file mode 100644 index 0000000..2b265cf --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/query_values.dart @@ -0,0 +1,89 @@ +import 'query.dart'; + +abstract class QueryValues { + Map get casts => {}; + + Map toMap(); + + String applyCast(String name, String sub) { + if (casts.containsKey(name)) { + var type = casts[name]; + return 'CAST ($sub as $type)'; + } else { + return sub; + } + } + + String compileInsert(Query query, String tableName) { + var data = Map.from(toMap()); + var now = DateTime.now(); + if (data.containsKey('created_at') && data['created_at'] == null) { + data['created_at'] = now; + } + if (data.containsKey('createdAt') && data['createdAt'] == null) { + data['createdAt'] = now; + } + if (data.containsKey('updated_at') && data['updated_at'] == null) { + data['updated_at'] = now; + } + if (data.containsKey('updatedAt') && data['updatedAt'] == null) { + data['updatedAt'] = now; + } + var keys = data.keys.toList(); + keys.where((k) => !query.fields.contains(k)).forEach(data.remove); + if (data.isEmpty) { + return ''; + } + var fieldSet = data.keys.join(', '); + var b = StringBuffer('INSERT INTO $tableName ($fieldSet) VALUES ('); + var i = 0; + + for (var entry in data.entries) { + if (i++ > 0) b.write(', '); + + var name = query.reserveName(entry.key); + + var s = applyCast(entry.key, '@$name'); + query.substitutionValues[name] = entry.value; + b.write(s); + } + + b.write(')'); + return b.toString(); + } + + String compileForUpdate(Query query) { + var data = toMap(); + if (data.isEmpty) { + return ''; + } + var now = DateTime.now(); + if (data.containsKey('created_at') && data['created_at'] == null) { + data.remove('created_at'); + } + if (data.containsKey('createdAt') && data['createdAt'] == null) { + data.remove('createdAt'); + } + if (data.containsKey('updated_at') && data['updated_at'] == null) { + data['updated_at'] = now; + } + if (data.containsKey('updatedAt') && data['updatedAt'] == null) { + data['updatedAt'] = now; + } + var b = StringBuffer('SET'); + var i = 0; + + for (var entry in data.entries) { + if (i++ > 0) b.write(','); + b.write(' '); + b.write(entry.key); + b.write('='); + + var name = query.reserveName(entry.key); + var s = applyCast(entry.key, '@$name'); + query.substitutionValues[name] = entry.value; + b.write(s); + } + return b.toString(); + } +} diff --git a/api/3rd_party/angel3_orm/lib/src/query_where.dart b/api/3rd_party/angel3_orm/lib/src/query_where.dart new file mode 100644 index 0000000..5a700c2 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/query_where.dart @@ -0,0 +1,69 @@ +import 'builder.dart'; + +/// Builds a SQL `WHERE` clause. +abstract class QueryWhere { + final Set _and = {}; + final Set _not = {}; + final Set _or = {}; + final Set _raw = {}; + + Iterable get expressionBuilders; + + void and(QueryWhere other) { + _and.add(other); + } + + void not(QueryWhere other) { + _not.add(other); + } + + void or(QueryWhere other) { + _or.add(other); + } + + void raw(String whereRaw) { + _raw.add(whereRaw); + } + + String compile({String? tableName}) { + var b = StringBuffer(); + var i = 0; + + for (var builder in expressionBuilders) { + var key = builder.columnName; + if (tableName != null) key = '$tableName.$key'; + if (builder.hasValue) { + if (i++ > 0) b.write(' AND '); + if (builder is DateTimeSqlExpressionBuilder || + (builder is JsonSqlExpressionBuilder && builder.hasRaw)) { + if (tableName != null) b.write('$tableName.'); + b.write(builder.compile()); + } else { + b.write('$key ${builder.compile()}'); + } + } + } + + for (var raw in _raw) { + if (i++ > 0) b.write(' AND '); + b.write(' ($raw)'); + } + + for (var other in _and) { + var sql = other.compile(); + if (sql.isNotEmpty) b.write(' AND ($sql)'); + } + + for (var other in _not) { + var sql = other.compile(); + if (sql.isNotEmpty) b.write(' NOT ($sql)'); + } + + for (var other in _or) { + var sql = other.compile(); + if (sql.isNotEmpty) b.write(' OR ($sql)'); + } + + return b.toString(); + } +} diff --git a/api/3rd_party/angel3_orm/lib/src/relations.dart b/api/3rd_party/angel3_orm/lib/src/relations.dart new file mode 100644 index 0000000..ab7097e --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/relations.dart @@ -0,0 +1,91 @@ +import 'annotations.dart'; + +abstract class RelationshipType { + static const int hasMany = 0; + static const int hasOne = 1; + static const int belongsTo = 2; + static const int manyToMany = 3; +} + +class Relationship { + final int type; + final String? localKey; + final String? foreignKey; + final String? foreignTable; + final bool cascadeOnDelete; + final JoinType? joinType; + + const Relationship(this.type, + {this.localKey, + this.foreignKey, + this.foreignTable, + this.cascadeOnDelete = false, + this.joinType}); +} + +class HasMany extends Relationship { + const HasMany( + {String? localKey, + String? foreignKey, + String? foreignTable, + bool cascadeOnDelete = false, + JoinType? joinType}) + : super(RelationshipType.hasMany, + localKey: localKey, + foreignKey: foreignKey, + foreignTable: foreignTable, + cascadeOnDelete: cascadeOnDelete == true, + joinType: joinType); +} + +const HasMany hasMany = HasMany(); + +class HasOne extends Relationship { + const HasOne( + {String? localKey, + String? foreignKey, + String? foreignTable, + bool cascadeOnDelete = false, + JoinType? joinType}) + : super(RelationshipType.hasOne, + localKey: localKey, + foreignKey: foreignKey, + foreignTable: foreignTable, + cascadeOnDelete: cascadeOnDelete == true, + joinType: joinType); +} + +const HasOne hasOne = HasOne(); + +class BelongsTo extends Relationship { + const BelongsTo( + {String? localKey, + String? foreignKey, + String? foreignTable, + JoinType? joinType}) + : super(RelationshipType.belongsTo, + localKey: localKey, + foreignKey: foreignKey, + foreignTable: foreignTable, + joinType: joinType); +} + +const BelongsTo belongsTo = BelongsTo(); + +class ManyToMany extends Relationship { + final Type through; + + const ManyToMany(this.through, + {String? localKey, + String? foreignKey, + String? foreignTable, + bool cascadeOnDelete = false, + JoinType? joinType}) + : super( + RelationshipType.hasMany, // Many-to-Many is actually just a hasMany + localKey: localKey, + foreignKey: foreignKey, + foreignTable: foreignTable, + cascadeOnDelete: cascadeOnDelete == true, + joinType: joinType); +} diff --git a/api/3rd_party/angel3_orm/lib/src/union.dart b/api/3rd_party/angel3_orm/lib/src/union.dart new file mode 100644 index 0000000..5ab2f86 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/union.dart @@ -0,0 +1,38 @@ +import 'query_base.dart'; +import 'package:optional/optional.dart'; + +/// Represents the `UNION` of two subqueries. +class Union extends QueryBase { + /// The subject(s) of this binary operation. + final QueryBase left, right; + + /// Whether this is a `UNION ALL` operation. + final bool all; + + @override + final String tableName; + + Union(this.left, this.right, {this.all = false, String? tableName}) + : tableName = tableName ?? left.tableName { + substitutionValues + ..addAll(left.substitutionValues) + ..addAll(right.substitutionValues); + } + + @override + List get fields => left.fields; + + @override + Optional deserialize(List row) => left.deserialize(row); + + @override + String compile(Set trampoline, + {bool includeTableName = false, + String? preamble, + bool withFields = true}) { + var selector = all == true ? 'UNION ALL' : 'UNION'; + var t1 = Set.from(trampoline); + var t2 = Set.from(trampoline); + return '(${left.compile(t1, includeTableName: includeTableName)}) $selector (${right.compile(t2, includeTableName: includeTableName)})'; + } +} diff --git a/api/3rd_party/angel3_orm/lib/src/util.dart b/api/3rd_party/angel3_orm/lib/src/util.dart new file mode 100644 index 0000000..939b36c --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/util.dart @@ -0,0 +1,3 @@ +import 'package:charcode/ascii.dart'; + +bool isAscii(int ch) => ch >= $nul && ch <= $del; diff --git a/api/3rd_party/angel3_orm/mono_pkg.yaml b/api/3rd_party/angel3_orm/mono_pkg.yaml new file mode 100644 index 0000000..e69de29 diff --git a/api/3rd_party/angel3_orm/pubspec.yaml b/api/3rd_party/angel3_orm/pubspec.yaml new file mode 100644 index 0000000..c688aba --- /dev/null +++ b/api/3rd_party/angel3_orm/pubspec.yaml @@ -0,0 +1,21 @@ +name: angel3_orm +version: 4.0.4 +description: Runtime support for Angel3 ORM. Includes base classes for queries. +homepage: https://angel3-framework.web.app/ +repository: https://github.com/dukefirehawk/angel/tree/master/packages/orm/angel_orm +environment: + sdk: '>=2.12.0 <3.0.0' +dependencies: + charcode: ^1.2.0 + intl: ^0.17.0 + meta: ^1.3.0 + string_scanner: ^1.1.0 + optional: ^6.0.0 + logging: ^1.0.0 +dev_dependencies: + angel3_model: ^3.0.0 + angel3_serialize: ^4.1.0 + angel3_serialize_generator: ^4.1.0 + build_runner: ^2.1.1 + test: ^1.17.4 + lints: ^1.0.0 \ No newline at end of file diff --git a/api/bin/migrate.dart b/api/bin/migrate.dart index eec2b33..44584a5 100644 --- a/api/bin/migrate.dart +++ b/api/bin/migrate.dart @@ -5,6 +5,8 @@ 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'; @@ -29,6 +31,8 @@ void main(List args) async { UserMigration(), UserSeed(), SchemeMigration(), + DownloadHistoryMigration(), + LikeRecordMigration(), ]); await runMigrations(migrationRunner, args); } diff --git a/api/lib/apis.dart b/api/lib/apis.dart index 3692087..ada4277 100644 --- a/api/lib/apis.dart +++ b/api/lib/apis.dart @@ -31,10 +31,17 @@ class SchemeApis { String get upload => [path, 'upload'].joinPath(); - String get userUploads => [path, 'user', 'uploads'].joinPath(); + String markAsShared({required StringParam schemeId}) => [path, 'mark_as_shared', schemeId].joinPath(); + + String user({required StringParam type}) => [path, 'user', type].joinPath(); + + String download({required StringParam schemeId}) => [path, 'download', schemeId].joinPath(); + + String like({required StringParam schemeId, required StringParam isLike}) => [path, 'like', schemeId, isLike].joinPath(); } final _paramsMap = { + 'BoolParam': BoolParam.nameOnRoute, 'IntParam': IntParam.nameOnRoute, 'DoubleParam': DoubleParam.nameOnRoute, 'StringParam': StringParam.nameOnRoute, @@ -58,6 +65,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; @@ -94,6 +113,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); } diff --git a/api/lib/src/models/download_history.dart b/api/lib/src/models/download_history.dart new file mode 100644 index 0000000..10218d6 --- /dev/null +++ b/api/lib/src/models/download_history.dart @@ -0,0 +1,19 @@ +import 'package:angel3_orm/angel3_orm.dart'; +import 'package:angel3_serialize/angel3_serialize.dart'; +import 'package:dde_gesture_manager_api/src/models/base_model.dart'; +import 'package:angel3_migration/angel3_migration.dart'; +import 'package:optional/optional.dart'; + +part 'download_history.g.dart'; + +@serializable +@orm +abstract class _DownloadHistory extends BaseModel { + @Column(isNullable: false, indexType: IndexType.standardIndex) + @SerializableField(isNullable: false) + int? get uid; + + @Column(isNullable: false, indexType: IndexType.standardIndex) + @SerializableField(isNullable: false) + int? get schemeId; +} \ No newline at end of file diff --git a/api/lib/src/models/like_record.dart b/api/lib/src/models/like_record.dart new file mode 100644 index 0000000..c794cfe --- /dev/null +++ b/api/lib/src/models/like_record.dart @@ -0,0 +1,23 @@ +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; +} \ No newline at end of file diff --git a/api/lib/src/models/scheme.dart b/api/lib/src/models/scheme.dart index 49e3516..ff665c3 100644 --- a/api/lib/src/models/scheme.dart +++ b/api/lib/src/models/scheme.dart @@ -33,3 +33,91 @@ abstract class _Scheme extends BaseModel { @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? get metadata; + + @SerializableField(isNullable: true) + @Column(expression: 'lr.liked') + bool? get liked; +} + +@serializable +abstract class _SimpleSchemeTransMetaData { + @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( + 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, + ); diff --git a/api/lib/src/routes/controllers/auth_controllers.dart b/api/lib/src/routes/controllers/auth_controllers.dart index 35d8823..41d4539 100644 --- a/api/lib/src/routes/controllers/auth_controllers.dart +++ b/api/lib/src/routes/controllers/auth_controllers.dart @@ -84,7 +84,7 @@ Future configureServer(Angel app) async { chain( [ jwtMiddleware(), - (req, res) => req.user.blocked == false ? res.noContent() : res.forbidden(), + (req, res) => req.user!.blocked == false ? res.noContent() : res.forbidden(), ], ), ); diff --git a/api/lib/src/routes/controllers/controller_extensions.dart b/api/lib/src/routes/controllers/controller_extensions.dart index b33549b..117703d 100644 --- a/api/lib/src/routes/controllers/controller_extensions.dart +++ b/api/lib/src/routes/controllers/controller_extensions.dart @@ -48,5 +48,11 @@ extension RedisClient on RequestContext { } extension JWTUserInstance on RequestContext { - User get user => container!.make(); + User? get user { + try { + return container!.make(); + } catch (_) { + return null; + } + } } diff --git a/api/lib/src/routes/controllers/middlewares.dart b/api/lib/src/routes/controllers/middlewares.dart index 2ea42ff..f565a6e 100644 --- a/api/lib/src/routes/controllers/middlewares.dart +++ b/api/lib/src/routes/controllers/middlewares.dart @@ -4,9 +4,10 @@ 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.forbidden(); } @@ -18,17 +19,22 @@ RequestHandler jwtMiddleware() { if (reqContainer.has() || req.method == 'OPTIONS') { return true; } else if (reqContainer.has>()) { - User user = await reqContainer.makeAsync(); - var authToken = req.container!.make(); - if (user.secret(req.app!.configuration['password_salt']) != authToken.payload[UserFields.password]) { - return _reject(res); + try { + User user = await reqContainer.makeAsync(); + var authToken = req.container!.make(); + 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); } }; } diff --git a/api/lib/src/routes/controllers/scheme_controllers.dart b/api/lib/src/routes/controllers/scheme_controllers.dart index 4d4a1ca..03f9306 100644 --- a/api/lib/src/routes/controllers/scheme_controllers.dart +++ b/api/lib/src/routes/controllers/scheme_controllers.dart @@ -2,6 +2,8 @@ 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'; @@ -20,16 +22,18 @@ Future configureServer(Angel app) async { var scheme = SchemeSerializer.fromMap(req.bodyAsMap); var schemeQuery = SchemeQuery(); schemeQuery.where!.uuid.equals(scheme.uuid!); - var one = await schemeQuery.getOne(req.queryExecutor); - schemeQuery = SchemeQuery(); - schemeQuery.values.copyFrom(scheme); - schemeQuery.values.uid = int.parse(req.user.id!); - if (one.isEmpty) { - await schemeQuery.insert(req.queryExecutor); - } else { - schemeQuery.whereId = int.parse(one.value.id!); - await schemeQuery.updateOne(req.queryExecutor); - } + 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(); @@ -40,21 +44,145 @@ Future configureServer(Angel app) async { ), ); + 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.user.route, + chain( + [ + jwtMiddleware(), + (req, res) async { + var schemeQuery = SimpleSchemeQuery(); + var type = req.params['type']; + var likeRecordTableName = LikeRecordQuery().tableName; + schemeQuery.leftJoin(likeRecordTableName, SchemeFields.id, LikeRecordFields.schemeId, alias: 'lr'); + + switch (type) { + case 'uploaded': + schemeQuery.where!.uid.equals(req.user!.idAsInt); + break; + case 'downloaded': + var downloadHistoryTableName = DownloadHistoryQuery().tableName; + schemeQuery.leftJoin(downloadHistoryTableName, SchemeFields.id, DownloadHistoryFields.schemeId, + alias: 'dh'); + schemeQuery.where!.raw('dh.${DownloadHistoryFields.uid} = ${req.user!.idAsInt}'); + break; + case 'liked': + schemeQuery.where!.raw('lr.${LikeRecordFields.uid} = ${req.user!.idAsInt}'); + schemeQuery.where!.raw('lr.${LikeRecordFields.liked} = true'); + break; + default: + return res.unProcessableEntity(); + } + schemeQuery.orderBy('${schemeQuery.tableName}.${SchemeFields.updatedAt}', descending: true); + return schemeQuery.get(req.queryExecutor).then((value) => value.map(transSimpleSchemeMetaData).toList()); + }, + ], + ), + ); + app.get( - Apis.scheme.userUploads, + 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 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!.uid.equals(int.parse(req.user.id!)); - schemeQuery.orderBy(SchemeFields.updatedAt, descending: true); - return schemeQuery.get(req.queryExecutor).then((value) => value.map((e) => { - 'name': e.name, - 'description': e.description, - }).toList()); + 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 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(); }, ], ), ); -} \ No newline at end of file +} diff --git a/api/pubspec.yaml b/api/pubspec.yaml index 9831fbc..244c628 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -8,8 +8,7 @@ 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_migration: ^4.0.3 angel3_orm_postgres: ^3.0.0 angel3_serialize: ^4.1.0 angel3_production: ^3.1.0 @@ -21,13 +20,15 @@ dependencies: yaml: ^3.1.0 mailer: ^5.0.2 uuid: ^3.0.5 + angel3_orm: + path: 3rd_party/angel3_orm neat_cache: path: 3rd_party/neat_cache dev_dependencies: angel3_hot: ^4.2.0 angel3_jinja: ^2.0.1 angel3_migration_runner: ^4.0.0 - angel3_orm_generator: ^4.1.0 + angel3_orm_generator: ^4.1.3 angel3_serialize_generator: ^4.2.0 angel3_test: ^4.0.0 build_runner: ^2.0.3 @@ -35,5 +36,6 @@ dev_dependencies: test: ^1.17.5 lints: ^1.0.0 - - +dependency_overrides: + angel3_orm: + path: 3rd_party/angel3_orm diff --git a/app/lib/http/api.dart b/app/lib/http/api.dart index b3264fe..4841c11 100644 --- a/app/lib/http/api.dart +++ b/app/lib/http/api.dart @@ -6,6 +6,7 @@ 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; @@ -85,7 +86,7 @@ class Api { if (ignoreErrorHandle) throw e; else - return _handleHttpError(e); + _handleHttpError(e); }, ); @@ -116,7 +117,7 @@ class Api { if (ignoreErrorHandle) throw e; else - return _handleHttpError(e); + _handleHttpError(e); }, ); @@ -158,6 +159,19 @@ class Api { ), ).then((value) => value == HttpStatus.noContent); - static Future> userUploads() => - _get(Apis.scheme.userUploads, listRespBuilderWrap(SchemeSerializer.fromMap)); + static Future> userSchemes({required SchemeListType type}) => + _get(Apis.scheme.user(type: type.name.param), listRespBuilderWrap(SimpleSchemeTransMetaDataSerializer.fromMap)); + + static Future likeScheme({required String schemeId, required bool isLike}) => _get( + Apis.scheme.like(schemeId: schemeId.param, isLike: StringParam(isLike ? 'like' : 'unlike')), + getStatusCodeFunc) + .then((value) { + 123.sout(); + return value == HttpStatus.noContent; + }); + + static Future downloadScheme({required String schemeId}) => _get( + Apis.scheme.download(schemeId: schemeId.param), + SchemeForDownloadSerializer.fromMap, + ); } diff --git a/app/lib/main.dart b/app/lib/main.dart index a507d48..93e1f5d 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -11,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'; @@ -71,15 +72,7 @@ class MyApp extends StatelessWidget { firstChild: Builder(builder: (context) { Future.microtask(() { initEvents(context); - if (H().lastCheckAuthStatusTime != null && - H().lastCheckAuthStatusTime!.difference(DateTime.now()) < Duration(minutes: 10)) return; - if (context.read().accessToken.notNull) { - Api.checkAuthStatus().then((value) { - if (!value) context.read().setProps(email: '', accessToken: ''); - }); - } else { - H().lastCheckAuthStatusTime = DateTime.now(); - } + SimpleThrottle.throttledFunc(_checkAuthStatus, timeout: const Duration(minutes: 5))?.call(context); }); return Container(); }), @@ -91,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().accessToken.notNull) { + Api.checkAuthStatus().then((value) { + if (!value) context.read().setProps(email: '', accessToken: ''); + }); + } else { + H().lastCheckAuthStatusTime = DateTime.now(); + } +} diff --git a/app/lib/models/content_layout.dart b/app/lib/models/content_layout.dart index fed062f..57325ef 100644 --- a/app/lib/models/content_layout.dart +++ b/app/lib/models/content_layout.dart @@ -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; } diff --git a/app/lib/pages/local_manager.dart b/app/lib/pages/local_manager.dart index 14276bd..514a168 100644 --- a/app/lib/pages/local_manager.dart +++ b/app/lib/pages/local_manager.dart @@ -184,8 +184,8 @@ class _LocalManagerState extends State { ), ), ), - Container(height: 5), - Container( + Padding( + padding: const EdgeInsets.only(top: 5), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ diff --git a/app/lib/pages/market_or_me.dart b/app/lib/pages/market_or_me.dart index 00e1a00..af2bf2a 100644 --- a/app/lib/pages/market_or_me.dart +++ b/app/lib/pages/market_or_me.dart @@ -82,7 +82,7 @@ class MarketOrMe extends StatelessWidget { Widget buildMeContent(BuildContext context) { var accessToken = context.watch().accessToken; if (accessToken.isNull) return LoginWidget(); - return MeWidget(); + return Expanded(child: MeWidget()); } Widget buildMarketContent(BuildContext context) { diff --git a/app/lib/utils/init_linux.dart b/app/lib/utils/init_linux.dart index d650cce..b69cbf0 100644 --- a/app/lib/utils/init_linux.dart +++ b/app/lib/utils/init_linux.dart @@ -49,9 +49,9 @@ Future 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 initEvents(BuildContext context) async { }); } }); + } } Future initConfigs() async { diff --git a/app/lib/utils/simple_throttle.dart b/app/lib/utils/simple_throttle.dart new file mode 100644 index 0000000..7e22507 --- /dev/null +++ b/app/lib/utils/simple_throttle.dart @@ -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 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); +} diff --git a/app/lib/widgets/dde_button.dart b/app/lib/widgets/dde_button.dart index 7c726e7..f0bac0a 100644 --- a/app/lib/widgets/dde_button.dart +++ b/app/lib/widgets/dde_button.dart @@ -141,6 +141,57 @@ class DButton extends StatefulWidget { 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, diff --git a/app/lib/widgets/me.dart b/app/lib/widgets/me.dart index 3cd6663..7e3d71b 100644 --- a/app/lib/widgets/me.dart +++ b/app/lib/widgets/me.dart @@ -1,13 +1,23 @@ import 'package:auto_size_text/auto_size_text.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:dde_gesture_manager/extensions.dart'; +import 'package:flutter_platform_alert/flutter_platform_alert.dart'; import 'dde_button.dart'; +enum SchemeListType { + uploaded, + downloaded, + liked, +} + class MeWidget extends StatefulWidget { const MeWidget({Key? key}) : super(key: key); @@ -16,23 +26,46 @@ class MeWidget extends StatefulWidget { } class _MeWidgetState extends State { - List uploads = []; + List _schemes = []; + SchemeListType _type = SchemeListType.uploaded; + String? _selected; + String? _hovering; @override void initState() { super.initState(); - Api.userUploads().then((value) { + Api.userSchemes(type: _type).then((value) { if (mounted) setState(() { - uploads = value; + _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().currentActiveColor; + return _color; + } + + _refreshList() { + Future.delayed(const Duration(milliseconds: 100), () { + Api.userSchemes(type: _type).then((value) { + if (mounted) + setState(() { + _schemes = value; + }); + }); + }); + } + @override Widget build(BuildContext context) { + var currentSelectedScheme = _schemes.firstWhereOrNull((e) => e.uuid == _selected); return Padding( - padding: EdgeInsets.symmetric(vertical: 10), + padding: EdgeInsets.only(top: 10), child: Column( children: [ Row( @@ -54,12 +87,157 @@ class _MeWidgetState extends State { ), ], ), - Text('我的上传'), - Container( - height: 400, - child: ListView.builder( - itemBuilder: (context, index) => Text(uploads[index].name ?? ''), - itemCount: uploads.length, + 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) + setState(() { + _schemes = value; + }); + }); + }, + 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: Padding( + padding: EdgeInsets.symmetric(horizontal: 1, vertical: 2), + child: ListView.builder( + itemBuilder: (context, index) => GestureDetector( + onTap: () { + setState(() { + _selected = _schemes[index].uuid; + }); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) { + setState(() { + _hovering = _schemes[index].uuid; + }); + }, + child: Container( + color: _getItemBackgroundColor(index, _schemes[index].uuid), + child: Padding( + padding: const EdgeInsets.only(left: 6, right: 12.0), + child: DefaultTextStyle( + style: context.t.textTheme.bodyText2!.copyWith( + color: _selected == _schemes[index].uuid ? Colors.white : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(_schemes[index].name ?? ''), + Row( + children: [ + Text('${_schemes[index].downloads ?? 0}'.padLeft(4)), + Icon( + Icons.file_download, + size: 18, + ), + Text('${_schemes[index].likes ?? 0}'.padLeft(4)), + Icon(_schemes[index].liked == true ? Icons.thumb_up : Icons.thumb_up_off_alt, + size: 17), + ] + .map((e) => Padding( + padding: const EdgeInsets.only(right: 3), + child: e, + )) + .toList(), + ), + ], + ), + ), + ), + ), + ), + ), + itemCount: _schemes.length, + ), + ), + ), + ), + 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(), ), ), ], diff --git a/app/pubspec.yaml b/app/pubspec.yaml index b774f56..4176979 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -57,6 +57,10 @@ dev_dependencies: build_runner: 2.1.2 source_gen: 1.1.0 +dependency_overrides: + angel3_orm: + path: ../api/3rd_party/angel3_orm + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/app/resources/langs/en.json b/app/resources/langs/en.json index 5fbc48c..e6ae3be 100644 --- a/app/resources/langs/en.json +++ b/app/resources/langs/en.json @@ -72,7 +72,10 @@ "apply": "apply", "paste": "paste", "logout": "sign out", - "upload": "upload" + "upload": "upload", + "download": "download", + "share": "share to market", + "like": "like" }, "str": { "null": "Null", @@ -146,6 +149,11 @@ "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": { @@ -155,6 +163,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" } } } \ No newline at end of file diff --git a/app/resources/langs/zh-CN.json b/app/resources/langs/zh-CN.json index d613146..467bd79 100644 --- a/app/resources/langs/zh-CN.json +++ b/app/resources/langs/zh-CN.json @@ -72,7 +72,10 @@ "apply": "应用", "paste": "粘贴", "logout": "退出登录", - "upload": "上传" + "upload": "上传", + "download": "下载", + "share": "分享到市场", + "like": "点赞" }, "str": { "null": "无", @@ -146,6 +149,11 @@ "upload": { "success": "上传成功~", "failed": "上传失败。。" + }, + "share": { + "title": "确定分享?", + "description": "分享后其他用户可以看到本方案并下载使用", + "success": "分享成功" } }, "me": { @@ -155,6 +163,11 @@ "email_hint": "请输入邮箱", "password_hint": "请输入8-16位密码", "email_error_hint": "请输入正确的邮箱" + }, + "scheme_types": { + "uploaded": "我的上传", + "downloaded": "我的下载", + "liked": "我的点赞" } } } \ No newline at end of file