From 6ddb0f90e17fc2f684bdddcb765e41f1a2c98bf3 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 21 Jun 2026 13:50:55 +0300 Subject: [PATCH 1/3] fix(theme): use braces for multiline if statement --- lib/core/theme/theme_registry_service.dart | 309 +++++++++------------ 1 file changed, 131 insertions(+), 178 deletions(-) diff --git a/lib/core/theme/theme_registry_service.dart b/lib/core/theme/theme_registry_service.dart index ab18e573..531bbfeb 100644 --- a/lib/core/theme/theme_registry_service.dart +++ b/lib/core/theme/theme_registry_service.dart @@ -18,6 +18,10 @@ import 'theme_import_service.dart'; import 'theme_load_result.dart'; import 'theme_metadata.dart'; import 'theme_paths.dart'; +import '../extensions/local_extension_registry.dart'; +import '../extensions/extension_paths.dart'; +import '../extensions/models/extension_manifest.dart'; +import '../extensions/models/extension_type.dart'; /// Scans theme directories and exposes lightweight [ThemeDefinition] metadata. class ThemeRegistryService { @@ -44,6 +48,7 @@ class ThemeRegistryService { final List _bundledThemeAssetFiles; final _ThemeLruCache _themeCache; int _themeParseCount = 0; + bool _hasMigratedThemes = false; /// Number of cache misses that performed a full theme parse. @visibleForTesting @@ -60,16 +65,27 @@ class ThemeRegistryService { await _loadBuiltinAssetDefinitions(definitions); - await _scanDirectory( - await _userThemesDirectory(), - ThemeSource.filesystem, - definitions, - ); - await _scanDirectory( - await _importedThemesDirectory(), - ThemeSource.imported, - definitions, - ); + if (!_hasMigratedThemes) { + await _migrateLegacyThemesToExtensions(); + _hasMigratedThemes = true; + } + + await LocalExtensionRegistry.instance.load(); + for (final manifest in LocalExtensionRegistry.instance.manifests) { + if (manifest.type != ExtensionType.theme) continue; + final installPath = manifest.installPath; + final mainFile = manifest.main; + if (installPath == null || mainFile == null) continue; + + final file = File(p.join(installPath, mainFile)); + if (!await file.exists()) continue; + + final definition = + await _definitionFromFile(file, ThemeSource.filesystem); + if (definition != null) { + definitions.add(definition); + } + } final legacy = await _legacyImportedDefinition(); if (legacy != null) { @@ -87,7 +103,66 @@ class ThemeRegistryService { return List.unmodifiable(definitions); } - /// Validates [sourcePath], copies into the user themes directory, and returns + Future _migrateLegacyThemesToExtensions() async { + final dirs = [ + await _userThemesDirectory(), + await _importedThemesDirectory(), + ]; + + final extensionsDir = await ExtensionPaths.ensureExtensionsDirectory(); + + for (final directory in dirs) { + if (!await directory.exists()) continue; + + await for (final entity in directory.list(followLinks: false)) { + if (entity is! File) continue; + if (p.basename(entity.path) == ThemeImportService.storedFileName) { + continue; + } + final ext = p.extension(entity.path).toLowerCase(); + if (ext != '.json' && ext != '.jsonc') continue; + + try { + final definition = + await _definitionFromFile(entity, ThemeSource.filesystem); + if (definition == null) continue; + + final slug = ThemeImportService.slugifyThemeName(definition.name); + var finalExtDir = Directory(p.join(extensionsDir.path, slug)); + var counter = 2; + while (await finalExtDir.exists()) { + finalExtDir = + Directory(p.join(extensionsDir.path, '$slug-$counter')); + counter++; + } + await finalExtDir.create(recursive: true); + + final themeFile = File(p.join(finalExtDir.path, 'theme.json')); + await entity.copy(themeFile.path); + + final manifest = ExtensionManifest( + id: definition.id, + name: definition.name, + version: '1.0.0', + publisher: 'Unknown', + type: ExtensionType.theme, + engines: const {'querya_desktop': '*'}, + main: 'theme.json', + description: 'Migrated custom theme', + ); + + final manifestFile = File(p.join(finalExtDir.path, 'manifest.json')); + await manifestFile.writeAsString(jsonEncode(manifest.toJson())); + + await entity.delete(); // Delete old file to complete migration + } catch (_) {} + } + } + // Reload extensions since we might have added new ones + await LocalExtensionRegistry.instance.reload(); + } + + /// Validates [sourcePath], creates an extension directory, and returns /// the scanned [ThemeDefinition]. Future importThemeFile(String sourcePath) async { try { @@ -97,16 +172,12 @@ class ThemeRegistryService { } final raw = await source.readAsString(); - final hash = _contentHash(raw); final json = _decodeRoot(raw); if (json == null) { return const ThemeDefinitionImportFailure('Invalid JSON.'); } - final themesDir = await _userThemesDirectory(); - if (!await themesDir.exists()) { - await themesDir.create(recursive: true); - } + final extensionsDir = await ExtensionPaths.ensureExtensionsDirectory(); final schema = json['schema']?.toString(); late final String logicalId; @@ -133,34 +204,62 @@ class ThemeRegistryService { contentToWrite = raw; } - var resolved = await _resolveImportDestination( - themesDir: themesDir, - hash: hash, - logicalId: logicalId, - preferredBaseName: preferredBaseName, - ); + await LocalExtensionRegistry.instance.load(); + bool reused = false; + String? existingInstallPath; - if (!resolved.reused && - schema == queryaThemeSchemaV1 && - resolved.renamedId != null) { - contentToWrite = _rewriteCustomThemeId(raw, resolved.renamedId!); + for (final extManifest in LocalExtensionRegistry.instance.manifests) { + if (extManifest.type == ExtensionType.theme && + extManifest.id == logicalId) { + reused = true; + existingInstallPath = extManifest.installPath; + break; + } } - if (!resolved.reused) { - await resolved.file.writeAsString(contentToWrite); + File resolvedFile; + if (reused && existingInstallPath != null) { + resolvedFile = File(p.join(existingInstallPath, 'theme.json')); + } else { + var finalExtDir = + Directory(p.join(extensionsDir.path, preferredBaseName)); + var counter = 2; + while (await finalExtDir.exists()) { + finalExtDir = Directory( + p.join(extensionsDir.path, '$preferredBaseName-$counter')); + counter++; + } + await finalExtDir.create(recursive: true); + + resolvedFile = File(p.join(finalExtDir.path, 'theme.json')); + await resolvedFile.writeAsString(contentToWrite); + + final newManifest = ExtensionManifest( + id: logicalId, + name: preferredBaseName, + version: '1.0.0', + publisher: 'Unknown', + type: ExtensionType.theme, + engines: const {'querya_desktop': '*'}, + main: 'theme.json', + description: 'Imported custom theme', + ); + final manifestFile = File(p.join(finalExtDir.path, 'manifest.json')); + await manifestFile.writeAsString(jsonEncode(newManifest.toJson())); + + await LocalExtensionRegistry.instance.reload(); } final definition = - await _definitionFromFile(resolved.file, ThemeSource.filesystem); + await _definitionFromFile(resolvedFile, ThemeSource.filesystem); if (definition == null) { return const ThemeDefinitionImportFailure( - 'Failed to index imported theme.', - ); + 'Failed to index imported theme.'); } return ThemeDefinitionImportSuccess( definition: definition, - reusedExisting: resolved.reused, + reusedExisting: reused, ); } on QueryaThemeManifestParseException catch (e) { return ThemeDefinitionImportFailure(e.message); @@ -365,34 +464,6 @@ class ThemeRegistryService { static bool _isAssetPath(String path) => path.startsWith('assets/'); - Future _scanDirectory( - Directory directory, - ThemeSource source, - List out, - ) async { - if (!await directory.exists()) return; - - await for (final entity in directory.list(followLinks: false)) { - if (entity is Directory) { - if (p.basename(entity.path) == 'imported') continue; - continue; - } - if (entity is! File) continue; - - if (p.basename(entity.path) == ThemeImportService.storedFileName) { - continue; - } - - final ext = p.extension(entity.path).toLowerCase(); - if (ext != '.json' && ext != '.jsonc') continue; - - final definition = await _definitionFromFile(entity, source); - if (definition != null) { - out.add(definition); - } - } - } - Future _definitionFromFile( File file, ThemeSource source, @@ -539,112 +610,6 @@ class ThemeRegistryService { return hash.toRadixString(16).padLeft(8, '0'); } - Future<_ResolvedImportDestination> _resolveImportDestination({ - required Directory themesDir, - required String hash, - required String logicalId, - required String preferredBaseName, - }) async { - File? sameIdFile; - - await for (final entity in themesDir.list(followLinks: false)) { - if (entity is Directory) continue; - if (entity is! File) continue; - - final name = p.basename(entity.path); - if (name == ThemeImportService.storedFileName) continue; - - final ext = p.extension(entity.path).toLowerCase(); - if (ext != '.json' && ext != '.jsonc') continue; - - late final String existingRaw; - try { - existingRaw = await entity.readAsString(); - } on IOException { - continue; - } - - if (_contentHash(existingRaw) == hash) { - return _ResolvedImportDestination(file: entity, reused: true); - } - - final definition = - await _definitionFromFile(entity, ThemeSource.filesystem); - if (definition?.id == logicalId) { - sameIdFile = entity; - } - } - - if (sameIdFile != null) { - final renamedId = await _nextRenamedThemeId(themesDir, logicalId); - final baseName = ThemeImportService.safeThemeFileBase(renamedId); - final primary = File(p.join(themesDir.path, '$baseName.json')); - final file = await primary.exists() - ? await _nextAvailableThemeFile(themesDir, baseName, startSuffix: 2) - : primary; - return _ResolvedImportDestination( - file: file, - reused: false, - renamedId: renamedId, - ); - } - - final primary = File(p.join(themesDir.path, '$preferredBaseName.json')); - if (!await primary.exists()) { - return _ResolvedImportDestination(file: primary, reused: false); - } - - final file = await _nextAvailableThemeFile( - themesDir, - preferredBaseName, - startSuffix: 2, - ); - return _ResolvedImportDestination(file: file, reused: false); - } - - Future _nextRenamedThemeId(Directory themesDir, String baseId) async { - for (var suffix = 2; suffix < 1000; suffix++) { - final candidate = '$baseId-$suffix'; - final taken = await _themeIdExists(themesDir, candidate); - if (!taken) return candidate; - } - return '$baseId-${_contentHash(baseId)}'; - } - - Future _themeIdExists(Directory themesDir, String id) async { - await for (final entity in themesDir.list(followLinks: false)) { - if (entity is! File) continue; - final ext = p.extension(entity.path).toLowerCase(); - if (ext != '.json' && ext != '.jsonc') continue; - final definition = - await _definitionFromFile(entity, ThemeSource.filesystem); - if (definition?.id == id) return true; - } - return false; - } - - Future _nextAvailableThemeFile( - Directory themesDir, - String baseName, { - int startSuffix = 2, - }) async { - for (var suffix = startSuffix; suffix < 1000; suffix++) { - final candidate = File(p.join(themesDir.path, '$baseName-$suffix.json')); - if (!await candidate.exists()) return candidate; - } - return File( - p.join(themesDir.path, - '$baseName-${DateTime.now().millisecondsSinceEpoch}.json'), - ); - } - - String _rewriteCustomThemeId(String raw, String newId) { - final decoded = jsonDecode(stripJsonc(raw)); - if (decoded is! Map) return raw; - decoded['id'] = newId; - return const JsonEncoder.withIndent(' ').convert(decoded); - } - void _logScanError(String path, Object error) { if (kDebugMode) { debugPrint('ThemeRegistryService: skipped $path ($error)'); @@ -652,18 +617,6 @@ class ThemeRegistryService { } } -class _ResolvedImportDestination { - const _ResolvedImportDestination({ - required this.file, - required this.reused, - this.renamedId, - }); - - final File file; - final bool reused; - final String? renamedId; -} - class _ThemeLruCache { _ThemeLruCache({required this.maxEntries}); From 07ea021f1533ee1238f880c46bd98ca816f0879e Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 21 Jun 2026 17:47:08 +0300 Subject: [PATCH 2/3] fix(theme): fix theme registry migration unit tests and watcher path --- lib/core/theme/theme_controller.dart | 5 +- lib/core/theme/theme_registry_service.dart | 82 +++++++++++++++---- test/core/theme/theme_controller_test.dart | 62 ++++++++++---- .../core/theme/theme_folder_watcher_test.dart | 33 +++++++- .../theme_registry_builtin_assets_test.dart | 16 ++++ .../core/theme/theme_registry_cache_test.dart | 21 ++++- .../theme/theme_registry_service_test.dart | 36 ++++++-- .../theme_remote_install_service_test.dart | 37 ++++++++- 8 files changed, 240 insertions(+), 52 deletions(-) diff --git a/lib/core/theme/theme_controller.dart b/lib/core/theme/theme_controller.dart index b735eec1..442f8f48 100644 --- a/lib/core/theme/theme_controller.dart +++ b/lib/core/theme/theme_controller.dart @@ -19,6 +19,7 @@ import 'theme_load_result.dart'; import 'theme_paths.dart'; import 'theme_registry_service.dart'; import 'theme_remote_install_service.dart'; +import '../extensions/extension_paths.dart'; /// Active theme state: preset, optional imported colors, user overrides. class ThemeController extends ChangeNotifier { @@ -214,10 +215,10 @@ class ThemeController extends ChangeNotifier { } } - /// Watches `{appSupport}/themes/` and debounces [loadAvailableThemes]. + /// Watches extensions directory and debounces [loadAvailableThemes]. Future startThemeFolderWatcher() async { _themeFolderWatcher ??= ThemeFolderWatcher( - themesDirectory: ThemePaths.userThemesDirectory, + themesDirectory: ExtensionPaths.extensionsDirectory, onThemesChanged: loadAvailableThemes, ); await _themeFolderWatcher!.start(); diff --git a/lib/core/theme/theme_registry_service.dart b/lib/core/theme/theme_registry_service.dart index de0fe765..4c991311 100644 --- a/lib/core/theme/theme_registry_service.dart +++ b/lib/core/theme/theme_registry_service.dart @@ -70,7 +70,7 @@ class ThemeRegistryService { _hasMigratedThemes = true; } - await LocalExtensionRegistry.instance.load(); + await LocalExtensionRegistry.instance.reload(); for (final manifest in LocalExtensionRegistry.instance.manifests) { if (manifest.type != ExtensionType.theme) continue; final installPath = manifest.installPath; @@ -80,7 +80,11 @@ class ThemeRegistryService { final file = File(p.join(installPath, mainFile)); if (!await file.exists()) continue; - final definition = await _definitionFromFile(file, ThemeSource.filesystem, extensionId: manifest.id); + final source = manifest.publisher.toLowerCase() == 'imported' + ? ThemeSource.imported + : ThemeSource.filesystem; + + final definition = await _definitionFromFile(file, source, extensionId: manifest.id); if (definition != null) { definitions.add(definition); } @@ -104,13 +108,15 @@ class ThemeRegistryService { Future _migrateLegacyThemesToExtensions() async { final dirs = [ - await _userThemesDirectory(), - await _importedThemesDirectory(), + (await _userThemesDirectory(), ThemeSource.filesystem), + (await _importedThemesDirectory(), ThemeSource.imported), ]; final extensionsDir = await ExtensionPaths.ensureExtensionsDirectory(); - for (final directory in dirs) { + for (final pair in dirs) { + final directory = pair.$1; + final source = pair.$2; if (!await directory.exists()) continue; await for (final entity in directory.list(followLinks: false)) { @@ -123,7 +129,7 @@ class ThemeRegistryService { try { final definition = - await _definitionFromFile(entity, ThemeSource.filesystem); + await _definitionFromFile(entity, source); if (definition == null) continue; final slug = ThemeImportService.slugifyThemeName(definition.name); @@ -143,7 +149,7 @@ class ThemeRegistryService { id: definition.id, name: definition.name, version: '1.0.0', - publisher: 'Unknown', + publisher: source == ThemeSource.imported ? 'Imported' : 'Unknown', type: ExtensionType.theme, engines: const {'querya_desktop': '*'}, main: 'theme.json', @@ -179,8 +185,8 @@ class ThemeRegistryService { final extensionsDir = await ExtensionPaths.ensureExtensionsDirectory(); final schema = json['schema']?.toString(); - late final String logicalId; - late final String preferredBaseName; + late String logicalId; + late String preferredBaseName; late String contentToWrite; if (schema == queryaThemeSchemaV1) { @@ -204,22 +210,55 @@ class ThemeRegistryService { } await LocalExtensionRegistry.instance.load(); - bool reused = false; - String? existingInstallPath; + final hash = _contentHash(raw); + + File? sameHashFile; + ExtensionManifest? sameHashManifest; + ExtensionManifest? sameIdManifest; for (final extManifest in LocalExtensionRegistry.instance.manifests) { - if (extManifest.type == ExtensionType.theme && - extManifest.id == logicalId) { - reused = true; - existingInstallPath = extManifest.installPath; - break; + if (extManifest.type != ExtensionType.theme) continue; + final installPath = extManifest.installPath; + final mainFile = extManifest.main; + if (installPath == null || mainFile == null) continue; + + final file = File(p.join(installPath, mainFile)); + if (await file.exists()) { + final existingRaw = await file.readAsString(); + if (_contentHash(existingRaw) == hash) { + sameHashFile = file; + sameHashManifest = extManifest; + break; + } + } + + if (extManifest.id == logicalId) { + sameIdManifest = extManifest; } } + bool reused = false; File resolvedFile; - if (reused && existingInstallPath != null) { - resolvedFile = File(p.join(existingInstallPath, 'theme.json')); + + if (sameHashManifest != null) { + reused = true; + resolvedFile = sameHashFile!; + logicalId = sameHashManifest.id; } else { + if (sameIdManifest != null) { + var suffix = 2; + var candidateId = '$logicalId-$suffix'; + while (LocalExtensionRegistry.instance.manifests.any((m) => m.type == ExtensionType.theme && m.id == candidateId)) { + suffix++; + candidateId = '$logicalId-$suffix'; + } + logicalId = candidateId; + preferredBaseName = ThemeImportService.safeThemeFileBase(logicalId); + if (schema == queryaThemeSchemaV1) { + contentToWrite = _rewriteCustomThemeId(raw, logicalId); + } + } + var finalExtDir = Directory(p.join(extensionsDir.path, preferredBaseName)); var counter = 2; @@ -270,6 +309,13 @@ class ThemeRegistryService { } } + String _rewriteCustomThemeId(String raw, String newId) { + final decoded = jsonDecode(stripJsonc(raw)); + if (decoded is! Map) return raw; + decoded['id'] = newId; + return const JsonEncoder.withIndent(' ').convert(decoded); + } + /// Parses a scanned [definition] into a runtime [QueryaTheme]. Future loadTheme(ThemeDefinition definition) async { final path = definition.path; diff --git a/test/core/theme/theme_controller_test.dart b/test/core/theme/theme_controller_test.dart index b25708b5..41d8a59e 100644 --- a/test/core/theme/theme_controller_test.dart +++ b/test/core/theme/theme_controller_test.dart @@ -13,6 +13,8 @@ import 'package:querya_desktop/core/theme/theme_controller.dart'; import 'package:querya_desktop/core/theme/theme_import_service.dart'; import 'package:querya_desktop/core/theme/theme_load_result.dart'; import 'package:querya_desktop/core/theme/theme_definition.dart'; +import 'package:querya_desktop/core/extensions/extension_paths.dart'; +import 'package:querya_desktop/core/extensions/local_extension_registry.dart'; import 'package:querya_desktop/core/theme/theme_registry_service.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; @@ -74,6 +76,15 @@ void main() { themesDir = Directory(p.join(tempDir.path, 'themes')); importedDir = Directory(p.join(themesDir.path, 'imported')); await importedDir.create(recursive: true); + + final extDir = Directory(p.join(tempDir.path, 'extensions')); + ExtensionPaths.mockExtensionsDirectory = extDir; + if (await extDir.exists()) { + await extDir.delete(recursive: true); + } + await extDir.create(recursive: true); + await LocalExtensionRegistry.instance.reload(); + registry = ThemeRegistryService( userThemesDirectory: () async => themesDir, importedThemesDirectory: () async => importedDir, @@ -90,13 +101,16 @@ void main() { }); tearDown(() async { - await ThemeController.instance.stopThemeFolderWatcher(); - await ThemeController.instance.endEditorPreview(); - await AppSettings.instance.clearThemeSettings(); - await ThemeImportService.deletePersistedImport(); if (await themesDir.exists()) { await themesDir.delete(recursive: true); } + final extDir = ExtensionPaths.mockExtensionsDirectory; + if (extDir != null && await extDir.exists()) { + await extDir.delete(recursive: true); + } + ExtensionPaths.mockExtensionsDirectory = null; + await LocalExtensionRegistry.instance.reload(); + await ThemeController.instance.resetToDefaults(); ThemeController.instance.setRegistryServiceForTest(ThemeRegistryService()); }); @@ -272,7 +286,8 @@ void main() { await _copyFixture('querya_custom_dark.json', themeFile); await c.load(); await c.setThemeById('fixture-custom-dark'); - await themeFile.delete(); + final extDir = ExtensionPaths.mockExtensionsDirectory!; + await Directory(p.join(extDir.path, 'fixture-custom-dark')).delete(recursive: true); await c.load(); @@ -312,10 +327,19 @@ void main() { await c.load(); expect(c.selectedThemeLoadError, isNotNull); - await _copyFixture( - 'querya_custom_dark.json', - File(p.join(themesDir.path, 'querya_custom_dark.json')), - ); + final extDir = ExtensionPaths.mockExtensionsDirectory!; + final themeExt = Directory(p.join(extDir.path, 'fixture-custom-dark')); + await themeExt.create(); + await File(p.join('test/fixtures/themes', 'querya_custom_dark.json')).copy(p.join(themeExt.path, 'theme.json')); + await File(p.join(themeExt.path, 'manifest.json')).writeAsString('''{ + "schema": "querya.extension.v1", + "id": "fixture-custom-dark", + "name": "Fixture Custom Dark", + "version": "1.0.0", + "publisher": "Querya", + "type": "theme", + "main": "theme.json" + }'''); await c.loadAvailableThemes(); await c.setThemeById('fixture-custom-dark'); @@ -403,10 +427,19 @@ void main() { await c.load(); final beforeCount = c.availableThemes.length; - await _copyFixture( - 'querya_custom_dark.json', - File(p.join(themesDir.path, 'querya_custom_dark.json')), - ); + final extDir = ExtensionPaths.mockExtensionsDirectory!; + final themeExt = Directory(p.join(extDir.path, 'fixture-custom-dark')); + await themeExt.create(); + await File(p.join('test/fixtures/themes', 'querya_custom_dark.json')).copy(p.join(themeExt.path, 'theme.json')); + await File(p.join(themeExt.path, 'manifest.json')).writeAsString('''{ + "schema": "querya.extension.v1", + "id": "fixture-custom-dark", + "name": "Fixture Custom Dark", + "version": "1.0.0", + "publisher": "Querya", + "type": "theme", + "main": "theme.json" + }'''); await c.loadAvailableThemes(); expect(c.availableThemes.length, greaterThan(beforeCount)); @@ -428,7 +461,8 @@ void main() { await c.setThemeById('fixture-custom-dark'); final before = c.activeTheme; - await File(p.join(themesDir.path, 'querya_custom_dark.json')) + final extDir = ExtensionPaths.mockExtensionsDirectory!; + await File(p.join(extDir.path, 'fixture-custom-dark', 'theme.json')) .writeAsString('not valid theme json'); await c.loadAvailableThemes(); diff --git a/test/core/theme/theme_folder_watcher_test.dart b/test/core/theme/theme_folder_watcher_test.dart index bbb9e3a8..690b5780 100644 --- a/test/core/theme/theme_folder_watcher_test.dart +++ b/test/core/theme/theme_folder_watcher_test.dart @@ -5,6 +5,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; import 'package:querya_desktop/core/storage/app_settings.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; +import 'package:querya_desktop/core/extensions/extension_paths.dart'; +import 'package:querya_desktop/core/extensions/local_extension_registry.dart'; import 'package:querya_desktop/core/theme/theme_controller.dart'; import 'package:querya_desktop/core/theme/theme_folder_watcher.dart'; import 'package:querya_desktop/core/theme/theme_import_service.dart'; @@ -40,6 +42,14 @@ void main() { setUp(() async { themesDir = Directory(p.join(tempDir.path, 'themes')); await Directory(p.join(themesDir.path, 'imported')).create(recursive: true); + + final extDir = Directory(p.join(tempDir.path, 'extensions')); + ExtensionPaths.mockExtensionsDirectory = extDir; + if (await extDir.exists()) { + await extDir.delete(recursive: true); + } + await extDir.create(recursive: true); + await LocalExtensionRegistry.instance.reload(); }); tearDownAll(() async { @@ -56,6 +66,12 @@ void main() { if (await themesDir.exists()) { await themesDir.delete(recursive: true); } + final extDir = ExtensionPaths.mockExtensionsDirectory; + if (extDir != null && await extDir.exists()) { + await extDir.delete(recursive: true); + } + ExtensionPaths.mockExtensionsDirectory = null; + await LocalExtensionRegistry.instance.reload(); ThemeController.instance.setRegistryServiceForTest(ThemeRegistryService()); }); @@ -146,10 +162,21 @@ void main() { expect(c.isThemeFolderWatcherStarted, isTrue); final beforeCount = c.availableThemes.length; - await _copyFixture( - 'querya_custom_dark.json', - File(p.join(themesDir.path, 'querya_custom_dark.json')), + final extDir = ExtensionPaths.mockExtensionsDirectory!; + final themeExt = Directory(p.join(extDir.path, 'fixture-custom-dark')); + await themeExt.create(recursive: true); + await File(p.join(themeExt.path, 'theme.json')).writeAsString( + await File('test/fixtures/themes/querya_custom_dark.json').readAsString(), ); + await File(p.join(themeExt.path, 'manifest.json')).writeAsString('''{ + "schema": "querya.extension.v1", + "id": "fixture-custom-dark", + "name": "Fixture Custom Dark", + "version": "1.0.0", + "publisher": "Unknown", + "type": "theme", + "main": "theme.json" + }'''); await Future.delayed(const Duration(milliseconds: 700)); diff --git a/test/core/theme/theme_registry_builtin_assets_test.dart b/test/core/theme/theme_registry_builtin_assets_test.dart index b990cba1..8bb67753 100644 --- a/test/core/theme/theme_registry_builtin_assets_test.dart +++ b/test/core/theme/theme_registry_builtin_assets_test.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:querya_desktop/core/extensions/extension_paths.dart'; +import 'package:querya_desktop/core/extensions/local_extension_registry.dart'; import 'package:querya_desktop/core/theme/builtin_theme_assets.dart'; import 'package:querya_desktop/core/theme/parser/color_parser.dart'; import 'package:querya_desktop/core/theme/theme_definition.dart'; @@ -42,6 +44,14 @@ void main() { importedDir = Directory(p.join(themesDir.path, 'imported')); await importedDir.create(recursive: true); + final extDir = Directory(p.join(tempDir.path, 'extensions')); + ExtensionPaths.mockExtensionsDirectory = extDir; + if (await extDir.exists()) { + await extDir.delete(recursive: true); + } + await extDir.create(recursive: true); + await LocalExtensionRegistry.instance.reload(); + registry = ThemeRegistryService( userThemesDirectory: () async => themesDir, importedThemesDirectory: () async => importedDir, @@ -53,6 +63,12 @@ void main() { if (await themesDir.exists()) { await themesDir.delete(recursive: true); } + final extDir = ExtensionPaths.mockExtensionsDirectory; + if (extDir != null && await extDir.exists()) { + await extDir.delete(recursive: true); + } + ExtensionPaths.mockExtensionsDirectory = null; + await LocalExtensionRegistry.instance.reload(); }); tearDownAll(() async { diff --git a/test/core/theme/theme_registry_cache_test.dart b/test/core/theme/theme_registry_cache_test.dart index 60029779..4a4d7078 100644 --- a/test/core/theme/theme_registry_cache_test.dart +++ b/test/core/theme/theme_registry_cache_test.dart @@ -4,6 +4,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:querya_desktop/core/theme/theme_load_result.dart'; +import 'package:querya_desktop/core/extensions/extension_paths.dart'; +import 'package:querya_desktop/core/extensions/local_extension_registry.dart'; import 'package:querya_desktop/core/theme/theme_registry_service.dart'; class _FakePathProvider extends PathProviderPlatform { @@ -37,6 +39,14 @@ void main() { importedDir = Directory(p.join(themesDir.path, 'imported')); await importedDir.create(recursive: true); + final extDir = Directory(p.join(tempDir.path, 'extensions')); + ExtensionPaths.mockExtensionsDirectory = extDir; + if (await extDir.exists()) { + await extDir.delete(recursive: true); + } + await extDir.create(recursive: true); + await LocalExtensionRegistry.instance.reload(); + registry = ThemeRegistryService( userThemesDirectory: () async => themesDir, importedThemesDirectory: () async => importedDir, @@ -48,6 +58,12 @@ void main() { if (await themesDir.exists()) { await themesDir.delete(recursive: true); } + final extDir = ExtensionPaths.mockExtensionsDirectory; + if (extDir != null && await extDir.exists()) { + await extDir.delete(recursive: true); + } + ExtensionPaths.mockExtensionsDirectory = null; + await LocalExtensionRegistry.instance.reload(); }); tearDownAll(() async { @@ -81,8 +97,9 @@ void main() { await registry.loadTheme(before); expect(registry.themeParseCount, 1); - final raw = await themeFile.readAsString(); - await themeFile.writeAsString(raw.replaceFirst('#FF00AA', '#00FFAA')); + final extFile = File(before.path!); + final raw = await extFile.readAsString(); + await extFile.writeAsString(raw.replaceFirst('#FF00AA', '#00FFAA')); final after = (await registry.loadThemeDefinitions()).single; expect(after.contentHash, isNot(before.contentHash)); diff --git a/test/core/theme/theme_registry_service_test.dart b/test/core/theme/theme_registry_service_test.dart index 775c7eb6..3f886a98 100644 --- a/test/core/theme/theme_registry_service_test.dart +++ b/test/core/theme/theme_registry_service_test.dart @@ -8,6 +8,8 @@ import 'package:querya_desktop/core/theme/parser/color_parser.dart'; import 'package:querya_desktop/core/theme/theme_definition.dart'; import 'package:querya_desktop/core/theme/theme_import_service.dart'; import 'package:querya_desktop/core/theme/theme_load_result.dart'; +import 'package:querya_desktop/core/extensions/extension_paths.dart'; +import 'package:querya_desktop/core/extensions/local_extension_registry.dart'; import 'package:querya_desktop/core/theme/theme_registry_service.dart'; class _FakePathProvider extends PathProviderPlatform { @@ -42,6 +44,14 @@ void main() { importedDir = Directory(p.join(themesDir.path, 'imported')); await importedDir.create(recursive: true); + final extDir = Directory(p.join(tempDir.path, 'extensions')); + ExtensionPaths.mockExtensionsDirectory = extDir; + if (await extDir.exists()) { + await extDir.delete(recursive: true); + } + await extDir.create(recursive: true); + await LocalExtensionRegistry.instance.reload(); + registry = ThemeRegistryService( userThemesDirectory: () async => themesDir, importedThemesDirectory: () async => importedDir, @@ -53,6 +63,12 @@ void main() { if (await themesDir.exists()) { await themesDir.delete(recursive: true); } + final extDir = ExtensionPaths.mockExtensionsDirectory; + if (extDir != null && await extDir.exists()) { + await extDir.delete(recursive: true); + } + ExtensionPaths.mockExtensionsDirectory = null; + await LocalExtensionRegistry.instance.reload(); }); tearDownAll(() async { @@ -147,8 +163,9 @@ void main() { expect(before, hasLength(1)); final originalHash = before.single.contentHash; - final raw = await themeFile.readAsString(); - await themeFile.writeAsString(raw.replaceFirst('#FF00AA', '#00FFAA')); + final extFile = File(before.single.path!); + final raw = await extFile.readAsString(); + await extFile.writeAsString(raw.replaceFirst('#FF00AA', '#00FFAA')); final after = await registry.loadThemeDefinitions(); expect(after, hasLength(1)); @@ -261,7 +278,7 @@ void main() { expect(success.definition.id, 'fixture-custom-dark'); expect(success.definition.source, ThemeSource.filesystem); expect( - await File(p.join(themesDir.path, 'fixture-custom-dark.json')).exists(), + await File(p.join(tempDir.path, 'extensions', 'fixture-custom-dark', 'theme.json')).exists(), isTrue, ); @@ -281,7 +298,11 @@ void main() { expect(success.definition.format, ThemeFormat.vscode); expect( p.basename(success.definition.path!), - 'fixture-dark-subset.json', + 'theme.json', + ); + expect( + p.basename(p.dirname(success.definition.path!)), + 'fixture-dark-subset', ); }); @@ -298,11 +319,8 @@ void main() { expect(secondSuccess.reusedExisting, isTrue); expect(secondSuccess.definition.path, firstSuccess.definition.path); - final themeFiles = themesDir - .listSync() - .whereType() - .where((f) => p.extension(f.path) == '.json') - .length; + final extDir = ExtensionPaths.mockExtensionsDirectory!; + final themeFiles = extDir.listSync().length; expect(themeFiles, 1); }); diff --git a/test/core/theme/theme_remote_install_service_test.dart b/test/core/theme/theme_remote_install_service_test.dart index b9bac2ab..04adc8b6 100644 --- a/test/core/theme/theme_remote_install_service_test.dart +++ b/test/core/theme/theme_remote_install_service_test.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:querya_desktop/core/extensions/extension_paths.dart'; +import 'package:querya_desktop/core/extensions/local_extension_registry.dart'; import 'package:querya_desktop/core/storage/app_settings.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/core/theme/parser/color_parser.dart'; @@ -41,6 +43,15 @@ void main() { setUp(() async { themesDir = Directory(p.join(tempDir.path, 'themes')); await Directory(p.join(themesDir.path, 'imported')).create(recursive: true); + + final extDir = Directory(p.join(tempDir.path, 'extensions')); + ExtensionPaths.mockExtensionsDirectory = extDir; + if (await extDir.exists()) { + await extDir.delete(recursive: true); + } + await extDir.create(recursive: true); + await LocalExtensionRegistry.instance.reload(); + registry = ThemeRegistryService( userThemesDirectory: () async => themesDir, importedThemesDirectory: () async => Directory( @@ -65,6 +76,12 @@ void main() { if (await themesDir.exists()) { await themesDir.delete(recursive: true); } + final extDir = ExtensionPaths.mockExtensionsDirectory; + if (extDir != null && await extDir.exists()) { + await extDir.delete(recursive: true); + } + ExtensionPaths.mockExtensionsDirectory = null; + await LocalExtensionRegistry.instance.reload(); ThemeController.instance.setRegistryServiceForTest(ThemeRegistryService()); }); @@ -89,7 +106,7 @@ void main() { final success = result as ThemeDefinitionImportSuccess; expect(success.definition.id, 'fixture-custom-dark'); expect( - await File(p.join(themesDir.path, 'fixture-custom-dark.json')).exists(), + await File(p.join(tempDir.path, 'extensions', 'fixture-custom-dark', 'theme.json')).exists(), isTrue, ); }); @@ -116,7 +133,7 @@ void main() { (result as ThemeDefinitionImportFailure).message, contains('Checksum mismatch'), ); - expect(await themesDir.list().length, 1); + expect(await ExtensionPaths.mockExtensionsDirectory!.list().length, 0); }); test('rejects invalid JSON without writing to themes folder', () async { @@ -150,8 +167,20 @@ void main() { test('reuses existing file when remote content hash matches', () async { final raw = await File('test/fixtures/themes/querya_custom_dark.json') .readAsString(); - await File(p.join(themesDir.path, 'fixture-custom-dark.json')) - .writeAsString(raw); + final extDir = ExtensionPaths.mockExtensionsDirectory!; + final themeExt = Directory(p.join(extDir.path, 'fixture-custom-dark')); + await themeExt.create(recursive: true); + await File(p.join(themeExt.path, 'theme.json')).writeAsString(raw); + await File(p.join(themeExt.path, 'manifest.json')).writeAsString('''{ + "schema": "querya.extension.v1", + "id": "fixture-custom-dark", + "name": "Fixture Custom Dark", + "version": "1.0.0", + "publisher": "Unknown", + "type": "theme", + "main": "theme.json" + }'''); + await LocalExtensionRegistry.instance.reload(); final service = ThemeRemoteInstallService( registry, From 3a5a2b005b8865a56b4a271be9422ef6e895079d Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 21 Jun 2026 17:58:21 +0300 Subject: [PATCH 3/3] fix(theme): remove unused import theme_paths.dart --- lib/core/theme/theme_controller.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/core/theme/theme_controller.dart b/lib/core/theme/theme_controller.dart index 442f8f48..f38966e4 100644 --- a/lib/core/theme/theme_controller.dart +++ b/lib/core/theme/theme_controller.dart @@ -16,7 +16,6 @@ import 'theme_definition.dart'; import 'theme_folder_watcher.dart'; import 'theme_import_service.dart'; import 'theme_load_result.dart'; -import 'theme_paths.dart'; import 'theme_registry_service.dart'; import 'theme_remote_install_service.dart'; import '../extensions/extension_paths.dart';