mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2025-01-23 09:16:54 +00:00
Binds migration screen
This commit is contained in:
parent
34837d8e29
commit
12d4cd23ec
|
@ -2,12 +2,17 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:ionicons/ionicons.dart';
|
import 'package:ionicons/ionicons.dart';
|
||||||
import 'package:selfprivacy/config/brand_theme.dart';
|
import 'package:selfprivacy/config/brand_theme.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart';
|
||||||
|
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
|
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
|
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
||||||
import 'package:selfprivacy/ui/pages/devices/devices.dart';
|
import 'package:selfprivacy/ui/pages/devices/devices.dart';
|
||||||
import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart';
|
import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart';
|
||||||
|
import 'package:selfprivacy/ui/pages/server_storage/data_migration.dart';
|
||||||
|
import 'package:selfprivacy/ui/pages/server_storage/disk_status.dart';
|
||||||
import 'package:selfprivacy/ui/pages/setup/initializing.dart';
|
import 'package:selfprivacy/ui/pages/setup/initializing.dart';
|
||||||
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
|
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
|
||||||
import 'package:selfprivacy/ui/pages/root_route.dart';
|
import 'package:selfprivacy/ui/pages/root_route.dart';
|
||||||
|
@ -27,6 +32,9 @@ class MorePage extends StatelessWidget {
|
||||||
final bool isReady = context.watch<ServerInstallationCubit>().state
|
final bool isReady = context.watch<ServerInstallationCubit>().state
|
||||||
is ServerInstallationFinished;
|
is ServerInstallationFinished;
|
||||||
|
|
||||||
|
final bool? usesBinds =
|
||||||
|
context.watch<ApiServerVolumeCubit>().state.usesBinds;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: PreferredSize(
|
appBar: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(52),
|
preferredSize: const Size.fromHeight(52),
|
||||||
|
@ -40,6 +48,32 @@ class MorePage extends StatelessWidget {
|
||||||
padding: paddingH15V0,
|
padding: paddingH15V0,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (isReady && usesBinds != null && !usesBinds)
|
||||||
|
_MoreMenuItem(
|
||||||
|
title: 'providers.storage.start_migration_button'.tr(),
|
||||||
|
iconData: Icons.drive_file_move_outline,
|
||||||
|
goTo: DataMigrationPage(
|
||||||
|
diskStatus: DiskStatus.fromVolumes(
|
||||||
|
context.read<ApiServerVolumeCubit>().state.volumes,
|
||||||
|
context.read<ApiProviderVolumeCubit>().state.volumes,
|
||||||
|
),
|
||||||
|
services: context
|
||||||
|
.read<ServicesCubit>()
|
||||||
|
.state
|
||||||
|
.services
|
||||||
|
.where(
|
||||||
|
(final service) =>
|
||||||
|
service.id == 'bitwarden' ||
|
||||||
|
service.id == 'gitea' ||
|
||||||
|
service.id == 'pleroma' ||
|
||||||
|
service.id == 'mailserver' ||
|
||||||
|
service.id == 'nextcloud',
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
subtitle: 'not_ready_card.in_menu'.tr(),
|
||||||
|
accent: true,
|
||||||
|
),
|
||||||
if (!isReady)
|
if (!isReady)
|
||||||
_MoreMenuItem(
|
_MoreMenuItem(
|
||||||
title: 'more.configuration_wizard'.tr(),
|
title: 'more.configuration_wizard'.tr(),
|
||||||
|
|
|
@ -1,44 +1,157 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:selfprivacy/logic/models/disk_size.dart';
|
import 'package:selfprivacy/logic/models/disk_size.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
|
import 'package:selfprivacy/logic/models/service.dart';
|
||||||
|
import 'package:selfprivacy/ui/components/brand_button/filled_button.dart';
|
||||||
|
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
|
||||||
|
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
|
||||||
import 'package:selfprivacy/ui/pages/server_storage/disk_status.dart';
|
import 'package:selfprivacy/ui/pages/server_storage/disk_status.dart';
|
||||||
import 'package:selfprivacy/ui/pages/server_storage/server_storage_list_item.dart';
|
import 'package:selfprivacy/ui/pages/server_storage/server_storage_list_item.dart';
|
||||||
|
import 'package:selfprivacy/ui/pages/server_storage/service_migration_list_item.dart';
|
||||||
|
|
||||||
class DataMigrationPage extends StatefulWidget {
|
class DataMigrationPage extends StatefulWidget {
|
||||||
const DataMigrationPage({
|
const DataMigrationPage({
|
||||||
required this.diskVolumeToResize,
|
required this.services,
|
||||||
required this.diskStatus,
|
required this.diskStatus,
|
||||||
required this.resizeTarget,
|
|
||||||
final super.key,
|
final super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final DiskVolume diskVolumeToResize;
|
|
||||||
final DiskStatus diskStatus;
|
final DiskStatus diskStatus;
|
||||||
final DiskSize resizeTarget;
|
final List<Service> services;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DataMigrationPage> createState() => _DataMigrationPageState();
|
State<DataMigrationPage> createState() => _DataMigrationPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DataMigrationPageState extends State<DataMigrationPage> {
|
class _DataMigrationPageState extends State<DataMigrationPage> {
|
||||||
|
/// Service id to target migration disk name
|
||||||
|
final Map<String, String> serviceToDisk = {};
|
||||||
|
|
||||||
|
static const headerHeight = 52.0;
|
||||||
|
static const headerVerticalPadding = 8.0;
|
||||||
|
static const listItemHeight = 62.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) => BrandHeroScreen(
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
for (final Service service in widget.services) {
|
||||||
|
if (service.storageUsage.volume != null) {
|
||||||
|
serviceToDisk[service.id] = service.storageUsage.volume!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onChange(final String volumeName, final String serviceId) {
|
||||||
|
setState(() {
|
||||||
|
serviceToDisk[serviceId] = volumeName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check the services and if a service is moved (in a serviceToDisk entry)
|
||||||
|
/// subtract the used storage from the old volume and add it to the new volume.
|
||||||
|
/// The old volume is the volume the service is currently on, shown in services list.
|
||||||
|
DiskVolume recalculatedDiskUsages(final DiskVolume volume, final List<Service> services) {
|
||||||
|
DiskSize used = volume.sizeUsed;
|
||||||
|
|
||||||
|
for (final Service service in services) {
|
||||||
|
if (service.storageUsage.volume != null) {
|
||||||
|
if (service.storageUsage.volume == volume.name) {
|
||||||
|
if (serviceToDisk[service.id] != null && serviceToDisk[service.id] != volume.name) {
|
||||||
|
used -= service.storageUsage.used;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (serviceToDisk[service.id] != null && serviceToDisk[service.id] == volume.name) {
|
||||||
|
used += service.storageUsage.used;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return volume.copyWith(sizeUsed: used);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) {
|
||||||
|
final Size appBarHeight = Size.fromHeight(
|
||||||
|
headerHeight +
|
||||||
|
headerVerticalPadding * 2 +
|
||||||
|
listItemHeight * widget.diskStatus.diskVolumes.length +
|
||||||
|
headerVerticalPadding * widget.diskStatus.diskVolumes.length,
|
||||||
|
);
|
||||||
|
return SafeArea(
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: PreferredSize(
|
||||||
|
preferredSize: appBarHeight,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
BrandHeader(
|
||||||
|
title: 'providers.storage.data_migration_title'.tr(),
|
||||||
hasBackButton: true,
|
hasBackButton: true,
|
||||||
heroTitle: 'providers.storage.data_migration_title'.tr(),
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0,
|
||||||
|
vertical: headerVerticalPadding,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
...widget.diskStatus.diskVolumes
|
...widget.diskStatus.diskVolumes
|
||||||
.map(
|
.map(
|
||||||
(final volume) => Column(
|
(final volume) => Column(
|
||||||
children: [
|
children: [
|
||||||
ServerStorageListItem(
|
ServerStorageListItem(
|
||||||
volume: volume,
|
volume: recalculatedDiskUsages(volume, widget.services),
|
||||||
|
dense: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: headerVerticalPadding),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
children: <Widget>[
|
||||||
|
if (widget.services.isEmpty) const Center(child: CircularProgressIndicator()),
|
||||||
|
...widget.services
|
||||||
|
.map(
|
||||||
|
(final service) => Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ServiceMigrationListItem(
|
||||||
|
service: service,
|
||||||
|
diskStatus: widget.diskStatus,
|
||||||
|
selectedVolume: serviceToDisk[service.id]!,
|
||||||
|
onChange: onChange,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const Divider(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: InfoBox(text: 'providers.storage.data_migration_notice'.tr(), isWarning: true,),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton(
|
||||||
|
title: 'providers.storage.start_migration_button'.tr(),
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: Implement migration
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -59,6 +59,24 @@ class DiskVolume {
|
||||||
sizeTotal.byte == 0 ? 0 : sizeUsed.byte / sizeTotal.byte;
|
sizeTotal.byte == 0 ? 0 : sizeUsed.byte / sizeTotal.byte;
|
||||||
bool get isDiskOkay =>
|
bool get isDiskOkay =>
|
||||||
percentage < 0.8 && sizeTotal.gibibyte - sizeUsed.gibibyte > 2.0;
|
percentage < 0.8 && sizeTotal.gibibyte - sizeUsed.gibibyte > 2.0;
|
||||||
|
|
||||||
|
DiskVolume copyWith({
|
||||||
|
final DiskSize? sizeUsed,
|
||||||
|
final DiskSize? sizeTotal,
|
||||||
|
final String? name,
|
||||||
|
final bool? root,
|
||||||
|
final bool? isResizable,
|
||||||
|
final ServerDiskVolume? serverDiskVolume,
|
||||||
|
final ServerVolume? providerVolume,
|
||||||
|
}) => DiskVolume(
|
||||||
|
sizeUsed: sizeUsed ?? this.sizeUsed,
|
||||||
|
sizeTotal: sizeTotal ?? this.sizeTotal,
|
||||||
|
name: name ?? this.name,
|
||||||
|
root: root ?? this.root,
|
||||||
|
isResizable: isResizable ?? this.isResizable,
|
||||||
|
serverDiskVolume: serverDiskVolume ?? this.serverDiskVolume,
|
||||||
|
providerVolume: providerVolume ?? this.providerVolume,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class DiskStatus {
|
class DiskStatus {
|
||||||
|
|
90
lib/ui/pages/server_storage/service_migration_list_item.dart
Normal file
90
lib/ui/pages/server_storage/service_migration_list_item.dart
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:selfprivacy/logic/models/service.dart';
|
||||||
|
import 'package:selfprivacy/ui/pages/server_storage/disk_status.dart';
|
||||||
|
|
||||||
|
class ServiceMigrationListItem extends StatelessWidget {
|
||||||
|
const ServiceMigrationListItem({
|
||||||
|
required this.service,
|
||||||
|
required this.diskStatus,
|
||||||
|
required this.selectedVolume,
|
||||||
|
required this.onChange,
|
||||||
|
final super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Service service;
|
||||||
|
final DiskStatus diskStatus;
|
||||||
|
final String selectedVolume;
|
||||||
|
final Function onChange;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(final BuildContext context) => Column(
|
||||||
|
children: [
|
||||||
|
_headerRow(context),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
..._radioRows(context),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _headerRow(final BuildContext context) => SizedBox(
|
||||||
|
height: 24,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: SvgPicture.string(
|
||||||
|
service.svgIcon,
|
||||||
|
width: 24.0,
|
||||||
|
height: 24.0,
|
||||||
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Text(
|
||||||
|
service.displayName,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text(
|
||||||
|
service.storageUsage.used.toString(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> _radioRows(final BuildContext context) {
|
||||||
|
final List<Widget> volumeRows = [];
|
||||||
|
|
||||||
|
for (final DiskVolume volume in diskStatus.diskVolumes) {
|
||||||
|
volumeRows.add(
|
||||||
|
RadioListTile(
|
||||||
|
title: Text(
|
||||||
|
volume.displayName,
|
||||||
|
),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
activeColor: Theme.of(context).colorScheme.secondary,
|
||||||
|
dense: true,
|
||||||
|
value: volume.name,
|
||||||
|
groupValue: selectedVolume,
|
||||||
|
onChanged: (final value) {
|
||||||
|
onChange(value, service.id);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return volumeRows;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue