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
1 change: 1 addition & 0 deletions app/lib/backend/schema/conversation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ enum ConversationSource {
apple_watch,
phone,
desktop,
limitless,
}

class ConversationExternalData {
Expand Down
182 changes: 46 additions & 136 deletions app/lib/pages/home/widgets/sync_bottom_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'package:omi/backend/schema/bt_device/bt_device.dart';
import 'package:omi/gen/assets.gen.dart';
import 'package:omi/providers/device_provider.dart';
import 'package:omi/providers/sync_provider.dart';
import 'package:omi/services/services.dart';
import 'package:omi/services/wals.dart';
import 'package:provider/provider.dart';

Expand Down Expand Up @@ -34,25 +33,13 @@ class SyncBottomSheet extends StatelessWidget {
final progress = syncProvider.walsSyncedProgress;
final hasPendingData = pendingFlashPages.isNotEmpty;

// Check for orphaned files from previous failed syncs
final flashPageSync = ServiceManager.instance().wal.getSyncs().flashPage;
final hasOrphanedFiles = flashPageSync.hasOrphanedFiles;
final orphanedCount = flashPageSync.orphanedFilesCount;
final isUploadingOrphans = flashPageSync.isUploadingOrphans;
final isSyncingFromPendant = syncProvider.isSyncingFromPendant;
final isUploadingToCloud = syncProvider.isUploadingToCloud;

// Calculate time ago for pending data
String timeAgo = '';
if (hasPendingData && pendingFlashPages.isNotEmpty) {
final oldestWal = pendingFlashPages.reduce((a, b) => a.timerStart < b.timerStart ? a : b);
final minutesAgo = ((DateTime.now().millisecondsSinceEpoch ~/ 1000) - oldestWal.timerStart) ~/ 60;
if (minutesAgo < 60) {
timeAgo = '$minutesAgo minutes ago';
} else if (minutesAgo < 1440) {
timeAgo = '${minutesAgo ~/ 60} hours ago';
} else {
timeAgo = '${minutesAgo ~/ 1440} days ago';
}
}
// Consider sync in progress if EITHER pendant sync OR cloud upload is happening
final isAnySyncInProgress = isSyncing || isSyncingFromPendant || isUploadingToCloud;
final hasOrphanedFiles = syncProvider.hasOrphanedFiles;
final orphanedCount = syncProvider.orphanedFilesCount;

return Container(
decoration: const BoxDecoration(
Expand Down Expand Up @@ -97,7 +84,9 @@ class SyncBottomSheet extends StatelessWidget {
],
),
child: Icon(
isSyncing ? Icons.sync_rounded : (hasPendingData ? Icons.graphic_eq_rounded : Icons.check_rounded),
isAnySyncInProgress
? Icons.sync_rounded
: (hasPendingData ? Icons.graphic_eq_rounded : Icons.check_rounded),
color: Colors.white,
size: 36,
),
Expand All @@ -106,7 +95,7 @@ class SyncBottomSheet extends StatelessWidget {

// Title
Text(
isSyncing ? 'Catching Up' : (hasPendingData ? 'Recordings Available' : 'All Synced'),
isAnySyncInProgress ? 'Syncing recordings' : (hasPendingData ? 'Recordings to sync' : 'All caught up'),
style: const TextStyle(
color: Colors.white,
fontSize: 24,
Expand All @@ -116,47 +105,29 @@ class SyncBottomSheet extends StatelessWidget {
const SizedBox(height: 12),

// Description
if (isSyncing) ...[
RichText(
if (isAnySyncInProgress) ...[
Text(
'We\'ll keep syncing your recordings in the background.',
textAlign: TextAlign.center,
text: TextSpan(
style: TextStyle(
color: Colors.grey.shade400,
fontSize: 16,
height: 1.4,
),
children: [
const TextSpan(text: 'Processing audio and generating summaries from '),
TextSpan(
text: timeAgo.isNotEmpty ? timeAgo : 'earlier',
style: const TextStyle(color: Colors.deepPurpleAccent),
),
const TextSpan(text: '.'),
],
style: TextStyle(
color: Colors.grey.shade400,
fontSize: 16,
height: 1.4,
),
),
] else if (hasPendingData) ...[
RichText(
Text(
'You have recordings that aren\'t synced yet.',
textAlign: TextAlign.center,
text: TextSpan(
style: TextStyle(
color: Colors.grey.shade400,
fontSize: 16,
height: 1.4,
),
children: [
const TextSpan(text: 'You have '),
TextSpan(
text: _formatDuration(pendingFlashPages.fold(0, (sum, w) => sum + w.seconds)),
style: const TextStyle(color: Colors.deepPurpleAccent),
),
const TextSpan(text: ' of offline recordings to sync.'),
],
style: TextStyle(
color: Colors.grey.shade400,
fontSize: 16,
height: 1.4,
),
),
] else ...[
Text(
'Your pendant is fully synced with the cloud.',
'Everything is already synced.',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey.shade400,
Expand All @@ -169,9 +140,11 @@ class SyncBottomSheet extends StatelessWidget {
const SizedBox(height: 8),

// Explanation text
if ((isSyncing || hasPendingData) && isLimitless) ...[
if ((isAnySyncInProgress || hasPendingData) && isLimitless) ...[
Text(
'This happens when your pendant is away from your phone for an extended period, the app is closed, or if Bluetooth is turned off on your phone.',
isAnySyncInProgress
? 'We\'re catching up on earlier recordings. New moments are still being saved and will appear once sync finishes.'
: 'This usually happens when your pendant and phone were apart or Bluetooth was off.',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey.shade600,
Expand Down Expand Up @@ -217,7 +190,7 @@ class SyncBottomSheet extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isSyncing
isAnySyncInProgress
? 'Syncing in progress'
: (hasPendingData ? 'Ready to sync' : 'Pendant is up to date'),
style: const TextStyle(
Expand All @@ -228,11 +201,10 @@ class SyncBottomSheet extends StatelessWidget {
),
const SizedBox(height: 2),
Text(
isSyncing
? _getSyncStatusText(progress)
: (hasPendingData
? '${_formatDuration(pendingFlashPages.fold(0, (sum, w) => sum + w.seconds))} waiting'
: 'All audio has been sent to phone'),
isAnySyncInProgress
? _getSyncStatusText(
progress, isSyncingFromPendant, isUploadingToCloud, hasOrphanedFiles, orphanedCount)
: (hasPendingData ? 'Tap Sync to start' : 'All recordings are synced'),
style: TextStyle(
color: Colors.grey.shade500,
fontSize: 13,
Expand All @@ -242,7 +214,7 @@ class SyncBottomSheet extends StatelessWidget {
),
),
// Status indicator or button
if (isSyncing) ...[
if (isAnySyncInProgress) ...[
const SizedBox(
width: 28,
height: 28,
Expand Down Expand Up @@ -287,68 +259,6 @@ class SyncBottomSheet extends StatelessWidget {
),
),

// Orphaned files card - files saved to phone but not yet uploaded
if ((hasOrphanedFiles || isUploadingOrphans) && !isSyncing) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.phone_android_rounded, color: Colors.blue.shade400, size: 22),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isUploadingOrphans
? 'Uploading to cloud...'
: '$orphanedCount file${orphanedCount > 1 ? 's' : ''} saved on phone',
style: TextStyle(color: Colors.blue.shade300, fontSize: 14, fontWeight: FontWeight.w600),
),
const SizedBox(height: 2),
Text(
isUploadingOrphans ? 'Processing saved recordings' : 'Ready to upload to cloud',
style: TextStyle(color: Colors.blue.shade400.withOpacity(0.7), fontSize: 12),
),
],
),
),
if (isUploadingOrphans)
const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.blue,
),
)
else
ElevatedButton(
onPressed: () {
flashPageSync.uploadOrphanedFiles();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 0,
),
child: const Text('Upload', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
),
],
),
),
],

// Not connected warning
if (!isConnected && isLimitless) ...[
const SizedBox(height: 16),
Expand Down Expand Up @@ -380,19 +290,19 @@ class SyncBottomSheet extends StatelessWidget {
);
}

String _formatDuration(int seconds) {
if (seconds < 60) return '${seconds}s';
if (seconds < 3600) return '${(seconds / 60).round()} min';
final hours = seconds ~/ 3600;
final mins = (seconds % 3600) ~/ 60;
return '${hours}h ${mins}m';
}

String _getSyncStatusText(double progress) {
if (progress <= 0.4) {
return 'Syncing from device...';
String _getSyncStatusText(
double progress, bool isSyncingFromPendant, bool isUploadingToCloud, bool hasOrphanedFiles, int orphanedCount) {
if (isSyncingFromPendant) {
return 'Downloading your recordings…';
} else if (isUploadingToCloud) {
if (hasOrphanedFiles && orphanedCount > 0) {
return 'Uploading to cloud… ($orphanedCount file${orphanedCount > 1 ? 's' : ''} remaining)';
}
return 'Uploading to cloud…';
} else if (progress <= 0.4) {
return 'Downloading your recordings…';
} else {
return 'Processing audio...';
return 'Processing your audio';
}
}
}
8 changes: 8 additions & 0 deletions app/lib/providers/sync_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ class SyncProvider extends ChangeNotifier implements IWalServiceListener, IWalSy
String? get syncError => _syncState.errorMessage;
Wal? get failedWal => _syncState.failedWal;

// Flash page (Limitless) sync states - distinct phases
// isSyncingFromPendant: true when receiving data from pendant (pendant → phone)
// isUploadingToCloud: true when uploading files to cloud (phone → cloud)
bool get isSyncingFromPendant => _walService.getSyncs().flashPage.isSyncing;
bool get isUploadingToCloud => _walService.getSyncs().flashPage.isUploading;
bool get hasOrphanedFiles => _walService.getSyncs().flashPage.hasOrphanedFiles;
int get orphanedFilesCount => _walService.getSyncs().flashPage.orphanedFilesCount;

// Audio playback delegates
String? get currentPlayingWalId => _audioPlayerUtils.currentPlayingId;
bool get isProcessingAudio => _audioPlayerUtils.isProcessingAudio;
Expand Down
Loading