master
gitea 2 months ago
parent f8aa20eb5c
commit d68124d975

@ -7,7 +7,6 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_analytics/firebase_analytics.dart';
import '../local/local_storage_service.dart'; import '../local/local_storage_service.dart';
import '../local/models/item.dart'; import '../local/models/item.dart';
import '../session/models/user.dart';
import 'models/firebase_config.dart'; import 'models/firebase_config.dart';
/// Exception thrown when Firebase operations fail. /// Exception thrown when Firebase operations fail.
@ -157,7 +156,8 @@ class FirebaseService {
} }
if (!_initialized || _auth == null) { if (!_initialized || _auth == null) {
throw FirebaseException('Firebase not initialized. Call initialize() first.'); throw FirebaseException(
'Firebase not initialized. Call initialize() first.');
} }
try { try {
@ -181,7 +181,8 @@ class FirebaseService {
} }
if (!_initialized || _auth == null) { if (!_initialized || _auth == null) {
throw FirebaseException('Firebase not initialized. Call initialize() first.'); throw FirebaseException(
'Firebase not initialized. Call initialize() first.');
} }
try { try {
@ -203,7 +204,8 @@ class FirebaseService {
} }
if (!_initialized || _firestore == null) { if (!_initialized || _firestore == null) {
throw FirebaseException('Firestore not initialized. Call initialize() first.'); throw FirebaseException(
'Firestore not initialized. Call initialize() first.');
} }
try { try {
@ -212,7 +214,8 @@ class FirebaseService {
// Batch write to Firestore // Batch write to Firestore
final batch = _firestore!.batch(); final batch = _firestore!.batch();
final collection = _firestore!.collection('users').doc(userId).collection('items'); final collection =
_firestore!.collection('users').doc(userId).collection('items');
for (final item in items) { for (final item in items) {
final docRef = collection.doc(item.id); final docRef = collection.doc(item.id);
@ -241,7 +244,8 @@ class FirebaseService {
} }
if (!_initialized || _firestore == null) { if (!_initialized || _firestore == null) {
throw FirebaseException('Firestore not initialized. Call initialize() first.'); throw FirebaseException(
'Firestore not initialized. Call initialize() first.');
} }
try { try {
@ -285,7 +289,8 @@ class FirebaseService {
} }
if (!_initialized || _storage == null) { if (!_initialized || _storage == null) {
throw FirebaseException('Firebase Storage not initialized. Call initialize() first.'); throw FirebaseException(
'Firebase Storage not initialized. Call initialize() first.');
} }
try { try {
@ -318,7 +323,8 @@ class FirebaseService {
/// [parameters] - Optional event parameters. /// [parameters] - Optional event parameters.
/// ///
/// Does nothing if Analytics is disabled. /// Does nothing if Analytics is disabled.
Future<void> logEvent(String eventName, {Map<String, dynamic>? parameters}) async { Future<void> logEvent(String eventName,
{Map<String, dynamic>? parameters}) async {
if (!config.enabled || !config.analyticsEnabled || _analytics == null) { if (!config.enabled || !config.analyticsEnabled || _analytics == null) {
return; return;
} }
@ -327,7 +333,8 @@ class FirebaseService {
// Convert Map<String, dynamic> to Map<String, Object> for Firebase Analytics // Convert Map<String, dynamic> to Map<String, Object> for Firebase Analytics
Map<String, Object>? analyticsParams; Map<String, Object>? analyticsParams;
if (parameters != null) { if (parameters != null) {
analyticsParams = parameters.map((key, value) => MapEntry(key, value as Object)); analyticsParams =
parameters.map((key, value) => MapEntry(key, value as Object));
} }
await _analytics!.logEvent( await _analytics!.logEvent(
@ -350,4 +357,3 @@ class FirebaseService {
_initialized = false; _initialized = false;
} }
} }

@ -175,8 +175,6 @@ class ImmichService {
} }
final assetsJson = assetsData['items'] as List<dynamic>; final assetsJson = assetsData['items'] as List<dynamic>;
final total = assetsData['total'] as int? ?? 0;
final count = assetsData['count'] as int? ?? 0;
final List<ImmichAsset> assets = assetsJson final List<ImmichAsset> assets = assetsJson
.map((json) => ImmichAsset.fromJson(json as Map<String, dynamic>)) .map((json) => ImmichAsset.fromJson(json as Map<String, dynamic>))

@ -1,10 +1,7 @@
import 'dart:async'; import 'dart:async';
import '../local/local_storage_service.dart'; import '../local/local_storage_service.dart';
import '../local/models/item.dart';
import '../immich/immich_service.dart'; import '../immich/immich_service.dart';
import '../immich/models/immich_asset.dart';
import '../nostr/nostr_service.dart'; import '../nostr/nostr_service.dart';
import '../nostr/models/nostr_event.dart';
import '../nostr/models/nostr_keypair.dart'; import '../nostr/models/nostr_keypair.dart';
import 'models/sync_status.dart'; import 'models/sync_status.dart';
import 'models/sync_operation.dart'; import 'models/sync_operation.dart';
@ -50,7 +47,8 @@ class SyncEngine {
SyncOperation? _currentOperation; SyncOperation? _currentOperation;
/// Stream controller for sync status updates. /// Stream controller for sync status updates.
final StreamController<SyncOperation> _statusController = StreamController<SyncOperation>.broadcast(); final StreamController<SyncOperation> _statusController =
StreamController<SyncOperation>.broadcast();
/// Whether the engine has been disposed. /// Whether the engine has been disposed.
bool _isDisposed = false; bool _isDisposed = false;
@ -97,7 +95,9 @@ class SyncEngine {
/// Gets the current queue of pending operations. /// Gets the current queue of pending operations.
List<SyncOperation> getPendingOperations() { List<SyncOperation> getPendingOperations() {
return _operationQueue.where((op) => op.status == SyncStatus.pending).toList(); return _operationQueue
.where((op) => op.status == SyncStatus.pending)
.toList();
} }
/// Gets all operations (pending, in-progress, completed, failed). /// Gets all operations (pending, in-progress, completed, failed).
@ -134,7 +134,8 @@ class SyncEngine {
/// [priority] - Priority of the sync operation. /// [priority] - Priority of the sync operation.
/// ///
/// Returns the sync operation ID. /// Returns the sync operation ID.
Future<String> syncToImmich(String itemId, {SyncPriority priority = SyncPriority.normal}) async { Future<String> syncToImmich(String itemId,
{SyncPriority priority = SyncPriority.normal}) async {
if (_immichService == null) { if (_immichService == null) {
throw SyncException('Immich service not configured'); throw SyncException('Immich service not configured');
} }
@ -158,7 +159,8 @@ class SyncEngine {
/// [priority] - Priority of the sync operation. /// [priority] - Priority of the sync operation.
/// ///
/// Returns the sync operation ID. /// Returns the sync operation ID.
Future<String> syncFromImmich(String assetId, {SyncPriority priority = SyncPriority.normal}) async { Future<String> syncFromImmich(String assetId,
{SyncPriority priority = SyncPriority.normal}) async {
if (_immichService == null) { if (_immichService == null) {
throw SyncException('Immich service not configured'); throw SyncException('Immich service not configured');
} }
@ -182,7 +184,8 @@ class SyncEngine {
/// [priority] - Priority of the sync operation. /// [priority] - Priority of the sync operation.
/// ///
/// Returns the sync operation ID. /// Returns the sync operation ID.
Future<String> syncToNostr(String itemId, {SyncPriority priority = SyncPriority.normal}) async { Future<String> syncToNostr(String itemId,
{SyncPriority priority = SyncPriority.normal}) async {
if (_nostrService == null) { if (_nostrService == null) {
throw SyncException('Nostr service not configured'); throw SyncException('Nostr service not configured');
} }
@ -209,7 +212,8 @@ class SyncEngine {
/// [priority] - Priority of sync operations. /// [priority] - Priority of sync operations.
/// ///
/// Returns a list of operation IDs. /// Returns a list of operation IDs.
Future<List<String>> syncAll({SyncPriority priority = SyncPriority.normal}) async { Future<List<String>> syncAll(
{SyncPriority priority = SyncPriority.normal}) async {
final operationIds = <String>[]; final operationIds = <String>[];
// Sync local items to Immich // Sync local items to Immich
@ -239,7 +243,8 @@ class SyncEngine {
/// Processes the sync queue. /// Processes the sync queue.
Future<void> _processQueue() async { Future<void> _processQueue() async {
if (_currentOperation != null || _isDisposed) return; // Already processing or disposed if (_currentOperation != null || _isDisposed)
return; // Already processing or disposed
// Sort queue by priority (high first) // Sort queue by priority (high first)
_operationQueue.sort((a, b) { _operationQueue.sort((a, b) {
@ -250,7 +255,8 @@ class SyncEngine {
}); });
// Process pending operations // Process pending operations
while (!_isDisposed && _operationQueue.any((op) => op.status == SyncStatus.pending)) { while (!_isDisposed &&
_operationQueue.any((op) => op.status == SyncStatus.pending)) {
final operation = _operationQueue.firstWhere( final operation = _operationQueue.firstWhere(
(op) => op.status == SyncStatus.pending, (op) => op.status == SyncStatus.pending,
); );
@ -421,4 +427,3 @@ class SyncEngine {
} }
} }
} }

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig"

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig"

@ -0,0 +1,42 @@
platform :osx, '10.15'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end

@ -1,11 +1,9 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart'; import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:app_boilerplate/data/firebase/firebase_service.dart'; import 'package:app_boilerplate/data/firebase/firebase_service.dart';
import 'package:app_boilerplate/data/firebase/models/firebase_config.dart'; import 'package:app_boilerplate/data/firebase/models/firebase_config.dart';
import 'package:app_boilerplate/data/local/local_storage_service.dart'; import 'package:app_boilerplate/data/local/local_storage_service.dart';
import 'package:app_boilerplate/data/local/models/item.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@ -307,4 +305,3 @@ void main() {
}); });
}); });
} }

@ -1,10 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:app_boilerplate/data/immich/immich_service.dart'; import 'package:app_boilerplate/data/immich/immich_service.dart';
import 'package:app_boilerplate/data/immich/models/immich_asset.dart'; import 'package:app_boilerplate/data/immich/models/immich_asset.dart';
import 'package:app_boilerplate/data/immich/models/upload_response.dart';
import 'package:app_boilerplate/data/local/local_storage_service.dart'; import 'package:app_boilerplate/data/local/local_storage_service.dart';
import 'package:app_boilerplate/data/local/models/item.dart'; import 'package:app_boilerplate/data/local/models/item.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@ -318,7 +316,10 @@ void main() {
if (path == '/api/search/metadata') { if (path == '/api/search/metadata') {
return Response( return Response(
statusCode: 500, statusCode: 500,
data: {'error': 'Internal server error', 'message': 'Internal server error'}, data: {
'error': 'Internal server error',
'message': 'Internal server error'
},
requestOptions: RequestOptions(path: path), requestOptions: RequestOptions(path: path),
); );
} }
@ -445,7 +446,8 @@ void main() {
// Assert // Assert
expect(cached.length, equals(2)); expect(cached.length, equals(2));
expect(cached.map((a) => a.id).toList(), containsAll(['asset-1', 'asset-2'])); expect(cached.map((a) => a.id).toList(),
containsAll(['asset-1', 'asset-2']));
}); });
}); });
} }

@ -2,9 +2,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:app_boilerplate/data/nostr/nostr_service.dart'; import 'package:app_boilerplate/data/nostr/nostr_service.dart';
import 'package:app_boilerplate/data/nostr/models/nostr_keypair.dart'; import 'package:app_boilerplate/data/nostr/models/nostr_keypair.dart';
import 'package:app_boilerplate/data/nostr/models/nostr_event.dart'; import 'package:app_boilerplate/data/nostr/models/nostr_event.dart';
import 'package:app_boilerplate/data/nostr/models/nostr_relay.dart';
import 'dart:async';
import 'dart:convert';
void main() { void main() {
group('NostrService - Keypair Generation', () { group('NostrService - Keypair Generation', () {
@ -303,4 +300,3 @@ void main() {
}); });
}); });
} }

@ -3,10 +3,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:app_boilerplate/data/session/session_service.dart'; import 'package:app_boilerplate/data/session/session_service.dart';
import 'package:app_boilerplate/data/session/models/user.dart';
import 'package:app_boilerplate/data/local/local_storage_service.dart'; import 'package:app_boilerplate/data/local/local_storage_service.dart';
import 'package:app_boilerplate/data/local/models/item.dart'; import 'package:app_boilerplate/data/local/models/item.dart';
import 'package:app_boilerplate/data/sync/sync_engine.dart';
void main() { void main() {
// Initialize Flutter bindings and sqflite for testing // Initialize Flutter bindings and sqflite for testing
@ -191,7 +189,8 @@ void main() {
expect(newUser.id, equals('user2')); expect(newUser.id, equals('user2'));
}); });
test('switchSession - clears previous user data when clearCache is true', () async { test('switchSession - clears previous user data when clearCache is true',
() async {
// Arrange // Arrange
await sessionService.login(id: 'user1', username: 'user1'); await sessionService.login(id: 'user1', username: 'user1');
@ -216,7 +215,9 @@ void main() {
expect(items.length, equals(0)); expect(items.length, equals(0));
}); });
test('switchSession - preserves previous user data when clearCache is false', () async { test(
'switchSession - preserves previous user data when clearCache is false',
() async {
// Arrange // Arrange
await sessionService.login(id: 'user1', username: 'user1'); await sessionService.login(id: 'user1', username: 'user1');
@ -323,4 +324,3 @@ void main() {
}); });
}); });
} }

@ -11,7 +11,6 @@ import 'package:app_boilerplate/data/nostr/models/nostr_keypair.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'dart:convert';
void main() { void main() {
// Initialize Flutter bindings and sqflite for testing // Initialize Flutter bindings and sqflite for testing
@ -511,4 +510,3 @@ Dio _createMockDio({
return dio; return dio;
} }

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:app_boilerplate/ui/relay_management/relay_management_controller.dart'; import 'package:app_boilerplate/ui/relay_management/relay_management_controller.dart';
import 'package:app_boilerplate/data/nostr/nostr_service.dart'; import 'package:app_boilerplate/data/nostr/nostr_service.dart';
import 'package:app_boilerplate/data/nostr/models/nostr_relay.dart';
import 'package:app_boilerplate/data/sync/sync_engine.dart'; import 'package:app_boilerplate/data/sync/sync_engine.dart';
import 'package:app_boilerplate/data/local/local_storage_service.dart'; import 'package:app_boilerplate/data/local/local_storage_service.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@ -157,10 +156,10 @@ void main() {
final result = await controllerWithoutSync.triggerManualSync(); final result = await controllerWithoutSync.triggerManualSync();
expect(result, isFalse); expect(result, isFalse);
expect(controllerWithoutSync.error, isNotNull); expect(controllerWithoutSync.error, isNotNull);
expect(controllerWithoutSync.error, contains('Sync engine not configured')); expect(
controllerWithoutSync.error, contains('Sync engine not configured'));
controllerWithoutSync.dispose(); controllerWithoutSync.dispose();
}); });
}); });
} }

@ -3,7 +3,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:app_boilerplate/ui/relay_management/relay_management_screen.dart'; import 'package:app_boilerplate/ui/relay_management/relay_management_screen.dart';
import 'package:app_boilerplate/ui/relay_management/relay_management_controller.dart'; import 'package:app_boilerplate/ui/relay_management/relay_management_controller.dart';
import 'package:app_boilerplate/data/nostr/nostr_service.dart'; import 'package:app_boilerplate/data/nostr/nostr_service.dart';
import 'package:app_boilerplate/data/nostr/models/nostr_relay.dart';
import 'package:app_boilerplate/data/sync/sync_engine.dart'; import 'package:app_boilerplate/data/sync/sync_engine.dart';
import 'package:app_boilerplate/data/local/local_storage_service.dart'; import 'package:app_boilerplate/data/local/local_storage_service.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@ -72,7 +71,8 @@ void main() {
} }
group('RelayManagementScreen', () { group('RelayManagementScreen', () {
testWidgets('displays empty state when no relays', (WidgetTester tester) async { testWidgets('displays empty state when no relays',
(WidgetTester tester) async {
await tester.pumpWidget(createTestWidget()); await tester.pumpWidget(createTestWidget());
expect(find.text('No relays configured'), findsOneWidget); expect(find.text('No relays configured'), findsOneWidget);
@ -95,7 +95,8 @@ void main() {
expect(find.text('Disconnected'), findsNWidgets(2)); expect(find.text('Disconnected'), findsNWidgets(2));
}); });
testWidgets('adds relay when Add button is pressed', (WidgetTester tester) async { testWidgets('adds relay when Add button is pressed',
(WidgetTester tester) async {
await tester.pumpWidget(createTestWidget()); await tester.pumpWidget(createTestWidget());
// Find and enter relay URL // Find and enter relay URL
@ -131,7 +132,8 @@ void main() {
expect(find.byIcon(Icons.error), findsOneWidget); expect(find.byIcon(Icons.error), findsOneWidget);
}); });
testWidgets('removes relay when delete button is pressed', (WidgetTester tester) async { testWidgets('removes relay when delete button is pressed',
(WidgetTester tester) async {
controller.addRelay('wss://relay.example.com'); controller.addRelay('wss://relay.example.com');
await tester.pumpWidget(createTestWidget()); await tester.pumpWidget(createTestWidget());
await tester.pump(); await tester.pump();
@ -159,14 +161,16 @@ void main() {
expect(find.byIcon(Icons.health_and_safety), findsOneWidget); expect(find.byIcon(Icons.health_and_safety), findsOneWidget);
}); });
testWidgets('displays manual sync button when sync engine is configured', (WidgetTester tester) async { testWidgets('displays manual sync button when sync engine is configured',
(WidgetTester tester) async {
await tester.pumpWidget(createTestWidget()); await tester.pumpWidget(createTestWidget());
expect(find.text('Manual Sync'), findsOneWidget); expect(find.text('Manual Sync'), findsOneWidget);
expect(find.byIcon(Icons.sync), findsOneWidget); expect(find.byIcon(Icons.sync), findsOneWidget);
}); });
testWidgets('shows loading state during health check', (WidgetTester tester) async { testWidgets('shows loading state during health check',
(WidgetTester tester) async {
controller.addRelay('wss://relay.example.com'); controller.addRelay('wss://relay.example.com');
await tester.pumpWidget(createTestWidget()); await tester.pumpWidget(createTestWidget());
await tester.pump(); await tester.pump();
@ -183,7 +187,8 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
testWidgets('shows error message when present', (WidgetTester tester) async { testWidgets('shows error message when present',
(WidgetTester tester) async {
await tester.pumpWidget(createTestWidget()); await tester.pumpWidget(createTestWidget());
// Trigger an error by adding invalid URL // Trigger an error by adding invalid URL
@ -198,7 +203,8 @@ void main() {
expect(find.textContaining('Invalid relay URL'), findsOneWidget); expect(find.textContaining('Invalid relay URL'), findsOneWidget);
}); });
testWidgets('dismisses error when close button is pressed', (WidgetTester tester) async { testWidgets('dismisses error when close button is pressed',
(WidgetTester tester) async {
await tester.pumpWidget(createTestWidget()); await tester.pumpWidget(createTestWidget());
// Trigger an error // Trigger an error
@ -234,4 +240,3 @@ void main() {
}); });
}); });
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.