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
29 changes: 29 additions & 0 deletions lib/core/extensions/extension_paths.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'dart:io';

import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

/// Centralizes extension file locations.
abstract final class ExtensionPaths {
static const _extensionsSegment = 'extensions';

/// Returns `~/.querya/extensions` on Linux/Mac, or equivalent `USERPROFILE\.querya\extensions` on Windows.
/// Falls back to application support directory if HOME is unavailable.
static Future<Directory> extensionsDirectory() async {
final home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
if (home == null || home.isEmpty) {
final support = await getApplicationSupportDirectory();
return Directory(p.join(support.path, _extensionsSegment));
}
return Directory(p.join(home, '.querya', _extensionsSegment));
}

/// Creates the extensions directory if it doesn't exist.
static Future<Directory> ensureExtensionsDirectory() async {
final dir = await extensionsDirectory();
if (!await dir.exists()) {
await dir.create(recursive: true);
}
return dir;
}
}
61 changes: 61 additions & 0 deletions lib/core/extensions/local_extension_registry.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'dart:convert';
import 'dart:io';

import 'package:path/path.dart' as p;

import 'extension_paths.dart';
import 'models/extension_manifest.dart';

/// Scans the local filesystem for extensions and loads their manifests.
class LocalExtensionRegistry {
LocalExtensionRegistry._();
static final LocalExtensionRegistry instance = LocalExtensionRegistry._();

List<ExtensionManifest> _manifests = [];
bool _loaded = false;

/// Returns an unmodifiable list of loaded manifests.
List<ExtensionManifest> get manifests => List.unmodifiable(_manifests);

/// Reloads manifests from the disk.
Future<void> reload() async {
_loaded = false;
await load();
}

/// Loads manifests from the extensions directory if not already loaded.
Future<List<ExtensionManifest>> load() async {
if (_loaded) return manifests;

final dir = await ExtensionPaths.extensionsDirectory();
final loadedManifests = <ExtensionManifest>[];

if (await dir.exists()) {
// Use list() rather than listSync() to prevent blocking the UI
final entities = await dir.list().toList();
for (final entity in entities) {
if (entity is Directory) {
final manifestFile = File(p.join(entity.path, 'manifest.json'));
if (await manifestFile.exists()) {
try {
final content = await manifestFile.readAsString();
final json = jsonDecode(content) as Map<String, dynamic>;
final manifest = ExtensionManifest.fromJson(
json,
installPath: entity.path,
);
loadedManifests.add(manifest);
} catch (e) {
// Log or ignore invalid manifests
// In the future, we could report these to an error logging service
}
}
}
}
}

_manifests = loadedManifests;
_loaded = true;
return manifests;
}
}
5 changes: 4 additions & 1 deletion lib/core/extensions/models/extension_manifest.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class ExtensionManifest {
final String? main;
final String? icon;
final String? description;
final String? installPath;

const ExtensionManifest({
required this.id,
Expand All @@ -21,9 +22,10 @@ class ExtensionManifest {
this.main,
this.icon,
this.description,
this.installPath,
});

factory ExtensionManifest.fromJson(Map<String, dynamic> json) {
factory ExtensionManifest.fromJson(Map<String, dynamic> json, {String? installPath}) {
return ExtensionManifest(
id: json['id'] as String,
name: json['name'] as String,
Expand All @@ -34,6 +36,7 @@ class ExtensionManifest {
main: json['main'] as String?,
icon: json['icon'] as String?,
description: json['description'] as String?,
installPath: installPath,
);
}

Expand Down
Loading