Skip to content

Commit

Permalink
Initial version of multi-project support!!
Browse files Browse the repository at this point in the history
Needs a looot more polish though...
  • Loading branch information
TechnicJelle committed Oct 13, 2024
1 parent cbe5e53 commit fbd09f2
Show file tree
Hide file tree
Showing 14 changed files with 360 additions and 151 deletions.
34 changes: 34 additions & 0 deletions lib/hover.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import "package:flutter/material.dart";

class Hover extends StatefulWidget {
final Widget alwaysChild;
final Widget hoverChild;

const Hover({
super.key,
required this.alwaysChild,
required this.hoverChild,
});

@override
State<Hover> createState() => _HoverState();
}

class _HoverState extends State<Hover> {
bool _isHovering = false;

@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovering = true),
onHover: (_) => setState(() => _isHovering = true),
onExit: (_) => setState(() => _isHovering = false),
child: Stack(
children: [
widget.alwaysChild,
if (_isHovering) widget.hoverChild,
],
),
);
}
}
3 changes: 2 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";

import "main_menu/main_menu.dart";
import "main_menu/settings/projects_screen.dart";
import "prefs.dart";
import "project_view/close_project_button.dart";
import "project_view/project_view.dart";
Expand Down Expand Up @@ -46,7 +47,7 @@ class MyHomePage extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final Directory? projectDirectory = ref.watch(projectDirectoryProvider);
final Directory? projectDirectory = ref.watch(openProjectProvider);

String title = "BlueMap GUI";
if (projectDirectory != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import "dart:io";

import "package:file_picker/file_picker.dart";
import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:path/path.dart" as p;
Expand All @@ -9,10 +8,11 @@ import "package:url_launcher/url_launcher_string.dart";
import "../main.dart";
import "../prefs.dart";
import "../utils.dart";
import "settings/projects_screen.dart";

enum _PickingState {
nothing,
picking,
directoryNotFound,
scanning,
downloading,
downloadFailed,
Expand All @@ -21,35 +21,40 @@ enum _PickingState {
running,
}

class PathPickerButton extends ConsumerStatefulWidget {
const PathPickerButton({super.key});
class ProjectTile extends ConsumerStatefulWidget {
final Directory projectDirectory;
const ProjectTile(this.projectDirectory, {super.key});

@override
ConsumerState<PathPickerButton> createState() => _PathPickerButtonState();
ConsumerState<ProjectTile> createState() => _PathPickerButtonState();
}

class _PathPickerButtonState extends ConsumerState<PathPickerButton> {
class _PathPickerButtonState extends ConsumerState<ProjectTile> {
_PickingState _pickingState = _PickingState.nothing;
String? errorText;

Directory get projectDirectory => widget.projectDirectory;

@override
void initState() {
super.initState();

if (!projectDirectory.existsSync()) {
setState(() => _pickingState = _PickingState.directoryNotFound);
return;
}
}

@override
Widget build(BuildContext context) {
return switch (_pickingState) {
_PickingState.nothing => ElevatedButton(
onPressed: () async {
// == Picking Project Directory ==
setState(() => _pickingState = _PickingState.picking);
final String? result = await FilePicker.platform.getDirectoryPath(
dialogTitle: "Select project folder",
);
if (result == null) {
setState(() => _pickingState = _PickingState.nothing);
return; // User canceled the picker
}

//TODO: The states other than ListTile should get more polish
_PickingState.nothing => ListTile(
title: Text(p.basename(projectDirectory.path)),
subtitle: Text(projectDirectory.path),
onTap: () async {
// == Scanning for BlueMap CLI JAR ==
setState(() => _pickingState = _PickingState.scanning);
final Directory projectDirectory = Directory(result);
final contents = projectDirectory.listSync();

NonHashedFile? susBlueMapJar;
Expand Down Expand Up @@ -108,21 +113,11 @@ class _PathPickerButtonState extends ConsumerState<PathPickerButton> {
mapsDir.createSync(); //recreate maps dir (now empty)
}

ref.read(projectDirectoryProvider.notifier).openProject(projectDirectory);
ref.read(openProjectProvider.notifier).openProject(projectDirectory);
},
child: const Text("Select project folder"),
),
_PickingState.picking => Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("Waiting for user to pick a folder..."),
const SizedBox(height: 8),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: const LinearProgressIndicator(),
),
],
),
_PickingState.directoryNotFound =>
Text("Directory ${projectDirectory.path} not found!"),
_PickingState.scanning => Column(
mainAxisSize: MainAxisSize.min,
children: [
Expand Down
85 changes: 85 additions & 0 deletions lib/main_menu/settings/new_project_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import "dart:io";

import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:path/path.dart" as p;
import "package:path_provider/path_provider.dart";

import "../../prefs.dart";

//TODO: This thing needs a LOT of polish and error checking!
class NewProjectDialog extends ConsumerStatefulWidget {
const NewProjectDialog({super.key});

@override
NewProjectDialogState createState() => NewProjectDialogState();
}

class NewProjectDialogState extends ConsumerState<NewProjectDialog> {
final TextEditingController _nameController = TextEditingController(text: "untitled");
TextEditingController? _locationController;

String get _projectPath =>
p.join(_locationController?.text ?? "", _nameController.text);

@override
void initState() {
super.initState();
getApplicationDocumentsDirectory().then((documentsDirectory) {
final String projectDir = p.join(documentsDirectory.path, "BlueMapGUI");
setState(() {
_locationController = TextEditingController(text: projectDir);
});
});
}

@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text("New project"),
content: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 600),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _nameController,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
labelText: "Name:",
),
),
const SizedBox(height: 16),
TextField(
controller: _locationController,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
labelText: "Location:",
),
),
const SizedBox(height: 8),
Text(
"Project will be created in: $_projectPath",
style:
Theme.of(context).textTheme.labelMedium?.copyWith(color: Colors.grey),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text("Cancel"),
),
ElevatedButton(
onPressed: () {
ref.read(knownProjectsProvider.notifier).addProject(Directory(_projectPath));
Navigator.of(context).pop();
},
child: const Text("Create"),
),
],
);
}
}
100 changes: 84 additions & 16 deletions lib/main_menu/settings/projects_screen.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,33 @@
import "dart:io";

import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:url_launcher/url_launcher.dart";

import "../../hover.dart";
import "../../prefs.dart";
import "../path_picker_button.dart";
import "../../project_view/project_view.dart";
import "../project_tile.dart";
import "new_project_dialog.dart";

class OpenProjectNotifier extends Notifier<Directory?> {
@override
Directory? build() {
return null;
}

void openProject(Directory projectDirectory) {
state = projectDirectory;
}

void closeProject() {
ref.read(openConfigProvider.notifier).close();
state = null;
}
}

final openProjectProvider =
NotifierProvider<OpenProjectNotifier, Directory?>(() => OpenProjectNotifier());

class ProjectsScreen extends ConsumerWidget {
const ProjectsScreen({super.key});
Expand All @@ -15,21 +40,64 @@ class ProjectsScreen extends ConsumerWidget {
);
}

return const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Select an empty folder to store your BlueMap files in:"),
SizedBox(height: 8),
PathPickerButton(),
SizedBox(height: 8),
Text("The BlueMap CLI tool will be downloaded into that folder."),
SizedBox(height: 4),
Text("It will generate some default config files for you."),
SizedBox(height: 4),
Text("You will then need to configure your maps in the BlueMap GUI."),
],
),
final List<Directory> projects = ref.watch(knownProjectsProvider);

return Stack(
children: [
ListView.builder(
itemCount: projects.length,
itemBuilder: (BuildContext context, int index) {
final Directory project = projects[index];
return Hover(
alwaysChild: ProjectTile(project),
hoverChild: Positioned(
right: 16,
top: 12,
child: PopupMenuButton(
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
PopupMenuItem(
child: const Row(
children: [
Icon(Icons.folder_open),
SizedBox(width: 8),
Text("Open in file manager"),
],
),
onTap: () => launchUrl(project.uri),
// does nothing when dir doesn't exist ↑
),
PopupMenuItem(
child: const Row(
children: [
Icon(Icons.clear),
SizedBox(width: 8),
Text("Remove from projects"),
],
),
onTap: () {
ref.read(knownProjectsProvider.notifier).removeProject(project);
},
),
],
),
),
);
},
),
Positioned(
bottom: 16,
right: 16,
child: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
showDialog(
context: context,
builder: (context) => const NewProjectDialog(),
);
},
),
),
],
);
}
}
Loading

0 comments on commit fbd09f2

Please sign in to comment.