From cf71d5457a8beec40b4800d1b690b8e69546420b Mon Sep 17 00:00:00 2001 From: Dan Cojocaru Date: Sun, 19 Jun 2022 09:23:04 +0300 Subject: [PATCH] Implemented project import --- lib/dialogs/new_project.dart | 131 +++++++++++++++++++++++++++++++++-- lib/state/projects.dart | 60 ++++++++++++++++ 2 files changed, 185 insertions(+), 6 deletions(-) diff --git a/lib/dialogs/new_project.dart b/lib/dialogs/new_project.dart index da4163f..5cb6701 100644 --- a/lib/dialogs/new_project.dart +++ b/lib/dialogs/new_project.dart @@ -1,7 +1,12 @@ +import 'dart:io'; + +import 'package:archive/archive.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:logic_circuits_simulator/state/projects.dart'; import 'package:logic_circuits_simulator/utils/provider_hook.dart'; +import 'package:provider/provider.dart'; class NewProjectDialog extends HookWidget { const NewProjectDialog({Key? key}) : super(key: key); @@ -20,6 +25,125 @@ class NewProjectDialog extends HookWidget { }; }, [newDialogNameController.text]); + final importProjectAction = useMemoized(() { + return () async { + final projectsState = Provider.of(context, listen: false); + final msg = ScaffoldMessenger.of(context); + final nav = Navigator.of(context); + + try { + final inputFiles = await FilePicker.platform.pickFiles( + dialogTitle: 'Import Project', + allowedExtensions: Platform.isLinux || Platform.isWindows ? ['lcsproj'] : null, + lockParentWindow: true, + type: Platform.isLinux || Platform.isWindows ? FileType.custom : FileType.any, + allowMultiple: false, + withData: true, + ); + + if (inputFiles == null) { + return; + } + + final inputFile = inputFiles.files.first; + + final dec = ZipDecoder(); + final archive = dec.decodeBytes(inputFile.bytes!); + + // bool editAfter = false; + final result = await projectsState.importProject( + archive: archive, + onConflictingId: () async { + final response = await showDialog( + barrierDismissible: false, + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Conflicting ID'), + content: const Text('You already have a project with the same ID as the one you are importing.\n\nAre you sure you want to replace the current project with the imported one?'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: const Text('Cancel'), + ), + Theme( + data: ThemeData( + brightness: Theme.of(context).brightness, + primarySwatch: Colors.red, + ), + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: const Text('Overwrite and import'), + ), + ), + ], + ); + }, + ); + + // Allow conflicting id ONLY if allow button is explicitly tapped + return response == true; + }, + onConflictingName: (String name) async { + final response = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Conflicting name'), + content: Text('You already have a project named $name.\n\nYou may import the project and have both coexist, but confusion may arise.'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: const Text('Import anyway'), + ), + ], + ); + }, + ); + + // Allow conflicting name UNLESS deny button is explicitly tapped + return response != false; + }, + ); + + if (result != null) { + nav.pop(); + // if (!editAfter) { + msg.showSnackBar( + SnackBar( + content: Text('Project ${result.projectName} imported'), + ), + ); + // } + // else { + // // TODO: Allow editing project name in the future + // } + } + } + catch (e) { + nav.pop(); + msg.showSnackBar( + SnackBar( + content: Text('Failed to import project: $e'), + duration: const Duration(seconds: 10), + ), + ); + } + }; + }); + return Dialog( child: SingleChildScrollView( child: Padding( @@ -43,12 +167,7 @@ class NewProjectDialog extends HookWidget { child: Padding( padding: const EdgeInsets.all(8.0), child: OutlinedButton.icon( - onPressed: () { - // TODO: Implement project importing - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Import coming soon...'), - )); - }, + onPressed: importProjectAction, icon: const Icon(Icons.download), label: const Text('Import Project'), ), diff --git a/lib/state/projects.dart b/lib/state/projects.dart index 96e1858..3828188 100644 --- a/lib/state/projects.dart +++ b/lib/state/projects.dart @@ -126,4 +126,64 @@ class ProjectsState extends ChangeNotifier { return result; } + + Future importProject({required Archive archive, required Future Function() onConflictingId, required Future Function(String name) onConflictingName}) async { + final projectsDir = await _getProjectsDir(); + + // Create dir where import is prepared + final importDir = Directory(path.join(projectsDir.path, '.import')); + await importDir.create(); + + extractArchiveToDisk(archive, importDir.path); + + final projectIdFile = File(path.join(importDir.path, 'projectId.txt')); + final projectId = (await projectIdFile.readAsString()).trim(); + + final indexFile = File(path.join(importDir.path, 'index.json')); + final importIndex = ProjectsIndex.fromJson(jsonDecode(await indexFile.readAsString())); + + if (index.projects.map((p) => p.projectId).contains(projectId)) { + if (!await onConflictingId()) { + return null; + } + } + final importIndexEntry = importIndex.projects.where((p) => p.projectId == projectId).first; + + final importProjectName = importIndexEntry.projectName; + if (index.projects.where((p) => p.projectId != projectId).map((p) => p.projectName).contains(importProjectName)) { + if (!await onConflictingName(importProjectName)) { + return null; + } + } + + await _updateIndex(index.copyWith( + projects: index.projects.where((p) => p.projectId != projectId).followedBy([importIndexEntry]).toList(), + )); + + // Copy project folder + final projectDir = Directory(path.join(projectsDir.path, projectId)); + if (await projectDir.exists()) { + await projectDir.delete(recursive: true); + } + await projectDir.create(); + final importProjectDir = Directory(path.join(importDir.path, projectId)); + await for (final entry in importProjectDir.list(recursive: true, followLinks: false)) { + final filename = path.relative(entry.path, from: importProjectDir.path); + if (entry is Directory) { + final newDir = Directory(path.join(projectDir.path, filename)); + await newDir.create(recursive: true); + } + else if (entry is File) { + await entry.copy(path.join(projectDir.path, filename)); + } + else if (entry is Link) { + final newLink = Link(path.join(projectDir.path, filename)); + await newLink.create(await entry.target()); + } + } + + await importDir.delete(recursive: true); + + return index.projects.where((p) => p.projectId == projectId).first; + } }