mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2024-11-10 19:03:12 +00:00
feat(services): Service settings
This commit is contained in:
parent
7dff880147
commit
ef51f27e91
|
@ -349,7 +349,13 @@
|
||||||
"activating": "Activating",
|
"activating": "Activating",
|
||||||
"deactivating": "Deactivating",
|
"deactivating": "Deactivating",
|
||||||
"reloading": "Restarting"
|
"reloading": "Restarting"
|
||||||
}
|
},
|
||||||
|
"settings": "Service settings",
|
||||||
|
"modified": "Modified",
|
||||||
|
"invalid_input": "Invalid input",
|
||||||
|
"create_job": "Create job",
|
||||||
|
"update_job": "Update job",
|
||||||
|
"wait_for_jobs": "Server is busy with other jobs. Please wait until they are finished."
|
||||||
},
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
"login_info": "Use username and password from users tab. IMAP port is 143 with STARTTLS, SMTP port is 587 with STARTTLS."
|
"login_info": "Use username and password from users tab. IMAP port is 143 with STARTTLS, SMTP port is 587 with STARTTLS."
|
||||||
|
@ -602,7 +608,8 @@
|
||||||
"change_ssh_settings": "Change SSH settings",
|
"change_ssh_settings": "Change SSH settings",
|
||||||
"update_dns_records": "Update DNS records",
|
"update_dns_records": "Update DNS records",
|
||||||
"dns_records_did_not_change": "No changes needed",
|
"dns_records_did_not_change": "No changes needed",
|
||||||
"dns_records_changed": "DNS records updated"
|
"dns_records_changed": "DNS records updated",
|
||||||
|
"change_service_settings": "Change service settings for {}"
|
||||||
},
|
},
|
||||||
"validations": {
|
"validations": {
|
||||||
"required": "Required",
|
"required": "Required",
|
||||||
|
|
|
@ -130,6 +130,22 @@ enum BackupReason {
|
||||||
PRE_RESTORE
|
PRE_RESTORE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BoolConfigItem implements ConfigItem {
|
||||||
|
id: String!
|
||||||
|
description: String!
|
||||||
|
widget: String!
|
||||||
|
type: String!
|
||||||
|
value: Boolean!
|
||||||
|
defaultValue: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConfigItem {
|
||||||
|
id: String!
|
||||||
|
description: String!
|
||||||
|
widget: String!
|
||||||
|
type: String!
|
||||||
|
}
|
||||||
|
|
||||||
"""Date with time (isoformat)"""
|
"""Date with time (isoformat)"""
|
||||||
scalar DateTime
|
scalar DateTime
|
||||||
|
|
||||||
|
@ -155,6 +171,16 @@ type DnsRecord {
|
||||||
displayName: String!
|
displayName: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EnumConfigItem implements ConfigItem {
|
||||||
|
id: String!
|
||||||
|
description: String!
|
||||||
|
widget: String!
|
||||||
|
type: String!
|
||||||
|
value: String!
|
||||||
|
defaultValue: String!
|
||||||
|
options: [String!]!
|
||||||
|
}
|
||||||
|
|
||||||
type GenericBackupConfigReturn implements MutationReturnInterface {
|
type GenericBackupConfigReturn implements MutationReturnInterface {
|
||||||
success: Boolean!
|
success: Boolean!
|
||||||
message: String!
|
message: String!
|
||||||
|
@ -183,6 +209,11 @@ input InitializeRepositoryInput {
|
||||||
password: String!
|
password: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
|
||||||
|
"""
|
||||||
|
scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf")
|
||||||
|
|
||||||
type Job {
|
type Job {
|
||||||
getJobs: [ApiJob!]!
|
getJobs: [ApiJob!]!
|
||||||
getJob(jobId: String!): ApiJob
|
getJob(jobId: String!): ApiJob
|
||||||
|
@ -192,6 +223,24 @@ type JobMutations {
|
||||||
removeJob(jobId: String!): GenericMutationReturn!
|
removeJob(jobId: String!): GenericMutationReturn!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LogEntry {
|
||||||
|
message: String!
|
||||||
|
timestamp: DateTime!
|
||||||
|
priority: Int
|
||||||
|
systemdUnit: String
|
||||||
|
systemdSlice: String
|
||||||
|
cursor: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Logs {
|
||||||
|
paginated(limit: Int! = 20, upCursor: String = null, downCursor: String = null): PaginatedEntries!
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogsPageMeta {
|
||||||
|
upCursor: String
|
||||||
|
downCursor: String
|
||||||
|
}
|
||||||
|
|
||||||
input MigrateToBindsInput {
|
input MigrateToBindsInput {
|
||||||
emailBlockDevice: String!
|
emailBlockDevice: String!
|
||||||
bitwardenBlockDevice: String!
|
bitwardenBlockDevice: String!
|
||||||
|
@ -252,9 +301,18 @@ interface MutationReturnInterface {
|
||||||
code: Int!
|
code: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PaginatedEntries {
|
||||||
|
"""Metadata to aid in pagination."""
|
||||||
|
pageMeta: LogsPageMeta!
|
||||||
|
|
||||||
|
"""The list of log entries."""
|
||||||
|
entries: [LogEntry!]!
|
||||||
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
api: Api!
|
api: Api!
|
||||||
system: System!
|
system: System!
|
||||||
|
logs: Logs!
|
||||||
users: Users!
|
users: Users!
|
||||||
storage: Storage!
|
storage: Storage!
|
||||||
jobs: Job!
|
jobs: Job!
|
||||||
|
@ -305,6 +363,7 @@ type Service {
|
||||||
url: String
|
url: String
|
||||||
dnsRecords: [DnsRecord!]
|
dnsRecords: [DnsRecord!]
|
||||||
storageUsage: ServiceStorageUsage!
|
storageUsage: ServiceStorageUsage!
|
||||||
|
configuration: [ConfigItem!]
|
||||||
backupSnapshots: [SnapshotInfo!]
|
backupSnapshots: [SnapshotInfo!]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,9 +409,15 @@ type ServicesMutations {
|
||||||
stopService(serviceId: String!): ServiceMutationReturn!
|
stopService(serviceId: String!): ServiceMutationReturn!
|
||||||
startService(serviceId: String!): ServiceMutationReturn!
|
startService(serviceId: String!): ServiceMutationReturn!
|
||||||
restartService(serviceId: String!): ServiceMutationReturn!
|
restartService(serviceId: String!): ServiceMutationReturn!
|
||||||
|
setServiceConfiguration(input: SetServiceConfigurationInput!): ServiceMutationReturn!
|
||||||
moveService(input: MoveServiceInput!): ServiceJobMutationReturn!
|
moveService(input: MoveServiceInput!): ServiceJobMutationReturn!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input SetServiceConfigurationInput {
|
||||||
|
serviceId: String!
|
||||||
|
configuration: JSON!
|
||||||
|
}
|
||||||
|
|
||||||
enum Severity {
|
enum Severity {
|
||||||
INFO
|
INFO
|
||||||
WARNING
|
WARNING
|
||||||
|
@ -408,9 +473,20 @@ type StorageVolume {
|
||||||
usages: [StorageUsageInterface!]!
|
usages: [StorageUsageInterface!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StringConfigItem implements ConfigItem {
|
||||||
|
id: String!
|
||||||
|
description: String!
|
||||||
|
widget: String!
|
||||||
|
type: String!
|
||||||
|
value: String!
|
||||||
|
defaultValue: String!
|
||||||
|
regex: String
|
||||||
|
}
|
||||||
|
|
||||||
type Subscription {
|
type Subscription {
|
||||||
jobUpdates: [ApiJob!]!
|
jobUpdates: [ApiJob!]!
|
||||||
count: Int!
|
count: Int!
|
||||||
|
logEntries: LogEntry!
|
||||||
}
|
}
|
||||||
|
|
||||||
type System {
|
type System {
|
||||||
|
|
|
@ -1111,6 +1111,138 @@ class _CopyWithStubImpl$Input$SSHSettingsInput<TRes>
|
||||||
_res;
|
_res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Input$SetServiceConfigurationInput {
|
||||||
|
factory Input$SetServiceConfigurationInput({
|
||||||
|
required String serviceId,
|
||||||
|
required Map<String, dynamic> configuration,
|
||||||
|
}) =>
|
||||||
|
Input$SetServiceConfigurationInput._({
|
||||||
|
r'serviceId': serviceId,
|
||||||
|
r'configuration': configuration,
|
||||||
|
});
|
||||||
|
|
||||||
|
Input$SetServiceConfigurationInput._(this._$data);
|
||||||
|
|
||||||
|
factory Input$SetServiceConfigurationInput.fromJson(
|
||||||
|
Map<String, dynamic> data) {
|
||||||
|
final result$data = <String, dynamic>{};
|
||||||
|
final l$serviceId = data['serviceId'];
|
||||||
|
result$data['serviceId'] = (l$serviceId as String);
|
||||||
|
final l$configuration = data['configuration'];
|
||||||
|
result$data['configuration'] = (l$configuration as Map<String, dynamic>);
|
||||||
|
return Input$SetServiceConfigurationInput._(result$data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _$data;
|
||||||
|
|
||||||
|
String get serviceId => (_$data['serviceId'] as String);
|
||||||
|
|
||||||
|
Map<String, dynamic> get configuration =>
|
||||||
|
(_$data['configuration'] as Map<String, dynamic>);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final result$data = <String, dynamic>{};
|
||||||
|
final l$serviceId = serviceId;
|
||||||
|
result$data['serviceId'] = l$serviceId;
|
||||||
|
final l$configuration = configuration;
|
||||||
|
result$data['configuration'] = l$configuration;
|
||||||
|
return result$data;
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyWith$Input$SetServiceConfigurationInput<
|
||||||
|
Input$SetServiceConfigurationInput>
|
||||||
|
get copyWith => CopyWith$Input$SetServiceConfigurationInput(
|
||||||
|
this,
|
||||||
|
(i) => i,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(other is Input$SetServiceConfigurationInput) ||
|
||||||
|
runtimeType != other.runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final l$serviceId = serviceId;
|
||||||
|
final lOther$serviceId = other.serviceId;
|
||||||
|
if (l$serviceId != lOther$serviceId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final l$configuration = configuration;
|
||||||
|
final lOther$configuration = other.configuration;
|
||||||
|
if (l$configuration != lOther$configuration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
final l$serviceId = serviceId;
|
||||||
|
final l$configuration = configuration;
|
||||||
|
return Object.hashAll([
|
||||||
|
l$serviceId,
|
||||||
|
l$configuration,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class CopyWith$Input$SetServiceConfigurationInput<TRes> {
|
||||||
|
factory CopyWith$Input$SetServiceConfigurationInput(
|
||||||
|
Input$SetServiceConfigurationInput instance,
|
||||||
|
TRes Function(Input$SetServiceConfigurationInput) then,
|
||||||
|
) = _CopyWithImpl$Input$SetServiceConfigurationInput;
|
||||||
|
|
||||||
|
factory CopyWith$Input$SetServiceConfigurationInput.stub(TRes res) =
|
||||||
|
_CopyWithStubImpl$Input$SetServiceConfigurationInput;
|
||||||
|
|
||||||
|
TRes call({
|
||||||
|
String? serviceId,
|
||||||
|
Map<String, dynamic>? configuration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CopyWithImpl$Input$SetServiceConfigurationInput<TRes>
|
||||||
|
implements CopyWith$Input$SetServiceConfigurationInput<TRes> {
|
||||||
|
_CopyWithImpl$Input$SetServiceConfigurationInput(
|
||||||
|
this._instance,
|
||||||
|
this._then,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Input$SetServiceConfigurationInput _instance;
|
||||||
|
|
||||||
|
final TRes Function(Input$SetServiceConfigurationInput) _then;
|
||||||
|
|
||||||
|
static const _undefined = <dynamic, dynamic>{};
|
||||||
|
|
||||||
|
TRes call({
|
||||||
|
Object? serviceId = _undefined,
|
||||||
|
Object? configuration = _undefined,
|
||||||
|
}) =>
|
||||||
|
_then(Input$SetServiceConfigurationInput._({
|
||||||
|
..._instance._$data,
|
||||||
|
if (serviceId != _undefined && serviceId != null)
|
||||||
|
'serviceId': (serviceId as String),
|
||||||
|
if (configuration != _undefined && configuration != null)
|
||||||
|
'configuration': (configuration as Map<String, dynamic>),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CopyWithStubImpl$Input$SetServiceConfigurationInput<TRes>
|
||||||
|
implements CopyWith$Input$SetServiceConfigurationInput<TRes> {
|
||||||
|
_CopyWithStubImpl$Input$SetServiceConfigurationInput(this._res);
|
||||||
|
|
||||||
|
TRes _res;
|
||||||
|
|
||||||
|
call({
|
||||||
|
String? serviceId,
|
||||||
|
Map<String, dynamic>? configuration,
|
||||||
|
}) =>
|
||||||
|
_res;
|
||||||
|
}
|
||||||
|
|
||||||
class Input$SshMutationInput {
|
class Input$SshMutationInput {
|
||||||
factory Input$SshMutationInput({
|
factory Input$SshMutationInput({
|
||||||
required String username,
|
required String username,
|
||||||
|
@ -2152,5 +2284,10 @@ const possibleTypesMap = <String, Set<String>>{
|
||||||
'TimezoneMutationReturn',
|
'TimezoneMutationReturn',
|
||||||
'UserMutationReturn',
|
'UserMutationReturn',
|
||||||
},
|
},
|
||||||
|
'ConfigItem': {
|
||||||
|
'BoolConfigItem',
|
||||||
|
'EnumConfigItem',
|
||||||
|
'StringConfigItem',
|
||||||
|
},
|
||||||
'StorageUsageInterface': {'ServiceStorageUsage'},
|
'StorageUsageInterface': {'ServiceStorageUsage'},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,32 @@
|
||||||
|
fragment BoolConfigItem on BoolConfigItem {
|
||||||
|
id
|
||||||
|
description
|
||||||
|
type
|
||||||
|
boolValue: value
|
||||||
|
defaultBoolValue: defaultValue
|
||||||
|
widget
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment EnumConfigItem on EnumConfigItem {
|
||||||
|
id
|
||||||
|
description
|
||||||
|
type
|
||||||
|
stringValue: value
|
||||||
|
defaultStringValue: defaultValue
|
||||||
|
options
|
||||||
|
widget
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment StringConfigItem on StringConfigItem {
|
||||||
|
id
|
||||||
|
description
|
||||||
|
type
|
||||||
|
stringValue: value
|
||||||
|
defaultStringValue: defaultValue
|
||||||
|
regex
|
||||||
|
widget
|
||||||
|
}
|
||||||
|
|
||||||
query AllServices {
|
query AllServices {
|
||||||
services {
|
services {
|
||||||
allServices {
|
allServices {
|
||||||
|
@ -22,6 +51,14 @@ query AllServices {
|
||||||
}
|
}
|
||||||
svgIcon
|
svgIcon
|
||||||
url
|
url
|
||||||
|
configuration {
|
||||||
|
id
|
||||||
|
description
|
||||||
|
type
|
||||||
|
... BoolConfigItem
|
||||||
|
... EnumConfigItem
|
||||||
|
... StringConfigItem
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,3 +114,10 @@ mutation MoveService($input: MoveServiceInput!) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutation SetServiceConfiguration($input: SetServiceConfigurationInput!) {
|
||||||
|
services {
|
||||||
|
setServiceConfiguration(input: $input) {
|
||||||
|
...basicMutationReturnFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -174,4 +174,38 @@ mixin ServicesApi on GraphQLApiMap {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<GenericResult> setServiceConfiguration(
|
||||||
|
final String serviceId,
|
||||||
|
final Map<String, dynamic> settings,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final GraphQLClient client = await getClient();
|
||||||
|
final variables = Variables$Mutation$SetServiceConfiguration(
|
||||||
|
input: Input$SetServiceConfigurationInput(
|
||||||
|
serviceId: serviceId,
|
||||||
|
configuration: settings,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final mutation =
|
||||||
|
Options$Mutation$SetServiceConfiguration(variables: variables);
|
||||||
|
final response = await client.mutate$SetServiceConfiguration(mutation);
|
||||||
|
return GenericResult(
|
||||||
|
data: null,
|
||||||
|
success:
|
||||||
|
response.parsedData?.services.setServiceConfiguration.success ??
|
||||||
|
false,
|
||||||
|
code: response.parsedData?.services.setServiceConfiguration.code ?? 0,
|
||||||
|
message: response.parsedData?.services.setServiceConfiguration.message,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
return GenericResult(
|
||||||
|
data: null,
|
||||||
|
success: false,
|
||||||
|
code: 0,
|
||||||
|
message: e.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,11 @@ class JobsStateWithJobs extends JobsState {
|
||||||
JobsState addJob(final ClientJob job) {
|
JobsState addJob(final ClientJob job) {
|
||||||
if (job is ReplaceableJob) {
|
if (job is ReplaceableJob) {
|
||||||
final List<ClientJob> newJobsList = clientJobList
|
final List<ClientJob> newJobsList = clientJobList
|
||||||
.where((final element) => element.runtimeType != job.runtimeType)
|
.where(
|
||||||
|
(final element) => job.shouldReplaceOnlyIfSameId
|
||||||
|
? element.runtimeType != job.runtimeType || element.id != job.id
|
||||||
|
: element.runtimeType != job.runtimeType,
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
if (job.shouldRemoveInsteadOfAdd(clientJobList)) {
|
if (job.shouldRemoveInsteadOfAdd(clientJobList)) {
|
||||||
getIt<NavigationService>().showSnackBar('jobs.job_removed'.tr());
|
getIt<NavigationService>().showSnackBar('jobs.job_removed'.tr());
|
||||||
|
|
|
@ -242,6 +242,19 @@ class ApiConnectionRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<(bool, String)> setServiceConfiguration(
|
||||||
|
final String serviceId,
|
||||||
|
final Map<String, dynamic> settings,
|
||||||
|
) async {
|
||||||
|
final GenericResult result =
|
||||||
|
await api.setServiceConfiguration(serviceId, settings);
|
||||||
|
if (result.success) {
|
||||||
|
return (true, result.message ?? 'basis.done'.tr());
|
||||||
|
} else {
|
||||||
|
return (false, result.message ?? 'jobs.generic_error'.tr());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_dataStream.close();
|
_dataStream.close();
|
||||||
_connectionStatusStream.close();
|
_connectionStatusStream.close();
|
||||||
|
|
|
@ -367,9 +367,12 @@ abstract class ReplaceableJob extends ClientJob {
|
||||||
super.id,
|
super.id,
|
||||||
super.status,
|
super.status,
|
||||||
super.message,
|
super.message,
|
||||||
|
super.requiresRebuild,
|
||||||
|
super.requiresDnsUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool shouldRemoveInsteadOfAdd(final List<ClientJob> jobs) => false;
|
bool shouldRemoveInsteadOfAdd(final List<ClientJob> jobs) => false;
|
||||||
|
bool get shouldReplaceOnlyIfSameId => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChangeAutoUpgradeSettingsJob extends ReplaceableJob {
|
class ChangeAutoUpgradeSettingsJob extends ReplaceableJob {
|
||||||
|
@ -502,3 +505,45 @@ class ChangeSshSettingsJob extends ReplaceableJob {
|
||||||
id: id,
|
id: id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ChangeServiceConfiguration extends ReplaceableJob {
|
||||||
|
ChangeServiceConfiguration({
|
||||||
|
required this.serviceId,
|
||||||
|
required this.serviceDisplayName,
|
||||||
|
required this.settings,
|
||||||
|
super.status,
|
||||||
|
super.message,
|
||||||
|
}) : super(
|
||||||
|
title: 'jobs.change_service_settings'.tr(args: [serviceDisplayName]),
|
||||||
|
id: 'change_settings_$serviceId',
|
||||||
|
requiresDnsUpdate: true,
|
||||||
|
requiresRebuild: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final String serviceId;
|
||||||
|
final String serviceDisplayName;
|
||||||
|
final Map<String, dynamic> settings;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get shouldReplaceOnlyIfSameId => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<(bool, String)> execute() async => getIt<ApiConnectionRepository>()
|
||||||
|
.setServiceConfiguration(serviceId, settings);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [...super.props, serviceId, settings];
|
||||||
|
|
||||||
|
@override
|
||||||
|
ChangeServiceConfiguration copyWithNewStatus({
|
||||||
|
required final JobStatusEnum status,
|
||||||
|
final String? message,
|
||||||
|
}) =>
|
||||||
|
ChangeServiceConfiguration(
|
||||||
|
serviceId: serviceId,
|
||||||
|
serviceDisplayName: serviceDisplayName,
|
||||||
|
settings: settings,
|
||||||
|
status: status,
|
||||||
|
message: message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -36,6 +36,16 @@ class Service extends Equatable {
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[],
|
[],
|
||||||
url: service.url,
|
url: service.url,
|
||||||
|
configuration: service.configuration
|
||||||
|
?.map(
|
||||||
|
(
|
||||||
|
final Query$AllServices$services$allServices$configuration
|
||||||
|
configItem,
|
||||||
|
) =>
|
||||||
|
ServiceConfigItem.fromGraphQL(configItem),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
const Service({
|
const Service({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
@ -50,6 +60,7 @@ class Service extends Equatable {
|
||||||
required this.storageUsage,
|
required this.storageUsage,
|
||||||
required this.svgIcon,
|
required this.svgIcon,
|
||||||
required this.dnsRecords,
|
required this.dnsRecords,
|
||||||
|
required this.configuration,
|
||||||
this.url,
|
this.url,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -89,6 +100,7 @@ class Service extends Equatable {
|
||||||
svgIcon: '',
|
svgIcon: '',
|
||||||
dnsRecords: [],
|
dnsRecords: [],
|
||||||
url: '',
|
url: '',
|
||||||
|
configuration: [],
|
||||||
);
|
);
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
|
@ -104,6 +116,7 @@ class Service extends Equatable {
|
||||||
final String svgIcon;
|
final String svgIcon;
|
||||||
final String? url;
|
final String? url;
|
||||||
final List<DnsRecord> dnsRecords;
|
final List<DnsRecord> dnsRecords;
|
||||||
|
final List<ServiceConfigItem> configuration;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
|
@ -120,6 +133,7 @@ class Service extends Equatable {
|
||||||
svgIcon,
|
svgIcon,
|
||||||
dnsRecords,
|
dnsRecords,
|
||||||
url,
|
url,
|
||||||
|
configuration,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,3 +180,146 @@ enum ServiceStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class ServiceConfigItem extends Equatable {
|
||||||
|
const ServiceConfigItem({
|
||||||
|
required this.id,
|
||||||
|
required this.description,
|
||||||
|
required this.widget,
|
||||||
|
required this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ServiceConfigItem.fromGraphQL(
|
||||||
|
final Query$AllServices$services$allServices$configuration configItem,
|
||||||
|
) =>
|
||||||
|
configItem.when<ServiceConfigItem>(
|
||||||
|
boolConfigItem: (final boolConfigItem) => BoolServiceConfigItem(
|
||||||
|
id: boolConfigItem.id,
|
||||||
|
description: boolConfigItem.description,
|
||||||
|
widget: boolConfigItem.widget,
|
||||||
|
type: boolConfigItem.type,
|
||||||
|
value: boolConfigItem.boolValue,
|
||||||
|
defaultValue: boolConfigItem.defaultBoolValue,
|
||||||
|
),
|
||||||
|
enumConfigItem: (final enumConfigItem) => EnumServiceConfigItem(
|
||||||
|
id: enumConfigItem.id,
|
||||||
|
description: enumConfigItem.description,
|
||||||
|
widget: enumConfigItem.widget,
|
||||||
|
type: enumConfigItem.type,
|
||||||
|
value: enumConfigItem.stringValue,
|
||||||
|
defaultValue: enumConfigItem.defaultStringValue,
|
||||||
|
options: enumConfigItem.options,
|
||||||
|
),
|
||||||
|
stringConfigItem: (final stringConfigItem) => StringServiceConfigItem(
|
||||||
|
id: stringConfigItem.id,
|
||||||
|
description: stringConfigItem.description,
|
||||||
|
widget: stringConfigItem.widget,
|
||||||
|
type: stringConfigItem.type,
|
||||||
|
value: stringConfigItem.stringValue,
|
||||||
|
defaultValue: stringConfigItem.defaultStringValue,
|
||||||
|
regex: stringConfigItem.regex,
|
||||||
|
),
|
||||||
|
orElse: () => FallbackServiceConfigItem(
|
||||||
|
id: configItem.id,
|
||||||
|
description: configItem.description,
|
||||||
|
type: configItem.type,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String description;
|
||||||
|
final String widget;
|
||||||
|
final String type;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringServiceConfigItem extends ServiceConfigItem {
|
||||||
|
const StringServiceConfigItem({
|
||||||
|
required super.id,
|
||||||
|
required super.description,
|
||||||
|
required super.widget,
|
||||||
|
required super.type,
|
||||||
|
required this.value,
|
||||||
|
required this.defaultValue,
|
||||||
|
this.regex,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
final String defaultValue;
|
||||||
|
final String? regex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props =>
|
||||||
|
[id, description, widget, type, value, defaultValue, regex];
|
||||||
|
}
|
||||||
|
|
||||||
|
class BoolServiceConfigItem extends ServiceConfigItem {
|
||||||
|
const BoolServiceConfigItem({
|
||||||
|
required super.id,
|
||||||
|
required super.description,
|
||||||
|
required super.widget,
|
||||||
|
required super.type,
|
||||||
|
required this.value,
|
||||||
|
required this.defaultValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool value;
|
||||||
|
final bool defaultValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props =>
|
||||||
|
[id, description, widget, type, value, defaultValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
class EnumServiceConfigItem extends ServiceConfigItem {
|
||||||
|
const EnumServiceConfigItem({
|
||||||
|
required super.id,
|
||||||
|
required super.description,
|
||||||
|
required super.widget,
|
||||||
|
required super.type,
|
||||||
|
required this.value,
|
||||||
|
required this.defaultValue,
|
||||||
|
required this.options,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
final String defaultValue;
|
||||||
|
final List<String> options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props =>
|
||||||
|
[id, description, widget, type, value, defaultValue, options];
|
||||||
|
}
|
||||||
|
|
||||||
|
class FallbackServiceConfigItem extends ServiceConfigItem {
|
||||||
|
const FallbackServiceConfigItem({
|
||||||
|
required super.id,
|
||||||
|
required super.description,
|
||||||
|
required super.type,
|
||||||
|
}) : super(widget: 'fallback');
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [id, description, widget, type];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Not used yet by the API
|
||||||
|
class IntServiceConfigItem extends ServiceConfigItem {
|
||||||
|
const IntServiceConfigItem({
|
||||||
|
required super.id,
|
||||||
|
required super.description,
|
||||||
|
required super.widget,
|
||||||
|
required super.type,
|
||||||
|
required this.value,
|
||||||
|
required this.defaultValue,
|
||||||
|
required this.min,
|
||||||
|
required this.max,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int value;
|
||||||
|
final int defaultValue;
|
||||||
|
final int min;
|
||||||
|
final int max;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props =>
|
||||||
|
[id, description, widget, type, value, defaultValue, min, max];
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
part of '../service_settings_page.dart';
|
||||||
|
|
||||||
|
class BasicBoolConfigItem extends StatefulWidget {
|
||||||
|
const BasicBoolConfigItem({
|
||||||
|
required this.configItem,
|
||||||
|
required this.onChanged,
|
||||||
|
this.newValue,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BoolServiceConfigItem configItem;
|
||||||
|
final Function(bool) onChanged;
|
||||||
|
final bool? newValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BasicBoolConfigItem> createState() => _BasicBoolConfigItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BasicBoolConfigItemState extends State<BasicBoolConfigItem> {
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => Column(
|
||||||
|
children: [
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
title: Text(widget.configItem.description),
|
||||||
|
subtitle: (widget.newValue != null &&
|
||||||
|
widget.newValue != widget.configItem.value)
|
||||||
|
? Text('service_page.modified'.tr())
|
||||||
|
: null,
|
||||||
|
value: widget.newValue ?? widget.configItem.value,
|
||||||
|
onChanged: (final bool value) {
|
||||||
|
widget.onChanged(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
part of '../service_settings_page.dart';
|
||||||
|
|
||||||
|
class BasicEnumConfigItem extends StatefulWidget {
|
||||||
|
const BasicEnumConfigItem({
|
||||||
|
required this.configItem,
|
||||||
|
required this.onChanged,
|
||||||
|
this.newValue,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EnumServiceConfigItem configItem;
|
||||||
|
final Function(String) onChanged;
|
||||||
|
final String? newValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BasicEnumConfigItem> createState() => _BasicEnumConfigItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BasicEnumConfigItemState extends State<BasicEnumConfigItem> {
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text(widget.configItem.description),
|
||||||
|
subtitle: (widget.newValue != null &&
|
||||||
|
widget.newValue != widget.configItem.value)
|
||||||
|
? Text('service_page.modified'.tr())
|
||||||
|
: null,
|
||||||
|
trailing: DropdownButton<String>(
|
||||||
|
value: widget.newValue ?? widget.configItem.value,
|
||||||
|
items: widget.configItem.options
|
||||||
|
.map<DropdownMenuItem<String>>(
|
||||||
|
(final String option) => DropdownMenuItem<String>(
|
||||||
|
value: option,
|
||||||
|
child: Text(option),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (final String? value) {
|
||||||
|
widget.onChanged(value!);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
part of '../service_settings_page.dart';
|
||||||
|
|
||||||
|
class BasicStringConfigItem extends StatefulWidget {
|
||||||
|
const BasicStringConfigItem({
|
||||||
|
required this.configItem,
|
||||||
|
required this.onChanged,
|
||||||
|
this.newValue,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final StringServiceConfigItem configItem;
|
||||||
|
final Function(String, bool) onChanged;
|
||||||
|
final String? newValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BasicStringConfigItem> createState() => _BasicStringConfigItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BasicStringConfigItemState extends State<BasicStringConfigItem> {
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
bool _isValid = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller.text = widget.newValue ?? widget.configItem.value;
|
||||||
|
_controller.addListener(() {
|
||||||
|
final String value = _controller.text;
|
||||||
|
final bool isValid = _validateInput(value);
|
||||||
|
if (isValid) {
|
||||||
|
widget.onChanged(value, isValid);
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
widget.onChanged(widget.newValue ?? widget.configItem.value, isValid);
|
||||||
|
_isValid = isValid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _validateInput(final String value) {
|
||||||
|
if (value == '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final regexPattern = widget.configItem.regex;
|
||||||
|
if (regexPattern == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final regex = RegExp(regexPattern);
|
||||||
|
return regex.hasMatch(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => Column(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: widget.configItem.description,
|
||||||
|
hintText: widget.configItem.value,
|
||||||
|
counter: _controller.text != widget.configItem.value
|
||||||
|
? InkWell(
|
||||||
|
onTap: () {
|
||||||
|
_controller.text = widget.configItem.value;
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'service_page.modified'.tr(),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelSmall
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8.0),
|
||||||
|
Icon(
|
||||||
|
Icons.undo_outlined,
|
||||||
|
size: 16.0,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
' ',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: _isValid ? null : 'service_page.invalid_input'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
part of '../service_settings_page.dart';
|
||||||
|
|
||||||
|
class DomainStringConfigItem extends StatefulWidget {
|
||||||
|
const DomainStringConfigItem({
|
||||||
|
required this.configItem,
|
||||||
|
required this.onChanged,
|
||||||
|
this.newValue,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final StringServiceConfigItem configItem;
|
||||||
|
final Function(String, bool) onChanged;
|
||||||
|
final String? newValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DomainStringConfigItem> createState() => _DomainStringConfigItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DomainStringConfigItemState extends State<DomainStringConfigItem> {
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
bool _isValid = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller.text = widget.newValue ?? widget.configItem.value;
|
||||||
|
_controller.addListener(() {
|
||||||
|
final String value = _controller.text;
|
||||||
|
final bool isValid = _validateInput(value);
|
||||||
|
if (isValid) {
|
||||||
|
setState(() {
|
||||||
|
widget.onChanged(value, isValid);
|
||||||
|
_isValid = isValid;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
widget.onChanged(widget.newValue ?? widget.configItem.value, isValid);
|
||||||
|
_isValid = isValid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _validateInput(final String value) {
|
||||||
|
if (value == '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final regexPattern = widget.configItem.regex;
|
||||||
|
if (regexPattern == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final regex = RegExp(regexPattern);
|
||||||
|
return regex.hasMatch(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) {
|
||||||
|
final String domain =
|
||||||
|
getIt<ResourcesModel>().serverDomain?.domainName ?? '';
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: widget.configItem.description,
|
||||||
|
hintText: widget.configItem.value,
|
||||||
|
suffixText: '.$domain',
|
||||||
|
counter: _controller.text != widget.configItem.value
|
||||||
|
? InkWell(
|
||||||
|
onTap: () {
|
||||||
|
_controller.text = widget.configItem.value;
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'service_page.modified'.tr(),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelSmall
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8.0),
|
||||||
|
Icon(
|
||||||
|
Icons.undo_outlined,
|
||||||
|
size: 16.0,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
' ',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: _isValid ? null : 'service_page.invalid_input'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -96,23 +96,36 @@ class _ServicePageState extends State<ServicePage> {
|
||||||
),
|
),
|
||||||
enabled: !serviceDisabled && !serviceLocked,
|
enabled: !serviceDisabled && !serviceLocked,
|
||||||
),
|
),
|
||||||
ListTile(
|
if (!service.isRequired)
|
||||||
iconColor: Theme.of(context).colorScheme.onBackground,
|
ListTile(
|
||||||
onTap: () => context.read<JobsCubit>().addJob(
|
iconColor: Theme.of(context).colorScheme.onBackground,
|
||||||
ServiceToggleJob(
|
onTap: () => context.read<JobsCubit>().addJob(
|
||||||
service: service,
|
ServiceToggleJob(
|
||||||
needToTurnOn: serviceDisabled,
|
service: service,
|
||||||
|
needToTurnOn: serviceDisabled,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
leading: const Icon(Icons.power_settings_new),
|
||||||
leading: const Icon(Icons.power_settings_new),
|
title: Text(
|
||||||
title: Text(
|
serviceDisabled
|
||||||
serviceDisabled
|
? 'service_page.enable'.tr()
|
||||||
? 'service_page.enable'.tr()
|
: 'service_page.disable'.tr(),
|
||||||
: 'service_page.disable'.tr(),
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
),
|
||||||
|
enabled: !serviceLocked,
|
||||||
|
),
|
||||||
|
if (service.configuration.isNotEmpty)
|
||||||
|
ListTile(
|
||||||
|
iconColor: Theme.of(context).colorScheme.onBackground,
|
||||||
|
onTap: () => context.pushRoute(
|
||||||
|
ServiceSettingsRoute(serviceId: service.id),
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.settings_outlined),
|
||||||
|
title: Text(
|
||||||
|
'service_page.settings'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
enabled: !serviceLocked,
|
|
||||||
),
|
|
||||||
if (service.isMovable)
|
if (service.isMovable)
|
||||||
ListTile(
|
ListTile(
|
||||||
iconColor: Theme.of(context).colorScheme.onBackground,
|
iconColor: Theme.of(context).colorScheme.onBackground,
|
||||||
|
|
238
lib/ui/pages/services/service_settings_page.dart
Normal file
238
lib/ui/pages/services/service_settings_page.dart
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:selfprivacy/config/get_it_config.dart';
|
||||||
|
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/get_it/resources_model.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/job.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/service.dart';
|
||||||
|
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
||||||
|
|
||||||
|
part 'config_item_fields/basic_string_config_item.dart';
|
||||||
|
part 'config_item_fields/basic_bool_config_item.dart';
|
||||||
|
part 'config_item_fields/basic_enum_config_item.dart';
|
||||||
|
part 'config_item_fields/domain_string_config_item.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class ServiceSettingsPage extends StatefulWidget {
|
||||||
|
const ServiceSettingsPage({required this.serviceId, super.key});
|
||||||
|
|
||||||
|
final String serviceId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ServiceSettingsPage> createState() => _ServiceSettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ServiceSettingsPageState extends State<ServiceSettingsPage> {
|
||||||
|
Map<String, dynamic> settings = {};
|
||||||
|
bool isFormValid = true;
|
||||||
|
bool isJobAlreadyExists = false;
|
||||||
|
|
||||||
|
Widget configurationItemToWidget(
|
||||||
|
final BuildContext context,
|
||||||
|
final ServiceConfigItem configItem,
|
||||||
|
final Map<String, dynamic> settings,
|
||||||
|
) {
|
||||||
|
switch (configItem) {
|
||||||
|
case StringServiceConfigItem():
|
||||||
|
void onChanged(final String value, final bool isFieldValid) {
|
||||||
|
if (isFieldValid) {
|
||||||
|
setState(() {
|
||||||
|
if (value == configItem.value) {
|
||||||
|
settings.remove(configItem.id);
|
||||||
|
} else {
|
||||||
|
settings[configItem.id] = value;
|
||||||
|
}
|
||||||
|
isFormValid = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
isFormValid = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (configItem.widget == 'subdomain') {
|
||||||
|
return DomainStringConfigItem(
|
||||||
|
configItem: configItem,
|
||||||
|
newValue: settings[configItem.id],
|
||||||
|
onChanged: onChanged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return BasicStringConfigItem(
|
||||||
|
configItem: configItem,
|
||||||
|
newValue: settings[configItem.id],
|
||||||
|
onChanged: onChanged,
|
||||||
|
);
|
||||||
|
case BoolServiceConfigItem():
|
||||||
|
void onChanged(final bool value) {
|
||||||
|
setState(() {
|
||||||
|
if (value == configItem.value) {
|
||||||
|
settings.remove(configItem.id);
|
||||||
|
} else {
|
||||||
|
settings[configItem.id] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return BasicBoolConfigItem(
|
||||||
|
configItem: configItem,
|
||||||
|
newValue: settings[configItem.id],
|
||||||
|
onChanged: onChanged,
|
||||||
|
);
|
||||||
|
|
||||||
|
case EnumServiceConfigItem():
|
||||||
|
void onChanged(final String value) {
|
||||||
|
setState(() {
|
||||||
|
if (value == configItem.value) {
|
||||||
|
settings.remove(configItem.id);
|
||||||
|
} else {
|
||||||
|
settings[configItem.id] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return BasicEnumConfigItem(
|
||||||
|
configItem: configItem,
|
||||||
|
newValue: settings[configItem.id],
|
||||||
|
onChanged: onChanged,
|
||||||
|
);
|
||||||
|
case FallbackServiceConfigItem():
|
||||||
|
return ListTile(
|
||||||
|
title: Text(configItem.description),
|
||||||
|
subtitle: Text(configItem.id),
|
||||||
|
trailing: Text(configItem.type),
|
||||||
|
leading: const Icon(Icons.error),
|
||||||
|
);
|
||||||
|
case IntServiceConfigItem():
|
||||||
|
return ListTile(
|
||||||
|
title: Text(configItem.description),
|
||||||
|
subtitle: Text(configItem.id),
|
||||||
|
trailing: Text(configItem.value.toString()),
|
||||||
|
leading: const Icon(Icons.error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
final JobsState state = context.read<JobsCubit>().state;
|
||||||
|
|
||||||
|
if (state is JobsStateWithJobs) {
|
||||||
|
final ChangeServiceConfiguration? existingJob =
|
||||||
|
state.clientJobList.firstWhereOrNull(
|
||||||
|
(final ClientJob job) =>
|
||||||
|
job is ChangeServiceConfiguration &&
|
||||||
|
job.serviceId == widget.serviceId,
|
||||||
|
) as ChangeServiceConfiguration?;
|
||||||
|
if (existingJob != null) {
|
||||||
|
setState(() {
|
||||||
|
settings = existingJob.settings;
|
||||||
|
isJobAlreadyExists = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) {
|
||||||
|
final Service? service =
|
||||||
|
context.watch<ServicesBloc>().state.getServiceById(widget.serviceId);
|
||||||
|
|
||||||
|
if (service == null) {
|
||||||
|
return const BrandHeroScreen(
|
||||||
|
hasBackButton: true,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final JobsState state = context.watch<JobsCubit>().state;
|
||||||
|
|
||||||
|
if (state is JobsStateLoading) {
|
||||||
|
return BrandHeroScreen(
|
||||||
|
hasBackButton: true,
|
||||||
|
hasFlashButton: true,
|
||||||
|
heroIconWidget: SvgPicture.string(
|
||||||
|
service.svgIcon,
|
||||||
|
width: 48.0,
|
||||||
|
height: 48.0,
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
Theme.of(context).colorScheme.onBackground,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
heroTitle: service.displayName,
|
||||||
|
heroSubtitle: 'service_page.settings'.tr(),
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'service_page.wait_for_jobs'.tr(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const Gap(16.0),
|
||||||
|
const CircularProgressIndicator.adaptive(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final bool isModified = settings.isNotEmpty;
|
||||||
|
|
||||||
|
return BrandHeroScreen(
|
||||||
|
hasBackButton: true,
|
||||||
|
hasFlashButton: true,
|
||||||
|
heroIconWidget: SvgPicture.string(
|
||||||
|
service.svgIcon,
|
||||||
|
width: 48.0,
|
||||||
|
height: 48.0,
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
Theme.of(context).colorScheme.onBackground,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
heroTitle: service.displayName,
|
||||||
|
heroSubtitle: 'service_page.settings'.tr(),
|
||||||
|
children: [
|
||||||
|
...service.configuration.map(
|
||||||
|
(final ServiceConfigItem configItem) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
|
child: configurationItemToWidget(context, configItem, settings),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: (isModified && isFormValid)
|
||||||
|
? () {
|
||||||
|
context.read<JobsCubit>().addJob(
|
||||||
|
ChangeServiceConfiguration(
|
||||||
|
serviceId: service.id,
|
||||||
|
serviceDisplayName: service.displayName,
|
||||||
|
settings: settings,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
context.router.maybePop();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Text(
|
||||||
|
isJobAlreadyExists
|
||||||
|
? 'service_page.update_job'.tr()
|
||||||
|
: 'service_page.create_job'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import 'package:selfprivacy/ui/pages/server_storage/binds_migration/services_mig
|
||||||
import 'package:selfprivacy/ui/pages/server_storage/extending_volume.dart';
|
import 'package:selfprivacy/ui/pages/server_storage/extending_volume.dart';
|
||||||
import 'package:selfprivacy/ui/pages/server_storage/server_storage.dart';
|
import 'package:selfprivacy/ui/pages/server_storage/server_storage.dart';
|
||||||
import 'package:selfprivacy/ui/pages/services/service_page.dart';
|
import 'package:selfprivacy/ui/pages/services/service_page.dart';
|
||||||
|
import 'package:selfprivacy/ui/pages/services/service_settings_page.dart';
|
||||||
import 'package:selfprivacy/ui/pages/services/services.dart';
|
import 'package:selfprivacy/ui/pages/services/services.dart';
|
||||||
import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart';
|
import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart';
|
||||||
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart';
|
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart';
|
||||||
|
@ -96,6 +97,7 @@ class RootRouter extends _$RootRouter {
|
||||||
AutoRoute(page: AboutApplicationRoute.page),
|
AutoRoute(page: AboutApplicationRoute.page),
|
||||||
AutoRoute(page: DeveloperSettingsRoute.page),
|
AutoRoute(page: DeveloperSettingsRoute.page),
|
||||||
AutoRoute(page: ServiceRoute.page),
|
AutoRoute(page: ServiceRoute.page),
|
||||||
|
AutoRoute(page: ServiceSettingsRoute.page),
|
||||||
AutoRoute(page: ServerDetailsRoute.page),
|
AutoRoute(page: ServerDetailsRoute.page),
|
||||||
AutoRoute(page: DnsDetailsRoute.page),
|
AutoRoute(page: DnsDetailsRoute.page),
|
||||||
AutoRoute(page: BackupDetailsRoute.page),
|
AutoRoute(page: BackupDetailsRoute.page),
|
||||||
|
@ -120,6 +122,8 @@ String getRouteTitle(final String routeName) {
|
||||||
case 'ServicesRoute':
|
case 'ServicesRoute':
|
||||||
case 'ServiceRoute':
|
case 'ServiceRoute':
|
||||||
return 'basis.services';
|
return 'basis.services';
|
||||||
|
case 'ServiceSettingsRoute':
|
||||||
|
return 'service_page.settings';
|
||||||
case 'UsersRoute':
|
case 'UsersRoute':
|
||||||
return 'basis.users';
|
return 'basis.users';
|
||||||
case 'MoreRoute':
|
case 'MoreRoute':
|
||||||
|
|
|
@ -158,6 +158,16 @@ abstract class _$RootRouter extends RootStackRouter {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
ServiceSettingsRoute.name: (routeData) {
|
||||||
|
final args = routeData.argsAs<ServiceSettingsRouteArgs>();
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: ServiceSettingsPage(
|
||||||
|
serviceId: args.serviceId,
|
||||||
|
key: args.key,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
ServicesMigrationRoute.name: (routeData) {
|
ServicesMigrationRoute.name: (routeData) {
|
||||||
final args = routeData.argsAs<ServicesMigrationRouteArgs>();
|
final args = routeData.argsAs<ServicesMigrationRouteArgs>();
|
||||||
return AutoRoutePage<dynamic>(
|
return AutoRoutePage<dynamic>(
|
||||||
|
@ -590,6 +600,44 @@ class ServiceRouteArgs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [ServiceSettingsPage]
|
||||||
|
class ServiceSettingsRoute extends PageRouteInfo<ServiceSettingsRouteArgs> {
|
||||||
|
ServiceSettingsRoute({
|
||||||
|
required String serviceId,
|
||||||
|
Key? key,
|
||||||
|
List<PageRouteInfo>? children,
|
||||||
|
}) : super(
|
||||||
|
ServiceSettingsRoute.name,
|
||||||
|
args: ServiceSettingsRouteArgs(
|
||||||
|
serviceId: serviceId,
|
||||||
|
key: key,
|
||||||
|
),
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'ServiceSettingsRoute';
|
||||||
|
|
||||||
|
static const PageInfo<ServiceSettingsRouteArgs> page =
|
||||||
|
PageInfo<ServiceSettingsRouteArgs>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServiceSettingsRouteArgs {
|
||||||
|
const ServiceSettingsRouteArgs({
|
||||||
|
required this.serviceId,
|
||||||
|
this.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String serviceId;
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ServiceSettingsRouteArgs{serviceId: $serviceId, key: $key}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [ServicesMigrationPage]
|
/// [ServicesMigrationPage]
|
||||||
class ServicesMigrationRoute extends PageRouteInfo<ServicesMigrationRouteArgs> {
|
class ServicesMigrationRoute extends PageRouteInfo<ServicesMigrationRouteArgs> {
|
||||||
|
|
Loading…
Reference in a new issue