Skip to content

Commit 4c80d61

Browse files
committed
corrected collab
1 parent 0ac6bce commit 4c80d61

6 files changed

Lines changed: 201 additions & 60 deletions

File tree

lib/app/app.dart

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,9 +269,26 @@ class _FulaFilesAppState extends ConsumerState<FulaFilesApp>
269269
}
270270

271271
// File is synced — show the public link dialog
272-
final category = FileCategory.fromPath(filePath);
273-
final bucket = category.bucketName;
274-
final pathScope = isDirectory ? '$name/' : name;
272+
final String bucket;
273+
final String pathScope;
274+
if (isDirectory) {
275+
// Determine bucket from actual uploaded files, not folder name
276+
final children = LocalStorageService.instance.getSyncStatesUnderPath(filePath);
277+
final bucketCounts = <String, int>{};
278+
for (final s in children) {
279+
if (s.bucket != null && s.bucket!.isNotEmpty) {
280+
bucketCounts[s.bucket!] = (bucketCounts[s.bucket!] ?? 0) + 1;
281+
}
282+
}
283+
bucket = bucketCounts.isEmpty
284+
? FileCategory.other.bucketName
285+
: bucketCounts.entries.reduce((a, b) => a.value >= b.value ? a : b).key;
286+
pathScope = '$name/';
287+
} else {
288+
final category = FileCategory.fromPath(filePath);
289+
bucket = category.bucketName;
290+
pathScope = name;
291+
}
275292

276293
final ext = name.contains('.') ? name.split('.').last.toLowerCase() : '';
277294
const mimeTypes = {

lib/core/services/collab_folder_sync_service.dart

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,40 @@ class CollabFolderSyncService {
6161
await dir.create(recursive: true);
6262
}
6363

64+
// Clear stale sync metadata from a previous collab group on this folder
65+
await _resetSyncMetaIfDifferentGroup(groupId, folderPath);
66+
6467
// Update persistence
6568
await CollaborationService.instance.updateFolderAssignment(
6669
groupId,
6770
folderPath: folderPath,
6871
syncEnabled: true,
6972
);
7073

71-
// Initial download of existing files
72-
await _pollForNewFiles(groupId);
73-
74-
// Start watching + polling
74+
// Start watching + polling immediately so the caller isn't blocked
7575
await startSync(groupId);
7676

77-
_emitStatus(groupId, CollabSyncState.synced);
77+
// Download remote files and upload existing local files in the background.
78+
// The collab link is valid immediately — files appear as they're synced.
79+
unawaited(_initialSyncInBackground(groupId, folderPath));
80+
}
81+
82+
/// Runs the initial download + upload scan without blocking [assignFolder].
83+
Future<void> _initialSyncInBackground(String groupId, String folderPath) async {
84+
try {
85+
_emitStatus(groupId, CollabSyncState.syncing);
86+
87+
// Download any existing remote files first
88+
await _pollForNewFiles(groupId);
89+
90+
// Then upload local files not yet in the manifest
91+
await _scanAndUploadExistingFiles(groupId, folderPath);
92+
93+
_emitStatus(groupId, CollabSyncState.synced);
94+
} catch (e) {
95+
debugPrint('[CollabFolderSync] Background initial sync error ($groupId): $e');
96+
_emitStatus(groupId, CollabSyncState.error);
97+
}
7898
}
7999

80100
/// Remove folder assignment and stop sync.
@@ -248,6 +268,26 @@ class CollabFolderSyncService {
248268
// UPLOAD SYNC (local → remote)
249269
// ============================================================================
250270

271+
/// Scan existing files in the folder and upload any not yet in the manifest.
272+
Future<void> _scanAndUploadExistingFiles(String groupId, String folderPath) async {
273+
final dir = Directory(folderPath);
274+
if (!await dir.exists()) return;
275+
276+
debugPrint('[CollabFolderSync] Scanning existing files in $folderPath');
277+
int uploaded = 0;
278+
279+
await for (final entity in dir.list(recursive: true, followLinks: false)) {
280+
if (entity is! File) continue;
281+
final fileName = entity.path.split(Platform.pathSeparator).last;
282+
if (fileName == _syncMetaFile || fileName.startsWith('.')) continue;
283+
284+
await _uploadLocalFileIfNew(groupId, entity.path);
285+
uploaded++;
286+
}
287+
288+
debugPrint('[CollabFolderSync] Scan complete: processed $uploaded files');
289+
}
290+
251291
void _handleLocalFileChange(String groupId, FileSystemEvent event) {
252292
// Only handle file creation/modification
253293
if (event is! FileSystemCreateEvent && event is! FileSystemModifyEvent) return;
@@ -261,6 +301,8 @@ class CollabFolderSyncService {
261301
// Skip directories
262302
if (FileSystemEntity.isDirectorySync(path)) return;
263303

304+
debugPrint('[CollabFolderSync] File change detected: $fileName (${event.runtimeType}) for group $groupId');
305+
264306
// Debounce: cancel any pending upload for this path, reschedule
265307
_uploadDebounce[path]?.cancel();
266308
_uploadDebounce[path] = Timer(const Duration(seconds: 3), () {
@@ -272,7 +314,10 @@ class CollabFolderSyncService {
272314
Future<void> _uploadLocalFileIfNew(String groupId, String filePath) async {
273315
try {
274316
final info = await _getGroupInfo(groupId);
275-
if (info == null || info.folderPath == null || info.linkSecretKey == null) return;
317+
if (info == null || info.folderPath == null || info.linkSecretKey == null) {
318+
debugPrint('[CollabFolderSync] Skipping upload: group info missing (info=${info != null}, folder=${info?.folderPath != null}, key=${info?.linkSecretKey != null})');
319+
return;
320+
}
276321

277322
final folderPath = info.folderPath!;
278323
final file = File(filePath);
@@ -292,13 +337,19 @@ class CollabFolderSyncService {
292337
final alreadyUploaded = syncMeta.values.any(
293338
(e) => e.fileName == fileName && e.direction == 'upload',
294339
);
295-
if (alreadyUploaded) return;
340+
if (alreadyUploaded) {
341+
debugPrint('[CollabFolderSync] Skipping $fileName: already uploaded');
342+
return;
343+
}
296344

297345
// Also skip if the file was just downloaded (avoid re-uploading downloads)
298346
final justDownloaded = syncMeta.values.any(
299347
(e) => e.fileName == fileName && e.direction == 'download',
300348
);
301-
if (justDownloaded) return;
349+
if (justDownloaded) {
350+
debugPrint('[CollabFolderSync] Skipping $fileName: was downloaded');
351+
return;
352+
}
302353

303354
// Skip files that previously failed with non-retryable errors
304355
if (_permanentlyFailed.contains(filePath)) return;
@@ -386,6 +437,24 @@ class CollabFolderSyncService {
386437
return null;
387438
}
388439

440+
/// Clear sync metadata if it belongs to a different collab group.
441+
/// This prevents stale entries from a previous group on the same folder
442+
/// from blocking uploads for the new group.
443+
Future<void> _resetSyncMetaIfDifferentGroup(String groupId, String folderPath) async {
444+
final file = File('$folderPath${Platform.pathSeparator}$_syncMetaFile');
445+
if (!await file.exists()) return;
446+
try {
447+
final json = jsonDecode(await file.readAsString()) as Map<String, dynamic>;
448+
final storedGroupId = json['groupId'] as String?;
449+
if (storedGroupId != null && storedGroupId != groupId) {
450+
debugPrint('[CollabFolderSync] Clearing stale sync meta (was group $storedGroupId, now $groupId)');
451+
await file.delete();
452+
}
453+
} catch (e) {
454+
debugPrint('[CollabFolderSync] Failed to check sync meta group: $e');
455+
}
456+
}
457+
389458
Future<Map<String, _SyncedFileEntry>> _readSyncMeta(String folderPath) async {
390459
final file = File('$folderPath${Platform.pathSeparator}$_syncMetaFile');
391460
if (!await file.exists()) return {};

lib/features/browser/screens/file_browser_screen.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2687,11 +2687,26 @@ class _FileBrowserScreenState extends ConsumerState<FileBrowserScreen> {
26872687
);
26882688
}
26892689

2690+
// For directories, determine bucket from child files' sync states
2691+
if (file.isDirectory) {
2692+
final children = LocalStorageService.instance.getSyncStatesUnderPath(file.path);
2693+
final bucketCounts = <String, int>{};
2694+
for (final s in children) {
2695+
if (s.bucket != null && s.bucket!.isNotEmpty) {
2696+
bucketCounts[s.bucket!] = (bucketCounts[s.bucket!] ?? 0) + 1;
2697+
}
2698+
}
2699+
final bucket = bucketCounts.isEmpty
2700+
? FileCategory.other.bucketName
2701+
: bucketCounts.entries.reduce((a, b) => a.value >= b.value ? a : b).key;
2702+
return (bucket: bucket, pathScope: '${file.name}/');
2703+
}
2704+
26902705
// Fallback: derive from local file path
26912706
final category = FileCategory.fromPath(file.path);
26922707
return (
26932708
bucket: category.bucketName,
2694-
pathScope: file.isDirectory ? '${file.name}/' : file.name,
2709+
pathScope: file.name,
26952710
);
26962711
}
26972712

lib/features/home/screens/home_screen.dart

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -285,42 +285,67 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
285285
Widget _buildSetupBanner(BuildContext context, bool isLoggedIn, String? jwtToken, StorageState storageState) {
286286
final steps = <_SetupStep>[];
287287

288-
if (!isLoggedIn) {
289-
steps.add(_SetupStep(
290-
icon: LucideIcons.userCircle,
291-
title: 'Sign in to your account',
292-
subtitle: 'Required for cloud sync and sharing',
293-
action: 'Sign In',
294-
onTap: () => _showProfileSheet(context),
295-
isComplete: false,
296-
));
288+
// On desktop, sign-in and API key are a single step (browser handles both).
289+
// On mobile, they are separate steps (native OAuth first, then get API key).
290+
if (PlatformCapabilities.isDesktop) {
291+
if (jwtToken == null || jwtToken.isEmpty) {
292+
steps.add(_SetupStep(
293+
icon: LucideIcons.key,
294+
title: 'Get API Key',
295+
subtitle: 'Sign in via browser and configure cloud access',
296+
action: _isGettingApiKey ? 'Getting...' : 'Get API Key',
297+
onTap: _isGettingApiKey ? null : () => _getApiKey(context),
298+
onCancel: _isGettingApiKey ? () => _cancelGettingApiKey() : null,
299+
isComplete: false,
300+
isLoading: _isGettingApiKey,
301+
));
302+
} else {
303+
steps.add(_SetupStep(
304+
icon: LucideIcons.checkCircle,
305+
title: 'Signed in & API Key configured',
306+
subtitle: AuthService.instance.currentUser?.email ?? 'Cloud storage is ready',
307+
isComplete: true,
308+
));
309+
}
297310
} else {
298-
steps.add(_SetupStep(
299-
icon: LucideIcons.checkCircle,
300-
title: 'Signed in',
301-
subtitle: AuthService.instance.currentUser?.email ?? '',
302-
isComplete: true,
303-
));
304-
}
311+
// Mobile: separate sign-in and API key steps
312+
if (!isLoggedIn) {
313+
steps.add(_SetupStep(
314+
icon: LucideIcons.userCircle,
315+
title: 'Sign in to your account',
316+
subtitle: 'Required for cloud sync and sharing',
317+
action: 'Sign In',
318+
onTap: () => _showProfileSheet(context),
319+
isComplete: false,
320+
));
321+
} else {
322+
steps.add(_SetupStep(
323+
icon: LucideIcons.checkCircle,
324+
title: 'Signed in',
325+
subtitle: AuthService.instance.currentUser?.email ?? '',
326+
isComplete: true,
327+
));
328+
}
305329

306-
if (jwtToken == null || jwtToken.isEmpty) {
307-
steps.add(_SetupStep(
308-
icon: LucideIcons.key,
309-
title: 'Set up API Key',
310-
subtitle: 'Required for cloud storage access',
311-
action: _isGettingApiKey ? 'Getting...' : 'Get API Key',
312-
onTap: _isGettingApiKey ? null : () => _getApiKey(context),
313-
onCancel: _isGettingApiKey ? () => _cancelGettingApiKey() : null,
314-
isComplete: false,
315-
isLoading: _isGettingApiKey,
316-
));
317-
} else {
318-
steps.add(_SetupStep(
319-
icon: LucideIcons.checkCircle,
320-
title: 'API Key configured',
321-
subtitle: 'Cloud storage is ready',
322-
isComplete: true,
323-
));
330+
if (jwtToken == null || jwtToken.isEmpty) {
331+
steps.add(_SetupStep(
332+
icon: LucideIcons.key,
333+
title: 'Set up API Key',
334+
subtitle: 'Required for cloud storage access',
335+
action: _isGettingApiKey ? 'Getting...' : 'Get API Key',
336+
onTap: _isGettingApiKey ? null : () => _getApiKey(context),
337+
onCancel: _isGettingApiKey ? () => _cancelGettingApiKey() : null,
338+
isComplete: false,
339+
isLoading: _isGettingApiKey,
340+
));
341+
} else {
342+
steps.add(_SetupStep(
343+
icon: LucideIcons.checkCircle,
344+
title: 'API Key configured',
345+
subtitle: 'Cloud storage is ready',
346+
isComplete: true,
347+
));
348+
}
324349
}
325350

326351
// Wallet linking step - only show if API key is configured and no error fetching wallets
@@ -823,12 +848,14 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
823848
),
824849
],
825850
if (PlatformCapabilities.isDesktop) ...[
826-
Padding(
827-
padding: const EdgeInsets.symmetric(horizontal: 16),
828-
child: Text(
829-
'On desktop, use "Get API Key" below to sign in via your browser.',
830-
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey),
831-
),
851+
ListTile(
852+
leading: const Icon(LucideIcons.key),
853+
title: const Text('Get API Key'),
854+
subtitle: const Text('Sign in via your browser'),
855+
onTap: () {
856+
Navigator.pop(ctx);
857+
_getApiKey(context);
858+
},
832859
),
833860
],
834861
],

lib/features/settings/screens/settings_screen.dart

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'dart:io';
23
import 'package:flutter/material.dart';
34
import 'package:flutter/services.dart';
@@ -22,6 +23,7 @@ import 'package:fula_files/features/sharing/providers/collaboration_provider.dar
2223
import 'package:fula_files/features/settings/screens/blox_pairing_screen.dart';
2324
import 'package:fula_files/features/settings/screens/sync_queue_screen.dart';
2425
import 'package:fula_files/core/services/nft_wallet_service.dart';
26+
import 'package:fula_files/core/services/deep_link_service.dart';
2527
import 'package:fula_files/core/utils/platform_capabilities.dart';
2628
import 'package:fula_files/shared/utils/error_messages.dart';
2729

@@ -43,11 +45,19 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
4345
bool _isEditingApi = false;
4446
bool _isLoading = false;
4547
bool _showPrivateKey = false;
48+
StreamSubscription<String>? _apiKeySubscription;
4649

4750
@override
4851
void initState() {
4952
super.initState();
5053
_loadSettings();
54+
_apiKeySubscription = DeepLinkService.instance.onApiKeyReceived.listen((apiKey) {
55+
if (mounted) {
56+
setState(() {
57+
_jwtTokenController.text = apiKey;
58+
});
59+
}
60+
});
5161
}
5262

5363
static const String _defaultApiGateway = 'https://s3.cloud.fx.land';
@@ -76,6 +86,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
7686

7787
@override
7888
void dispose() {
89+
_apiKeySubscription?.cancel();
7990
_apiGatewayController.dispose();
8091
_ipfsServerController.dispose();
8192
_billingServerController.dispose();
@@ -786,12 +797,14 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
786797
),
787798
],
788799
if (PlatformCapabilities.isDesktop) ...[
789-
Padding(
790-
padding: const EdgeInsets.symmetric(horizontal: 16),
791-
child: Text(
792-
'On desktop, use "Get API Key" below to sign in via your browser.',
793-
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey),
794-
),
800+
ListTile(
801+
leading: const Icon(LucideIcons.key),
802+
title: const Text('Get API Key'),
803+
subtitle: const Text('Sign in via your browser'),
804+
onTap: () {
805+
Navigator.pop(dialogContext);
806+
DeepLinkService.instance.openGetApiKeyPage();
807+
},
795808
),
796809
],
797810
],

0 commit comments

Comments
 (0)