Binds migration screen

pull/116/head
Inex Code 2022-09-14 19:46:38 +03:00
parent 34837d8e29
commit 12d4cd23ec
4 changed files with 275 additions and 20 deletions

View File

@ -2,12 +2,17 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:ionicons/ionicons.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_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_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/pages/devices/devices.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/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
@ -27,6 +32,9 @@ class MorePage extends StatelessWidget {
final bool isReady = context.watch<ServerInstallationCubit>().state
is ServerInstallationFinished;
final bool? usesBinds =
context.watch<ApiServerVolumeCubit>().state.usesBinds;
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(52),
@ -40,6 +48,32 @@ class MorePage extends StatelessWidget {
padding: paddingH15V0,
child: Column(
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)
_MoreMenuItem(
title: 'more.configuration_wizard'.tr(),

View File

@ -1,44 +1,157 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/server_storage_list_item.dart';
import 'package:selfprivacy/ui/pages/server_storage/service_migration_list_item.dart';
class DataMigrationPage extends StatefulWidget {
const DataMigrationPage({
required this.diskVolumeToResize,
required this.services,
required this.diskStatus,
required this.resizeTarget,
final super.key,
});
final DiskVolume diskVolumeToResize;
final DiskStatus diskStatus;
final DiskSize resizeTarget;
final List<Service> services;
@override
State<DataMigrationPage> createState() => _DataMigrationPageState();
}
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
Widget build(final BuildContext context) => BrandHeroScreen(
hasBackButton: true,
heroTitle: 'providers.storage.data_migration_title'.tr(),
children: [
...widget.diskStatus.diskVolumes
.map(
(final volume) => Column(
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,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: headerVerticalPadding,
),
child: Column(
children: [
ServerStorageListItem(
volume: volume,
),
const SizedBox(height: 16),
...widget.diskStatus.diskVolumes
.map(
(final volume) => Column(
children: [
ServerStorageListItem(
volume: recalculatedDiskUsages(volume, widget.services),
dense: true,
),
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),
],
),
),
);
}
}

View File

@ -59,6 +59,24 @@ class DiskVolume {
sizeTotal.byte == 0 ? 0 : sizeUsed.byte / sizeTotal.byte;
bool get isDiskOkay =>
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 {

View 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;
}
}