Merge branch 'revolution' into 'master'
Revolution See merge request nuark/tuuli_app!1
This commit is contained in:
commit
a01a66c88d
53 changed files with 5745 additions and 2187 deletions
15
.metadata
15
.metadata
|
|
@ -15,21 +15,6 @@ migration:
|
|||
- platform: root
|
||||
create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||
base_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||
- platform: android
|
||||
create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||
base_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||
- platform: ios
|
||||
create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||
base_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||
- platform: linux
|
||||
create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||
base_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||
- platform: macos
|
||||
create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||
base_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||
- platform: web
|
||||
create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||
base_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||
- platform: windows
|
||||
create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||
base_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||
|
|
|
|||
25
.vscode/launch.json
vendored
Normal file
25
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "tuuli_app",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "tuuli_app (profile mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "tuuli_app (release mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,395 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tuuli_app/api/model/access_token_model.dart';
|
||||
import 'package:http/browser_client.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:tuuli_app/api/model/table_field_model.dart';
|
||||
import 'package:tuuli_app/api/model/tables_list_model.dart';
|
||||
import 'package:tuuli_app/api/model/user_model.dart';
|
||||
|
||||
class ErrorOrData<T> {
|
||||
final T? data;
|
||||
final Exception? error;
|
||||
|
||||
ErrorOrData(this.data, this.error);
|
||||
|
||||
void unfold(
|
||||
void Function(T data) onData, void Function(Exception error) onError) {
|
||||
if (data != null) {
|
||||
onData(data as T);
|
||||
} else {
|
||||
onError(error!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typedef FutureErrorOrData<T> = Future<ErrorOrData<T>>;
|
||||
typedef TableItemsData = Map<String, dynamic>;
|
||||
typedef TableItemsDataList = List<TableItemsData>;
|
||||
|
||||
class ApiClient {
|
||||
final BrowserClient _client = BrowserClient();
|
||||
var _accessToken = '';
|
||||
|
||||
final Uri baseUrl;
|
||||
|
||||
ApiClient(this.baseUrl);
|
||||
|
||||
ApiClient.fromString(String baseUrl) : this(Uri.parse(baseUrl));
|
||||
|
||||
void setAccessToken(String accessToken) {
|
||||
_accessToken = accessToken;
|
||||
}
|
||||
|
||||
FutureErrorOrData<AccessTokenModel> login(
|
||||
String username,
|
||||
String password,
|
||||
) async {
|
||||
AccessTokenModel? data;
|
||||
Exception? error;
|
||||
|
||||
final response = await post('/api/getAccessToken', body: {
|
||||
'username': username,
|
||||
'password': password,
|
||||
}, headers: {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(await response.stream.bytesToString());
|
||||
if (body['error'] != null) {
|
||||
error = Exception(body['error']);
|
||||
} else if (body['access_token'] == null) {
|
||||
error = Exception('No access token');
|
||||
} else {
|
||||
data = AccessTokenModel(accessToken: body['access_token']);
|
||||
}
|
||||
} else if (response.statusCode == 422) {
|
||||
error = Exception('Invalid request parameters');
|
||||
} else {
|
||||
error = Exception('HTTP ${response.statusCode}');
|
||||
}
|
||||
|
||||
return ErrorOrData(data, error);
|
||||
}
|
||||
|
||||
FutureErrorOrData<TablesListModel> tablesList() async {
|
||||
TablesListModel? data;
|
||||
Exception? error;
|
||||
|
||||
final response = await get('/api/listTables');
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(await response.stream.bytesToString());
|
||||
if (body['error'] != null) {
|
||||
error = Exception(body['error']);
|
||||
} else if (body['tables'] == null) {
|
||||
error = Exception('Server error');
|
||||
} else {
|
||||
data = TablesListModel.fromJson(body);
|
||||
}
|
||||
} else if (response.statusCode == 422) {
|
||||
error = Exception('Invalid request parameters');
|
||||
} else {
|
||||
error = Exception('HTTP ${response.statusCode}');
|
||||
}
|
||||
|
||||
return ErrorOrData(data, error);
|
||||
}
|
||||
|
||||
FutureErrorOrData<bool> createTable(
|
||||
String tableName, List<TableField> columns) async {
|
||||
bool? ignored;
|
||||
Exception? error;
|
||||
|
||||
final response =
|
||||
await post('/api/createTable/${Uri.encodeComponent(tableName)}', body: {
|
||||
'columns':
|
||||
columns.map((e) => e.toColumnDefinition()).toList(growable: false),
|
||||
}, headers: {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(await response.stream.bytesToString());
|
||||
if (body['error'] != null) {
|
||||
error = Exception(body['error']);
|
||||
} else {
|
||||
ignored = true;
|
||||
}
|
||||
} else if (response.statusCode == 422) {
|
||||
error = Exception('Invalid request parameters');
|
||||
} else {
|
||||
error = Exception('HTTP ${response.statusCode}');
|
||||
}
|
||||
|
||||
return ErrorOrData(ignored, error);
|
||||
}
|
||||
|
||||
FutureErrorOrData<bool> dropTable(String tableName) async {
|
||||
bool? ignored;
|
||||
Exception? error;
|
||||
|
||||
final response =
|
||||
await post('/api/dropTable/${Uri.encodeComponent(tableName)}');
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(await response.stream.bytesToString());
|
||||
if (body['error'] != null) {
|
||||
error = Exception(body['error']);
|
||||
} else {
|
||||
ignored = true;
|
||||
}
|
||||
} else if (response.statusCode == 422) {
|
||||
error = Exception('Invalid request parameters');
|
||||
} else {
|
||||
error = Exception('HTTP ${response.statusCode}');
|
||||
}
|
||||
|
||||
return ErrorOrData(ignored, error);
|
||||
}
|
||||
|
||||
FutureErrorOrData<TableItemsDataList> getTableItems(TableModel table) async {
|
||||
TableItemsDataList? data;
|
||||
Exception? error;
|
||||
|
||||
final response = await post(
|
||||
'/items/${Uri.encodeComponent(table.tableName)}',
|
||||
body: {
|
||||
"fields": ["*"]
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(await response.stream.bytesToString())
|
||||
as Map<String, dynamic>;
|
||||
if (body['error'] != null) {
|
||||
error = Exception(body['error']);
|
||||
} else if (body['items'] == null) {
|
||||
error = Exception('Server error');
|
||||
} else {
|
||||
data = (body['items'] as List)
|
||||
.map((e) => e as TableItemsData)
|
||||
.toList(growable: false);
|
||||
}
|
||||
} else if (response.statusCode == 422) {
|
||||
error = Exception('Invalid request parameters');
|
||||
} else {
|
||||
error = Exception('HTTP ${response.statusCode}');
|
||||
}
|
||||
|
||||
return ErrorOrData(data, error);
|
||||
}
|
||||
|
||||
FutureErrorOrData<bool> insertItem(
|
||||
TableModel table, TableItemsData newItem) async {
|
||||
bool? ignored;
|
||||
Exception? error;
|
||||
|
||||
final response = await post(
|
||||
'/items/${Uri.encodeComponent(table.tableName)}/+',
|
||||
body: newItem.map((key, value) =>
|
||||
MapEntry(key, value is DateTime ? value.toIso8601String() : value)),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(await response.stream.bytesToString());
|
||||
if (body['error'] != null) {
|
||||
error = Exception(body['error']);
|
||||
} else {
|
||||
ignored = true;
|
||||
}
|
||||
} else if (response.statusCode == 422) {
|
||||
error = Exception('Invalid request parameters');
|
||||
} else {
|
||||
error = Exception('HTTP ${response.statusCode}');
|
||||
}
|
||||
|
||||
return ErrorOrData(ignored, error);
|
||||
}
|
||||
|
||||
FutureErrorOrData<bool> updateItem(
|
||||
TableModel table, TableItemsData newItem, TableItemsData oldItem) async {
|
||||
bool? ignored;
|
||||
Exception? error;
|
||||
|
||||
final response = await post(
|
||||
'/items/${Uri.encodeComponent(table.tableName)}/*',
|
||||
body: {
|
||||
"item": newItem.map((key, value) =>
|
||||
MapEntry(key, value is DateTime ? value.toIso8601String() : value)),
|
||||
"oldItem": oldItem.map((key, value) =>
|
||||
MapEntry(key, value is DateTime ? value.toIso8601String() : value)),
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(await response.stream.bytesToString());
|
||||
if (body['error'] != null) {
|
||||
error = Exception(body['error']);
|
||||
} else {
|
||||
ignored = true;
|
||||
}
|
||||
} else if (response.statusCode == 422) {
|
||||
error = Exception('Invalid request parameters');
|
||||
} else {
|
||||
error = Exception('HTTP ${response.statusCode}');
|
||||
}
|
||||
|
||||
return ErrorOrData(ignored, error);
|
||||
}
|
||||
|
||||
FutureErrorOrData<bool> deleteItem(TableModel table, TableItemsData e) async {
|
||||
bool? ignored;
|
||||
Exception? error;
|
||||
|
||||
TableField? primaryField =
|
||||
table.columns.firstWhereOrNull((el) => el.isPrimary);
|
||||
TableField? uniqueField =
|
||||
table.columns.firstWhereOrNull((el) => el.isUnique);
|
||||
|
||||
final response = await post(
|
||||
'/items/${Uri.encodeComponent(table.tableName)}/-',
|
||||
body: {
|
||||
"defs": [
|
||||
if (primaryField != null)
|
||||
{
|
||||
"name": primaryField.fieldName,
|
||||
"value": e[primaryField.fieldName],
|
||||
}
|
||||
else if (uniqueField != null)
|
||||
{
|
||||
"name": uniqueField.fieldName,
|
||||
"value": e[uniqueField.fieldName],
|
||||
}
|
||||
else
|
||||
for (final field in table.columns)
|
||||
{
|
||||
"name": field.fieldName,
|
||||
"value": e[field.fieldName],
|
||||
}
|
||||
],
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(await response.stream.bytesToString());
|
||||
if (body['error'] != null) {
|
||||
error = Exception(body['error']);
|
||||
} else {
|
||||
ignored = true;
|
||||
}
|
||||
} else if (response.statusCode == 422) {
|
||||
error = Exception('Invalid request parameters');
|
||||
} else {
|
||||
error = Exception('HTTP ${response.statusCode}');
|
||||
}
|
||||
|
||||
return ErrorOrData(ignored, error);
|
||||
}
|
||||
|
||||
FutureErrorOrData<bool> createUser(
|
||||
TableModel table, String username, String password) async {
|
||||
bool? ignored;
|
||||
Exception? error;
|
||||
|
||||
final response = await post(
|
||||
'/api/createUser',
|
||||
body: {
|
||||
"username": username,
|
||||
"password": password,
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(await response.stream.bytesToString());
|
||||
if (body['error'] != null) {
|
||||
error = Exception(body['error']);
|
||||
} else {
|
||||
ignored = true;
|
||||
}
|
||||
} else if (response.statusCode == 422) {
|
||||
error = Exception('Invalid request parameters');
|
||||
} else {
|
||||
error = Exception('HTTP ${response.statusCode}');
|
||||
}
|
||||
|
||||
return ErrorOrData(ignored, error);
|
||||
}
|
||||
|
||||
FutureErrorOrData<bool> updateUser(
|
||||
TableModel table, int userId, String password, String accessToken) async {
|
||||
bool? ignored;
|
||||
Exception? error;
|
||||
|
||||
final response = await post(
|
||||
'/api/updateUser',
|
||||
body: {
|
||||
"user_id": userId,
|
||||
"password": password,
|
||||
"access_token": accessToken,
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(await response.stream.bytesToString());
|
||||
if (body['error'] != null) {
|
||||
error = Exception(body['error']);
|
||||
} else {
|
||||
ignored = true;
|
||||
}
|
||||
} else if (response.statusCode == 422) {
|
||||
error = Exception('Invalid request parameters');
|
||||
} else {
|
||||
error = Exception('HTTP ${response.statusCode}');
|
||||
}
|
||||
|
||||
return ErrorOrData(ignored, error);
|
||||
}
|
||||
|
||||
// REGION: HTTP Methods implementation
|
||||
|
||||
Future<StreamedResponse> get(
|
||||
String path, {
|
||||
Map<String, String>? headers,
|
||||
}) {
|
||||
return _request(path, 'GET', headers: headers);
|
||||
}
|
||||
|
||||
Future<StreamedResponse> post(
|
||||
String path, {
|
||||
Map<String, String>? headers,
|
||||
dynamic body,
|
||||
}) {
|
||||
return _request(path, 'POST', headers: headers, body: body);
|
||||
}
|
||||
|
||||
Future<StreamedResponse> _request(
|
||||
String path,
|
||||
String method, {
|
||||
Map<String, String>? headers,
|
||||
dynamic body,
|
||||
}) async {
|
||||
final uri = baseUrl.resolve(path);
|
||||
final request = Request(method, uri);
|
||||
if (headers != null) {
|
||||
request.headers.addAll(headers);
|
||||
}
|
||||
if (_accessToken.isNotEmpty) {
|
||||
request.headers["Access-Token"] = _accessToken;
|
||||
}
|
||||
if (body != null) {
|
||||
request.body = json.encode(body);
|
||||
}
|
||||
return _client.send(request);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
class AccessTokenModel {
|
||||
final String accessToken;
|
||||
|
||||
const AccessTokenModel({
|
||||
required this.accessToken,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
import 'package:uuid/uuid.dart';
|
||||
|
||||
typedef SerialTableField = TableField<int>;
|
||||
typedef UUIDTableField = TableField<UuidValue>;
|
||||
typedef StringTableField = TableField<String>;
|
||||
typedef BigIntTableField = TableField<BigInt>;
|
||||
typedef BoolTableField = TableField<bool>;
|
||||
typedef DateTableField = TableField<DateTime>;
|
||||
typedef DateTimeTableField = TableField<DateTime>;
|
||||
typedef FloatTableField = TableField<double>;
|
||||
typedef IntTableField = TableField<int>;
|
||||
|
||||
final possibleFieldTypes = {
|
||||
"serial": SerialTableField,
|
||||
"uuid": UUIDTableField,
|
||||
"str": StringTableField,
|
||||
"bigint": BigIntTableField,
|
||||
"bool": BoolTableField,
|
||||
"date": DateTableField,
|
||||
"datetime": DateTimeTableField,
|
||||
"float": FloatTableField,
|
||||
"int": IntTableField,
|
||||
};
|
||||
|
||||
class TableField<T> {
|
||||
final String fieldName;
|
||||
final String fieldType;
|
||||
final bool isUnique;
|
||||
final bool isPrimary;
|
||||
final Type type = T;
|
||||
|
||||
TableField({
|
||||
required this.fieldName,
|
||||
required this.fieldType,
|
||||
required this.isUnique,
|
||||
required this.isPrimary,
|
||||
});
|
||||
|
||||
bool canBePrimary() {
|
||||
return fieldType == "serial" || fieldType == "uuid";
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "TableField<$T>(fieldName: $fieldName, fieldType: $fieldType, isUnique: $isUnique, isPrimary: $isPrimary)";
|
||||
}
|
||||
|
||||
String toColumnDefinition() {
|
||||
return "$fieldName:$fieldType${isPrimary ? ":primary" : ""}${isUnique ? ":unique" : ""}";
|
||||
}
|
||||
|
||||
static TableField parseTableField(String definition) {
|
||||
final parts = definition.split(":");
|
||||
final fieldName = parts[0];
|
||||
final fieldType = parts[1];
|
||||
final isUnique = parts.contains("unique");
|
||||
final isPrimary = parts.contains("primary");
|
||||
|
||||
switch (fieldType) {
|
||||
case "serial":
|
||||
return SerialTableField(
|
||||
fieldName: fieldName,
|
||||
fieldType: fieldType,
|
||||
isUnique: isUnique,
|
||||
isPrimary: isPrimary,
|
||||
);
|
||||
case "uuid":
|
||||
return UUIDTableField(
|
||||
fieldName: fieldName,
|
||||
fieldType: fieldType,
|
||||
isUnique: isUnique,
|
||||
isPrimary: isPrimary,
|
||||
);
|
||||
case "str":
|
||||
return StringTableField(
|
||||
fieldName: fieldName,
|
||||
fieldType: fieldType,
|
||||
isUnique: isUnique,
|
||||
isPrimary: isPrimary,
|
||||
);
|
||||
case "bigint":
|
||||
return BigIntTableField(
|
||||
fieldName: fieldName,
|
||||
fieldType: fieldType,
|
||||
isUnique: isUnique,
|
||||
isPrimary: isPrimary,
|
||||
);
|
||||
case "bool":
|
||||
return BoolTableField(
|
||||
fieldName: fieldName,
|
||||
fieldType: fieldType,
|
||||
isUnique: isUnique,
|
||||
isPrimary: isPrimary,
|
||||
);
|
||||
case "date":
|
||||
return DateTableField(
|
||||
fieldName: fieldName,
|
||||
fieldType: fieldType,
|
||||
isUnique: isUnique,
|
||||
isPrimary: isPrimary,
|
||||
);
|
||||
case "datetime":
|
||||
return DateTimeTableField(
|
||||
fieldName: fieldName,
|
||||
fieldType: fieldType,
|
||||
isUnique: isUnique,
|
||||
isPrimary: isPrimary,
|
||||
);
|
||||
case "float":
|
||||
return FloatTableField(
|
||||
fieldName: fieldName,
|
||||
fieldType: fieldType,
|
||||
isUnique: isUnique,
|
||||
isPrimary: isPrimary,
|
||||
);
|
||||
case "int":
|
||||
return IntTableField(
|
||||
fieldName: fieldName,
|
||||
fieldType: fieldType,
|
||||
isUnique: isUnique,
|
||||
isPrimary: isPrimary,
|
||||
);
|
||||
default:
|
||||
throw Exception("Unknown field type: $fieldType");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import 'package:tuuli_app/api/model/table_field_model.dart';
|
||||
|
||||
class TablesListModel {
|
||||
final List<TableModel> tables;
|
||||
|
||||
TablesListModel(this.tables);
|
||||
|
||||
factory TablesListModel.fromJson(Map<String, dynamic> json) =>
|
||||
TablesListModel(
|
||||
List<TableModel>.from(
|
||||
json["tables"].map((x) => TableModel.fromJson(x)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class TableModel {
|
||||
final String tableId;
|
||||
final String tableName;
|
||||
final String columnsDefinition;
|
||||
final List<TableField> columns;
|
||||
final bool system;
|
||||
final bool hidden;
|
||||
|
||||
TableModel({
|
||||
required this.tableId,
|
||||
required this.tableName,
|
||||
required this.columnsDefinition,
|
||||
required this.system,
|
||||
required this.hidden,
|
||||
}) : columns = columnsDefinition
|
||||
.split(",")
|
||||
.map(TableField.parseTableField)
|
||||
.toList(growable: false);
|
||||
|
||||
factory TableModel.fromJson(Map<String, dynamic> json) => TableModel(
|
||||
tableId: json["table_id"],
|
||||
tableName: json["table_name"],
|
||||
columnsDefinition: json["columns"],
|
||||
system: json["system"],
|
||||
hidden: json["hidden"],
|
||||
);
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
class UserModel {
|
||||
final int id;
|
||||
final String username;
|
||||
final String? password;
|
||||
final String? accessToken;
|
||||
|
||||
UserModel({
|
||||
required this.id,
|
||||
required this.username,
|
||||
this.password,
|
||||
this.accessToken,
|
||||
});
|
||||
|
||||
factory UserModel.fromJson(Map<String, dynamic> json) => UserModel(
|
||||
id: json["id"],
|
||||
username: json["username"],
|
||||
password: json["password"],
|
||||
accessToken: json["access_token"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"access_token": accessToken,
|
||||
};
|
||||
}
|
||||
37
lib/api_controller.dart
Normal file
37
lib/api_controller.dart
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:tuuli_api/tuuli_api.dart';
|
||||
|
||||
class ApiController extends GetxController {
|
||||
static ApiController get to => Get.find();
|
||||
|
||||
final apiStorageBox = GetStorage();
|
||||
|
||||
String get endPoint => apiStorageBox.read<String>("endPoint") ?? "";
|
||||
set endPoint(String value) => apiStorageBox.write("endPoint", value);
|
||||
|
||||
String get token => apiStorageBox.read<String>("accessToken") ?? "";
|
||||
set token(String value) => apiStorageBox.write("accessToken", value);
|
||||
|
||||
TuuliApi? _apiClientBase;
|
||||
TuuliApi get apiClientBase {
|
||||
_apiClientBase ??= TuuliApi(
|
||||
dio: Dio(BaseOptions(
|
||||
baseUrl: endPoint,
|
||||
connectTimeout: 5000.milliseconds,
|
||||
receiveTimeout: 3000.milliseconds,
|
||||
receiveDataWhenStatusError: true,
|
||||
)),
|
||||
interceptors: [
|
||||
ApiKeyAuthInterceptor(),
|
||||
],
|
||||
);
|
||||
_apiClientBase!.setApiKey("access-token", token);
|
||||
return _apiClientBase!;
|
||||
}
|
||||
|
||||
DefaultApi get apiClient {
|
||||
return apiClientBase.getDefaultApi();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,29 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:tuuli_app/api/api_client.dart';
|
||||
import 'package:tuuli_app/api_controller.dart';
|
||||
import 'package:tuuli_app/pages/checkup_page.dart';
|
||||
import 'package:tuuli_app/pages/home_page.dart';
|
||||
import 'package:tuuli_app/pages/home_panels/tables_list_panel.dart';
|
||||
import 'package:tuuli_app/pages/home_panels/users_list_panel.dart';
|
||||
import 'package:tuuli_app/pages/login_page.dart';
|
||||
import 'package:tuuli_app/pages/not_found_page.dart';
|
||||
|
||||
void main() async {
|
||||
await GetStorage.init();
|
||||
|
||||
Get.put(
|
||||
ApiClient.fromString("http://127.0.0.1:8000"),
|
||||
permanent: true,
|
||||
builder: () {
|
||||
final client = ApiClient.fromString("http://127.0.0.1:8000");
|
||||
final accessToken = GetStorage().read<String>("accessToken");
|
||||
if (accessToken != null) {
|
||||
client.setAccessToken(accessToken);
|
||||
}
|
||||
return client;
|
||||
},
|
||||
Get.put(ApiController(), permanent: true);
|
||||
Get.lazyPut<CheckupPageController>(
|
||||
() => CheckupPageController(),
|
||||
fenix: true,
|
||||
);
|
||||
Get.lazyPut<LoginPageController>(
|
||||
() => LoginPageController(),
|
||||
fenix: true,
|
||||
);
|
||||
Get.lazyPut<HomePageController>(
|
||||
() => HomePageController(),
|
||||
fenix: true,
|
||||
);
|
||||
|
||||
runApp(const MainApp());
|
||||
|
|
@ -34,7 +37,7 @@ class MainApp extends StatelessWidget {
|
|||
return GetMaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
debugShowMaterialGrid: false,
|
||||
initialRoute: "/login",
|
||||
initialRoute: "/",
|
||||
onGenerateRoute: _onGenerateRoute,
|
||||
theme: ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
|
|
|
|||
106
lib/models/db_column_definition.dart
Normal file
106
lib/models/db_column_definition.dart
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import 'package:tuuli_api/tuuli_api.dart';
|
||||
|
||||
extension ColumnsParser on TableDefinition {
|
||||
List<DBColumnDefinition> get parsedColumns {
|
||||
return columns
|
||||
.split(",")
|
||||
.map((e) {
|
||||
final parts = e.split(":");
|
||||
final name = parts[0];
|
||||
switch (parts[1]) {
|
||||
case "serial":
|
||||
return PrimarySerialColumnDefinition(name);
|
||||
case "str":
|
||||
return TextColumnDefinition(
|
||||
name,
|
||||
parts.contains("unique"),
|
||||
parts.contains("default"),
|
||||
);
|
||||
case "bool":
|
||||
return BooleanColumnDefinition(
|
||||
name,
|
||||
parts.contains("unique"),
|
||||
parts.contains("default"),
|
||||
);
|
||||
case "datetime":
|
||||
return TimestampColumnDefinition(
|
||||
name,
|
||||
parts.contains("unique"),
|
||||
parts.contains("default"),
|
||||
);
|
||||
case "float":
|
||||
return DoubleColumnDefinition(
|
||||
name,
|
||||
parts.contains("unique"),
|
||||
parts.contains("default"),
|
||||
);
|
||||
case "int":
|
||||
return IntegerColumnDefinition(
|
||||
name,
|
||||
parts.contains("unique"),
|
||||
parts.contains("default"),
|
||||
);
|
||||
case "int-user":
|
||||
return UserRefColumnDefinition(name);
|
||||
case "int-asset":
|
||||
return AssetRefColumnDefinition(name);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.whereType<DBColumnDefinition>()
|
||||
.toList(growable: false);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class DBColumnDefinition {
|
||||
final String name;
|
||||
final bool unique;
|
||||
final bool hasDefault;
|
||||
|
||||
DBColumnDefinition({
|
||||
required this.name,
|
||||
required this.unique,
|
||||
required this.hasDefault,
|
||||
});
|
||||
}
|
||||
|
||||
class PrimarySerialColumnDefinition extends DBColumnDefinition {
|
||||
PrimarySerialColumnDefinition(String name)
|
||||
: super(name: name, unique: false, hasDefault: false);
|
||||
}
|
||||
|
||||
class TextColumnDefinition extends DBColumnDefinition {
|
||||
TextColumnDefinition(String name, bool unique, bool hasDefault)
|
||||
: super(name: name, unique: unique, hasDefault: hasDefault);
|
||||
}
|
||||
|
||||
class BooleanColumnDefinition extends DBColumnDefinition {
|
||||
BooleanColumnDefinition(String name, bool unique, bool hasDefault)
|
||||
: super(name: name, unique: unique, hasDefault: hasDefault);
|
||||
}
|
||||
|
||||
class TimestampColumnDefinition extends DBColumnDefinition {
|
||||
TimestampColumnDefinition(String name, bool unique, bool hasDefault)
|
||||
: super(name: name, unique: unique, hasDefault: hasDefault);
|
||||
}
|
||||
|
||||
class DoubleColumnDefinition extends DBColumnDefinition {
|
||||
DoubleColumnDefinition(String name, bool unique, bool hasDefault)
|
||||
: super(name: name, unique: unique, hasDefault: hasDefault);
|
||||
}
|
||||
|
||||
class IntegerColumnDefinition extends DBColumnDefinition {
|
||||
IntegerColumnDefinition(String name, bool unique, bool hasDefault)
|
||||
: super(name: name, unique: unique, hasDefault: hasDefault);
|
||||
}
|
||||
|
||||
class UserRefColumnDefinition extends DBColumnDefinition {
|
||||
UserRefColumnDefinition(String name)
|
||||
: super(name: name, unique: false, hasDefault: false);
|
||||
}
|
||||
|
||||
class AssetRefColumnDefinition extends DBColumnDefinition {
|
||||
AssetRefColumnDefinition(String name)
|
||||
: super(name: name, unique: false, hasDefault: false);
|
||||
}
|
||||
11
lib/models/group_definition.dart
Normal file
11
lib/models/group_definition.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class GroupDefinition {
|
||||
final int id;
|
||||
final String name;
|
||||
final String description;
|
||||
|
||||
const GroupDefinition({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
});
|
||||
}
|
||||
24
lib/models/table_access.dart
Normal file
24
lib/models/table_access.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
enum TableAccess {
|
||||
read("r"),
|
||||
write("w"),
|
||||
readWrite("rw"),
|
||||
none("");
|
||||
|
||||
final String def;
|
||||
|
||||
const TableAccess(this.def);
|
||||
|
||||
static TableAccess fromString(String? def) {
|
||||
switch (def) {
|
||||
case "r":
|
||||
return TableAccess.read;
|
||||
case "w":
|
||||
return TableAccess.write;
|
||||
case "rw":
|
||||
return TableAccess.readWrite;
|
||||
case "none":
|
||||
default:
|
||||
return TableAccess.none;
|
||||
}
|
||||
}
|
||||
}
|
||||
88
lib/models/table_column_definition.dart
Normal file
88
lib/models/table_column_definition.dart
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
abstract class TableColumnDefinition {
|
||||
final String columnName;
|
||||
final bool isUnique;
|
||||
|
||||
TableColumnDefinition({
|
||||
required this.columnName,
|
||||
required this.isUnique,
|
||||
});
|
||||
|
||||
String get def => throw UnimplementedError("def getter not implemented");
|
||||
}
|
||||
|
||||
class SerialPrimaryColumnDefinition extends TableColumnDefinition {
|
||||
SerialPrimaryColumnDefinition({
|
||||
required super.columnName,
|
||||
}) : super(isUnique: true);
|
||||
|
||||
@override
|
||||
String get def => "$columnName:serial:primary";
|
||||
}
|
||||
|
||||
class TextColumnDefinition extends TableColumnDefinition {
|
||||
TextColumnDefinition({
|
||||
required super.columnName,
|
||||
required super.isUnique,
|
||||
});
|
||||
|
||||
@override
|
||||
String get def => "$columnName:str${isUnique ? ":unique" : ""}";
|
||||
}
|
||||
|
||||
class BooleanColumnDefinition extends TableColumnDefinition {
|
||||
BooleanColumnDefinition({
|
||||
required super.columnName,
|
||||
required super.isUnique,
|
||||
});
|
||||
|
||||
@override
|
||||
String get def => "$columnName:bool${isUnique ? ":unique" : ""}";
|
||||
}
|
||||
|
||||
class TimestampColumnDefinition extends TableColumnDefinition {
|
||||
TimestampColumnDefinition({
|
||||
required super.columnName,
|
||||
required super.isUnique,
|
||||
});
|
||||
|
||||
@override
|
||||
String get def => "$columnName:datetime${isUnique ? ":unique" : ""}";
|
||||
}
|
||||
|
||||
class DoubleColumnDefinition extends TableColumnDefinition {
|
||||
DoubleColumnDefinition({
|
||||
required super.columnName,
|
||||
required super.isUnique,
|
||||
});
|
||||
|
||||
@override
|
||||
String get def => "$columnName:float${isUnique ? ":unique" : ""}";
|
||||
}
|
||||
|
||||
class IntegerColumnDefinition extends TableColumnDefinition {
|
||||
IntegerColumnDefinition({
|
||||
required super.columnName,
|
||||
required super.isUnique,
|
||||
});
|
||||
|
||||
@override
|
||||
String get def => "$columnName:int${isUnique ? ":unique" : ""}";
|
||||
}
|
||||
|
||||
class UserRefColumnDefinition extends TableColumnDefinition {
|
||||
UserRefColumnDefinition({
|
||||
required super.columnName,
|
||||
}) : super(isUnique: false);
|
||||
|
||||
@override
|
||||
String get def => "$columnName:int-user";
|
||||
}
|
||||
|
||||
class AssetRefColumnDefinition extends TableColumnDefinition {
|
||||
AssetRefColumnDefinition({
|
||||
required super.columnName,
|
||||
}) : super(isUnique: false);
|
||||
|
||||
@override
|
||||
String get def => "$columnName:int-asset";
|
||||
}
|
||||
13
lib/models/user_definition.dart
Normal file
13
lib/models/user_definition.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
class UserDefinition {
|
||||
final int id;
|
||||
final String username;
|
||||
final String password;
|
||||
final String accessToken;
|
||||
|
||||
const UserDefinition({
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.password,
|
||||
required this.accessToken,
|
||||
});
|
||||
}
|
||||
11
lib/models/user_in_group_definition.dart
Normal file
11
lib/models/user_in_group_definition.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class UserInGroupDefinition {
|
||||
final int id;
|
||||
final int userId;
|
||||
final int groupId;
|
||||
|
||||
const UserInGroupDefinition({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.groupId,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_fast_forms/flutter_fast_forms.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tuuli_app/api/api_client.dart';
|
||||
import 'package:tuuli_app/api/model/table_field_model.dart';
|
||||
import 'package:tuuli_app/api/model/tables_list_model.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:uuid/uuid_util.dart';
|
||||
|
||||
class CreateTableItemBottomSheet extends StatefulWidget {
|
||||
final TableModel table;
|
||||
final TableItemsData? existingItem;
|
||||
|
||||
const CreateTableItemBottomSheet({
|
||||
super.key,
|
||||
required this.table,
|
||||
this.existingItem,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _CreateTableItemBottomSheetState();
|
||||
}
|
||||
|
||||
class _CreateTableItemBottomSheetState
|
||||
extends State<CreateTableItemBottomSheet> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
final _values = <String, dynamic>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
for (final field in widget.table.columns.where((e) => !e.isPrimary)) {
|
||||
_values[field.fieldName] = null;
|
||||
}
|
||||
widget.existingItem?.forEach((key, value) {
|
||||
_values[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: FastForm(
|
||||
formKey: _formKey,
|
||||
children: [
|
||||
Text(
|
||||
widget.existingItem == null ? "Create new item" : "Update item",
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
for (final field in widget.table.columns.where((e) => !e.isPrimary))
|
||||
Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: _createFormField(field),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
Navigator.of(context).pop(_values);
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Please fill in all fields"),
|
||||
),
|
||||
);
|
||||
},
|
||||
child:
|
||||
Text(widget.existingItem == null ? "Create" : "Update"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createFormField(TableField field) {
|
||||
switch (field.fieldType) {
|
||||
case "serial":
|
||||
case "bigint":
|
||||
// ignore: no_duplicate_case_values
|
||||
case "int":
|
||||
return FastTextField(
|
||||
name: field.fieldName,
|
||||
labelText: field.fieldName,
|
||||
validator: (value) {
|
||||
if (value == null ||
|
||||
value.isEmpty ||
|
||||
double.tryParse(value) is! double) {
|
||||
return "Please enter a value";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
initialValue: (_values[field.fieldName] ?? "").toString(),
|
||||
onChanged: (value) {
|
||||
_values[field.fieldName] = int.tryParse(value ?? "");
|
||||
},
|
||||
);
|
||||
case "uuid":
|
||||
return FastTextField(
|
||||
name: field.fieldName,
|
||||
labelText: field.fieldName,
|
||||
validator: (value) {
|
||||
if (value == null ||
|
||||
value.isEmpty ||
|
||||
!Uuid.isValidUUID(fromString: value)) {
|
||||
return "Please enter a value";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
initialValue: (_values[field.fieldName] ?? "").toString(),
|
||||
onChanged: (value) {
|
||||
_values[field.fieldName] =
|
||||
Uuid.isValidUUID(fromString: value ?? "") ? value : null;
|
||||
},
|
||||
);
|
||||
case "str":
|
||||
return FastTextField(
|
||||
name: field.fieldName,
|
||||
labelText: field.fieldName,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter a value";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
initialValue: _values[field.fieldName],
|
||||
onChanged: (value) {
|
||||
_values[field.fieldName] = value;
|
||||
},
|
||||
);
|
||||
case "bool":
|
||||
return FastCheckbox(
|
||||
name: field.fieldName,
|
||||
labelText: field.fieldName,
|
||||
titleText: field.fieldName,
|
||||
initialValue: _values[field.fieldName],
|
||||
onChanged: (value) {
|
||||
_values[field.fieldName] = value;
|
||||
},
|
||||
);
|
||||
case "date":
|
||||
// ignore: no_duplicate_case_values
|
||||
case "datetime":
|
||||
return FastCalendar(
|
||||
name: field.fieldName,
|
||||
firstDate: DateTime.now().subtract(const Duration(days: 365 * 200)),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365 * 200)),
|
||||
initialValue: DateTime.tryParse(_values[field.fieldName] ?? ""),
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return "Please enter a value";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
_values[field.fieldName] = value;
|
||||
},
|
||||
);
|
||||
case "float":
|
||||
return FastTextField(
|
||||
name: field.fieldName,
|
||||
labelText: field.fieldName,
|
||||
validator: (value) {
|
||||
if (value == null ||
|
||||
value.isEmpty ||
|
||||
double.tryParse(value) is! double) {
|
||||
return "Please enter a value";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
initialValue: (_values[field.fieldName] ?? "").toString(),
|
||||
onChanged: (value) {
|
||||
_values[field.fieldName] = double.tryParse(value ?? "");
|
||||
},
|
||||
);
|
||||
default:
|
||||
return const Text("Unknown field type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_fast_forms/flutter_fast_forms.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tuuli_app/api/api_client.dart';
|
||||
import 'package:tuuli_app/api/model/table_field_model.dart';
|
||||
import 'package:tuuli_app/api/model/tables_list_model.dart';
|
||||
import 'package:tuuli_app/api/model/user_model.dart';
|
||||
import 'package:tuuli_app/utils.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:uuid/uuid_util.dart';
|
||||
|
||||
class CreateUserResult {
|
||||
final String username;
|
||||
final String password;
|
||||
|
||||
CreateUserResult(this.username, this.password);
|
||||
}
|
||||
|
||||
class UpdateUserResult {
|
||||
final String username;
|
||||
final String password;
|
||||
final String accessToken;
|
||||
|
||||
UpdateUserResult(this.username, this.password, this.accessToken);
|
||||
}
|
||||
|
||||
// TODO: Add a way to change user's group
|
||||
class CreateUserBottomSheet extends StatefulWidget {
|
||||
final UserModel? existingUser;
|
||||
|
||||
const CreateUserBottomSheet({
|
||||
super.key,
|
||||
this.existingUser,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _CreateUserBottomSheetState();
|
||||
}
|
||||
|
||||
class _CreateUserBottomSheetState extends State<CreateUserBottomSheet> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
String? newUsername;
|
||||
String? newPassword;
|
||||
String? newAccessToken;
|
||||
|
||||
bool obscurePassword = true;
|
||||
bool obscureToken = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: FastForm(
|
||||
formKey: _formKey,
|
||||
children: [
|
||||
Text(
|
||||
widget.existingUser == null ? "Create new user" : "Update user",
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: FastTextField(
|
||||
name: "Username",
|
||||
labelText: "Username",
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter a value";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
readOnly: widget.existingUser != null,
|
||||
initialValue: widget.existingUser?.username,
|
||||
onChanged: (value) {
|
||||
newUsername = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: FastTextField(
|
||||
name: "Password",
|
||||
labelText: "Password",
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter a value";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
obscureText: obscurePassword,
|
||||
onChanged: (value) {
|
||||
newPassword = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
obscurePassword
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
obscurePassword = !obscurePassword;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.existingUser != null)
|
||||
Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: FastTextField(
|
||||
name: "Access token",
|
||||
labelText: "Access token",
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter a value";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
obscureText: obscureToken,
|
||||
initialValue: newAccessToken ??
|
||||
widget.existingUser?.accessToken,
|
||||
readOnly: true,
|
||||
onChanged: (value) {
|
||||
newAccessToken = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.shuffle),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
newAccessToken = randomHexString(64);
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
obscurePassword
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
obscureToken = !obscureToken;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
Navigator.of(context).pop(widget.existingUser == null
|
||||
? CreateUserResult(
|
||||
newUsername!,
|
||||
newPassword!,
|
||||
)
|
||||
: UpdateUserResult(
|
||||
newUsername!,
|
||||
newPassword!,
|
||||
newAccessToken!,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Please fill in all fields"),
|
||||
),
|
||||
);
|
||||
},
|
||||
child:
|
||||
Text(widget.existingUser == null ? "Create" : "Update"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:tuuli_app/api/model/table_field_model.dart';
|
||||
import 'package:tuuli_app/api/model/tables_list_model.dart';
|
||||
import 'package:tuuli_app/widgets/table_field_widget.dart';
|
||||
|
||||
class EditTableBottomSheetResult {
|
||||
final String tableName;
|
||||
final List<TableField> fields;
|
||||
|
||||
EditTableBottomSheetResult(this.tableName, this.fields);
|
||||
}
|
||||
|
||||
class EditTableBottomSheet extends StatefulWidget {
|
||||
final TableModel? table;
|
||||
|
||||
const EditTableBottomSheet({super.key, this.table});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _EditTableBottomSheetState();
|
||||
}
|
||||
|
||||
class _EditTableBottomSheetState extends State<EditTableBottomSheet> {
|
||||
var tableName = "".obs;
|
||||
|
||||
late final List<TableField> fields;
|
||||
|
||||
final newFieldName = TextEditingController();
|
||||
String? newFieldType;
|
||||
var newFieldPrimary = false;
|
||||
var newFieldUnique = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
fields = widget.table?.columns ?? [];
|
||||
if (widget.table != null) {
|
||||
tableName.value = widget.table!.tableName;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Obx(
|
||||
() => Text(
|
||||
tableName.isEmpty
|
||||
? "Edit table"
|
||||
: "Edit table \"${tableName.value.pascalCase}\"",
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: Get.back,
|
||||
icon: const Icon(Icons.cancel),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (widget.table == null) const Divider(),
|
||||
if (widget.table == null)
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Table name',
|
||||
hintText: 'Enter table name',
|
||||
),
|
||||
readOnly: widget.table != null,
|
||||
maxLength: 15,
|
||||
onChanged: (value) => tableName.value = value,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter table name';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
Text(
|
||||
"Fields",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
if (fields.isEmpty) const Text("No fields"),
|
||||
...fields
|
||||
.map((e) => TableFieldWidget(
|
||||
field: e,
|
||||
onRemove: () => _removeColumn(e.fieldName),
|
||||
))
|
||||
.toList(growable: false),
|
||||
if (widget.table == null) const SizedBox(width: 16),
|
||||
if (widget.table == null)
|
||||
Card(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("Add new field"),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: newFieldName,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Column name',
|
||||
hintText: 'Enter column name',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: DropdownButtonFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Column type',
|
||||
hintText: 'Choose column type',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: possibleFieldTypes.keys
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text(e.pascalCase),
|
||||
))
|
||||
.toList(growable: false),
|
||||
value: newFieldType,
|
||||
onChanged: (value) => newFieldType = value,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ToggleButtons(
|
||||
isSelected: [
|
||||
newFieldPrimary,
|
||||
newFieldUnique,
|
||||
!newFieldPrimary && !newFieldUnique
|
||||
],
|
||||
onPressed: (index) {
|
||||
setState(() {
|
||||
newFieldPrimary = index == 0;
|
||||
newFieldUnique = index == 1;
|
||||
});
|
||||
},
|
||||
children: const [
|
||||
Text("Primary"),
|
||||
Text("Unique"),
|
||||
Text("Normal"),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _addNewField,
|
||||
child: const Text("Add field"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(
|
||||
onPressed: _saveTable,
|
||||
child: const Text("Save table"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addNewField() {
|
||||
if (newFieldType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final fieldName = newFieldName.text;
|
||||
if (fieldName.isEmpty ||
|
||||
fields.any((element) => element.fieldName == fieldName)) {
|
||||
Get.defaultDialog(
|
||||
title: "Error",
|
||||
middleText: "Field name is empty or already exists",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final field = TableField.parseTableField(
|
||||
"$fieldName:$newFieldType${newFieldUnique ? ":unique" : ""}${newFieldPrimary ? ":primary" : ""}",
|
||||
);
|
||||
|
||||
if (field.isPrimary && !field.canBePrimary()) {
|
||||
Get.defaultDialog(
|
||||
title: "Error",
|
||||
middleText: "Field type \"${field.fieldType}\" can't be primary",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
newFieldName.clear();
|
||||
newFieldType = null;
|
||||
newFieldPrimary = false;
|
||||
newFieldUnique = false;
|
||||
fields.add(field);
|
||||
});
|
||||
}
|
||||
|
||||
void _saveTable() {
|
||||
if (tableName.isEmpty) {
|
||||
Get.defaultDialog(
|
||||
title: "Error",
|
||||
middleText: "Table name is empty",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fields.isEmpty) {
|
||||
Get.defaultDialog(
|
||||
title: "Error",
|
||||
middleText: "Table must have at least one field",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Get.back(result: EditTableBottomSheetResult(tableName.value, fields));
|
||||
}
|
||||
|
||||
void _removeColumn(String name) {
|
||||
setState(() {
|
||||
fields.removeWhere((element) => element.fieldName == name);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,257 +0,0 @@
|
|||
import 'package:bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:data_table_2/data_table_2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:tuuli_app/api/api_client.dart';
|
||||
import 'package:tuuli_app/api/model/tables_list_model.dart';
|
||||
import 'package:tuuli_app/pages/bottomsheets/create_table_item_bottomsheet.dart';
|
||||
|
||||
class OpenTableBottomSheet extends StatefulWidget {
|
||||
final TableModel table;
|
||||
|
||||
const OpenTableBottomSheet({super.key, required this.table});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _OpenTableBottomSheetState();
|
||||
}
|
||||
|
||||
class _OpenTableBottomSheetState extends State<OpenTableBottomSheet> {
|
||||
final apiClient = Get.find<ApiClient>();
|
||||
final tableItems = TableItemsDataList.empty(growable: true);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_refreshTableData();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.table.tableName.pascalCase,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: _addNewItem,
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _refreshTableData,
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _dropTable,
|
||||
icon: const Icon(Icons.delete),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: Get.back,
|
||||
icon: const Icon(Icons.cancel),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
child: DataTable2(
|
||||
columnSpacing: 12,
|
||||
horizontalMargin: 12,
|
||||
headingRowColor:
|
||||
MaterialStateColor.resolveWith((states) => Colors.black),
|
||||
columns: [
|
||||
...widget.table.columns.map((e) => DataColumn(
|
||||
label: Text(e.fieldName),
|
||||
)),
|
||||
const DataColumn(label: Text("Actions")),
|
||||
],
|
||||
rows: tableItems
|
||||
.map((e) => DataRow(cells: [
|
||||
for (int i = 0; i < widget.table.columns.length; i++)
|
||||
DataCell(
|
||||
Text(e[widget.table.columns[i].fieldName]
|
||||
?.toString() ??
|
||||
"null"),
|
||||
),
|
||||
DataCell(
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => _updateExistingItem(e),
|
||||
icon: const Icon(Icons.edit),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _deleteItem(e),
|
||||
icon: const Icon(Icons.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]))
|
||||
.toList(growable: false),
|
||||
empty: const Center(child: Text("No data")),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _dropTable() async {
|
||||
final really = await Get.defaultDialog<bool>(
|
||||
title: "Drop table",
|
||||
middleText:
|
||||
"Are you sure you want to drop this table \"${widget.table.tableName}\"?",
|
||||
textConfirm: "Drop",
|
||||
onConfirm: () => Get.back(result: true),
|
||||
onCancel: () {},
|
||||
barrierDismissible: false,
|
||||
);
|
||||
|
||||
if (really != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await apiClient.dropTable(widget.table.tableName);
|
||||
result.unfold((data) {
|
||||
Get.back();
|
||||
}, (error) {
|
||||
Get.snackbar("Error", error.toString());
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _refreshTableData() async {
|
||||
final result = await apiClient.getTableItems(widget.table);
|
||||
result.unfold((data) {
|
||||
setState(() {
|
||||
tableItems.clear();
|
||||
tableItems.addAll(data);
|
||||
});
|
||||
}, (error) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Error: $error"),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _addNewItem() async {
|
||||
final newItem = await showFlexibleBottomSheet<Map<String, dynamic>>(
|
||||
minHeight: 1,
|
||||
initHeight: 1,
|
||||
maxHeight: 1,
|
||||
context: context,
|
||||
builder: (_, __, ___) => CreateTableItemBottomSheet(table: widget.table),
|
||||
anchors: [0, 0.5, 1],
|
||||
isSafeArea: true,
|
||||
isDismissible: false,
|
||||
);
|
||||
|
||||
if (newItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await apiClient.insertItem(widget.table, newItem);
|
||||
result.unfold((data) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Item added"),
|
||||
),
|
||||
);
|
||||
_refreshTableData();
|
||||
}, (error) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Error: $error"),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _updateExistingItem(TableItemsData oldItem) async {
|
||||
final newItem = await showFlexibleBottomSheet<Map<String, dynamic>>(
|
||||
minHeight: 1,
|
||||
initHeight: 1,
|
||||
maxHeight: 1,
|
||||
context: context,
|
||||
builder: (_, __, ___) => CreateTableItemBottomSheet(
|
||||
table: widget.table,
|
||||
existingItem: Map.fromEntries(widget.table.columns
|
||||
.where((el) => !el.isPrimary)
|
||||
.map((el) => MapEntry(el.fieldName, oldItem[el.fieldName]))),
|
||||
),
|
||||
anchors: [0, 0.5, 1],
|
||||
isSafeArea: true,
|
||||
isDismissible: false,
|
||||
);
|
||||
|
||||
if (newItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await apiClient.updateItem(widget.table, newItem, oldItem);
|
||||
result.unfold((data) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Item added"),
|
||||
),
|
||||
);
|
||||
_refreshTableData();
|
||||
}, (error) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Error: $error"),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _deleteItem(TableItemsData e) async {
|
||||
final really = await Get.defaultDialog<bool>(
|
||||
title: "Delete item",
|
||||
middleText: "Are you sure you want to delete this item?",
|
||||
textConfirm: "Delete",
|
||||
onConfirm: () => Get.back(result: true),
|
||||
onCancel: () {},
|
||||
barrierDismissible: false,
|
||||
);
|
||||
|
||||
if (really != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await apiClient.deleteItem(widget.table, e);
|
||||
result.unfold((data) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Item deleted"),
|
||||
),
|
||||
);
|
||||
_refreshTableData();
|
||||
}, (error) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Error: $error"),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +1,59 @@
|
|||
import 'package:animated_background/animated_background.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:tuuli_app/api/api_client.dart';
|
||||
import 'package:tuuli_app/api_controller.dart';
|
||||
|
||||
class CheckupPage extends StatefulWidget {
|
||||
const CheckupPage({super.key});
|
||||
class CheckupPageController extends GetxController {
|
||||
Future<void> checkCredentials() async {
|
||||
if (ApiController.to.token.isEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ApiController.to.token = "";
|
||||
Get.offAllNamed("/login");
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
final resp = await ApiController.to.apiClient.listTables();
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _CheckupPageState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (resp.statusCode == 200) {
|
||||
Get.offAllNamed("/home");
|
||||
} else {
|
||||
Get.offAllNamed("/login");
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Get.offAllNamed("/login");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _CheckupPageState extends State<CheckupPage>
|
||||
with TickerProviderStateMixin {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
checkCredentials();
|
||||
});
|
||||
}
|
||||
class CheckupPage extends GetView<CheckupPageController> {
|
||||
const CheckupPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: AnimatedBackground(
|
||||
behaviour: RandomParticleBehaviour(),
|
||||
vsync: this,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Checking credentials...',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox.square(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Checking credentials...',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FutureBuilder(
|
||||
future: controller.checkCredentials(),
|
||||
builder: (ctx, _) => const SizedBox.square(
|
||||
dimension: 32,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> checkCredentials() async {
|
||||
final accessToken = GetStorage().read<String?>("accessToken");
|
||||
if (accessToken == null) {
|
||||
Get.offAllNamed("/login");
|
||||
} else {
|
||||
final apiClient = Get.find<ApiClient>();
|
||||
(await apiClient.tablesList()).unfold((data) {
|
||||
Get.offAllNamed("/home");
|
||||
}, (error) async {
|
||||
await GetStorage().remove("accessToken");
|
||||
Get.offAllNamed("/login");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
251
lib/pages/dialogs/create_table_dialog.dart
Normal file
251
lib/pages/dialogs/create_table_dialog.dart
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_fast_forms/flutter_fast_forms.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:tuuli_app/api_controller.dart';
|
||||
import 'package:tuuli_app/models/table_column_definition.dart';
|
||||
|
||||
const typeToNameMatcher = {
|
||||
SerialPrimaryColumnDefinition: "Serial ID",
|
||||
TextColumnDefinition: "Text",
|
||||
BooleanColumnDefinition: "Boolean",
|
||||
TimestampColumnDefinition: "Date/Time",
|
||||
DoubleColumnDefinition: "Double",
|
||||
IntegerColumnDefinition: "Integer",
|
||||
UserRefColumnDefinition: "User reference",
|
||||
AssetRefColumnDefinition: "Asset reference",
|
||||
};
|
||||
|
||||
class CreateTableController extends GetxController {
|
||||
final _tableName = "".obs;
|
||||
String get tableName => _tableName.value;
|
||||
set tableName(String value) => _tableName.value = value;
|
||||
|
||||
final _columnsDefinition = <TableColumnDefinition>[].obs;
|
||||
List<TableColumnDefinition> get columnsDefinition => _columnsDefinition;
|
||||
|
||||
Future<void> createTable() async {
|
||||
try {
|
||||
final resp = await ApiController.to.apiClient.createTable(
|
||||
tableName: tableName,
|
||||
columnsDefinition:
|
||||
_columnsDefinition.map((e) => e.def).toList(growable: false),
|
||||
);
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
Get.back(result: true);
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to update tables access",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to update tables access",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to update tables access",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createNewColumn() async {
|
||||
final columnName = "".obs;
|
||||
final columnType = "".obs;
|
||||
final columnIsUnique = false.obs;
|
||||
|
||||
final confirm = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: const Text("Create new column"),
|
||||
content: Wrap(
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
FastTextField(
|
||||
name: "ColumnName",
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Column name",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
initialValue: columnName.value,
|
||||
onChanged: (value) => columnName.value = value ?? "",
|
||||
),
|
||||
FastDropdown(
|
||||
name: "ColumnTypes",
|
||||
hint: const Text("Select column type"),
|
||||
items: typeToNameMatcher.keys.toList(growable: false),
|
||||
itemsBuilder: (items, field) => items
|
||||
.map(
|
||||
(e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text(typeToNameMatcher[e]!),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) =>
|
||||
columnType.value = typeToNameMatcher[value] ?? "",
|
||||
),
|
||||
FastCheckbox(
|
||||
name: "ColumnIsUnique",
|
||||
titleText: "Is Unique",
|
||||
initialValue: false,
|
||||
onChanged: (value) => columnIsUnique.value = value!,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back(result: false);
|
||||
},
|
||||
child: const Text('Close'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back(result: true);
|
||||
},
|
||||
child: const Text('Create'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm != true) return;
|
||||
|
||||
TableColumnDefinition? ct;
|
||||
switch (columnType.value) {
|
||||
case "Serial ID":
|
||||
ct = SerialPrimaryColumnDefinition(
|
||||
columnName: columnName.value,
|
||||
);
|
||||
break;
|
||||
case "Text":
|
||||
ct = TextColumnDefinition(
|
||||
columnName: columnName.value,
|
||||
isUnique: columnIsUnique.value,
|
||||
);
|
||||
break;
|
||||
case "Boolean":
|
||||
ct = BooleanColumnDefinition(
|
||||
columnName: columnName.value,
|
||||
isUnique: columnIsUnique.value,
|
||||
);
|
||||
break;
|
||||
case "Date/Time":
|
||||
ct = TimestampColumnDefinition(
|
||||
columnName: columnName.value,
|
||||
isUnique: columnIsUnique.value,
|
||||
);
|
||||
break;
|
||||
case "Double":
|
||||
ct = DoubleColumnDefinition(
|
||||
columnName: columnName.value,
|
||||
isUnique: columnIsUnique.value,
|
||||
);
|
||||
break;
|
||||
case "Integer":
|
||||
ct = IntegerColumnDefinition(
|
||||
columnName: columnName.value,
|
||||
isUnique: columnIsUnique.value,
|
||||
);
|
||||
break;
|
||||
case "User reference":
|
||||
ct = UserRefColumnDefinition(
|
||||
columnName: columnName.value,
|
||||
);
|
||||
break;
|
||||
case "Asset reference":
|
||||
ct = AssetRefColumnDefinition(
|
||||
columnName: columnName.value,
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (ct == null) return;
|
||||
_columnsDefinition.add(ct);
|
||||
}
|
||||
|
||||
void removeColumn(TableColumnDefinition e) {
|
||||
_columnsDefinition.remove(e);
|
||||
}
|
||||
}
|
||||
|
||||
class CreateTableDialog extends GetView<CreateTableController> {
|
||||
const CreateTableDialog({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Creating new table'),
|
||||
content: Column(
|
||||
children: [
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Table name",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (value) => controller.tableName = value,
|
||||
),
|
||||
const Divider(),
|
||||
Obx(
|
||||
() => ListView(
|
||||
children: controller.columnsDefinition
|
||||
.map(
|
||||
(e) => ListTile(
|
||||
title: Text(e.columnName),
|
||||
subtitle: Text(e.def),
|
||||
trailing: IconButton(
|
||||
onPressed: () => controller.removeColumn(e),
|
||||
icon: const Icon(Icons.delete),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
).expanded(),
|
||||
),
|
||||
const Divider(),
|
||||
[
|
||||
ElevatedButton(
|
||||
onPressed: () => controller.createNewColumn(),
|
||||
child: const Text("Create column"),
|
||||
).expanded()
|
||||
].toRow(),
|
||||
],
|
||||
).constrained(width: Get.width * 0.9, height: Get.height * 0.9),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('Close'),
|
||||
),
|
||||
Obx(
|
||||
() => TextButton(
|
||||
onPressed: controller.columnsDefinition.isEmpty
|
||||
? null
|
||||
: () => controller.createTable(),
|
||||
child: const Text('Create'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool?> show() async {
|
||||
Get.lazyPut<CreateTableController>(() => CreateTableController());
|
||||
|
||||
return Get.dialog<bool>(
|
||||
const CreateTableDialog(),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
409
lib/pages/dialogs/group_acl_dialog.dart
Normal file
409
lib/pages/dialogs/group_acl_dialog.dart
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
import 'package:data_table_2/data_table_2.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart' hide Response;
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:tuuli_api/tuuli_api.dart';
|
||||
import 'package:tuuli_app/api_controller.dart';
|
||||
import 'package:tuuli_app/models/group_definition.dart';
|
||||
import 'package:tuuli_app/models/table_access.dart';
|
||||
|
||||
class GroupACLController extends GetxController {
|
||||
final GroupDefinition group;
|
||||
|
||||
GroupACLController(this.group);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
refreshData();
|
||||
}
|
||||
|
||||
final _tables = <TableDefinition>[].obs;
|
||||
List<TableDefinition> get tables => _tables.toList();
|
||||
|
||||
final _access = <String, TableAccess>{}.obs;
|
||||
Map<String, TableAccess> get access => _access;
|
||||
|
||||
final _allowedColumns = <String, String?>{}.obs;
|
||||
Map<String, String?> get allowedColumns => _allowedColumns;
|
||||
|
||||
Future<void> refreshData() async {
|
||||
await refreshTables();
|
||||
for (final table in tables) {
|
||||
_access[table.tableId] = TableAccess.none;
|
||||
}
|
||||
|
||||
for (final table in tables) {
|
||||
await refreshTableAccess(table);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshTables() async {
|
||||
try {
|
||||
final resp = await ApiController.to.apiClient.listTables();
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
_tables.clear();
|
||||
_tables.addAll(respData.where((e) => e.hidden != true));
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to get tables",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to get tables",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to get tables",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshTableAccess(TableDefinition table) async {
|
||||
try {
|
||||
final resp = await ApiController.to.apiClient.getItemsFromTable(
|
||||
tableName: "table_access",
|
||||
itemsSelector: ItemsSelector(
|
||||
fields: ["access_type", "allowed_columns"],
|
||||
where: [
|
||||
ColumnConditionCompat(
|
||||
column: "user_group_id",
|
||||
operator_: ColumnConditionCompatOperator.eq,
|
||||
value: group.id,
|
||||
),
|
||||
ColumnConditionCompat(
|
||||
column: "table_name",
|
||||
operator_: ColumnConditionCompatOperator.eq,
|
||||
value: table.tableName,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
if (respData.isNotEmpty) {
|
||||
_access[table.tableId] =
|
||||
TableAccess.fromString(respData.first["access_type"]);
|
||||
_allowedColumns[table.tableId] = respData.first["allowed_columns"];
|
||||
} else {
|
||||
_access[table.tableId] = TableAccess.none;
|
||||
_allowedColumns[table.tableId] = null;
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to get tables access",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to get tables access",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to get tables access",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateTableAccess(
|
||||
TableDefinition table, {
|
||||
required bool read,
|
||||
required bool write,
|
||||
}) async {
|
||||
final oldTableAccess = _access[table.tableId];
|
||||
var newTableAccess = TableAccess.none;
|
||||
if (read && write) {
|
||||
newTableAccess = TableAccess.readWrite;
|
||||
} else if (read) {
|
||||
newTableAccess = TableAccess.read;
|
||||
} else if (write) {
|
||||
newTableAccess = TableAccess.write;
|
||||
}
|
||||
|
||||
try {
|
||||
Response<OkResponse> resp;
|
||||
if (newTableAccess == TableAccess.none) {
|
||||
resp = await ApiController.to.apiClient.deleteItemFromTable(
|
||||
tableName: "table_access",
|
||||
columnConditionCompat: [
|
||||
ColumnConditionCompat(
|
||||
column: "user_group_id",
|
||||
operator_: ColumnConditionCompatOperator.eq,
|
||||
value: group.id,
|
||||
),
|
||||
ColumnConditionCompat(
|
||||
column: "table_name",
|
||||
operator_: ColumnConditionCompatOperator.eq,
|
||||
value: table.tableName,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (oldTableAccess == TableAccess.none) {
|
||||
resp = await ApiController.to.apiClient.createItem(
|
||||
tableName: "table_access",
|
||||
itemDefinition: {
|
||||
"user_group_id": group.id,
|
||||
"table_name": table.tableName,
|
||||
"access_type": newTableAccess.def,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
resp = await ApiController.to.apiClient.updateItemInTable(
|
||||
tableName: "table_access",
|
||||
itemUpdate: ItemUpdate(
|
||||
item: {
|
||||
"access_type": newTableAccess.def,
|
||||
},
|
||||
oldItem: {
|
||||
"user_group_id": group.id,
|
||||
"table_name": table.tableName,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
refreshTableAccess(table);
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to update tables access",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to update tables access",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to update tables access",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> changeAllowedColumns(TableDefinition table) async {
|
||||
final tableColumns = table.columns
|
||||
.split(",")
|
||||
.map((e) => e.split(":").first)
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList();
|
||||
final currentlyAvailableColumns =
|
||||
_allowedColumns[table.tableId]!.split(",");
|
||||
|
||||
if (currentlyAvailableColumns.length == 1 &&
|
||||
currentlyAvailableColumns.first == "*") {
|
||||
currentlyAvailableColumns.clear();
|
||||
currentlyAvailableColumns.addAll(tableColumns);
|
||||
}
|
||||
|
||||
final selectedColumns = <String, bool>{}.obs;
|
||||
for (final column in tableColumns) {
|
||||
selectedColumns[column] = currentlyAvailableColumns.contains(column);
|
||||
}
|
||||
|
||||
final confirm = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: const Text("Allowed columns"),
|
||||
content: Obx(
|
||||
() => Wrap(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
for (final column in tableColumns) {
|
||||
selectedColumns[column] = !selectedColumns[column]!;
|
||||
}
|
||||
},
|
||||
child: const Text("Swap all"),
|
||||
),
|
||||
...selectedColumns.entries.map((e) {
|
||||
return CheckboxListTile(
|
||||
title: Text(e.key),
|
||||
value: e.value,
|
||||
onChanged: (value) => selectedColumns[e.key] = value!,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text("Ok"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm != true) return;
|
||||
|
||||
if (selectedColumns.values.every((e) => !e)) {
|
||||
await Get.dialog(
|
||||
AlertDialog(
|
||||
title: const Text("Error"),
|
||||
content: const Text("At least one column must be selected"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text("Ok"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Response<OkResponse> resp =
|
||||
await ApiController.to.apiClient.updateItemInTable(
|
||||
tableName: "table_access",
|
||||
itemUpdate: ItemUpdate(
|
||||
item: {
|
||||
"allowed_columns": selectedColumns.keys
|
||||
.where((k) => selectedColumns[k]!)
|
||||
.join(","),
|
||||
},
|
||||
oldItem: {
|
||||
"user_group_id": group.id,
|
||||
"table_name": table.tableName,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
refreshTableAccess(table);
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to update tables access",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to update tables access",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to update tables access",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GroupACLDialog extends GetView<GroupACLController> {
|
||||
const GroupACLDialog({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Group ACL'),
|
||||
content: Obx(
|
||||
() => DataTable2(
|
||||
columns: const [
|
||||
DataColumn2(label: Text('Table'), size: ColumnSize.L),
|
||||
DataColumn2(label: Text('Read'), size: ColumnSize.S),
|
||||
DataColumn2(label: Text('Write'), size: ColumnSize.S),
|
||||
DataColumn2(label: Text('Allowed columns'), size: ColumnSize.S),
|
||||
],
|
||||
empty: const Text("No tables"),
|
||||
rows: controller.access.entries.map((e) {
|
||||
final table = controller.tables.firstWhere(
|
||||
(element) => element.tableId == e.key,
|
||||
);
|
||||
final tableAccess = e.value;
|
||||
final read = tableAccess == TableAccess.read ||
|
||||
tableAccess == TableAccess.readWrite;
|
||||
final write = tableAccess == TableAccess.write ||
|
||||
tableAccess == TableAccess.readWrite;
|
||||
return DataRow(cells: [
|
||||
DataCell(Text(table.tableName.pascalCase)),
|
||||
DataCell(Checkbox(
|
||||
value: read,
|
||||
onChanged: (value) => controller.updateTableAccess(
|
||||
table,
|
||||
read: value ?? false,
|
||||
write: write,
|
||||
),
|
||||
)),
|
||||
DataCell(Checkbox(
|
||||
value: write,
|
||||
onChanged: (value) => controller.updateTableAccess(
|
||||
table,
|
||||
read: read,
|
||||
write: value ?? false,
|
||||
),
|
||||
)),
|
||||
controller.allowedColumns[table.tableId] == null
|
||||
? DataCell.empty
|
||||
: DataCell(
|
||||
Text(controller.allowedColumns[table.tableId]!),
|
||||
onTap: () => controller.changeAllowedColumns(table),
|
||||
),
|
||||
]);
|
||||
}).toList(growable: false),
|
||||
).constrained(width: Get.width * 0.9, height: Get.height * 0.9),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> show(GroupDefinition group) async {
|
||||
Get.lazyPut<GroupACLController>(() => GroupACLController(group));
|
||||
|
||||
await Get.dialog(
|
||||
const GroupACLDialog(),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
|
||||
Get.delete<GroupACLController>();
|
||||
}
|
||||
}
|
||||
944
lib/pages/dialogs/open_table_dialog.dart
Normal file
944
lib/pages/dialogs/open_table_dialog.dart
Normal file
|
|
@ -0,0 +1,944 @@
|
|||
import 'package:data_table_2/data_table_2.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_fast_forms/flutter_fast_forms.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:omni_datetime_picker/omni_datetime_picker.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:tuuli_api/tuuli_api.dart';
|
||||
import 'package:tuuli_app/api_controller.dart';
|
||||
import 'package:tuuli_app/models/db_column_definition.dart';
|
||||
import 'package:tuuli_app/models/user_definition.dart';
|
||||
import 'package:tuuli_app/utils.dart';
|
||||
import 'package:tuuli_app/widgets/data_input_dialog.dart';
|
||||
|
||||
class OpenTableController extends GetxController {
|
||||
final TableDefinition table;
|
||||
|
||||
OpenTableController({required this.table});
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
refreshTableData();
|
||||
}
|
||||
|
||||
final _userCache = <int, UserDefinition?>{}.obs;
|
||||
UserDefinition? getUserFromCache(int id) {
|
||||
return _userCache[id];
|
||||
}
|
||||
|
||||
void putUserInCache(UserDefinition user) {
|
||||
_userCache[user.id] = user;
|
||||
}
|
||||
|
||||
final _assetsCache = <int, Asset?>{}.obs;
|
||||
Asset? getAssetFromCache(int id) {
|
||||
return _assetsCache[id];
|
||||
}
|
||||
|
||||
void putAssetInCache(Asset asset) {
|
||||
_assetsCache[asset.id] = asset;
|
||||
}
|
||||
|
||||
final _tableData = <Map<String, dynamic>>[].obs;
|
||||
List<Map<String, dynamic>> get tableData => _tableData;
|
||||
|
||||
final _newRowData = <String, dynamic>{}.obs;
|
||||
Map<String, dynamic> get newRowData => _newRowData;
|
||||
void setNewRowData(String key, dynamic value) {
|
||||
_newRowData[key] = value;
|
||||
}
|
||||
|
||||
void clearNewRowData() {
|
||||
_newRowData.clear();
|
||||
}
|
||||
|
||||
Future<void> refreshTableData() async {
|
||||
try {
|
||||
final resp = await ApiController.to.apiClient.getItemsFromTable(
|
||||
tableName: table.tableName,
|
||||
itemsSelector: const ItemsSelector(
|
||||
fields: [
|
||||
"*",
|
||||
],
|
||||
where: [],
|
||||
),
|
||||
);
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
_tableData.clear();
|
||||
_tableData.addAll(respData);
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to get table data",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to get table data",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to get table data",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<UserDefinition?> showUserPicker() async {
|
||||
final username = "".obs;
|
||||
|
||||
final user = await Get.dialog<UserDefinition>(
|
||||
AlertDialog(
|
||||
title: const Text("Select user"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
onChanged: (value) {
|
||||
username.value = value;
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Username",
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => FutureBuilder<List<UserDefinition>>(
|
||||
future: () async {
|
||||
if (username.value.isEmpty) {
|
||||
return <UserDefinition>[];
|
||||
}
|
||||
|
||||
final resp =
|
||||
await ApiController.to.apiClient.getItemsFromTable(
|
||||
tableName: "users",
|
||||
itemsSelector: ItemsSelector(
|
||||
fields: [
|
||||
"id",
|
||||
"username",
|
||||
],
|
||||
where: [
|
||||
ColumnConditionCompat(
|
||||
column: "username",
|
||||
operator_:
|
||||
ColumnConditionCompatOperator.contains,
|
||||
value: username.value,
|
||||
)
|
||||
],
|
||||
));
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
return respData
|
||||
.map((e) => UserDefinition(
|
||||
id: e["id"],
|
||||
username: e["username"],
|
||||
password: "",
|
||||
accessToken: "",
|
||||
))
|
||||
.toList(growable: false);
|
||||
}(),
|
||||
initialData: const [],
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Text("${snapshot.error}");
|
||||
}
|
||||
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
|
||||
final users = snapshot.data!;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
for (final user in users)
|
||||
ListTile(
|
||||
title: Text(user.username),
|
||||
onTap: () {
|
||||
Get.back(result: user);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back(result: false);
|
||||
},
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
Future<Asset?> showAssetPicker() async {
|
||||
final name = "".obs;
|
||||
|
||||
final asset = await Get.dialog<Asset>(
|
||||
AlertDialog(
|
||||
title: const Text("Select asset"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
onChanged: (value) {
|
||||
name.value = value;
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Filename",
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => FutureBuilder<List<Asset>>(
|
||||
future: () async {
|
||||
if (name.value.isEmpty) {
|
||||
return <Asset>[];
|
||||
}
|
||||
|
||||
final resp =
|
||||
await ApiController.to.apiClient.getItemsFromTable(
|
||||
tableName: "assets",
|
||||
itemsSelector: ItemsSelector(
|
||||
fields: [
|
||||
"id",
|
||||
"name",
|
||||
"description",
|
||||
],
|
||||
where: [
|
||||
ColumnConditionCompat(
|
||||
column: "name",
|
||||
operator_:
|
||||
ColumnConditionCompatOperator.contains,
|
||||
value: name.value,
|
||||
)
|
||||
],
|
||||
));
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
return respData
|
||||
.map((e) => Asset(
|
||||
id: e["id"],
|
||||
name: e["name"],
|
||||
description: e["description"],
|
||||
fid: "",
|
||||
tags: "",
|
||||
mime: "",
|
||||
))
|
||||
.toList(growable: false);
|
||||
}(),
|
||||
initialData: const [],
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Text("${snapshot.error}");
|
||||
}
|
||||
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
|
||||
final assets = snapshot.data!;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
for (final asset in assets)
|
||||
ListTile(
|
||||
title: Text(asset.name),
|
||||
subtitle: Text(asset.description),
|
||||
onTap: () {
|
||||
Get.back(result: asset);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back(result: null);
|
||||
},
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
Future<void> addNewRow() async {
|
||||
try {
|
||||
final resp = await ApiController.to.apiClient.createItem(
|
||||
tableName: table.tableName,
|
||||
itemDefinition: convertToPayload(newRowData),
|
||||
);
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
clearNewRowData();
|
||||
refreshTableData();
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to get table data",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to get table data",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to get table data",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateItem(
|
||||
Map<String, dynamic> originalItem,
|
||||
String columnName,
|
||||
dynamic data,
|
||||
) async {
|
||||
final idCol = table.parsedColumns
|
||||
.firstWhereOrNull((e) => e is PrimarySerialColumnDefinition);
|
||||
try {
|
||||
final resp = await ApiController.to.apiClient.updateItemInTable(
|
||||
tableName: table.tableName,
|
||||
itemUpdate: ItemUpdate(
|
||||
oldItem: idCol == null
|
||||
? convertToPayload(originalItem)
|
||||
: {
|
||||
idCol.name: originalItem[idCol.name],
|
||||
},
|
||||
item: convertToPayload({columnName: data}),
|
||||
),
|
||||
);
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
refreshTableData();
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to update table data",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to update table data",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to update table data",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteItem(Map<String, dynamic> e) async {
|
||||
final idCol = table.parsedColumns
|
||||
.firstWhereOrNull((e) => e is PrimarySerialColumnDefinition);
|
||||
try {
|
||||
final resp = await ApiController.to.apiClient.deleteItemFromTable(
|
||||
tableName: table.tableName,
|
||||
columnConditionCompat: [
|
||||
if (idCol != null)
|
||||
ColumnConditionCompat(
|
||||
column: idCol.name,
|
||||
operator_: ColumnConditionCompatOperator.eq,
|
||||
value: e[idCol.name],
|
||||
)
|
||||
else
|
||||
...convertToPayload(e).entries.map(
|
||||
(e) => ColumnConditionCompat(
|
||||
column: e.key,
|
||||
operator_: ColumnConditionCompatOperator.eq,
|
||||
value: e.value,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
refreshTableData();
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to update table data",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to update table data",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to update table data",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OpenTableDialog extends GetView<OpenTableController> {
|
||||
const OpenTableDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Text(controller.table.tableName.pascalCase),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => controller.refreshTableData(),
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Obx(
|
||||
() => DataTable2(
|
||||
columns: [
|
||||
for (final col in controller.table.parsedColumns)
|
||||
DataColumn(
|
||||
label: Text(
|
||||
col.name.pascalCase,
|
||||
),
|
||||
),
|
||||
const DataColumn(label: Text("Actions")),
|
||||
],
|
||||
empty: const Text("No data"),
|
||||
rows: [
|
||||
DataRow(
|
||||
cells: [
|
||||
for (final col in controller.table.parsedColumns)
|
||||
if (col is PrimarySerialColumnDefinition)
|
||||
const DataCell(
|
||||
Text(
|
||||
"AUTO",
|
||||
),
|
||||
)
|
||||
else if (col is TextColumnDefinition)
|
||||
DataCell(
|
||||
Obx(
|
||||
() => TextField(
|
||||
controller: TextEditingController(
|
||||
text: controller.newRowData[col.name] ?? ""),
|
||||
decoration: InputDecoration(
|
||||
label: Text(col.name.pascalCase),
|
||||
),
|
||||
onChanged: (value) => controller.setNewRowData(
|
||||
col.name,
|
||||
value,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (col is BooleanColumnDefinition)
|
||||
DataCell(
|
||||
Obx(
|
||||
() => Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: controller.newRowData[col.name] ?? false,
|
||||
onChanged: (value) => controller.setNewRowData(
|
||||
col.name,
|
||||
value,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (col is TimestampColumnDefinition)
|
||||
DataCell(
|
||||
Obx(
|
||||
() => TextField(
|
||||
controller: TextEditingController(
|
||||
text: () {
|
||||
final dt = controller.newRowData[col.name];
|
||||
if (dt == null || dt is! DateTime) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return postgresDateFormat(dt);
|
||||
}(),
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
label: Text(col.name.pascalCase),
|
||||
),
|
||||
readOnly: true,
|
||||
onTap: () async {
|
||||
final dt = await showOmniDateTimePicker(
|
||||
context: context,
|
||||
is24HourMode: true,
|
||||
isForce2Digits: true,
|
||||
);
|
||||
if (dt != null) {
|
||||
controller.setNewRowData(
|
||||
col.name,
|
||||
dt,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (col is DoubleColumnDefinition)
|
||||
DataCell(
|
||||
Obx(
|
||||
() => TextField(
|
||||
controller: TextEditingController(
|
||||
text: (controller.newRowData[col.name] as double?)
|
||||
?.toString() ??
|
||||
""),
|
||||
decoration: InputDecoration(
|
||||
label: Text(col.name.pascalCase),
|
||||
),
|
||||
onChanged: (value) => controller.setNewRowData(
|
||||
col.name,
|
||||
double.tryParse(value),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (col is IntegerColumnDefinition)
|
||||
DataCell(
|
||||
Obx(
|
||||
() => TextField(
|
||||
controller: TextEditingController(
|
||||
text: (controller.newRowData[col.name] as int?)
|
||||
?.toString() ??
|
||||
""),
|
||||
decoration: InputDecoration(
|
||||
label: Text(col.name.pascalCase),
|
||||
),
|
||||
onChanged: (value) => controller.setNewRowData(
|
||||
col.name,
|
||||
int.tryParse(value),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (col is UserRefColumnDefinition)
|
||||
DataCell(Obx(
|
||||
() => TextField(
|
||||
controller: TextEditingController(
|
||||
text: (controller.newRowData[col.name]
|
||||
as UserDefinition?)
|
||||
?.username ??
|
||||
""),
|
||||
decoration: InputDecoration(
|
||||
label: Text(col.name.pascalCase),
|
||||
),
|
||||
readOnly: true,
|
||||
onTap: () async {
|
||||
final user = await controller.showUserPicker();
|
||||
if (user == null) return;
|
||||
|
||||
controller.setNewRowData(
|
||||
col.name,
|
||||
user,
|
||||
);
|
||||
},
|
||||
),
|
||||
))
|
||||
else if (col is AssetRefColumnDefinition)
|
||||
DataCell(Obx(
|
||||
() => TextField(
|
||||
controller: TextEditingController(
|
||||
text: (controller.newRowData[col.name] as Asset?)
|
||||
?.name ??
|
||||
""),
|
||||
decoration: InputDecoration(
|
||||
label: Text(col.name.pascalCase),
|
||||
),
|
||||
readOnly: true,
|
||||
onTap: () async {
|
||||
final asset = await controller.showAssetPicker();
|
||||
if (asset == null) return;
|
||||
|
||||
controller.setNewRowData(
|
||||
col.name,
|
||||
asset,
|
||||
);
|
||||
},
|
||||
),
|
||||
))
|
||||
else
|
||||
DataCell.empty,
|
||||
DataCell(Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
controller.addNewRow();
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => controller.clearNewRowData(),
|
||||
icon: const Icon(Icons.clear),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
...controller.tableData.map((e) {
|
||||
return DataRow(
|
||||
cells: [
|
||||
for (final col in controller.table.parsedColumns)
|
||||
if (col is PrimarySerialColumnDefinition)
|
||||
DataCell(
|
||||
Tooltip(
|
||||
message: e[col.name].toString(),
|
||||
child: Text(
|
||||
e[col.name].toString(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (col is TextColumnDefinition)
|
||||
DataCell(
|
||||
Tooltip(
|
||||
message: e[col.name].toString(),
|
||||
child: Text(
|
||||
e[col.name].toString(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
onDoubleTap: () async {
|
||||
final text = await showStringInputDialog(
|
||||
originalValue: e[col.name].toString());
|
||||
if (text != null) {
|
||||
await controller.updateItem(
|
||||
e,
|
||||
col.name,
|
||||
text,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
else if (col is BooleanColumnDefinition)
|
||||
DataCell(
|
||||
Checkbox(
|
||||
value: e[col.name],
|
||||
onChanged: (v) async {
|
||||
await controller.updateItem(
|
||||
e,
|
||||
col.name,
|
||||
v ?? false,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
else if (col is TimestampColumnDefinition)
|
||||
DataCell(
|
||||
() {
|
||||
final msg = () {
|
||||
final dt = e[col.name];
|
||||
if (dt == null) {
|
||||
return "#error#";
|
||||
}
|
||||
if (dt is String) {
|
||||
final rdt = DateTime.parse(dt);
|
||||
return postgresDateFormat(rdt);
|
||||
}
|
||||
|
||||
return postgresDateFormat(dt);
|
||||
}();
|
||||
return Tooltip(
|
||||
message: msg,
|
||||
child: Text(
|
||||
msg,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}(),
|
||||
onDoubleTap: () async {
|
||||
final dt = await showOmniDateTimePicker(
|
||||
context: context,
|
||||
is24HourMode: true,
|
||||
isForce2Digits: true,
|
||||
);
|
||||
if (dt != null) {
|
||||
await controller.updateItem(
|
||||
e,
|
||||
col.name,
|
||||
dt,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
else if (col is DoubleColumnDefinition)
|
||||
DataCell(
|
||||
Tooltip(
|
||||
message: e[col.name].toString(),
|
||||
child: Text(
|
||||
e[col.name].toString(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
onDoubleTap: () async {
|
||||
final dblVal = await showDoubleInputDialog(
|
||||
originalValue: e[col.name]);
|
||||
if (dblVal != null) {
|
||||
await controller.updateItem(
|
||||
e,
|
||||
col.name,
|
||||
dblVal,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
else if (col is IntegerColumnDefinition)
|
||||
DataCell(
|
||||
Tooltip(
|
||||
message: e[col.name].toString(),
|
||||
child: Text(
|
||||
e[col.name].toString(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
onDoubleTap: () async {
|
||||
final intVal = await showIntInputDialog(
|
||||
originalValue: e[col.name]);
|
||||
if (intVal != null) {
|
||||
await controller.updateItem(
|
||||
e,
|
||||
col.name,
|
||||
intVal,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
else if (col is UserRefColumnDefinition)
|
||||
DataCell(
|
||||
FutureBuilder<String>(
|
||||
future: () async {
|
||||
final cachedUser =
|
||||
controller.getUserFromCache(e[col.name]);
|
||||
if (cachedUser != null) {
|
||||
return cachedUser.username;
|
||||
}
|
||||
|
||||
final user = await ApiController.to.apiClient
|
||||
.getItemsFromTable(
|
||||
tableName: "users",
|
||||
itemsSelector: ItemsSelector(
|
||||
fields: ["username"],
|
||||
where: [
|
||||
ColumnConditionCompat(
|
||||
column: "id",
|
||||
operator_: ColumnConditionCompatOperator.eq,
|
||||
value: e[col.name],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final ud = user.data;
|
||||
if (ud == null ||
|
||||
ud.isEmpty ||
|
||||
ud.first["username"] == null) {
|
||||
return "#error#";
|
||||
}
|
||||
|
||||
controller.putUserInCache(UserDefinition(
|
||||
id: e[col.name],
|
||||
username: ud.first["username"],
|
||||
password: "",
|
||||
accessToken: "",
|
||||
));
|
||||
|
||||
return ud.first["username"].toString();
|
||||
}(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.waiting) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
|
||||
return Tooltip(
|
||||
message: snapshot.data ?? "#error#",
|
||||
child: Text(
|
||||
snapshot.data ?? "#error#",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
onDoubleTap: () async {
|
||||
final user = await controller.showUserPicker();
|
||||
if (user != null) {
|
||||
await controller.updateItem(
|
||||
e,
|
||||
col.name,
|
||||
user.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
else if (col is AssetRefColumnDefinition)
|
||||
DataCell(
|
||||
FutureBuilder<String>(
|
||||
future: () async {
|
||||
final cachedAsset =
|
||||
controller.getAssetFromCache(e[col.name]);
|
||||
if (cachedAsset != null) {
|
||||
return cachedAsset.name;
|
||||
}
|
||||
|
||||
final asset = await ApiController.to.apiClient
|
||||
.getItemsFromTable(
|
||||
tableName: "assets",
|
||||
itemsSelector: ItemsSelector(
|
||||
fields: ["name"],
|
||||
where: [
|
||||
ColumnConditionCompat(
|
||||
column: "id",
|
||||
operator_: ColumnConditionCompatOperator.eq,
|
||||
value: e[col.name],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final ad = asset.data;
|
||||
if (ad == null ||
|
||||
ad.isEmpty ||
|
||||
ad.first["name"] == null) {
|
||||
return "#error#";
|
||||
}
|
||||
|
||||
controller.putAssetInCache(Asset(
|
||||
id: e[col.name],
|
||||
name: ad.first["name"],
|
||||
description: "",
|
||||
fid: "",
|
||||
tags: "",
|
||||
mime: "",
|
||||
));
|
||||
|
||||
return ad.first["name"].toString();
|
||||
}(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.waiting) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
|
||||
return Tooltip(
|
||||
message: snapshot.data ?? "#error#",
|
||||
child: Text(
|
||||
snapshot.data ?? "#error#",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
onDoubleTap: () async {
|
||||
final asset = await controller.showAssetPicker();
|
||||
if (asset != null) {
|
||||
await controller.updateItem(
|
||||
e,
|
||||
col.name,
|
||||
asset.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
else
|
||||
DataCell.empty,
|
||||
DataCell(Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => controller.deleteItem(e),
|
||||
icon: const Icon(Icons.delete),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
).constrained(width: Get.width * 0.9, height: Get.height * 0.9),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> show(TableDefinition table) async {
|
||||
Get.lazyPut<OpenTableController>(() => OpenTableController(table: table));
|
||||
|
||||
await Get.dialog(
|
||||
const OpenTableDialog(),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:tuuli_app/api/api_client.dart';
|
||||
import 'package:tuuli_app/api/model/tables_list_model.dart';
|
||||
import 'package:tuuli_app/api_controller.dart';
|
||||
import 'package:tuuli_app/c.dart';
|
||||
import 'package:tuuli_app/pages/home_panels/assets_panel.dart';
|
||||
import 'package:tuuli_app/pages/home_panels/none_panel.dart';
|
||||
import 'package:tuuli_app/pages/home_panels/settings_panel.dart';
|
||||
import 'package:tuuli_app/pages/home_panels/tables_list_panel.dart';
|
||||
|
|
@ -13,47 +13,65 @@ enum PageType {
|
|||
none,
|
||||
tables,
|
||||
users,
|
||||
assets,
|
||||
settings,
|
||||
}
|
||||
|
||||
mixin HomePageStateRef {
|
||||
void refreshData();
|
||||
}
|
||||
class HomePageController extends GetxController {
|
||||
final _currentPage = PageType.none.obs;
|
||||
PageType get currentPage => _currentPage.value;
|
||||
set currentPage(PageType value) => _currentPage.value = value;
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
String get currentPageName => pageNames[currentPage]!;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> with HomePageStateRef {
|
||||
final apiClient = Get.find<ApiClient>();
|
||||
TablesListModel tables = TablesListModel([]);
|
||||
|
||||
TableModel get usersTable =>
|
||||
tables.tables.firstWhere((element) => element.tableName == "users");
|
||||
|
||||
bool get isNarrow =>
|
||||
(MediaQuery.of(context).size.width - C.materialDrawerWidth) <= 600;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
refreshData();
|
||||
}
|
||||
|
||||
var currentPage = PageType.none;
|
||||
final pageNames = {
|
||||
PageType.none: "Home",
|
||||
PageType.tables: "Tables",
|
||||
PageType.users: "Users",
|
||||
PageType.assets: "Assets",
|
||||
PageType.settings: "Settings",
|
||||
};
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
Get.lazyPut<TablesListPanelController>(
|
||||
() => TablesListPanelController(),
|
||||
fenix: true,
|
||||
);
|
||||
Get.lazyPut<UserListPanelController>(
|
||||
() => UserListPanelController(),
|
||||
fenix: true,
|
||||
);
|
||||
Get.lazyPut<AssetsPagePanelController>(
|
||||
() => AssetsPagePanelController(),
|
||||
fenix: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
ApiController.to.token = "";
|
||||
|
||||
await Future.wait([
|
||||
Get.delete<TablesListPanelController>(),
|
||||
Get.delete<UserListPanelController>(),
|
||||
Get.delete<AssetsPagePanelController>(),
|
||||
Get.delete<HomePageController>(),
|
||||
]);
|
||||
|
||||
Get.offAllNamed("/");
|
||||
}
|
||||
}
|
||||
|
||||
class HomePage extends GetView<HomePageController> {
|
||||
const HomePage({super.key});
|
||||
|
||||
bool get isNarrow =>
|
||||
(Get.mediaQuery.size.width - C.materialDrawerWidth) <= 600;
|
||||
|
||||
AppBar get appBar => AppBar(
|
||||
title: Text(pageNames[currentPage]!),
|
||||
title: Obx(() => Text(controller.currentPageName)),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.home),
|
||||
|
|
@ -62,51 +80,59 @@ class _HomePageState extends State<HomePage> with HomePageStateRef {
|
|||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: refreshData,
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => controller.logout(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
ListView get drawerOptions => ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.table_chart),
|
||||
title: const Text("Tables"),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
currentPage = PageType.tables;
|
||||
});
|
||||
},
|
||||
Obx(
|
||||
() => ListTile(
|
||||
leading: const Icon(Icons.table_chart),
|
||||
title: const Text("Tables"),
|
||||
onTap: () {
|
||||
controller.currentPage = PageType.tables;
|
||||
},
|
||||
selected: controller.currentPage == PageType.tables,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person),
|
||||
title: const Text("Users"),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
currentPage = PageType.users;
|
||||
});
|
||||
},
|
||||
Obx(
|
||||
() => ListTile(
|
||||
leading: const Icon(Icons.person),
|
||||
title: const Text("Users"),
|
||||
onTap: () {
|
||||
controller.currentPage = PageType.users;
|
||||
},
|
||||
selected: controller.currentPage == PageType.users,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: const Text("Settings"),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
currentPage = PageType.settings;
|
||||
});
|
||||
},
|
||||
Obx(
|
||||
() => ListTile(
|
||||
leading: const Icon(Icons.dataset_outlined),
|
||||
title: const Text("Assets"),
|
||||
onTap: () {
|
||||
controller.currentPage = PageType.assets;
|
||||
},
|
||||
selected: controller.currentPage == PageType.assets,
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: const Text("Settings"),
|
||||
onTap: () {
|
||||
controller.currentPage = PageType.settings;
|
||||
},
|
||||
selected: controller.currentPage == PageType.settings,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: const Text("Logout"),
|
||||
onTap: () {
|
||||
GetStorage().erase().then((value) {
|
||||
GetStorage().save();
|
||||
Get.offAllNamed("/");
|
||||
});
|
||||
},
|
||||
onTap: () => controller.logout(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -140,12 +166,14 @@ class _HomePageState extends State<HomePage> with HomePageStateRef {
|
|||
),
|
||||
LimitedBox(
|
||||
maxWidth: MediaQuery.of(context).size.width - C.materialDrawerWidth,
|
||||
child: Builder(builder: (context) {
|
||||
switch (currentPage) {
|
||||
child: Obx(() {
|
||||
switch (controller.currentPage) {
|
||||
case PageType.tables:
|
||||
return TablesListPanel(parent: this, tables: tables);
|
||||
return const TablesListPanel();
|
||||
case PageType.users:
|
||||
return UsersListPanel(usersTable: usersTable);
|
||||
return const UsersListPanel();
|
||||
case PageType.assets:
|
||||
return const AssetsPagePanel();
|
||||
case PageType.settings:
|
||||
return const SettingsPanel();
|
||||
case PageType.none:
|
||||
|
|
@ -158,25 +186,4 @@ class _HomePageState extends State<HomePage> with HomePageStateRef {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void refreshData() {
|
||||
apiClient.tablesList().then(
|
||||
(value) => value.unfold(
|
||||
(tables) {
|
||||
setState(() {
|
||||
this.tables = tables;
|
||||
});
|
||||
},
|
||||
(error) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error.toString()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
568
lib/pages/home_panels/assets_panel.dart
Normal file
568
lib/pages/home_panels/assets_panel.dart
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
import 'package:data_table_2/data_table_2.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:file_icon/file_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_fast_forms/flutter_fast_forms.dart';
|
||||
import 'package:get/get.dart' hide MultipartFile;
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:super_drag_and_drop/super_drag_and_drop.dart';
|
||||
import 'package:tuuli_api/tuuli_api.dart';
|
||||
import 'package:tuuli_app/api_controller.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:tuuli_app/widgets/audio_player_widget.dart';
|
||||
|
||||
class AssetsPagePanelController extends GetxController {
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
refreshData();
|
||||
}
|
||||
|
||||
final scrollController = ScrollController();
|
||||
|
||||
final _isLoading = false.obs;
|
||||
bool get isLoading => _isLoading.value;
|
||||
|
||||
final _assetsList = <Asset>[].obs;
|
||||
List<Asset> get assetsList => _assetsList.toList();
|
||||
|
||||
final _tagsList = <String>[].obs;
|
||||
List<String> get tagsList => _tagsList.toList();
|
||||
|
||||
final _filterTags = <String>[].obs;
|
||||
List<String> get filterTags => _filterTags.toList();
|
||||
set filterTags(List<String> value) => _filterTags.value = value;
|
||||
|
||||
Future<void> refreshData() async {
|
||||
_isLoading.value = true;
|
||||
|
||||
await Future.wait([
|
||||
refreshAssets(),
|
||||
refreshTag(),
|
||||
]);
|
||||
|
||||
_isLoading.value = false;
|
||||
}
|
||||
|
||||
Future<void> refreshAssets() async {
|
||||
try {
|
||||
final resp = await ApiController.to.apiClient.getAssets();
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
_assetsList.clear();
|
||||
_assetsList.addAll(respData);
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to get assets",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to get assets",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to get assets",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshTag() async {
|
||||
try {
|
||||
final resp = await ApiController.to.apiClient.getAssetsTags();
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
_tagsList.clear();
|
||||
_tagsList.addAll(respData);
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to get tags",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to get tags",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to get tags",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openUploadDialog() async {
|
||||
final file = await Get.dialog<MultipartFile>(
|
||||
AlertDialog(
|
||||
content: DropRegion(
|
||||
formats: Formats.standardFormats,
|
||||
hitTestBehavior: HitTestBehavior.opaque,
|
||||
onDropOver: (event) {
|
||||
if (event.session.items.length == 1 &&
|
||||
event.session.allowedOperations.contains(DropOperation.copy)) {
|
||||
return DropOperation.copy;
|
||||
}
|
||||
return DropOperation.none;
|
||||
},
|
||||
onPerformDrop: (event) async {
|
||||
final item = event.session.items.first;
|
||||
final reader = item.dataReader;
|
||||
if (reader == null) return;
|
||||
|
||||
reader.getFile(
|
||||
null,
|
||||
(dataReader) async {
|
||||
final data = await dataReader.readAll();
|
||||
|
||||
final fileName =
|
||||
dataReader.fileName ?? await reader.getSuggestedName();
|
||||
const mimeType = "application/octet-stream";
|
||||
|
||||
final file = MultipartFile.fromBytes(
|
||||
data,
|
||||
filename: fileName,
|
||||
contentType: MediaType.parse(mimeType),
|
||||
);
|
||||
Get.back(result: file);
|
||||
},
|
||||
onError: (value) {
|
||||
Get.snackbar("Error", value.toString());
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text("Drop file here")
|
||||
.paddingAll(8)
|
||||
.fittedBox()
|
||||
.constrained(height: 200, width: 200),
|
||||
).border(all: 2, color: Colors.lightBlueAccent),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: null),
|
||||
child: const Text("Cancel"),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (file == null) return;
|
||||
|
||||
final sendProgress = 0.obs;
|
||||
final receiveProgress = 0.obs;
|
||||
final req = ApiController.to.apiClient.putAsset(
|
||||
asset: file,
|
||||
onSendProgress: (count, _) {
|
||||
sendProgress.value = count;
|
||||
},
|
||||
onReceiveProgress: (count, _) {
|
||||
receiveProgress.value = count;
|
||||
},
|
||||
);
|
||||
Get.dialog(
|
||||
SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: Obx(
|
||||
() => CircularProgressIndicator(
|
||||
value:
|
||||
sendProgress.value == 0 ? null : sendProgress.value.toDouble(),
|
||||
),
|
||||
),
|
||||
).paddingAll(32).card().center(),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
|
||||
try {
|
||||
final resp = await req;
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
refreshData();
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to put asset",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to put asset",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to put asset",
|
||||
"$e",
|
||||
);
|
||||
} finally {
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> editAsset(Asset e) async {
|
||||
final description = e.description.obs;
|
||||
final tags = e.tags.split(",").obs;
|
||||
|
||||
final confirm = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: const Text("Edit asset"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
controller: TextEditingController(text: description.value),
|
||||
onChanged: (value) => description.value = value,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Description",
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: TextEditingController(text: tags.join(", ")),
|
||||
onChanged: (value) => tags.value =
|
||||
value.split(",").map((e) => e.trim()).toList(growable: false),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Tags",
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Obx(
|
||||
() => Wrap(
|
||||
children: tags
|
||||
.where((p0) => p0.isNotEmpty)
|
||||
.map((tag) => Chip(label: Text(tag)))
|
||||
.toList(growable: false),
|
||||
).paddingAll(8).card(color: Colors.blueGrey.shade200).expanded(),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => previewAsset(e),
|
||||
child: const Text("View asset")),
|
||||
],
|
||||
).constrained(width: Get.width * 0.5, height: Get.width * 0.5),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text("Confirm"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm != true) return;
|
||||
|
||||
try {
|
||||
final resp =
|
||||
await ApiController.to.apiClient.updateAssetDescriptionAndTags(
|
||||
assetId: e.id,
|
||||
assetDescription: description.value,
|
||||
assetTags: tags,
|
||||
);
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
refreshData();
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to edit asset",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to edit asset",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to edit asset",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeAsset(Asset e) async {
|
||||
final checkReferences = false.obs;
|
||||
final deleteReferencing = false.obs;
|
||||
|
||||
final confirm = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: const Text("Remove asset"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text("You are about to remove an asset."),
|
||||
Obx(
|
||||
() => CheckboxListTile(
|
||||
value: checkReferences.value,
|
||||
onChanged: (value) => checkReferences.value = value ?? false,
|
||||
title: const Text("Check references"),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => CheckboxListTile(
|
||||
value: deleteReferencing.value,
|
||||
onChanged: (value) => deleteReferencing.value = value ?? false,
|
||||
title: const Text("Delete referencing"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text("Remove"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm != true) return;
|
||||
|
||||
try {
|
||||
final resp = await ApiController.to.apiClient.removeAsset(
|
||||
assetId: e.id,
|
||||
checkReferences: checkReferences.value,
|
||||
deleteReferencing: deleteReferencing.value,
|
||||
);
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
refreshData();
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to remove asset",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to remove asset",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to remove asset",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void previewAsset(Asset e) {
|
||||
Get.dialog(
|
||||
Stack(
|
||||
children: [
|
||||
if (e.mime.split("/").first == "image")
|
||||
PhotoView(
|
||||
imageProvider:
|
||||
NetworkImage("${ApiController.to.endPoint}/assets/${e.fid}"),
|
||||
enablePanAlways: true,
|
||||
)
|
||||
else if (e.mime.split("/").first == "audio")
|
||||
AudioPlayerWidget.create(
|
||||
title: e.name,
|
||||
url: "${ApiController.to.endPoint}/assets/${e.fid}",
|
||||
)
|
||||
else
|
||||
const Text("Unsupported media type")
|
||||
.fontSize(16)
|
||||
.paddingAll(8)
|
||||
.card()
|
||||
.center(),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: IconButton(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AssetsPagePanel extends GetView<AssetsPagePanelController> {
|
||||
const AssetsPagePanel({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
AppBar(
|
||||
elevation: 0,
|
||||
title: Row(
|
||||
children: [
|
||||
const Text("Tags:"),
|
||||
Obx(
|
||||
() => FastChipsInput(
|
||||
name: "FastChipsInput",
|
||||
options: controller.tagsList,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
),
|
||||
chipBuilder: (chipValue, chipIndex, field) => InputChip(
|
||||
label: Text(chipValue),
|
||||
isEnabled: field.widget.enabled,
|
||||
onDeleted: () => field
|
||||
.didChange([...field.value!]..remove(chipValue)),
|
||||
selected: chipIndex == field.selectedChipIndex,
|
||||
showCheckmark: false,
|
||||
backgroundColor: Colors.green.shade200,
|
||||
),
|
||||
onChanged: (value) => controller.filterTags = value ?? [],
|
||||
),
|
||||
).expanded(),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => controller.openUploadDialog(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () => controller.refreshData(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: assetsPanel,
|
||||
),
|
||||
],
|
||||
),
|
||||
Obx(
|
||||
() => Positioned(
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
child: controller.isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget get assetsPanel => Obx(
|
||||
() => DataTable2(
|
||||
horizontalScrollController: controller.scrollController,
|
||||
columns: const [
|
||||
DataColumn2(label: Text(""), fixedWidth: 16),
|
||||
DataColumn2(label: Text("Filename"), size: ColumnSize.M),
|
||||
DataColumn2(label: Text("Description"), size: ColumnSize.L),
|
||||
DataColumn2(label: Text("File ID"), size: ColumnSize.M),
|
||||
DataColumn2(label: Text("Tags"), size: ColumnSize.L),
|
||||
DataColumn2(label: Text("Actions")),
|
||||
],
|
||||
empty: const Text("No assets found"),
|
||||
rows: controller.assetsList
|
||||
.where((element) {
|
||||
if (controller.filterTags.isEmpty) return true;
|
||||
|
||||
return element.tags
|
||||
.split(",")
|
||||
.any(controller.filterTags.contains);
|
||||
})
|
||||
.map((e) => DataRow2(
|
||||
cells: [
|
||||
DataCell(FileIcon(e.name)),
|
||||
DataCell(Tooltip(
|
||||
message: e.name,
|
||||
child: Text(
|
||||
e.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)),
|
||||
DataCell(Tooltip(
|
||||
message: e.description,
|
||||
child: Text(
|
||||
e.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)),
|
||||
DataCell(Tooltip(
|
||||
message: e.fid,
|
||||
child: Text(
|
||||
e.fid,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)),
|
||||
DataCell(Tooltip(
|
||||
message: e.tags,
|
||||
child: Text(
|
||||
e.tags,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)),
|
||||
DataCell(Row(children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.preview),
|
||||
onPressed: () => controller.previewAsset(e),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () => controller.editAsset(e),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => controller.removeAsset(e),
|
||||
),
|
||||
])),
|
||||
],
|
||||
))
|
||||
.toList(growable: false),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -1,110 +1,152 @@
|
|||
import 'package:bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tuuli_api/tuuli_api.dart';
|
||||
import 'package:tuuli_app/api_controller.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:tuuli_app/api/api_client.dart';
|
||||
import 'package:tuuli_app/api/model/tables_list_model.dart';
|
||||
import 'package:tuuli_app/c.dart';
|
||||
import 'package:tuuli_app/pages/bottomsheets/edit_table_bottomsheet.dart';
|
||||
import 'package:tuuli_app/pages/bottomsheets/open_table_bottomsheet.dart';
|
||||
import 'package:tuuli_app/pages/home_page.dart';
|
||||
|
||||
class TablesListPanel extends StatefulWidget {
|
||||
final TablesListModel tables;
|
||||
final HomePageStateRef parent;
|
||||
|
||||
const TablesListPanel({
|
||||
super.key,
|
||||
required this.tables,
|
||||
required this.parent,
|
||||
});
|
||||
import 'package:tuuli_app/models/db_column_definition.dart';
|
||||
import 'package:tuuli_app/pages/dialogs/create_table_dialog.dart';
|
||||
import 'package:tuuli_app/pages/dialogs/open_table_dialog.dart';
|
||||
|
||||
class TablesListPanelController extends GetxController {
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TablesListPanelState();
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
refreshData();
|
||||
}
|
||||
|
||||
final _isLoading = false.obs;
|
||||
bool get isLoading => _isLoading.value;
|
||||
|
||||
final _tables = <TableDefinition>[].obs;
|
||||
List<TableDefinition> get tables => _tables;
|
||||
|
||||
Future<void> refreshData() async {
|
||||
_isLoading.value = true;
|
||||
try {
|
||||
final resp = await ApiController.to.apiClient.listTables();
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
_tables.clear();
|
||||
_tables.addAll(respData);
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to get tables",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to get tables",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to get tables",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
_isLoading.value = false;
|
||||
}
|
||||
|
||||
Future<void> createNewTable() async {
|
||||
final created = await CreateTableDialog.show();
|
||||
if (created == true) {
|
||||
refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openTable(TableDefinition table) async {
|
||||
await OpenTableDialog.show(table);
|
||||
}
|
||||
|
||||
Future<void> deleteTable(TableDefinition table) async {
|
||||
try {
|
||||
final resp = await ApiController.to.apiClient.dropTable(
|
||||
tableName: table.tableName,
|
||||
);
|
||||
|
||||
final respData = resp.data;
|
||||
if (respData == null) {
|
||||
throw Exception("No data in response");
|
||||
}
|
||||
|
||||
Get.snackbar(
|
||||
"Table deleted",
|
||||
"${table.tableName.pascalCase} was deleted",
|
||||
);
|
||||
refreshData();
|
||||
} on DioError catch (e) {
|
||||
final respData = e.response?.data;
|
||||
if (respData != null) {
|
||||
Get.snackbar(
|
||||
"Error trying to delete table",
|
||||
"${respData['error']}",
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error trying to delete table",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Error trying to delete table",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _TablesListPanelState extends State<TablesListPanel> {
|
||||
final apiClient = Get.find<ApiClient>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
class TablesListPanel extends GetView<TablesListPanelController> {
|
||||
const TablesListPanel({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _buildTableList();
|
||||
}
|
||||
|
||||
Widget _buildTableCard(BuildContext ctx, TableModel table) {
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () => _openTable(table),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(5),
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
table.tableName.pascalCase,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
table.system ? "System" : "Userland",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"${table.columns.length} column(s)",
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
AppBar(
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => controller.createNewTable(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () => controller.refreshData(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => controller.tables.isEmpty ? whenNoTables : cardGrid),
|
||||
),
|
||||
],
|
||||
),
|
||||
Obx(
|
||||
() => Positioned(
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
child: controller.isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget get newTableCard => Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: _createNewTable,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(5),
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildTableList() {
|
||||
var tableItems = widget.tables.tables
|
||||
.where((table) => !table.hidden && !table.system)
|
||||
.toList(growable: false);
|
||||
|
||||
if (tableItems.isEmpty) {
|
||||
return Center(
|
||||
Widget get whenNoTables => Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
@ -113,7 +155,7 @@ class _TablesListPanelState extends State<TablesListPanel> {
|
|||
Text(
|
||||
"No tables found",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).disabledColor,
|
||||
color: Get.theme.disabledColor,
|
||||
fontSize: 24,
|
||||
),
|
||||
),
|
||||
|
|
@ -121,81 +163,71 @@ class _TablesListPanelState extends State<TablesListPanel> {
|
|||
Text(
|
||||
"Maybe create one?",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).disabledColor,
|
||||
color: Get.theme.disabledColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _createNewTable,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text("Create table"),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: tableItems.length + 1,
|
||||
itemBuilder: (ctx, i) =>
|
||||
i == 0 ? newTableCard : _buildTableCard(ctx, tableItems[i - 1]),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 5,
|
||||
Widget get cardGrid => GridView.count(
|
||||
shrinkWrap: true,
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 5 / 2,
|
||||
children: [...controller.tables.map((e) => _buildTableCard(e))],
|
||||
);
|
||||
|
||||
Widget _buildTableCard(TableDefinition table) {
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () => controller.openTable(table),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(5),
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
table.tableName.pascalCase,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
table.system ? "System" : "Userland",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"${table.parsedColumns.length} column(s)",
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!table.system)
|
||||
ElevatedButton(
|
||||
onPressed: () => controller.deleteTable(table),
|
||||
child: const Text("Delete"),
|
||||
).paddingOnly(top: 8)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _createNewTable() async {
|
||||
final result = await showFlexibleBottomSheet<EditTableBottomSheetResult>(
|
||||
minHeight: 0,
|
||||
initHeight: 0.75,
|
||||
maxHeight: 1,
|
||||
context: context,
|
||||
builder: (_, __, ___) => const EditTableBottomSheet(),
|
||||
anchors: [0, 0.5, 1],
|
||||
isSafeArea: true,
|
||||
isDismissible: false,
|
||||
);
|
||||
|
||||
if (result == null) return;
|
||||
|
||||
await apiClient
|
||||
.createTable(result.tableName, result.fields)
|
||||
.then((e) => WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
e.unfold((_) {
|
||||
Get.snackbar(
|
||||
"Success",
|
||||
"Table created",
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}, (error) {
|
||||
Get.defaultDialog(
|
||||
title: "Error",
|
||||
middleText: error.toString(),
|
||||
textConfirm: "OK",
|
||||
onConfirm: () => Get.back(),
|
||||
);
|
||||
});
|
||||
}));
|
||||
|
||||
widget.parent.refreshData();
|
||||
}
|
||||
|
||||
void _openTable(TableModel table) async {
|
||||
await showFlexibleBottomSheet<void>(
|
||||
minHeight: 1,
|
||||
initHeight: 1,
|
||||
maxHeight: 1,
|
||||
context: context,
|
||||
builder: (_, __, ___) => OpenTableBottomSheet(table: table),
|
||||
anchors: [0, 0.5, 1],
|
||||
isSafeArea: true,
|
||||
isDismissible: false,
|
||||
);
|
||||
|
||||
widget.parent.refreshData();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,40 +1,95 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:animated_background/animated_background.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:tuuli_app/api/api_client.dart';
|
||||
import 'package:tuuli_api/tuuli_api.dart';
|
||||
import 'package:tuuli_app/api_controller.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
class LoginPageController extends GetxController {
|
||||
final _endpoint = ApiController.to.endPoint.obs;
|
||||
String get endpoint => _endpoint.value;
|
||||
set endpoint(String value) => _endpoint.value = value;
|
||||
|
||||
final _username = "".obs;
|
||||
String get username => _username.value;
|
||||
set username(String value) => _username.value = value;
|
||||
|
||||
final _password = "".obs;
|
||||
String get password => _password.value;
|
||||
set password(String value) => _password.value = value;
|
||||
|
||||
final _submitted = false.obs;
|
||||
bool get submitted => _submitted.value;
|
||||
set submitted(bool value) => _submitted.value = value;
|
||||
|
||||
bool get isFormValid =>
|
||||
endpoint.isNotEmpty && username.isNotEmpty && password.isNotEmpty;
|
||||
|
||||
Future<void> submitForm() async {
|
||||
submitted = true;
|
||||
if (isFormValid) {
|
||||
try {
|
||||
ApiController.to.endPoint = endpoint;
|
||||
final resp = await ApiController.to.apiClient.getAccessToken(
|
||||
authModel: AuthModel(
|
||||
username: username,
|
||||
password: password,
|
||||
),
|
||||
);
|
||||
|
||||
final respData = resp.data;
|
||||
if (resp.statusCode == 200 && respData != null) {
|
||||
final accessToken = respData.accessToken;
|
||||
Get.find<ApiController>().token = accessToken;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Get.offAllNamed("/home");
|
||||
});
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Login failed",
|
||||
resp.statusMessage ?? "Unknown error",
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
final errorData = e.response?.data;
|
||||
if (errorData != null) {
|
||||
final error = errorData["error"];
|
||||
if (error != null) {
|
||||
Get.snackbar(
|
||||
"Login failed",
|
||||
"$error",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Login failed",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Login failed",
|
||||
"$e",
|
||||
);
|
||||
}
|
||||
}
|
||||
submitted = false;
|
||||
}
|
||||
}
|
||||
|
||||
class LoginPage extends GetView<LoginPageController> {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
final apiClient = Get.find<ApiClient>();
|
||||
var submitted = false;
|
||||
|
||||
final loginController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final screenSize = Get.mediaQuery.size;
|
||||
final formWidth = screenSize.width <= 600 ? screenSize.width : 300.0;
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
AnimatedBackground(
|
||||
behaviour: RandomParticleBehaviour(),
|
||||
vsync: this,
|
||||
child: const SizedBox.square(
|
||||
dimension: 0,
|
||||
),
|
||||
const SizedBox.square(
|
||||
dimension: 0,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
|
|
@ -42,23 +97,37 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
|
|||
children: [
|
||||
LimitedBox(
|
||||
maxWidth: formWidth,
|
||||
child: Container(
|
||||
color: Colors.black.withAlpha(100),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Obx(
|
||||
() => Container(
|
||||
color: Colors.black.withAlpha(100),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: loginController,
|
||||
enabled: !submitted,
|
||||
enabled: !controller.submitted,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Endpoint',
|
||||
hintText: 'Enter Tuuli Endpoint',
|
||||
),
|
||||
initialValue: ApiController.to.endPoint,
|
||||
onChanged: (value) => controller.endpoint = value,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter Tuuli Endpoint';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
enabled: !controller.submitted,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Login',
|
||||
hintText: 'Enter your login',
|
||||
hintText: 'Enter your username',
|
||||
),
|
||||
onChanged: (value) => controller.username = value,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your Login';
|
||||
|
|
@ -67,13 +136,13 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
|
|||
},
|
||||
),
|
||||
TextFormField(
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
enabled: !submitted,
|
||||
enabled: !controller.submitted,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Password',
|
||||
hintText: 'Enter your password',
|
||||
),
|
||||
onChanged: (value) => controller.password = value,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your password';
|
||||
|
|
@ -83,7 +152,10 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: submitted ? null : _submit,
|
||||
onPressed:
|
||||
!controller.isFormValid || controller.submitted
|
||||
? null
|
||||
: () => controller.submitForm(),
|
||||
child: const Text('Login'),
|
||||
),
|
||||
],
|
||||
|
|
@ -97,54 +169,4 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
submitted = true;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Trying to login...'),
|
||||
),
|
||||
);
|
||||
|
||||
final response = await apiClient.login(
|
||||
loginController.text.trim(),
|
||||
passwordController.text.trim(),
|
||||
);
|
||||
response.unfold((data) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Login successful'),
|
||||
),
|
||||
);
|
||||
apiClient.setAccessToken(data.accessToken);
|
||||
GetStorage()
|
||||
.write("accessToken", data.accessToken)
|
||||
.then((value) => GetStorage().save())
|
||||
.then((value) {
|
||||
Timer(1.seconds, () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Get.offAllNamed("/home");
|
||||
});
|
||||
});
|
||||
});
|
||||
}, (error) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error.toString()),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
submitted = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:animated_background/animated_background.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
|
|
@ -14,14 +13,10 @@ class _NotFoundPageState extends State<NotFoundPage>
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: AnimatedBackground(
|
||||
behaviour: RandomParticleBehaviour(),
|
||||
vsync: this,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Page not found',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
body: Center(
|
||||
child: Text(
|
||||
'Page not found',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
bottomSheet: Container(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:tuuli_api/tuuli_api.dart';
|
||||
import 'package:tuuli_app/models/user_definition.dart';
|
||||
|
||||
Random _random = Random();
|
||||
|
||||
String randomHexString(int length) {
|
||||
|
|
@ -9,3 +12,35 @@ String randomHexString(int length) {
|
|||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
String postgresDateFormat(DateTime dt) {
|
||||
int yearSign = dt.year.sign;
|
||||
int absYear = dt.year.abs();
|
||||
String y = absYear
|
||||
.toString()
|
||||
.padLeft((dt.year >= -9999 && dt.year <= 9999) ? 4 : 6, "0");
|
||||
if (yearSign == -1) {
|
||||
y = "-$y";
|
||||
}
|
||||
String m = dt.month.toString().padLeft(2, "0");
|
||||
String d = dt.day.toString().padLeft(2, "0");
|
||||
String h = dt.hour.toString().padLeft(2, "0");
|
||||
String min = dt.minute.toString().padLeft(2, "0");
|
||||
String sec = dt.second.toString().padLeft(2, "0");
|
||||
|
||||
return "$y-$m-$d $h:$min:$sec";
|
||||
}
|
||||
|
||||
Map<String, dynamic> convertToPayload(Map<String, dynamic> data) {
|
||||
return data.map((key, value) {
|
||||
if (value is UserDefinition) {
|
||||
return MapEntry(key, value.id);
|
||||
} else if (value is Asset) {
|
||||
return MapEntry(key, value.id);
|
||||
} else if (value is DateTime) {
|
||||
return MapEntry(key, postgresDateFormat(value));
|
||||
}
|
||||
|
||||
return MapEntry(key, value);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
72
lib/widgets/audio_player_widget.dart
Normal file
72
lib/widgets/audio_player_widget.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class AudioPlayerWidgetController extends GetxController {
|
||||
final String title;
|
||||
final String url;
|
||||
|
||||
AudioPlayerWidgetController({
|
||||
required this.title,
|
||||
required this.url,
|
||||
});
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
player = AudioPlayer();
|
||||
player.play(UrlSource(url));
|
||||
|
||||
player.onPlayerStateChanged.listen((event) {
|
||||
_playerState.value = event;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
player.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
late AudioPlayer player;
|
||||
|
||||
final _playerState = PlayerState.stopped.obs;
|
||||
get playerState => _playerState.value;
|
||||
}
|
||||
|
||||
class AudioPlayerWidget extends GetView<AudioPlayerWidgetController> {
|
||||
const AudioPlayerWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Obx(
|
||||
() => controller.playerState == PlayerState.playing
|
||||
? IconButton(
|
||||
onPressed: () => controller.player.pause(),
|
||||
icon: const Icon(Icons.pause),
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: () => controller.player.resume(),
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
),
|
||||
),
|
||||
title: const Text("Playing"),
|
||||
subtitle: Text(controller.title),
|
||||
).card().paddingAll(16).center();
|
||||
}
|
||||
|
||||
static AudioPlayerWidget create(
|
||||
{required String url, required String title}) {
|
||||
Get.lazyPut<AudioPlayerWidgetController>(
|
||||
() => AudioPlayerWidgetController(
|
||||
title: title,
|
||||
url: url,
|
||||
),
|
||||
);
|
||||
|
||||
return const AudioPlayerWidget();
|
||||
}
|
||||
}
|
||||
126
lib/widgets/data_input_dialog.dart
Normal file
126
lib/widgets/data_input_dialog.dart
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_fast_forms/flutter_fast_forms.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
Future<String?> showStringInputDialog({String? originalValue}) async {
|
||||
final strVal = (originalValue ?? "").obs;
|
||||
return await Get.dialog<String>(
|
||||
AlertDialog(
|
||||
title: const Text("Enter a string"),
|
||||
content: TextField(
|
||||
controller: TextEditingController(text: originalValue),
|
||||
onChanged: (value) {
|
||||
strVal.value = value;
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back(result: null);
|
||||
},
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back(result: strVal.value);
|
||||
},
|
||||
child: const Text("OK"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<double?> showDoubleInputDialog({double? originalValue}) async {
|
||||
final strVal = (originalValue?.toString() ?? "").obs;
|
||||
return await Get.dialog<double>(
|
||||
AlertDialog(
|
||||
title: const Text("Enter a number"),
|
||||
content: FastTextField(
|
||||
name: "Number",
|
||||
initialValue: originalValue?.toString(),
|
||||
keyboardType: TextInputType.number,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter a number";
|
||||
}
|
||||
final parsed = double.tryParse(value);
|
||||
if (parsed == null) {
|
||||
return "Please enter a valid number";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
strVal.value = value ?? "";
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back(result: null);
|
||||
},
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final d = double.tryParse(strVal.value);
|
||||
if (d != null) {
|
||||
Get.back(result: d);
|
||||
} else {
|
||||
Get.back(result: null);
|
||||
}
|
||||
},
|
||||
child: const Text("OK"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int?> showIntInputDialog({int? originalValue}) async {
|
||||
final strVal = (originalValue?.toString() ?? "").obs;
|
||||
return await Get.dialog<int>(
|
||||
AlertDialog(
|
||||
title: const Text("Enter a number"),
|
||||
content: FastTextField(
|
||||
name: "Number",
|
||||
initialValue: originalValue?.toString(),
|
||||
keyboardType: TextInputType.number,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter a number";
|
||||
}
|
||||
final parsed = int.tryParse(value);
|
||||
if (parsed == null) {
|
||||
return "Please enter a valid number";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
strVal.value = value ?? "";
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back(result: null);
|
||||
},
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final i = int.tryParse(strVal.value);
|
||||
if (i != null) {
|
||||
Get.back(result: i);
|
||||
} else {
|
||||
Get.back(result: null);
|
||||
}
|
||||
},
|
||||
child: const Text("OK"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:tuuli_app/api/model/table_field_model.dart';
|
||||
|
||||
class TableFieldWidget<T> extends StatelessWidget {
|
||||
final TableField<T> field;
|
||||
final VoidCallback? onRemove;
|
||||
|
||||
const TableFieldWidget({
|
||||
super.key,
|
||||
required this.field,
|
||||
this.onRemove,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (field.isPrimary) const Icon(Icons.star),
|
||||
if (field.isUnique) const Icon(Icons.lock),
|
||||
Text(
|
||||
"Field \"${field.fieldName}\"",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text("Field type: ${field.fieldType} (baked by ${field.type})"),
|
||||
],
|
||||
),
|
||||
if (onRemove != null) const Spacer(),
|
||||
if (onRemove != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: onRemove,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
584
pubspec.lock
584
pubspec.lock
|
|
@ -1,14 +1,30 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
animated_background:
|
||||
dependency: "direct main"
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: animated_background
|
||||
sha256: "24b05a6dca2cb0231b011f9e8fd2e9d8060faac08a78cf0643915bb7d6e9b03b"
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "8880b4cfe7b5b17d57c052a5a3a8cc1d4f546261c7cc8fbd717bd53f48db0568"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "59.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: a89627f49b0e70e068130a36571409726b04dab12da7e5625941d2c8ec278b96
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.11.1"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -17,6 +33,62 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.0"
|
||||
audioplayers:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audioplayers
|
||||
sha256: "6063c05f987596ba7a3dad9bb9a5d8adfa5e7c07b9bae5301b27c11d0b3a239f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
audioplayers_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_android
|
||||
sha256: fb6bca878ad175d8f6ddc0e0a2d4226d81fa7c10747c12db420e96c7a096b2cc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
audioplayers_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_darwin
|
||||
sha256: c4a56c49347b2e85ac4e1efea218948ca0fba87f04d2a3d3de07ce2410037038
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
audioplayers_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_linux
|
||||
sha256: "897e24f190232a3fbb88134b062aa83a9240f55789b5e8d17c114283284ef56b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
audioplayers_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_platform_interface
|
||||
sha256: "3a90a46198d375fc7d47bc1d3070c8fd8863b6469b7d87ca80f953efb090f976"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
audioplayers_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_web
|
||||
sha256: "4f5dcbfec0bf98ea09e243d5f5b64ea43a4e6710a2f292724bed16cdba3c691e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
audioplayers_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_windows
|
||||
sha256: "010f575653c01ccbe9756050b18df83d89426740e04b684f6438aa26c775a965"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -41,6 +113,78 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "7b25ba738bc74c94187cebeb9cc29d38a32e8279ce950eabd821d3b454a5f03d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.7"
|
||||
built_collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: built_collection
|
||||
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.4.4"
|
||||
built_value_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: built_value_generator
|
||||
sha256: "6c7d31060667a309889e45fd86fcaec6477750d767cf32a7f7ce4b1578d3440c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.4.4"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -49,6 +193,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -57,6 +209,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -65,6 +225,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -73,6 +241,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "6d691edde054969f0e0f26abb1b30834b5138b963793e56f69d3a9a4435e6352"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
data_table_2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -81,6 +257,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "0894a098594263fe1caaba3520e3016d8a855caeb010a882273189cca10f11e9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -105,11 +289,35 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
file_icon:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_icon
|
||||
sha256: c46b6c24d9595d18995758b90722865baeda407f56308eadd757e1ab913f50a1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_chips_input:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_chips_input
|
||||
sha256: "9828e45e75f268ff51a08e8d05848776b0a4d8327867d2b347a733030bafe64f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
flutter_fast_forms:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -131,6 +339,19 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
get:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -147,6 +368,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -155,8 +392,16 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.5"
|
||||
http_parser:
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
http_parser:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||
|
|
@ -167,10 +412,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
|
||||
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.18.0"
|
||||
version: "0.18.1"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
irondash_engine_context:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: irondash_engine_context
|
||||
sha256: "086fbbcaef07b821b304b0371e472c687715f326219b69b18dc5c962dab09b55"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
irondash_message_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: irondash_message_channel
|
||||
sha256: "081ff9631a2c6782a47ef4fdf9c97206053af1bb174e2a25851692b04f3bc126"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -179,6 +448,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.7"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
lint:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lint
|
||||
sha256: "4a539aa34ec5721a2c7574ae2ca0336738ea4adc2a34887d54b7596310b33c85"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -187,6 +472,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -211,6 +504,46 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
mime:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mime
|
||||
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
omni_datetime_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: omni_datetime_picker
|
||||
sha256: c70ca19eba89b7ed3663ae8eab2e7922c12b443826cf06c841a7f0a19e22870d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.7"
|
||||
one_of:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: one_of
|
||||
sha256: "25fe0fcf181e761c6fcd604caf9d5fdf952321be17584ba81c72c06bdaa511f0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
one_of_serializer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: one_of_serializer
|
||||
sha256: "3f3dfb5c1578ba3afef1cb47fcc49e585e797af3f2b6c2cc7ed90aad0c5e7b83"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -231,18 +564,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7"
|
||||
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.24"
|
||||
version: "2.0.27"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "818b2dc38b0f178e0ea3f7cf3b28146faab11375985d815942a68eee11c2d0f7"
|
||||
sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.2"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -263,10 +596,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130
|
||||
sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
version: "2.1.6"
|
||||
photo_view:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: photo_view
|
||||
sha256: "8036802a00bae2a78fc197af8a158e3e2f7b500561ed23b4c458107685e645bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -283,6 +624,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -291,6 +640,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quiver
|
||||
sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
recase:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -299,11 +672,91 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.7"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -328,6 +781,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -336,6 +797,46 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
styled_widget:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: styled_widget
|
||||
sha256: "4d439802919b6ccf10d1488798656da8804633b03012682dd1c8ca70a084aa84"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
super_clipboard:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: super_clipboard
|
||||
sha256: "7628464b63fd18df486e87a989ecfd73565f7b01a15ee47a2448283931c3d14d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0+2"
|
||||
super_drag_and_drop:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: super_drag_and_drop
|
||||
sha256: "9a31045fcb264bcfe57e5001b24b68cb8d4880d8d4ecfd594dd4c11a92b1ca56"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0+2"
|
||||
super_native_extensions:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: super_native_extensions
|
||||
sha256: "18ad4c367cea763654d458d9e9aec8c0c00c6b1d542c4f746c94a223e8e4bd0c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0+2"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -352,6 +853,31 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.18"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
tuple:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tuple
|
||||
sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
tuuli_api:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: master
|
||||
resolved-ref: eedd7a336d42a455a9d46ca203b0d52e920c27bf
|
||||
url: "https://glab.nuark.xyz/nuark/tuuli_api.git"
|
||||
source: git
|
||||
version: "1.0.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -361,7 +887,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.3.1"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
|
||||
|
|
@ -376,14 +902,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
|
||||
sha256: dd8f9344bc305ae2923e3d11a2a911d9a4e2c7dd6fe0ed10626d63211a69676e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
version: "4.1.3"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -392,6 +934,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
sdks:
|
||||
dart: ">=3.0.0-322.0.dev <4.0.0"
|
||||
flutter: ">=3.7.0"
|
||||
|
|
|
|||
24
pubspec.yaml
24
pubspec.yaml
|
|
@ -10,19 +10,39 @@ dependencies:
|
|||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
animated_background: ^2.0.0
|
||||
audioplayers: ^4.0.1
|
||||
bottom_sheet: ^3.1.2
|
||||
built_collection: ^5.1.1
|
||||
built_value: ^8.4.4
|
||||
data_table_2: ^2.4.2
|
||||
dio: ^5.1.1
|
||||
file_icon: ^1.0.0
|
||||
flutter_chips_input: ^2.0.0
|
||||
flutter_fast_forms: ^10.0.0
|
||||
get: ^4.6.5
|
||||
get_storage: ^2.1.1
|
||||
http: ^0.13.5
|
||||
http_parser: ^4.0.2
|
||||
mime: ^1.0.4
|
||||
omni_datetime_picker: ^1.0.7
|
||||
one_of_serializer: ^1.5.0
|
||||
photo_view: ^0.14.0
|
||||
recase: ^4.1.0
|
||||
uuid: ^3.0.7
|
||||
shared_preferences: ^2.1.0
|
||||
styled_widget: ^0.4.1
|
||||
super_drag_and_drop: ^0.3.0+2
|
||||
tuple: ^2.0.1
|
||||
tuuli_api:
|
||||
git:
|
||||
url: https://glab.nuark.xyz/nuark/tuuli_api.git
|
||||
ref: master
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
build_runner: ^2.3.3
|
||||
built_value_generator: ^8.4.4
|
||||
flutter_lints: ^2.0.0
|
||||
|
||||
flutter:
|
||||
|
|
|
|||
17
windows/.gitignore
vendored
Normal file
17
windows/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
flutter/ephemeral/
|
||||
|
||||
# Visual Studio user-specific files.
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Visual Studio build-related files.
|
||||
x64/
|
||||
x86/
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
102
windows/CMakeLists.txt
Normal file
102
windows/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(gws_playground LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "gws_playground")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
cmake_policy(SET CMP0063 NEW)
|
||||
|
||||
# Define build configuration option.
|
||||
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||
if(IS_MULTICONFIG)
|
||||
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
|
||||
CACHE STRING "" FORCE)
|
||||
else()
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||
STRING "Flutter build mode" FORCE)
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Profile" "Release")
|
||||
endif()
|
||||
endif()
|
||||
# Define settings for the Profile build mode.
|
||||
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
|
||||
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
|
||||
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||
|
||||
# Use Unicode for all projects.
|
||||
add_definitions(-DUNICODE -D_UNICODE)
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
#
|
||||
# Be cautious about adding new options here, as plugins use this function by
|
||||
# default. In most cases, you should add new options to specific targets instead
|
||||
# of modifying this function.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_17)
|
||||
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
|
||||
target_compile_options(${TARGET} PRIVATE /EHsc)
|
||||
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
|
||||
endfunction()
|
||||
|
||||
# Flutter library and tool build rules.
|
||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
|
||||
# Application build; see runner/CMakeLists.txt.
|
||||
add_subdirectory("runner")
|
||||
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# Support files are copied into place next to the executable, so that it can
|
||||
# run in place. This is done instead of making a separate bundle (as on Linux)
|
||||
# so that building and running from within Visual Studio will work.
|
||||
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
|
||||
# Make the "install" step default, as it's required to run.
|
||||
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||
" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||
|
||||
# Install the AOT library on non-Debug builds only.
|
||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
CONFIGURATIONS Profile;Release
|
||||
COMPONENT Runtime)
|
||||
104
windows/flutter/CMakeLists.txt
Normal file
104
windows/flutter/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# This file controls Flutter-level build steps. It should not be edited.
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||
|
||||
# Configuration provided via flutter tool.
|
||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||
|
||||
# TODO: Move the rest of this into files in ephemeral. See
|
||||
# https://github.com/flutter/flutter/issues/57146.
|
||||
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
||||
|
||||
# === Flutter Library ===
|
||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
||||
|
||||
# Published to parent scope for install step.
|
||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
|
||||
|
||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||
"flutter_export.h"
|
||||
"flutter_windows.h"
|
||||
"flutter_messenger.h"
|
||||
"flutter_plugin_registrar.h"
|
||||
"flutter_texture_registrar.h"
|
||||
)
|
||||
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
|
||||
add_library(flutter INTERFACE)
|
||||
target_include_directories(flutter INTERFACE
|
||||
"${EPHEMERAL_DIR}"
|
||||
)
|
||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
|
||||
add_dependencies(flutter flutter_assemble)
|
||||
|
||||
# === Wrapper ===
|
||||
list(APPEND CPP_WRAPPER_SOURCES_CORE
|
||||
"core_implementations.cc"
|
||||
"standard_codec.cc"
|
||||
)
|
||||
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
|
||||
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
|
||||
"plugin_registrar.cc"
|
||||
)
|
||||
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
|
||||
list(APPEND CPP_WRAPPER_SOURCES_APP
|
||||
"flutter_engine.cc"
|
||||
"flutter_view_controller.cc"
|
||||
)
|
||||
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
|
||||
|
||||
# Wrapper sources needed for a plugin.
|
||||
add_library(flutter_wrapper_plugin STATIC
|
||||
${CPP_WRAPPER_SOURCES_CORE}
|
||||
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||
)
|
||||
apply_standard_settings(flutter_wrapper_plugin)
|
||||
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||
CXX_VISIBILITY_PRESET hidden)
|
||||
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
|
||||
target_include_directories(flutter_wrapper_plugin PUBLIC
|
||||
"${WRAPPER_ROOT}/include"
|
||||
)
|
||||
add_dependencies(flutter_wrapper_plugin flutter_assemble)
|
||||
|
||||
# Wrapper sources needed for the runner.
|
||||
add_library(flutter_wrapper_app STATIC
|
||||
${CPP_WRAPPER_SOURCES_CORE}
|
||||
${CPP_WRAPPER_SOURCES_APP}
|
||||
)
|
||||
apply_standard_settings(flutter_wrapper_app)
|
||||
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
|
||||
target_include_directories(flutter_wrapper_app PUBLIC
|
||||
"${WRAPPER_ROOT}/include"
|
||||
)
|
||||
add_dependencies(flutter_wrapper_app flutter_assemble)
|
||||
|
||||
# === Flutter tool backend ===
|
||||
# _phony_ is a non-existent file to force this command to run every time,
|
||||
# since currently there's no way to get a full input/output list from the
|
||||
# flutter tool.
|
||||
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
|
||||
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
|
||||
add_custom_command(
|
||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||
${CPP_WRAPPER_SOURCES_APP}
|
||||
${PHONY_OUTPUT}
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
${FLUTTER_TOOL_ENVIRONMENT}
|
||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
||||
windows-x64 $<CONFIG>
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
"${FLUTTER_LIBRARY}"
|
||||
${FLUTTER_LIBRARY_HEADERS}
|
||||
${CPP_WRAPPER_SOURCES_CORE}
|
||||
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||
${CPP_WRAPPER_SOURCES_APP}
|
||||
)
|
||||
20
windows/flutter/generated_plugin_registrant.cc
Normal file
20
windows/flutter/generated_plugin_registrant.cc
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <audioplayers_windows/audioplayers_windows_plugin.h>
|
||||
#include <irondash_engine_context/irondash_engine_context_plugin_c_api.h>
|
||||
#include <super_native_extensions/super_native_extensions_plugin_c_api.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
AudioplayersWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
|
||||
IrondashEngineContextPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi"));
|
||||
SuperNativeExtensionsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi"));
|
||||
}
|
||||
15
windows/flutter/generated_plugin_registrant.h
Normal file
15
windows/flutter/generated_plugin_registrant.h
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter/plugin_registry.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
26
windows/flutter/generated_plugins.cmake
Normal file
26
windows/flutter/generated_plugins.cmake
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
audioplayers_windows
|
||||
irondash_engine_context
|
||||
super_native_extensions
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
40
windows/runner/CMakeLists.txt
Normal file
40
windows/runner/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
cmake_minimum_required(VERSION 3.14)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# Define the application target. To change its name, change BINARY_NAME in the
|
||||
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||
# work.
|
||||
#
|
||||
# Any new source files that you add to the application should be added here.
|
||||
add_executable(${BINARY_NAME} WIN32
|
||||
"flutter_window.cpp"
|
||||
"main.cpp"
|
||||
"utils.cpp"
|
||||
"win32_window.cpp"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
"Runner.rc"
|
||||
"runner.exe.manifest"
|
||||
)
|
||||
|
||||
# Apply the standard set of build settings. This can be removed for applications
|
||||
# that need different build settings.
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
|
||||
# Add preprocessor definitions for the build version.
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
|
||||
|
||||
# Disable Windows macros that collide with C++ standard library functions.
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||
|
||||
# Add dependency libraries and include directories. Add any application-specific
|
||||
# dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
121
windows/runner/Runner.rc
Normal file
121
windows/runner/Runner.rc
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
// Microsoft Visual C++ generated resource script.
|
||||
//
|
||||
#pragma code_page(65001)
|
||||
#include "resource.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 2 resource.
|
||||
//
|
||||
#include "winres.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// English (United States) resources
|
||||
|
||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TEXTINCLUDE
|
||||
//
|
||||
|
||||
1 TEXTINCLUDE
|
||||
BEGIN
|
||||
"resource.h\0"
|
||||
END
|
||||
|
||||
2 TEXTINCLUDE
|
||||
BEGIN
|
||||
"#include ""winres.h""\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
3 TEXTINCLUDE
|
||||
BEGIN
|
||||
"\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
#endif // APSTUDIO_INVOKED
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Icon
|
||||
//
|
||||
|
||||
// Icon with lowest ID value placed first to ensure application icon
|
||||
// remains consistent on all systems.
|
||||
IDI_APP_ICON ICON "resources\\app_icon.ico"
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Version
|
||||
//
|
||||
|
||||
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
|
||||
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
|
||||
#else
|
||||
#define VERSION_AS_NUMBER 1,0,0,0
|
||||
#endif
|
||||
|
||||
#if defined(FLUTTER_VERSION)
|
||||
#define VERSION_AS_STRING FLUTTER_VERSION
|
||||
#else
|
||||
#define VERSION_AS_STRING "1.0.0"
|
||||
#endif
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION VERSION_AS_NUMBER
|
||||
PRODUCTVERSION VERSION_AS_NUMBER
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS__WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
FILESUBTYPE 0x0L
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904e4"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "com.example" "\0"
|
||||
VALUE "FileDescription", "gws_playground" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "gws_playground" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "gws_playground.exe" "\0"
|
||||
VALUE "ProductName", "gws_playground" "\0"
|
||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1252
|
||||
END
|
||||
END
|
||||
|
||||
#endif // English (United States) resources
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
#ifndef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 3 resource.
|
||||
//
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#endif // not APSTUDIO_INVOKED
|
||||
66
windows/runner/flutter_window.cpp
Normal file
66
windows/runner/flutter_window.cpp
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#include "flutter_window.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||||
: project_(project) {}
|
||||
|
||||
FlutterWindow::~FlutterWindow() {}
|
||||
|
||||
bool FlutterWindow::OnCreate() {
|
||||
if (!Win32Window::OnCreate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RECT frame = GetClientArea();
|
||||
|
||||
// The size here must match the window dimensions to avoid unnecessary surface
|
||||
// creation / destruction in the startup path.
|
||||
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
||||
frame.right - frame.left, frame.bottom - frame.top, project_);
|
||||
// Ensure that basic setup of the controller was successful.
|
||||
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
|
||||
return false;
|
||||
}
|
||||
RegisterPlugins(flutter_controller_->engine());
|
||||
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||
|
||||
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||||
this->Show();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FlutterWindow::OnDestroy() {
|
||||
if (flutter_controller_) {
|
||||
flutter_controller_ = nullptr;
|
||||
}
|
||||
|
||||
Win32Window::OnDestroy();
|
||||
}
|
||||
|
||||
LRESULT
|
||||
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
// Give Flutter, including plugins, an opportunity to handle window messages.
|
||||
if (flutter_controller_) {
|
||||
std::optional<LRESULT> result =
|
||||
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
|
||||
lparam);
|
||||
if (result) {
|
||||
return *result;
|
||||
}
|
||||
}
|
||||
|
||||
switch (message) {
|
||||
case WM_FONTCHANGE:
|
||||
flutter_controller_->engine()->ReloadSystemFonts();
|
||||
break;
|
||||
}
|
||||
|
||||
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
||||
}
|
||||
33
windows/runner/flutter_window.h
Normal file
33
windows/runner/flutter_window.h
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#ifndef RUNNER_FLUTTER_WINDOW_H_
|
||||
#define RUNNER_FLUTTER_WINDOW_H_
|
||||
|
||||
#include <flutter/dart_project.h>
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "win32_window.h"
|
||||
|
||||
// A window that does nothing but host a Flutter view.
|
||||
class FlutterWindow : public Win32Window {
|
||||
public:
|
||||
// Creates a new FlutterWindow hosting a Flutter view running |project|.
|
||||
explicit FlutterWindow(const flutter::DartProject& project);
|
||||
virtual ~FlutterWindow();
|
||||
|
||||
protected:
|
||||
// Win32Window:
|
||||
bool OnCreate() override;
|
||||
void OnDestroy() override;
|
||||
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept override;
|
||||
|
||||
private:
|
||||
// The project to run.
|
||||
flutter::DartProject project_;
|
||||
|
||||
// The Flutter instance hosted by this window.
|
||||
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
|
||||
};
|
||||
|
||||
#endif // RUNNER_FLUTTER_WINDOW_H_
|
||||
43
windows/runner/main.cpp
Normal file
43
windows/runner/main.cpp
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#include <flutter/dart_project.h>
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "flutter_window.h"
|
||||
#include "utils.h"
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||
// Attach to console when present (e.g., 'flutter run') or create a
|
||||
// new console when running with a debugger.
|
||||
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
||||
CreateAndAttachConsole();
|
||||
}
|
||||
|
||||
// Initialize COM, so that it is available for use in the library and/or
|
||||
// plugins.
|
||||
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
|
||||
flutter::DartProject project(L"data");
|
||||
|
||||
std::vector<std::string> command_line_arguments =
|
||||
GetCommandLineArguments();
|
||||
|
||||
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.Create(L"gws_playground", origin, size)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
window.SetQuitOnClose(true);
|
||||
|
||||
::MSG msg;
|
||||
while (::GetMessage(&msg, nullptr, 0, 0)) {
|
||||
::TranslateMessage(&msg);
|
||||
::DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
::CoUninitialize();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
16
windows/runner/resource.h
Normal file
16
windows/runner/resource.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by Runner.rc
|
||||
//
|
||||
#define IDI_APP_ICON 101
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 102
|
||||
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||
#define _APS_NEXT_CONTROL_VALUE 1001
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
BIN
windows/runner/resources/app_icon.ico
Normal file
BIN
windows/runner/resources/app_icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
20
windows/runner/runner.exe.manifest
Normal file
20
windows/runner/runner.exe.manifest
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 and Windows 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
65
windows/runner/utils.cpp
Normal file
65
windows/runner/utils.cpp
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#include "utils.h"
|
||||
|
||||
#include <flutter_windows.h>
|
||||
#include <io.h>
|
||||
#include <stdio.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
void CreateAndAttachConsole() {
|
||||
if (::AllocConsole()) {
|
||||
FILE *unused;
|
||||
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
|
||||
_dup2(_fileno(stdout), 1);
|
||||
}
|
||||
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
|
||||
_dup2(_fileno(stdout), 2);
|
||||
}
|
||||
std::ios::sync_with_stdio();
|
||||
FlutterDesktopResyncOutputStreams();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> GetCommandLineArguments() {
|
||||
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
|
||||
int argc;
|
||||
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
|
||||
if (argv == nullptr) {
|
||||
return std::vector<std::string>();
|
||||
}
|
||||
|
||||
std::vector<std::string> command_line_arguments;
|
||||
|
||||
// Skip the first argument as it's the binary name.
|
||||
for (int i = 1; i < argc; i++) {
|
||||
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
|
||||
}
|
||||
|
||||
::LocalFree(argv);
|
||||
|
||||
return command_line_arguments;
|
||||
}
|
||||
|
||||
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
|
||||
if (utf16_string == nullptr) {
|
||||
return std::string();
|
||||
}
|
||||
int target_length = ::WideCharToMultiByte(
|
||||
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||
-1, nullptr, 0, nullptr, nullptr)
|
||||
-1; // remove the trailing null character
|
||||
int input_length = (int)wcslen(utf16_string);
|
||||
std::string utf8_string;
|
||||
if (target_length <= 0 || target_length > utf8_string.max_size()) {
|
||||
return utf8_string;
|
||||
}
|
||||
utf8_string.resize(target_length);
|
||||
int converted_length = ::WideCharToMultiByte(
|
||||
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||
input_length, utf8_string.data(), target_length, nullptr, nullptr);
|
||||
if (converted_length == 0) {
|
||||
return std::string();
|
||||
}
|
||||
return utf8_string;
|
||||
}
|
||||
19
windows/runner/utils.h
Normal file
19
windows/runner/utils.h
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#ifndef RUNNER_UTILS_H_
|
||||
#define RUNNER_UTILS_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Creates a console for the process, and redirects stdout and stderr to
|
||||
// it for both the runner and the Flutter library.
|
||||
void CreateAndAttachConsole();
|
||||
|
||||
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
|
||||
// encoded in UTF-8. Returns an empty std::string on failure.
|
||||
std::string Utf8FromUtf16(const wchar_t* utf16_string);
|
||||
|
||||
// Gets the command line arguments passed in as a std::vector<std::string>,
|
||||
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
|
||||
std::vector<std::string> GetCommandLineArguments();
|
||||
|
||||
#endif // RUNNER_UTILS_H_
|
||||
288
windows/runner/win32_window.cpp
Normal file
288
windows/runner/win32_window.cpp
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
#include "win32_window.h"
|
||||
|
||||
#include <dwmapi.h>
|
||||
#include <flutter_windows.h>
|
||||
|
||||
#include "resource.h"
|
||||
|
||||
namespace {
|
||||
|
||||
/// Window attribute that enables dark mode window decorations.
|
||||
///
|
||||
/// Redefined in case the developer's machine has a Windows SDK older than
|
||||
/// version 10.0.22000.0.
|
||||
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
||||
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||
#endif
|
||||
|
||||
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||
|
||||
/// Registry key for app theme preference.
|
||||
///
|
||||
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
|
||||
/// value indicates apps should use light mode.
|
||||
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
|
||||
|
||||
// The number of Win32Window objects that currently exist.
|
||||
static int g_active_window_count = 0;
|
||||
|
||||
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||
|
||||
// Scale helper to convert logical scaler values to physical using passed in
|
||||
// scale factor
|
||||
int Scale(int source, double scale_factor) {
|
||||
return static_cast<int>(source * scale_factor);
|
||||
}
|
||||
|
||||
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
|
||||
// This API is only needed for PerMonitor V1 awareness mode.
|
||||
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
|
||||
HMODULE user32_module = LoadLibraryA("User32.dll");
|
||||
if (!user32_module) {
|
||||
return;
|
||||
}
|
||||
auto enable_non_client_dpi_scaling =
|
||||
reinterpret_cast<EnableNonClientDpiScaling*>(
|
||||
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
|
||||
if (enable_non_client_dpi_scaling != nullptr) {
|
||||
enable_non_client_dpi_scaling(hwnd);
|
||||
}
|
||||
FreeLibrary(user32_module);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Manages the Win32Window's window class registration.
|
||||
class WindowClassRegistrar {
|
||||
public:
|
||||
~WindowClassRegistrar() = default;
|
||||
|
||||
// Returns the singleton registrar instance.
|
||||
static WindowClassRegistrar* GetInstance() {
|
||||
if (!instance_) {
|
||||
instance_ = new WindowClassRegistrar();
|
||||
}
|
||||
return instance_;
|
||||
}
|
||||
|
||||
// Returns the name of the window class, registering the class if it hasn't
|
||||
// previously been registered.
|
||||
const wchar_t* GetWindowClass();
|
||||
|
||||
// Unregisters the window class. Should only be called if there are no
|
||||
// instances of the window.
|
||||
void UnregisterWindowClass();
|
||||
|
||||
private:
|
||||
WindowClassRegistrar() = default;
|
||||
|
||||
static WindowClassRegistrar* instance_;
|
||||
|
||||
bool class_registered_ = false;
|
||||
};
|
||||
|
||||
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
|
||||
|
||||
const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||
if (!class_registered_) {
|
||||
WNDCLASS window_class{};
|
||||
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
window_class.lpszClassName = kWindowClassName;
|
||||
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
||||
window_class.cbClsExtra = 0;
|
||||
window_class.cbWndExtra = 0;
|
||||
window_class.hInstance = GetModuleHandle(nullptr);
|
||||
window_class.hIcon =
|
||||
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||
window_class.hbrBackground = 0;
|
||||
window_class.lpszMenuName = nullptr;
|
||||
window_class.lpfnWndProc = Win32Window::WndProc;
|
||||
RegisterClass(&window_class);
|
||||
class_registered_ = true;
|
||||
}
|
||||
return kWindowClassName;
|
||||
}
|
||||
|
||||
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||
UnregisterClass(kWindowClassName, nullptr);
|
||||
class_registered_ = false;
|
||||
}
|
||||
|
||||
Win32Window::Win32Window() {
|
||||
++g_active_window_count;
|
||||
}
|
||||
|
||||
Win32Window::~Win32Window() {
|
||||
--g_active_window_count;
|
||||
Destroy();
|
||||
}
|
||||
|
||||
bool Win32Window::Create(const std::wstring& title,
|
||||
const Point& origin,
|
||||
const Size& size) {
|
||||
Destroy();
|
||||
|
||||
const wchar_t* window_class =
|
||||
WindowClassRegistrar::GetInstance()->GetWindowClass();
|
||||
|
||||
const POINT target_point = {static_cast<LONG>(origin.x),
|
||||
static_cast<LONG>(origin.y)};
|
||||
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
|
||||
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
|
||||
double scale_factor = dpi / 96.0;
|
||||
|
||||
HWND window = CreateWindow(
|
||||
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
|
||||
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
|
||||
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
|
||||
nullptr, nullptr, GetModuleHandle(nullptr), this);
|
||||
|
||||
if (!window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateTheme(window);
|
||||
|
||||
return OnCreate();
|
||||
}
|
||||
|
||||
bool Win32Window::Show() {
|
||||
return ShowWindow(window_handle_, SW_SHOWNORMAL);
|
||||
}
|
||||
|
||||
// static
|
||||
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
if (message == WM_NCCREATE) {
|
||||
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
|
||||
SetWindowLongPtr(window, GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
||||
|
||||
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
||||
EnableFullDpiSupportIfAvailable(window);
|
||||
that->window_handle_ = window;
|
||||
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
||||
return that->MessageHandler(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
return DefWindowProc(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
LRESULT
|
||||
Win32Window::MessageHandler(HWND hwnd,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
switch (message) {
|
||||
case WM_DESTROY:
|
||||
window_handle_ = nullptr;
|
||||
Destroy();
|
||||
if (quit_on_close_) {
|
||||
PostQuitMessage(0);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DPICHANGED: {
|
||||
auto newRectSize = reinterpret_cast<RECT*>(lparam);
|
||||
LONG newWidth = newRectSize->right - newRectSize->left;
|
||||
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
||||
|
||||
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
|
||||
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
case WM_SIZE: {
|
||||
RECT rect = GetClientArea();
|
||||
if (child_content_ != nullptr) {
|
||||
// Size and position the child window.
|
||||
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
||||
rect.bottom - rect.top, TRUE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_ACTIVATE:
|
||||
if (child_content_ != nullptr) {
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DWMCOLORIZATIONCOLORCHANGED:
|
||||
UpdateTheme(hwnd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return DefWindowProc(window_handle_, message, wparam, lparam);
|
||||
}
|
||||
|
||||
void Win32Window::Destroy() {
|
||||
OnDestroy();
|
||||
|
||||
if (window_handle_) {
|
||||
DestroyWindow(window_handle_);
|
||||
window_handle_ = nullptr;
|
||||
}
|
||||
if (g_active_window_count == 0) {
|
||||
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
||||
}
|
||||
}
|
||||
|
||||
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
|
||||
return reinterpret_cast<Win32Window*>(
|
||||
GetWindowLongPtr(window, GWLP_USERDATA));
|
||||
}
|
||||
|
||||
void Win32Window::SetChildContent(HWND content) {
|
||||
child_content_ = content;
|
||||
SetParent(content, window_handle_);
|
||||
RECT frame = GetClientArea();
|
||||
|
||||
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
||||
frame.bottom - frame.top, true);
|
||||
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
|
||||
RECT Win32Window::GetClientArea() {
|
||||
RECT frame;
|
||||
GetClientRect(window_handle_, &frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
HWND Win32Window::GetHandle() {
|
||||
return window_handle_;
|
||||
}
|
||||
|
||||
void Win32Window::SetQuitOnClose(bool quit_on_close) {
|
||||
quit_on_close_ = quit_on_close;
|
||||
}
|
||||
|
||||
bool Win32Window::OnCreate() {
|
||||
// No-op; provided for subclasses.
|
||||
return true;
|
||||
}
|
||||
|
||||
void Win32Window::OnDestroy() {
|
||||
// No-op; provided for subclasses.
|
||||
}
|
||||
|
||||
void Win32Window::UpdateTheme(HWND const window) {
|
||||
DWORD light_mode;
|
||||
DWORD light_mode_size = sizeof(light_mode);
|
||||
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
|
||||
kGetPreferredBrightnessRegValue,
|
||||
RRF_RT_REG_DWORD, nullptr, &light_mode,
|
||||
&light_mode_size);
|
||||
|
||||
if (result == ERROR_SUCCESS) {
|
||||
BOOL enable_dark_mode = light_mode == 0;
|
||||
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
&enable_dark_mode, sizeof(enable_dark_mode));
|
||||
}
|
||||
}
|
||||
102
windows/runner/win32_window.h
Normal file
102
windows/runner/win32_window.h
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
#ifndef RUNNER_WIN32_WINDOW_H_
|
||||
#define RUNNER_WIN32_WINDOW_H_
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
|
||||
// inherited from by classes that wish to specialize with custom
|
||||
// rendering and input handling
|
||||
class Win32Window {
|
||||
public:
|
||||
struct Point {
|
||||
unsigned int x;
|
||||
unsigned int y;
|
||||
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
|
||||
};
|
||||
|
||||
struct Size {
|
||||
unsigned int width;
|
||||
unsigned int height;
|
||||
Size(unsigned int width, unsigned int height)
|
||||
: width(width), height(height) {}
|
||||
};
|
||||
|
||||
Win32Window();
|
||||
virtual ~Win32Window();
|
||||
|
||||
// Creates a win32 window with |title| that is positioned and sized using
|
||||
// |origin| and |size|. New windows are created on the default monitor. Window
|
||||
// sizes are specified to the OS in physical pixels, hence to ensure a
|
||||
// consistent size this function will scale the inputted width and height as
|
||||
// as appropriate for the default monitor. The window is invisible until
|
||||
// |Show| is called. Returns true if the window was created successfully.
|
||||
bool Create(const std::wstring& title, const Point& origin, const Size& size);
|
||||
|
||||
// Show the current window. Returns true if the window was successfully shown.
|
||||
bool Show();
|
||||
|
||||
// Release OS resources associated with window.
|
||||
void Destroy();
|
||||
|
||||
// Inserts |content| into the window tree.
|
||||
void SetChildContent(HWND content);
|
||||
|
||||
// Returns the backing Window handle to enable clients to set icon and other
|
||||
// window properties. Returns nullptr if the window has been destroyed.
|
||||
HWND GetHandle();
|
||||
|
||||
// If true, closing this window will quit the application.
|
||||
void SetQuitOnClose(bool quit_on_close);
|
||||
|
||||
// Return a RECT representing the bounds of the current client area.
|
||||
RECT GetClientArea();
|
||||
|
||||
protected:
|
||||
// Processes and route salient window messages for mouse handling,
|
||||
// size change and DPI. Delegates handling of these to member overloads that
|
||||
// inheriting classes can handle.
|
||||
virtual LRESULT MessageHandler(HWND window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept;
|
||||
|
||||
// Called when CreateAndShow is called, allowing subclass window-related
|
||||
// setup. Subclasses should return false if setup fails.
|
||||
virtual bool OnCreate();
|
||||
|
||||
// Called when Destroy is called.
|
||||
virtual void OnDestroy();
|
||||
|
||||
private:
|
||||
friend class WindowClassRegistrar;
|
||||
|
||||
// OS callback called by message pump. Handles the WM_NCCREATE message which
|
||||
// is passed when the non-client area is being created and enables automatic
|
||||
// non-client DPI scaling so that the non-client area automatically
|
||||
// responds to changes in DPI. All other messages are handled by
|
||||
// MessageHandler.
|
||||
static LRESULT CALLBACK WndProc(HWND const window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept;
|
||||
|
||||
// Retrieves a class instance pointer for |window|
|
||||
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
|
||||
|
||||
// Update the window frame's theme to match the system theme.
|
||||
static void UpdateTheme(HWND const window);
|
||||
|
||||
bool quit_on_close_ = false;
|
||||
|
||||
// window handle for top level window.
|
||||
HWND window_handle_ = nullptr;
|
||||
|
||||
// window handle for hosted content.
|
||||
HWND child_content_ = nullptr;
|
||||
};
|
||||
|
||||
#endif // RUNNER_WIN32_WINDOW_H_
|
||||
Loading…
Add table
Add a link
Reference in a new issue