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
|
- platform: root
|
||||||
create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||||
base_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
|
- platform: windows
|
||||||
create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
create_revision: 9ba0d08ebc074bf0da6dfd1fadea39f5c5566198
|
||||||
base_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:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.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/checkup_page.dart';
|
||||||
import 'package:tuuli_app/pages/home_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/login_page.dart';
|
||||||
import 'package:tuuli_app/pages/not_found_page.dart';
|
import 'package:tuuli_app/pages/not_found_page.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
await GetStorage.init();
|
await GetStorage.init();
|
||||||
|
|
||||||
Get.put(
|
Get.put(ApiController(), permanent: true);
|
||||||
ApiClient.fromString("http://127.0.0.1:8000"),
|
Get.lazyPut<CheckupPageController>(
|
||||||
permanent: true,
|
() => CheckupPageController(),
|
||||||
builder: () {
|
fenix: true,
|
||||||
final client = ApiClient.fromString("http://127.0.0.1:8000");
|
);
|
||||||
final accessToken = GetStorage().read<String>("accessToken");
|
Get.lazyPut<LoginPageController>(
|
||||||
if (accessToken != null) {
|
() => LoginPageController(),
|
||||||
client.setAccessToken(accessToken);
|
fenix: true,
|
||||||
}
|
);
|
||||||
return client;
|
Get.lazyPut<HomePageController>(
|
||||||
},
|
() => HomePageController(),
|
||||||
|
fenix: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
runApp(const MainApp());
|
runApp(const MainApp());
|
||||||
|
|
@ -34,7 +37,7 @@ class MainApp extends StatelessWidget {
|
||||||
return GetMaterialApp(
|
return GetMaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
debugShowMaterialGrid: false,
|
debugShowMaterialGrid: false,
|
||||||
initialRoute: "/login",
|
initialRoute: "/",
|
||||||
onGenerateRoute: _onGenerateRoute,
|
onGenerateRoute: _onGenerateRoute,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
brightness: Brightness.dark,
|
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,34 +1,41 @@
|
||||||
import 'package:animated_background/animated_background.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:tuuli_app/api_controller.dart';
|
||||||
import 'package:tuuli_app/api/api_client.dart';
|
|
||||||
|
|
||||||
class CheckupPage extends StatefulWidget {
|
class CheckupPageController extends GetxController {
|
||||||
const CheckupPage({super.key});
|
Future<void> checkCredentials() async {
|
||||||
|
if (ApiController.to.token.isEmpty) {
|
||||||
@override
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
State<StatefulWidget> createState() => _CheckupPageState();
|
ApiController.to.token = "";
|
||||||
}
|
Get.offAllNamed("/login");
|
||||||
|
});
|
||||||
class _CheckupPageState extends State<CheckupPage>
|
} else {
|
||||||
with TickerProviderStateMixin {
|
try {
|
||||||
@override
|
final resp = await ApiController.to.apiClient.listTables();
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
checkCredentials();
|
if (resp.statusCode == 200) {
|
||||||
|
Get.offAllNamed("/home");
|
||||||
|
} else {
|
||||||
|
Get.offAllNamed("/login");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
Get.offAllNamed("/login");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CheckupPage extends GetView<CheckupPageController> {
|
||||||
|
const CheckupPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: AnimatedBackground(
|
body: Center(
|
||||||
behaviour: RandomParticleBehaviour(),
|
|
||||||
vsync: this,
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -37,29 +44,16 @@ class _CheckupPageState extends State<CheckupPage>
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const SizedBox.square(
|
FutureBuilder(
|
||||||
|
future: controller.checkCredentials(),
|
||||||
|
builder: (ctx, _) => const SizedBox.square(
|
||||||
dimension: 32,
|
dimension: 32,
|
||||||
child: CircularProgressIndicator(),
|
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:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.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/api/model/tables_list_model.dart';
|
|
||||||
import 'package:tuuli_app/c.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/none_panel.dart';
|
||||||
import 'package:tuuli_app/pages/home_panels/settings_panel.dart';
|
import 'package:tuuli_app/pages/home_panels/settings_panel.dart';
|
||||||
import 'package:tuuli_app/pages/home_panels/tables_list_panel.dart';
|
import 'package:tuuli_app/pages/home_panels/tables_list_panel.dart';
|
||||||
|
|
@ -13,47 +13,65 @@ enum PageType {
|
||||||
none,
|
none,
|
||||||
tables,
|
tables,
|
||||||
users,
|
users,
|
||||||
|
assets,
|
||||||
settings,
|
settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
mixin HomePageStateRef {
|
class HomePageController extends GetxController {
|
||||||
void refreshData();
|
final _currentPage = PageType.none.obs;
|
||||||
}
|
PageType get currentPage => _currentPage.value;
|
||||||
|
set currentPage(PageType value) => _currentPage.value = value;
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
String get currentPageName => pageNames[currentPage]!;
|
||||||
const HomePage({super.key});
|
|
||||||
|
|
||||||
@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 = {
|
final pageNames = {
|
||||||
PageType.none: "Home",
|
PageType.none: "Home",
|
||||||
PageType.tables: "Tables",
|
PageType.tables: "Tables",
|
||||||
PageType.users: "Users",
|
PageType.users: "Users",
|
||||||
|
PageType.assets: "Assets",
|
||||||
PageType.settings: "Settings",
|
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(
|
AppBar get appBar => AppBar(
|
||||||
title: Text(pageNames[currentPage]!),
|
title: Obx(() => Text(controller.currentPageName)),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.home),
|
icon: const Icon(Icons.home),
|
||||||
|
|
@ -62,51 +80,59 @@ class _HomePageState extends State<HomePage> with HomePageStateRef {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.logout),
|
||||||
onPressed: refreshData,
|
onPressed: () => controller.logout(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
ListView get drawerOptions => ListView(
|
ListView get drawerOptions => ListView(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
Obx(
|
||||||
|
() => ListTile(
|
||||||
leading: const Icon(Icons.table_chart),
|
leading: const Icon(Icons.table_chart),
|
||||||
title: const Text("Tables"),
|
title: const Text("Tables"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
controller.currentPage = PageType.tables;
|
||||||
currentPage = PageType.tables;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
selected: controller.currentPage == PageType.tables,
|
||||||
),
|
),
|
||||||
ListTile(
|
),
|
||||||
|
Obx(
|
||||||
|
() => ListTile(
|
||||||
leading: const Icon(Icons.person),
|
leading: const Icon(Icons.person),
|
||||||
title: const Text("Users"),
|
title: const Text("Users"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
controller.currentPage = PageType.users;
|
||||||
currentPage = PageType.users;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
selected: controller.currentPage == PageType.users,
|
||||||
),
|
),
|
||||||
ListTile(
|
),
|
||||||
|
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),
|
leading: const Icon(Icons.settings),
|
||||||
title: const Text("Settings"),
|
title: const Text("Settings"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
controller.currentPage = PageType.settings;
|
||||||
currentPage = PageType.settings;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
selected: controller.currentPage == PageType.settings,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.logout),
|
leading: const Icon(Icons.logout),
|
||||||
title: const Text("Logout"),
|
title: const Text("Logout"),
|
||||||
onTap: () {
|
onTap: () => controller.logout(),
|
||||||
GetStorage().erase().then((value) {
|
|
||||||
GetStorage().save();
|
|
||||||
Get.offAllNamed("/");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -140,12 +166,14 @@ class _HomePageState extends State<HomePage> with HomePageStateRef {
|
||||||
),
|
),
|
||||||
LimitedBox(
|
LimitedBox(
|
||||||
maxWidth: MediaQuery.of(context).size.width - C.materialDrawerWidth,
|
maxWidth: MediaQuery.of(context).size.width - C.materialDrawerWidth,
|
||||||
child: Builder(builder: (context) {
|
child: Obx(() {
|
||||||
switch (currentPage) {
|
switch (controller.currentPage) {
|
||||||
case PageType.tables:
|
case PageType.tables:
|
||||||
return TablesListPanel(parent: this, tables: tables);
|
return const TablesListPanel();
|
||||||
case PageType.users:
|
case PageType.users:
|
||||||
return UsersListPanel(usersTable: usersTable);
|
return const UsersListPanel();
|
||||||
|
case PageType.assets:
|
||||||
|
return const AssetsPagePanel();
|
||||||
case PageType.settings:
|
case PageType.settings:
|
||||||
return const SettingsPanel();
|
return const SettingsPanel();
|
||||||
case PageType.none:
|
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,59 +1,201 @@
|
||||||
import 'package:bottom_sheet/bottom_sheet.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.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:recase/recase.dart';
|
||||||
import 'package:tuuli_app/api/api_client.dart';
|
import 'package:tuuli_app/models/db_column_definition.dart';
|
||||||
import 'package:tuuli_app/api/model/tables_list_model.dart';
|
import 'package:tuuli_app/pages/dialogs/create_table_dialog.dart';
|
||||||
import 'package:tuuli_app/c.dart';
|
import 'package:tuuli_app/pages/dialogs/open_table_dialog.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,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
class TablesListPanelController extends GetxController {
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _TablesListPanelState();
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
|
||||||
|
refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TablesListPanelState extends State<TablesListPanel> {
|
final _isLoading = false.obs;
|
||||||
final apiClient = Get.find<ApiClient>();
|
bool get isLoading => _isLoading.value;
|
||||||
|
|
||||||
@override
|
final _tables = <TableDefinition>[].obs;
|
||||||
void initState() {
|
List<TableDefinition> get tables => _tables;
|
||||||
super.initState();
|
|
||||||
|
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 TablesListPanel extends GetView<TablesListPanelController> {
|
||||||
|
const TablesListPanel({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return _buildTableList();
|
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 _buildTableCard(BuildContext ctx, TableModel table) {
|
Widget get whenNoTables => Center(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"No tables found",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Get.theme.disabledColor,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
"Maybe create one?",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Get.theme.disabledColor,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
return Card(
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => _openTable(table),
|
onTap: () => controller.openTable(table),
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.all(5),
|
margin: const EdgeInsets.all(5),
|
||||||
padding: const EdgeInsets.all(5),
|
padding: const EdgeInsets.all(5),
|
||||||
child: Stack(
|
child: Column(
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
table.tableName.pascalCase,
|
table.tableName.pascalCase,
|
||||||
|
maxLines: 1,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -69,133 +211,23 @@ class _TablesListPanelState extends State<TablesListPanel> {
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"${table.columns.length} column(s)",
|
"${table.parsedColumns.length} column(s)",
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
if (!table.system)
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => controller.deleteTable(table),
|
||||||
|
child: const Text("Delete"),
|
||||||
|
).paddingOnly(top: 8)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"No tables found",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).disabledColor,
|
|
||||||
fontSize: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
"Maybe create one?",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).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,
|
|
||||||
childAspectRatio: 5 / 2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,64 +1,133 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:animated_background/animated_background.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:tuuli_api/tuuli_api.dart';
|
||||||
import 'package:tuuli_app/api/api_client.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});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final screenSize = MediaQuery.of(context).size;
|
final screenSize = Get.mediaQuery.size;
|
||||||
final formWidth = screenSize.width <= 600 ? screenSize.width : 300.0;
|
final formWidth = screenSize.width <= 600 ? screenSize.width : 300.0;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
AnimatedBackground(
|
const SizedBox.square(
|
||||||
behaviour: RandomParticleBehaviour(),
|
|
||||||
vsync: this,
|
|
||||||
child: const SizedBox.square(
|
|
||||||
dimension: 0,
|
dimension: 0,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
LimitedBox(
|
LimitedBox(
|
||||||
maxWidth: formWidth,
|
maxWidth: formWidth,
|
||||||
child: Container(
|
child: Obx(
|
||||||
|
() => Container(
|
||||||
color: Colors.black.withAlpha(100),
|
color: Colors.black.withAlpha(100),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Form(
|
|
||||||
key: _formKey,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: loginController,
|
enabled: !controller.submitted,
|
||||||
enabled: !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(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Login',
|
labelText: 'Login',
|
||||||
hintText: 'Enter your login',
|
hintText: 'Enter your username',
|
||||||
),
|
),
|
||||||
|
onChanged: (value) => controller.username = value,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
return 'Please enter your Login';
|
return 'Please enter your Login';
|
||||||
|
|
@ -67,13 +136,13 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: passwordController,
|
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
enabled: !submitted,
|
enabled: !controller.submitted,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Password',
|
labelText: 'Password',
|
||||||
hintText: 'Enter your password',
|
hintText: 'Enter your password',
|
||||||
),
|
),
|
||||||
|
onChanged: (value) => controller.password = value,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
return 'Please enter your password';
|
return 'Please enter your password';
|
||||||
|
|
@ -83,7 +152,10 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: submitted ? null : _submit,
|
onPressed:
|
||||||
|
!controller.isFormValid || controller.submitted
|
||||||
|
? null
|
||||||
|
: () => controller.submitForm(),
|
||||||
child: const Text('Login'),
|
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:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
|
@ -14,16 +13,12 @@ class _NotFoundPageState extends State<NotFoundPage>
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: AnimatedBackground(
|
body: Center(
|
||||||
behaviour: RandomParticleBehaviour(),
|
|
||||||
vsync: this,
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
child: Text(
|
||||||
'Page not found',
|
'Page not found',
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
bottomSheet: Container(
|
bottomSheet: Container(
|
||||||
color: Colors.black.withAlpha(100),
|
color: Colors.black.withAlpha(100),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:tuuli_api/tuuli_api.dart';
|
||||||
|
import 'package:tuuli_app/models/user_definition.dart';
|
||||||
|
|
||||||
Random _random = Random();
|
Random _random = Random();
|
||||||
|
|
||||||
String randomHexString(int length) {
|
String randomHexString(int length) {
|
||||||
|
|
@ -9,3 +12,35 @@ String randomHexString(int length) {
|
||||||
}
|
}
|
||||||
return sb.toString();
|
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
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
animated_background:
|
_fe_analyzer_shared:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: animated_background
|
name: _fe_analyzer_shared
|
||||||
sha256: "24b05a6dca2cb0231b011f9e8fd2e9d8060faac08a78cf0643915bb7d6e9b03b"
|
sha256: "8880b4cfe7b5b17d57c052a5a3a8cc1d4f546261c7cc8fbd717bd53f48db0568"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -17,6 +33,62 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.10.0"
|
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:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -41,6 +113,78 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
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:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -49,6 +193,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
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:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -57,6 +209,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
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:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -65,6 +225,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.1"
|
version: "1.17.1"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -73,6 +241,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
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:
|
data_table_2:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -81,6 +257,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
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:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -105,11 +289,35 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.4"
|
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:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_fast_forms:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -131,6 +339,19 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
get:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -147,6 +368,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
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:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -155,8 +392,16 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.5"
|
version: "0.13.5"
|
||||||
http_parser:
|
http_multi_server:
|
||||||
dependency: transitive
|
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:
|
description:
|
||||||
name: http_parser
|
name: http_parser
|
||||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||||
|
|
@ -167,10 +412,34 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
|
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -179,6 +448,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.7"
|
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:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -187,6 +472,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -211,6 +504,46 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
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:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -231,18 +564,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7"
|
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.24"
|
version: "2.0.27"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_foundation
|
name: path_provider_foundation
|
||||||
sha256: "818b2dc38b0f178e0ea3f7cf3b28146faab11375985d815942a68eee11c2d0f7"
|
sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.2"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -263,10 +596,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_windows
|
name: path_provider_windows
|
||||||
sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130
|
sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -283,6 +624,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
pool:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pool
|
||||||
|
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.1"
|
||||||
process:
|
process:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -291,6 +640,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.4"
|
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:
|
recase:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -299,11 +672,91 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
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:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
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:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -328,6 +781,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
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:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -336,6 +797,46 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
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:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -352,6 +853,31 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.18"
|
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:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -361,7 +887,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: uuid
|
name: uuid
|
||||||
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
|
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
|
||||||
|
|
@ -376,14 +902,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
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:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
|
sha256: dd8f9344bc305ae2923e3d11a2a911d9a4e2c7dd6fe0ed10626d63211a69676e
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "4.1.3"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -392,6 +934,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.0.0-322.0.dev <4.0.0"
|
dart: ">=3.0.0-322.0.dev <4.0.0"
|
||||||
flutter: ">=3.7.0"
|
flutter: ">=3.7.0"
|
||||||
|
|
|
||||||
24
pubspec.yaml
24
pubspec.yaml
|
|
@ -10,19 +10,39 @@ dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
animated_background: ^2.0.0
|
audioplayers: ^4.0.1
|
||||||
bottom_sheet: ^3.1.2
|
bottom_sheet: ^3.1.2
|
||||||
|
built_collection: ^5.1.1
|
||||||
|
built_value: ^8.4.4
|
||||||
data_table_2: ^2.4.2
|
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
|
flutter_fast_forms: ^10.0.0
|
||||||
get: ^4.6.5
|
get: ^4.6.5
|
||||||
get_storage: ^2.1.1
|
get_storage: ^2.1.1
|
||||||
http: ^0.13.5
|
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
|
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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
build_runner: ^2.3.3
|
||||||
|
built_value_generator: ^8.4.4
|
||||||
flutter_lints: ^2.0.0
|
flutter_lints: ^2.0.0
|
||||||
|
|
||||||
flutter:
|
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