Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion gui/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'screens/about_screen.dart';
import 'screens/dreamcast_screen.dart';
import 'screens/home_screen.dart';
import 'screens/patcher_screen.dart';
import 'screens/plugins_screen.dart';
import 'screens/settings_screen.dart';
import 'screens/theme_editor_screen.dart';
import 'screens/tools_screen.dart';
Expand Down Expand Up @@ -44,7 +45,7 @@ class _OpenROMAppState extends State<OpenROMApp> {
String _outputDir = '';
String _audioCodec = 'flac';

static const List<int> _mobileScreenIndices = [0, 1, 2, 4];
static const List<int> _mobileScreenIndices = [0, 1, 2, 4, 5];

@override
void initState() {
Expand Down Expand Up @@ -143,6 +144,7 @@ class _OpenROMAppState extends State<OpenROMApp> {
PatcherScreen(theme: theme),
ToolsScreen(theme: theme),
DreamcastScreen(theme: theme),
PluginsScreen(theme: theme),
settingsWidget,
AboutScreen(theme: theme),
ThemeEditorScreen(
Expand Down Expand Up @@ -221,6 +223,11 @@ class _OpenROMAppState extends State<OpenROMApp> {
activeIcon: Icon(Icons.construction),
label: 'Tools',
),
BottomNavigationBarItem(
icon: Icon(Icons.extension_outlined),
activeIcon: Icon(Icons.extension),
label: 'Plugins',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings),
Expand Down
146 changes: 146 additions & 0 deletions gui/lib/controllers/plugin_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// OpenROM — Universal ROM Compression Suite
// M5 Dev | GPL v3

import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:openrom_core/openrom_core.dart';

import '../models/store_plugin.dart';
import '../services/plugin_service.dart';

class PluginController extends ChangeNotifier {
final PluginService _service;

List<PluginManifest> _installedPlugins = [];
List<StorePlugin> _storeCatalog = [];
bool _isLoading = false;
String? _errorMessage;
String? _statusMessage;

PluginController({PluginService? service})
: _service = service ?? PluginService();

List<PluginManifest> get installedPlugins =>
List.unmodifiable(_installedPlugins);
List<StorePlugin> get storeCatalog => List.unmodifiable(_storeCatalog);
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
String? get statusMessage => _statusMessage;

void clearError() {
_errorMessage = null;
notifyListeners();
}

/// Refreshes the list of locally installed plugins.
Future<void> loadInstalledPlugins() async {
_isLoading = true;
_errorMessage = null;
notifyListeners();

try {
_installedPlugins = await _service.loadInstalledPlugins();
} catch (e) {
_errorMessage = 'Failed to load installed plugins: $e';
} finally {
_isLoading = false;
notifyListeners();
}
}

/// Fetches available store catalog items.
Future<void> fetchStoreCatalog({String? endpointUrl}) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();

try {
_storeCatalog = await _service.fetchStoreCatalog(
endpointUrl: endpointUrl,
);
} catch (e) {
_errorMessage = 'Failed to fetch plugin store catalog: $e';
} finally {
_isLoading = false;
notifyListeners();
}
}

/// Installs a `.orplug` package from a file path.
Future<bool> installPluginFromPath(String orplugPath) async {
_isLoading = true;
_statusMessage = 'Installing plugin...';
_errorMessage = null;
notifyListeners();

try {
final success = await _service.installPlugin(orplugPath);
if (success) {
_statusMessage = 'Plugin installed successfully!';
await loadInstalledPlugins();
return true;
} else {
_errorMessage =
'Failed to install plugin. Check manifest and SHA256 integrity.';
return false;
}
} catch (e) {
_errorMessage = 'Error during plugin installation: $e';
return false;
} finally {
_isLoading = false;
_statusMessage = null;
notifyListeners();
}
}

/// Opens file picker for `.orplug` files and installs the selected file.
Future<bool> pickAndInstallPlugin() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['orplug', 'zip'],
);

if (result != null && result.files.single.path != null) {
return await installPluginFromPath(result.files.single.path!);
}
} catch (e) {
_errorMessage = 'Failed to pick plugin file: $e';
notifyListeners();
}
return false;
}

/// Uninstalls an installed plugin by ID.
Future<bool> uninstallPlugin(String pluginId) async {
_isLoading = true;
_statusMessage = 'Uninstalling plugin...';
_errorMessage = null;
notifyListeners();

try {
final success = await _service.uninstallPlugin(pluginId);
if (success) {
_statusMessage = 'Plugin uninstalled successfully.';
await loadInstalledPlugins();
return true;
} else {
_errorMessage = 'Failed to uninstall plugin "$pluginId".';
return false;
}
} catch (e) {
_errorMessage = 'Error during plugin uninstallation: $e';
return false;
} finally {
_isLoading = false;
_statusMessage = null;
notifyListeners();
}
}

/// Helper to check if a store plugin ID is already installed.
bool isPluginInstalled(String pluginId) {
return _installedPlugins.any((p) => p.id == pluginId);
}
}
51 changes: 51 additions & 0 deletions gui/lib/models/store_plugin.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// OpenROM — Universal ROM Compression Suite
// M5 Dev | GPL v3

class StorePlugin {
final String id;
final String name;
final String version;
final String author;
final String description;
final bool isVerified;
final String? downloadUrl;
final List<String> tags;

const StorePlugin({
required this.id,
required this.name,
required this.version,
required this.author,
required this.description,
this.isVerified = false,
this.downloadUrl,
this.tags = const [],
});

factory StorePlugin.fromJson(Map<String, dynamic> json) {
final rawTags = json['tags'] as List<dynamic>? ?? [];
return StorePlugin(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
version: json['version'] as String? ?? '',
author: json['author'] as String? ?? '',
description: json['description'] as String? ?? '',
isVerified: json['isVerified'] as bool? ?? false,
downloadUrl: json['downloadUrl'] as String?,
tags: rawTags.map((e) => e.toString()).toList(),
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'version': version,
'author': author,
'description': description,
'isVerified': isVerified,
if (downloadUrl != null) 'downloadUrl': downloadUrl,
'tags': tags,
};
}
}
Loading
Loading