diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json
index 324c9069fd..bb4f3efd26 100644
--- a/mobile/assets/i18n/en-US.json
+++ b/mobile/assets/i18n/en-US.json
@@ -588,5 +588,16 @@
   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
   "viewer_remove_from_stack": "Remove from Stack",
   "viewer_stack_use_as_main_asset": "Use as Main Asset",
-  "viewer_unstack": "Un-Stack"
-}
\ No newline at end of file
+  "viewer_unstack": "Un-Stack",
+  "downloading_media": "Downloading media",
+  "download_finished": "Download finished",
+  "download_filename": "file: {}",
+  "downloading": "Downloading...",
+  "download_complete": "Download complete",
+  "download_failed": "Download failed",
+  "download_canceled": "Download canceled",
+  "download_paused": "Download paused",
+  "download_enqueue": "Download enqueued",
+  "download_notfound": "Download not found",
+  "download_waiting_to_retry": "Waiting to retry"
+}
diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index 3b361c4e19..6a9d34ab83 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -1,4 +1,6 @@
 PODS:
+  - background_downloader (0.0.1):
+    - Flutter
   - connectivity_plus (0.0.1):
     - Flutter
     - ReachabilitySwift
@@ -99,6 +101,7 @@ PODS:
     - Flutter
 
 DEPENDENCIES:
+  - background_downloader (from `.symlinks/plugins/background_downloader/ios`)
   - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
   - file_picker (from `.symlinks/plugins/file_picker/ios`)
@@ -137,6 +140,8 @@ SPEC REPOS:
     - Toast
 
 EXTERNAL SOURCES:
+  background_downloader:
+    :path: ".symlinks/plugins/background_downloader/ios"
   connectivity_plus:
     :path: ".symlinks/plugins/connectivity_plus/ios"
   device_info_plus:
@@ -189,6 +194,7 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/wakelock_plus/ios"
 
 SPEC CHECKSUMS:
+  background_downloader: 9f788ffc5de45acf87d6380e91ca0841066c18cf
   connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
   device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
   DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
diff --git a/mobile/lib/interfaces/download.interface.dart b/mobile/lib/interfaces/download.interface.dart
new file mode 100644
index 0000000000..dc4f0f57f8
--- /dev/null
+++ b/mobile/lib/interfaces/download.interface.dart
@@ -0,0 +1,14 @@
+import 'package:background_downloader/background_downloader.dart';
+
+abstract interface class IDownloadRepository {
+  void Function(TaskStatusUpdate)? onImageDownloadStatus;
+  void Function(TaskStatusUpdate)? onVideoDownloadStatus;
+  void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
+  void Function(TaskProgressUpdate)? onTaskProgress;
+
+  Future<List<TaskRecord>> getLiveVideoTasks();
+  Future<bool> download(DownloadTask task);
+  Future<bool> cancel(String id);
+  Future<void> deleteAllTrackingRecords();
+  Future<void> deleteRecordsWithIds(List<String> id);
+}
diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart
index dc1df746cb..40eda30204 100644
--- a/mobile/lib/main.dart
+++ b/mobile/lib/main.dart
@@ -1,6 +1,7 @@
 import 'dart:async';
 import 'dart:io';
 
+import 'package:background_downloader/background_downloader.dart';
 import 'package:device_info_plus/device_info_plus.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/foundation.dart';
@@ -9,6 +10,7 @@ import 'package:flutter/services.dart';
 import 'package:flutter_displaymode/flutter_displaymode.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/utils/download.dart';
 import 'package:timezone/data/latest.dart';
 import 'package:immich_mobile/constants/locales.dart';
 import 'package:immich_mobile/services/background.service.dart';
@@ -72,7 +74,6 @@ Future<void> initApp() async {
   var log = Logger("ImmichErrorLogger");
 
   FlutterError.onError = (details) {
-    debugPrint("FlutterError - Catch all: $details");
     FlutterError.presentError(details);
     log.severe(
       'FlutterError - Catch all',
@@ -82,11 +83,29 @@ Future<void> initApp() async {
   };
 
   PlatformDispatcher.instance.onError = (error, stack) {
+    debugPrint("FlutterError - Catch all: $error");
     log.severe('PlatformDispatcher - Catch all', error, stack);
     return true;
   };
 
   initializeTimeZones();
+
+  FileDownloader().configureNotification(
+    running: TaskNotification(
+      'downloading_media'.tr(),
+      'file: {filename}',
+    ),
+    complete: TaskNotification(
+      'download_finished'.tr(),
+      'file: {filename}',
+    ),
+    progressBar: true,
+  );
+
+  FileDownloader().trackTasksInGroup(
+    downloadGroupLivePhoto,
+    markDownloadedComplete: false,
+  );
 }
 
 Future<Isar> loadDb() async {
@@ -188,8 +207,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
 
   @override
   Widget build(BuildContext context) {
-    var router = ref.watch(appRouterProvider);
-    var immichTheme = ref.watch(immichThemeProvider);
+    final router = ref.watch(appRouterProvider);
+    final immichTheme = ref.watch(immichThemeProvider);
 
     return MaterialApp(
       localizationsDelegates: context.localizationDelegates,
diff --git a/mobile/lib/models/asset_viewer/asset_viewer_page_state.model.dart b/mobile/lib/models/asset_viewer/asset_viewer_page_state.model.dart
deleted file mode 100644
index 0a354781f8..0000000000
--- a/mobile/lib/models/asset_viewer/asset_viewer_page_state.model.dart
+++ /dev/null
@@ -1,55 +0,0 @@
-import 'dart:convert';
-
-enum DownloadAssetStatus { idle, loading, success, error }
-
-class AssetViewerPageState {
-  // enum
-  final DownloadAssetStatus downloadAssetStatus;
-
-  AssetViewerPageState({
-    required this.downloadAssetStatus,
-  });
-
-  AssetViewerPageState copyWith({
-    DownloadAssetStatus? downloadAssetStatus,
-  }) {
-    return AssetViewerPageState(
-      downloadAssetStatus: downloadAssetStatus ?? this.downloadAssetStatus,
-    );
-  }
-
-  Map<String, dynamic> toMap() {
-    final result = <String, dynamic>{};
-
-    result.addAll({'downloadAssetStatus': downloadAssetStatus.index});
-
-    return result;
-  }
-
-  factory AssetViewerPageState.fromMap(Map<String, dynamic> map) {
-    return AssetViewerPageState(
-      downloadAssetStatus:
-          DownloadAssetStatus.values[map['downloadAssetStatus'] ?? 0],
-    );
-  }
-
-  String toJson() => json.encode(toMap());
-
-  factory AssetViewerPageState.fromJson(String source) =>
-      AssetViewerPageState.fromMap(json.decode(source));
-
-  @override
-  String toString() =>
-      'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)';
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-
-    return other is AssetViewerPageState &&
-        other.downloadAssetStatus == downloadAssetStatus;
-  }
-
-  @override
-  int get hashCode => downloadAssetStatus.hashCode;
-}
diff --git a/mobile/lib/models/download/download_state.model.dart b/mobile/lib/models/download/download_state.model.dart
new file mode 100644
index 0000000000..edd2fa183e
--- /dev/null
+++ b/mobile/lib/models/download/download_state.model.dart
@@ -0,0 +1,109 @@
+// ignore_for_file: public_member_api_docs, sort_constructors_first
+import 'dart:convert';
+
+import 'package:background_downloader/background_downloader.dart';
+import 'package:collection/collection.dart';
+
+class DownloadInfo {
+  final String fileName;
+  final double progress;
+  // enum
+  final TaskStatus status;
+
+  DownloadInfo({
+    required this.fileName,
+    required this.progress,
+    required this.status,
+  });
+
+  DownloadInfo copyWith({
+    String? fileName,
+    double? progress,
+    TaskStatus? status,
+  }) {
+    return DownloadInfo(
+      fileName: fileName ?? this.fileName,
+      progress: progress ?? this.progress,
+      status: status ?? this.status,
+    );
+  }
+
+  Map<String, dynamic> toMap() {
+    return <String, dynamic>{
+      'fileName': fileName,
+      'progress': progress,
+      'status': status.index,
+    };
+  }
+
+  factory DownloadInfo.fromMap(Map<String, dynamic> map) {
+    return DownloadInfo(
+      fileName: map['fileName'] as String,
+      progress: map['progress'] as double,
+      status: TaskStatus.values[map['status'] as int],
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory DownloadInfo.fromJson(String source) =>
+      DownloadInfo.fromMap(json.decode(source) as Map<String, dynamic>);
+
+  @override
+  String toString() =>
+      'DownloadInfo(fileName: $fileName, progress: $progress, status: $status)';
+
+  @override
+  bool operator ==(covariant DownloadInfo other) {
+    if (identical(this, other)) return true;
+
+    return other.fileName == fileName &&
+        other.progress == progress &&
+        other.status == status;
+  }
+
+  @override
+  int get hashCode => fileName.hashCode ^ progress.hashCode ^ status.hashCode;
+}
+
+class DownloadState {
+  // enum
+  final TaskStatus downloadStatus;
+  final Map<String, DownloadInfo> taskProgress;
+  final bool showProgress;
+  DownloadState({
+    required this.downloadStatus,
+    required this.taskProgress,
+    required this.showProgress,
+  });
+
+  DownloadState copyWith({
+    TaskStatus? downloadStatus,
+    Map<String, DownloadInfo>? taskProgress,
+    bool? showProgress,
+  }) {
+    return DownloadState(
+      downloadStatus: downloadStatus ?? this.downloadStatus,
+      taskProgress: taskProgress ?? this.taskProgress,
+      showProgress: showProgress ?? this.showProgress,
+    );
+  }
+
+  @override
+  String toString() =>
+      'DownloadState(downloadStatus: $downloadStatus, taskProgress: $taskProgress, showProgress: $showProgress)';
+
+  @override
+  bool operator ==(covariant DownloadState other) {
+    if (identical(this, other)) return true;
+    final mapEquals = const DeepCollectionEquality().equals;
+
+    return other.downloadStatus == downloadStatus &&
+        mapEquals(other.taskProgress, taskProgress) &&
+        other.showProgress == showProgress;
+  }
+
+  @override
+  int get hashCode =>
+      downloadStatus.hashCode ^ taskProgress.hashCode ^ showProgress.hashCode;
+}
diff --git a/mobile/lib/models/download/livephotos_medatada.model.dart b/mobile/lib/models/download/livephotos_medatada.model.dart
new file mode 100644
index 0000000000..9c0c7ae4e9
--- /dev/null
+++ b/mobile/lib/models/download/livephotos_medatada.model.dart
@@ -0,0 +1,60 @@
+// ignore_for_file: public_member_api_docs, sort_constructors_first
+import 'dart:convert';
+
+enum LivePhotosPart {
+  video,
+  image,
+}
+
+class LivePhotosMetadata {
+  // enum
+  LivePhotosPart part;
+
+  String id;
+  LivePhotosMetadata({
+    required this.part,
+    required this.id,
+  });
+
+  LivePhotosMetadata copyWith({
+    LivePhotosPart? part,
+    String? id,
+  }) {
+    return LivePhotosMetadata(
+      part: part ?? this.part,
+      id: id ?? this.id,
+    );
+  }
+
+  Map<String, dynamic> toMap() {
+    return <String, dynamic>{
+      'part': part.index,
+      'id': id,
+    };
+  }
+
+  factory LivePhotosMetadata.fromMap(Map<String, dynamic> map) {
+    return LivePhotosMetadata(
+      part: LivePhotosPart.values[map['part'] as int],
+      id: map['id'] as String,
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory LivePhotosMetadata.fromJson(String source) =>
+      LivePhotosMetadata.fromMap(json.decode(source) as Map<String, dynamic>);
+
+  @override
+  String toString() => 'LivePhotosMetadata(part: $part, id: $id)';
+
+  @override
+  bool operator ==(covariant LivePhotosMetadata other) {
+    if (identical(this, other)) return true;
+
+    return other.part == part && other.id == id;
+  }
+
+  @override
+  int get hashCode => part.hashCode ^ id.hashCode;
+}
diff --git a/mobile/lib/pages/common/download_panel.dart b/mobile/lib/pages/common/download_panel.dart
new file mode 100644
index 0000000000..95cefd742a
--- /dev/null
+++ b/mobile/lib/pages/common/download_panel.dart
@@ -0,0 +1,150 @@
+import 'package:background_downloader/background_downloader.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
+
+class DownloadPanel extends ConsumerWidget {
+  const DownloadPanel({
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final showProgress = ref.watch(
+      downloadStateProvider.select((state) => state.showProgress),
+    );
+
+    final tasks = ref
+        .watch(
+          downloadStateProvider.select((state) => state.taskProgress),
+        )
+        .entries
+        .toList();
+
+    onCancelDownload(String id) {
+      ref.watch(downloadStateProvider.notifier).cancelDownload(id);
+    }
+
+    return Positioned(
+      bottom: 140,
+      left: 16,
+      child: AnimatedSwitcher(
+        duration: const Duration(milliseconds: 300),
+        child: showProgress
+            ? ConstrainedBox(
+                constraints:
+                    BoxConstraints.loose(Size(context.width - 32, 300)),
+                child: ListView.builder(
+                  shrinkWrap: true,
+                  itemCount: tasks.length,
+                  itemBuilder: (context, index) {
+                    final task = tasks[index];
+                    return DownloadTaskTile(
+                      progress: task.value.progress,
+                      fileName: task.value.fileName,
+                      status: task.value.status,
+                      onCancelDownload: () => onCancelDownload(task.key),
+                    );
+                  },
+                ),
+              )
+            : const SizedBox.shrink(key: ValueKey('no_progress')),
+      ),
+    );
+  }
+}
+
+class DownloadTaskTile extends StatelessWidget {
+  final double progress;
+  final String fileName;
+  final TaskStatus status;
+  final VoidCallback onCancelDownload;
+
+  const DownloadTaskTile({
+    super.key,
+    required this.progress,
+    required this.fileName,
+    required this.status,
+    required this.onCancelDownload,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final progressPercent = (progress * 100).round();
+
+    getStatusText() {
+      switch (status) {
+        case TaskStatus.running:
+          return 'downloading'.tr();
+        case TaskStatus.complete:
+          return 'download_complete'.tr();
+        case TaskStatus.failed:
+          return 'download_failed'.tr();
+        case TaskStatus.canceled:
+          return 'download_canceled'.tr();
+        case TaskStatus.paused:
+          return 'download_paused'.tr();
+        case TaskStatus.enqueued:
+          return 'download_enqueue'.tr();
+        case TaskStatus.notFound:
+          return 'download_notfound'.tr();
+        case TaskStatus.waitingToRetry:
+          return 'download_waiting_to_retry'.tr();
+      }
+    }
+
+    return SizedBox(
+      key: const ValueKey('download_progress'),
+      width: MediaQuery.of(context).size.width - 32,
+      child: Card(
+        clipBehavior: Clip.antiAlias,
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(16),
+        ),
+        child: ListTile(
+          minVerticalPadding: 18,
+          leading: const Icon(Icons.video_file_outlined),
+          title: Text(
+            getStatusText(),
+            style: context.textTheme.labelLarge,
+          ),
+          trailing: IconButton(
+            icon: Icon(Icons.close, color: context.colorScheme.onError),
+            onPressed: onCancelDownload,
+            style: ElevatedButton.styleFrom(
+              backgroundColor: context.colorScheme.error.withAlpha(200),
+            ),
+          ),
+          subtitle: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Text(
+                fileName,
+                style: context.textTheme.labelMedium,
+              ),
+              Row(
+                children: [
+                  Expanded(
+                    child: LinearProgressIndicator(
+                      minHeight: 8.0,
+                      value: progress,
+                      borderRadius:
+                          const BorderRadius.all(Radius.circular(10.0)),
+                    ),
+                  ),
+                  const SizedBox(width: 8),
+                  Text(
+                    '$progressPercent%',
+                    style: context.textTheme.labelSmall,
+                  ),
+                ],
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart
index 1434d1cca5..57c75ca84d 100644
--- a/mobile/lib/pages/common/gallery_viewer.page.dart
+++ b/mobile/lib/pages/common/gallery_viewer.page.dart
@@ -11,6 +11,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/constants/constants.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/pages/common/download_panel.dart';
 import 'package:immich_mobile/pages/common/video_viewer.page.dart';
 import 'package:immich_mobile/providers/app_settings.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
@@ -421,6 +422,7 @@ class GalleryViewerPage extends HookConsumerWidget {
                 ],
               ),
             ),
+            const DownloadPanel(),
           ],
         ),
       ),
diff --git a/mobile/lib/providers/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart
new file mode 100644
index 0000000000..d4aa2823b5
--- /dev/null
+++ b/mobile/lib/providers/asset_viewer/download.provider.dart
@@ -0,0 +1,191 @@
+import 'package:background_downloader/background_downloader.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:fluttertoast/fluttertoast.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/models/download/download_state.model.dart';
+import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
+import 'package:immich_mobile/services/download.service.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/services/share.service.dart';
+import 'package:immich_mobile/widgets/common/immich_toast.dart';
+import 'package:immich_mobile/widgets/common/share_dialog.dart';
+
+class DownloadStateNotifier extends StateNotifier<DownloadState> {
+  final DownloadService _downloadService;
+  final ShareService _shareService;
+
+  DownloadStateNotifier(
+    this._downloadService,
+    this._shareService,
+  ) : super(
+          DownloadState(
+            downloadStatus: TaskStatus.complete,
+            showProgress: false,
+            taskProgress: <String, DownloadInfo>{},
+          ),
+        ) {
+    _downloadService.onImageDownloadStatus = _downloadImageCallback;
+    _downloadService.onVideoDownloadStatus = _downloadVideoCallback;
+    _downloadService.onLivePhotoDownloadStatus = _downloadLivePhotoCallback;
+    _downloadService.onTaskProgress = _taskProgressCallback;
+  }
+
+  void _updateDownloadStatus(String taskId, TaskStatus status) {
+    if (status == TaskStatus.canceled) {
+      return;
+    }
+
+    state = state.copyWith(
+      taskProgress: <String, DownloadInfo>{}
+        ..addAll(state.taskProgress)
+        ..addAll({
+          taskId: DownloadInfo(
+            progress: state.taskProgress[taskId]?.progress ?? 0,
+            fileName: state.taskProgress[taskId]?.fileName ?? '',
+            status: status,
+          ),
+        }),
+    );
+  }
+
+  // Download live photo callback
+  void _downloadLivePhotoCallback(TaskStatusUpdate update) {
+    _updateDownloadStatus(update.task.taskId, update.status);
+
+    switch (update.status) {
+      case TaskStatus.complete:
+        if (update.task.metaData.isEmpty) {
+          return;
+        }
+        final livePhotosId =
+            LivePhotosMetadata.fromJson(update.task.metaData).id;
+        _downloadService.saveLivePhotos(update.task, livePhotosId);
+        _onDownloadComplete(update.task.taskId);
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  // Download image callback
+  void _downloadImageCallback(TaskStatusUpdate update) {
+    _updateDownloadStatus(update.task.taskId, update.status);
+
+    switch (update.status) {
+      case TaskStatus.complete:
+        _downloadService.saveImage(update.task);
+        _onDownloadComplete(update.task.taskId);
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  // Download video callback
+  void _downloadVideoCallback(TaskStatusUpdate update) {
+    _updateDownloadStatus(update.task.taskId, update.status);
+
+    switch (update.status) {
+      case TaskStatus.complete:
+        _downloadService.saveVideo(update.task);
+        _onDownloadComplete(update.task.taskId);
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  void _taskProgressCallback(TaskProgressUpdate update) {
+    // Ignore if the task is cancled or completed
+    if (update.progress == -2 || update.progress == -1) {
+      return;
+    }
+
+    state = state.copyWith(
+      showProgress: true,
+      taskProgress: <String, DownloadInfo>{}
+        ..addAll(state.taskProgress)
+        ..addAll({
+          update.task.taskId: DownloadInfo(
+            progress: update.progress,
+            fileName: update.task.filename,
+            status: TaskStatus.running,
+          ),
+        }),
+    );
+  }
+
+  void _onDownloadComplete(String id) {
+    Future.delayed(const Duration(seconds: 2), () {
+      state = state.copyWith(
+        taskProgress: <String, DownloadInfo>{}
+          ..addAll(state.taskProgress)
+          ..remove(id),
+      );
+
+      if (state.taskProgress.isEmpty) {
+        state = state.copyWith(
+          showProgress: false,
+        );
+      }
+    });
+  }
+
+  void downloadAsset(Asset asset, BuildContext context) async {
+    await _downloadService.download(asset);
+  }
+
+  void cancelDownload(String id) async {
+    final isCanceled = await _downloadService.cancelDownload(id);
+
+    if (isCanceled) {
+      state = state.copyWith(
+        taskProgress: <String, DownloadInfo>{}
+          ..addAll(state.taskProgress)
+          ..remove(id),
+      );
+    }
+
+    if (state.taskProgress.isEmpty) {
+      state = state.copyWith(
+        showProgress: false,
+      );
+    }
+  }
+
+  void shareAsset(Asset asset, BuildContext context) async {
+    showDialog(
+      context: context,
+      builder: (BuildContext buildContext) {
+        _shareService.shareAsset(asset, context).then(
+          (bool status) {
+            if (!status) {
+              ImmichToast.show(
+                context: context,
+                msg: 'image_viewer_page_state_provider_share_error'.tr(),
+                toastType: ToastType.error,
+                gravity: ToastGravity.BOTTOM,
+              );
+            }
+            buildContext.pop();
+          },
+        );
+        return const ShareDialog();
+      },
+      barrierDismissible: false,
+    );
+  }
+}
+
+final downloadStateProvider =
+    StateNotifierProvider<DownloadStateNotifier, DownloadState>(
+  ((ref) => DownloadStateNotifier(
+        ref.watch(downloadServiceProvider),
+        ref.watch(shareServiceProvider),
+      )),
+);
diff --git a/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart b/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart
deleted file mode 100644
index 631011f200..0000000000
--- a/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart
+++ /dev/null
@@ -1,99 +0,0 @@
-import 'dart:io';
-
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
-import 'package:fluttertoast/fluttertoast.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/services/album.service.dart';
-import 'package:immich_mobile/models/asset_viewer/asset_viewer_page_state.model.dart';
-import 'package:immich_mobile/services/image_viewer.service.dart';
-import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/services/share.service.dart';
-import 'package:immich_mobile/widgets/common/immich_toast.dart';
-import 'package:immich_mobile/widgets/common/share_dialog.dart';
-
-class ImageViewerStateNotifier extends StateNotifier<AssetViewerPageState> {
-  final ImageViewerService _imageViewerService;
-  final ShareService _shareService;
-  final AlbumService _albumService;
-
-  ImageViewerStateNotifier(
-    this._imageViewerService,
-    this._shareService,
-    this._albumService,
-  ) : super(
-          AssetViewerPageState(
-            downloadAssetStatus: DownloadAssetStatus.idle,
-          ),
-        );
-
-  void downloadAsset(Asset asset, BuildContext context) async {
-    state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
-
-    ImmichToast.show(
-      context: context,
-      msg: 'download_started'.tr(),
-      toastType: ToastType.info,
-      gravity: ToastGravity.BOTTOM,
-    );
-
-    bool isSuccess = await _imageViewerService.downloadAsset(asset);
-
-    if (isSuccess) {
-      state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.success);
-
-      ImmichToast.show(
-        context: context,
-        msg: Platform.isAndroid
-            ? 'download_sucess_android'.tr()
-            : 'download_sucess'.tr(),
-        toastType: ToastType.success,
-        gravity: ToastGravity.BOTTOM,
-      );
-      _albumService.refreshDeviceAlbums();
-    } else {
-      state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error);
-      ImmichToast.show(
-        context: context,
-        msg: 'download_error'.tr(),
-        toastType: ToastType.error,
-        gravity: ToastGravity.BOTTOM,
-      );
-    }
-
-    state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
-  }
-
-  void shareAsset(Asset asset, BuildContext context) async {
-    showDialog(
-      context: context,
-      builder: (BuildContext buildContext) {
-        _shareService.shareAsset(asset, context).then(
-          (bool status) {
-            if (!status) {
-              ImmichToast.show(
-                context: context,
-                msg: 'image_viewer_page_state_provider_share_error'.tr(),
-                toastType: ToastType.error,
-                gravity: ToastGravity.BOTTOM,
-              );
-            }
-            buildContext.pop();
-          },
-        );
-        return const ShareDialog();
-      },
-      barrierDismissible: false,
-    );
-  }
-}
-
-final imageViewerStateProvider =
-    StateNotifierProvider<ImageViewerStateNotifier, AssetViewerPageState>(
-  ((ref) => ImageViewerStateNotifier(
-        ref.watch(imageViewerServiceProvider),
-        ref.watch(shareServiceProvider),
-        ref.watch(albumServiceProvider),
-      )),
-);
diff --git a/mobile/lib/repositories/download.repository.dart b/mobile/lib/repositories/download.repository.dart
new file mode 100644
index 0000000000..5b42f66b02
--- /dev/null
+++ b/mobile/lib/repositories/download.repository.dart
@@ -0,0 +1,68 @@
+import 'package:background_downloader/background_downloader.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/interfaces/download.interface.dart';
+import 'package:immich_mobile/utils/download.dart';
+
+final downloadRepositoryProvider = Provider((ref) => DownloadRepository());
+
+class DownloadRepository implements IDownloadRepository {
+  @override
+  void Function(TaskStatusUpdate)? onImageDownloadStatus;
+
+  @override
+  void Function(TaskStatusUpdate)? onVideoDownloadStatus;
+
+  @override
+  void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
+
+  @override
+  void Function(TaskProgressUpdate)? onTaskProgress;
+
+  DownloadRepository() {
+    FileDownloader().registerCallbacks(
+      group: downloadGroupImage,
+      taskStatusCallback: (update) => onImageDownloadStatus?.call(update),
+      taskProgressCallback: (update) => onTaskProgress?.call(update),
+    );
+
+    FileDownloader().registerCallbacks(
+      group: downloadGroupVideo,
+      taskStatusCallback: (update) => onVideoDownloadStatus?.call(update),
+      taskProgressCallback: (update) => onTaskProgress?.call(update),
+    );
+
+    FileDownloader().registerCallbacks(
+      group: downloadGroupLivePhoto,
+      taskStatusCallback: (update) => onLivePhotoDownloadStatus?.call(update),
+      taskProgressCallback: (update) => onTaskProgress?.call(update),
+    );
+  }
+
+  @override
+  Future<bool> download(DownloadTask task) {
+    return FileDownloader().enqueue(task);
+  }
+
+  @override
+  Future<void> deleteAllTrackingRecords() {
+    return FileDownloader().database.deleteAllRecords();
+  }
+
+  @override
+  Future<bool> cancel(String id) {
+    return FileDownloader().cancelTaskWithId(id);
+  }
+
+  @override
+  Future<List<TaskRecord>> getLiveVideoTasks() {
+    return FileDownloader().database.allRecordsWithStatus(
+          TaskStatus.complete,
+          group: downloadGroupLivePhoto,
+        );
+  }
+
+  @override
+  Future<void> deleteRecordsWithIds(List<String> ids) {
+    return FileDownloader().database.deleteRecordsWithIds(ids);
+  }
+}
diff --git a/mobile/lib/services/download.service.dart b/mobile/lib/services/download.service.dart
new file mode 100644
index 0000000000..996cbe61f1
--- /dev/null
+++ b/mobile/lib/services/download.service.dart
@@ -0,0 +1,193 @@
+import 'dart:io';
+
+import 'package:background_downloader/background_downloader.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/interfaces/download.interface.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
+import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
+import 'package:immich_mobile/repositories/download.repository.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
+import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/utils/download.dart';
+
+final downloadServiceProvider = Provider(
+  (ref) => DownloadService(
+    ref.watch(fileMediaRepositoryProvider),
+    ref.watch(downloadRepositoryProvider),
+  ),
+);
+
+class DownloadService {
+  final IDownloadRepository _downloadRepository;
+  final IFileMediaRepository _fileMediaRepository;
+  void Function(TaskStatusUpdate)? onImageDownloadStatus;
+  void Function(TaskStatusUpdate)? onVideoDownloadStatus;
+  void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
+  void Function(TaskProgressUpdate)? onTaskProgress;
+
+  DownloadService(
+    this._fileMediaRepository,
+    this._downloadRepository,
+  ) {
+    _downloadRepository.onImageDownloadStatus = _onImageDownloadCallback;
+    _downloadRepository.onVideoDownloadStatus = _onVideoDownloadCallback;
+    _downloadRepository.onLivePhotoDownloadStatus =
+        _onLivePhotoDownloadCallback;
+    _downloadRepository.onTaskProgress = _onTaskProgressCallback;
+  }
+
+  void _onTaskProgressCallback(TaskProgressUpdate update) {
+    onTaskProgress?.call(update);
+  }
+
+  void _onImageDownloadCallback(TaskStatusUpdate update) {
+    onImageDownloadStatus?.call(update);
+  }
+
+  void _onVideoDownloadCallback(TaskStatusUpdate update) {
+    onVideoDownloadStatus?.call(update);
+  }
+
+  void _onLivePhotoDownloadCallback(TaskStatusUpdate update) {
+    onLivePhotoDownloadStatus?.call(update);
+  }
+
+  Future<bool> saveImage(Task task) async {
+    final filePath = await task.filePath();
+    final title = task.filename;
+    final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
+    final data = await File(filePath).readAsBytes();
+
+    final Asset? resultAsset = await _fileMediaRepository.saveImage(
+      data,
+      title: title,
+      relativePath: relativePath,
+    );
+
+    return resultAsset != null;
+  }
+
+  Future<bool> saveVideo(Task task) async {
+    final filePath = await task.filePath();
+    final title = task.filename;
+    final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
+    final file = File(filePath);
+
+    final Asset? resultAsset = await _fileMediaRepository.saveVideo(
+      file,
+      title: title,
+      relativePath: relativePath,
+    );
+
+    return resultAsset != null;
+  }
+
+  Future<bool> saveLivePhotos(
+    Task task,
+    String livePhotosId,
+  ) async {
+    try {
+      final records = await _downloadRepository.getLiveVideoTasks();
+      if (records.length < 2) {
+        return false;
+      }
+
+      final imageRecord = records.firstWhere(
+        (record) {
+          final metadata = LivePhotosMetadata.fromJson(record.task.metaData);
+          return metadata.id == livePhotosId &&
+              metadata.part == LivePhotosPart.image;
+        },
+      );
+
+      final videoRecord = records.firstWhere((record) {
+        final metadata = LivePhotosMetadata.fromJson(record.task.metaData);
+        return metadata.id == livePhotosId &&
+            metadata.part == LivePhotosPart.video;
+      });
+
+      final imageFilePath = await imageRecord.task.filePath();
+      final videoFilePath = await videoRecord.task.filePath();
+
+      final resultAsset = await _fileMediaRepository.saveLivePhoto(
+        image: File(imageFilePath),
+        video: File(videoFilePath),
+        title: task.filename,
+      );
+
+      await _downloadRepository.deleteRecordsWithIds([
+        imageRecord.task.taskId,
+        videoRecord.task.taskId,
+      ]);
+
+      return resultAsset != null;
+    } catch (error) {
+      debugPrint("Error saving live photo: $error");
+      return false;
+    }
+  }
+
+  Future<bool> cancelDownload(String id) async {
+    return await FileDownloader().cancelTaskWithId(id);
+  }
+
+  Future<void> download(Asset asset) async {
+    if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) {
+      await _downloadRepository.download(
+        _buildDownloadTask(
+          asset.remoteId!,
+          asset.fileName,
+          group: downloadGroupLivePhoto,
+          metadata: LivePhotosMetadata(
+            part: LivePhotosPart.image,
+            id: asset.remoteId!,
+          ).toJson(),
+        ),
+      );
+
+      await _downloadRepository.download(
+        _buildDownloadTask(
+          asset.livePhotoVideoId!,
+          asset.fileName.toUpperCase().replaceAll(".HEIC", '.MOV'),
+          group: downloadGroupLivePhoto,
+          metadata: LivePhotosMetadata(
+            part: LivePhotosPart.video,
+            id: asset.remoteId!,
+          ).toJson(),
+        ),
+      );
+    } else {
+      await _downloadRepository.download(
+        _buildDownloadTask(
+          asset.remoteId!,
+          asset.fileName,
+          group: asset.isImage ? downloadGroupImage : downloadGroupVideo,
+        ),
+      );
+    }
+  }
+
+  DownloadTask _buildDownloadTask(
+    String id,
+    String filename, {
+    String? group,
+    String? metadata,
+  }) {
+    final path = r'/assets/{id}/original'.replaceAll('{id}', id);
+    final serverEndpoint = Store.get(StoreKey.serverEndpoint);
+    final headers = ApiService.getRequestHeaders();
+
+    return DownloadTask(
+      taskId: id,
+      url: serverEndpoint + path,
+      headers: headers,
+      filename: filename,
+      updates: Updates.statusAndProgress,
+      group: group ?? '',
+      metaData: metadata ?? '',
+    );
+  }
+}
diff --git a/mobile/lib/services/image_viewer.service.dart b/mobile/lib/services/image_viewer.service.dart
deleted file mode 100644
index c94244175b..0000000000
--- a/mobile/lib/services/image_viewer.service.dart
+++ /dev/null
@@ -1,117 +0,0 @@
-import 'dart:io';
-
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/response_extensions.dart';
-import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/interfaces/file_media.interface.dart';
-import 'package:immich_mobile/providers/api.provider.dart';
-import 'package:immich_mobile/repositories/file_media.repository.dart';
-import 'package:immich_mobile/services/api.service.dart';
-import 'package:logging/logging.dart';
-
-import 'package:path_provider/path_provider.dart';
-
-final imageViewerServiceProvider = Provider(
-  (ref) => ImageViewerService(
-    ref.watch(apiServiceProvider),
-    ref.watch(fileMediaRepositoryProvider),
-  ),
-);
-
-class ImageViewerService {
-  final ApiService _apiService;
-  final IFileMediaRepository _fileMediaRepository;
-  final Logger _log = Logger("ImageViewerService");
-
-  ImageViewerService(this._apiService, this._fileMediaRepository);
-
-  Future<bool> downloadAsset(Asset asset) async {
-    File? imageFile;
-    File? videoFile;
-    try {
-      // Download LivePhotos image and motion part
-      if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) {
-        var imageResponse =
-            await _apiService.assetsApi.downloadAssetWithHttpInfo(
-          asset.remoteId!,
-        );
-
-        var motionResponse =
-            await _apiService.assetsApi.downloadAssetWithHttpInfo(
-          asset.livePhotoVideoId!,
-        );
-
-        if (imageResponse.statusCode != 200 ||
-            motionResponse.statusCode != 200) {
-          final failedResponse =
-              imageResponse.statusCode != 200 ? imageResponse : motionResponse;
-          _log.severe(
-            "Motion asset download failed",
-            failedResponse.toLoggerString(),
-          );
-          return false;
-        }
-
-        Asset? resultAsset;
-
-        final tempDir = await getTemporaryDirectory();
-        videoFile = await File('${tempDir.path}/livephoto.mov').create();
-        imageFile = await File('${tempDir.path}/livephoto.heic').create();
-        videoFile.writeAsBytesSync(motionResponse.bodyBytes);
-        imageFile.writeAsBytesSync(imageResponse.bodyBytes);
-
-        resultAsset = await _fileMediaRepository.saveLivePhoto(
-          image: imageFile,
-          video: videoFile,
-          title: asset.fileName,
-        );
-
-        if (resultAsset == null) {
-          _log.warning(
-            "Asset cannot be saved as a live photo. This is most likely a motion photo. Saving only the image file",
-          );
-          resultAsset = await _fileMediaRepository
-              .saveImage(imageResponse.bodyBytes, title: asset.fileName);
-        }
-
-        return resultAsset != null;
-      } else {
-        var res = await _apiService.assetsApi
-            .downloadAssetWithHttpInfo(asset.remoteId!);
-
-        if (res.statusCode != 200) {
-          _log.severe("Asset download failed", res.toLoggerString());
-          return false;
-        }
-
-        final Asset? resultAsset;
-        final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
-
-        if (asset.isImage) {
-          resultAsset = await _fileMediaRepository.saveImage(
-            res.bodyBytes,
-            title: asset.fileName,
-            relativePath: relativePath,
-          );
-        } else {
-          final tempDir = await getTemporaryDirectory();
-          videoFile = await File('${tempDir.path}/${asset.fileName}').create();
-          videoFile.writeAsBytesSync(res.bodyBytes);
-          resultAsset = await _fileMediaRepository.saveVideo(
-            videoFile,
-            title: asset.fileName,
-            relativePath: relativePath,
-          );
-        }
-        return resultAsset != null;
-      }
-    } catch (error, stack) {
-      _log.severe("Error saving downloaded asset", error, stack);
-      return false;
-    } finally {
-      // Clear temp files
-      imageFile?.delete();
-      videoFile?.delete();
-    }
-  }
-}
diff --git a/mobile/lib/utils/download.dart b/mobile/lib/utils/download.dart
new file mode 100644
index 0000000000..c701f353a2
--- /dev/null
+++ b/mobile/lib/utils/download.dart
@@ -0,0 +1,3 @@
+const downloadGroupImage = 'group_image';
+const downloadGroupVideo = 'group_video';
+const downloadGroupLivePhoto = 'group_livephoto';
diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart
index 8b5684d0fa..c3f1390dba 100644
--- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart
+++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart
@@ -9,7 +9,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/providers/album/current_album.provider.dart';
 import 'package:immich_mobile/providers/album/shared_album.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
-import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
 import 'package:immich_mobile/services/stack.service.dart';
 import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
@@ -172,7 +172,16 @@ class BottomGalleryBar extends ConsumerWidget {
     }
 
     shareAsset() {
-      ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context);
+      if (asset.isOffline) {
+        ImmichToast.show(
+          durationInSecond: 1,
+          context: context,
+          msg: 'asset_action_share_err_offline'.tr(),
+          gravity: ToastGravity.BOTTOM,
+        );
+        return;
+      }
+      ref.read(downloadStateProvider.notifier).shareAsset(asset, context);
     }
 
     void handleEdit() async {
@@ -202,7 +211,17 @@ class BottomGalleryBar extends ConsumerWidget {
       if (asset.isLocal) {
         return;
       }
-      ref.read(imageViewerStateProvider.notifier).downloadAsset(
+      if (asset.isOffline) {
+        ImmichToast.show(
+          durationInSecond: 1,
+          context: context,
+          msg: 'asset_action_share_err_offline'.tr(),
+          gravity: ToastGravity.BOTTOM,
+        );
+        return;
+      }
+
+      ref.read(downloadStateProvider.notifier).downloadAsset(
             asset,
             context,
           );
diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart
index 6de8f5da33..f400224e0a 100644
--- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart
+++ b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart
@@ -5,7 +5,7 @@ import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/providers/album/current_album.provider.dart';
 import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart';
-import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart';
+import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
 import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
 import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart';
 import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
@@ -94,7 +94,7 @@ class GalleryAppBar extends ConsumerWidget {
     }
 
     handleDownloadAsset() {
-      ref.read(imageViewerStateProvider.notifier).downloadAsset(asset, context);
+      ref.read(downloadStateProvider.notifier).downloadAsset(asset, context);
     }
 
     return IgnorePointer(
diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart
index 51383fe195..01b717ef5b 100644
--- a/mobile/lib/widgets/forms/login/login_form.dart
+++ b/mobile/lib/widgets/forms/login/login_form.dart
@@ -176,7 +176,7 @@ class LoginForm extends HookConsumerWidget {
     populateTestLoginInfo1() {
       usernameController.text = 'testuser@email.com';
       passwordController.text = 'password';
-      serverEndpointController.text = 'http://10.1.15.216:2283/api';
+      serverEndpointController.text = 'http://192.168.1.16:2283/api';
     }
 
     login() async {
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index aaea00d699..9dadbd1028 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -78,6 +78,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "9.0.0"
+  background_downloader:
+    dependency: "direct main"
+    description:
+      name: background_downloader
+      sha256: "6a945db1a1c7727a4bc9c1d7c882cfb1a819f873b77e01d5e5dd6a3fb231cb28"
+      url: "https://pub.dev"
+    source: hosted
+    version: "8.5.5"
   boolean_selector:
     dependency: transitive
     description:
@@ -744,10 +752,10 @@ packages:
     dependency: "direct main"
     description:
       name: http
-      sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
+      sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
       url: "https://pub.dev"
     source: hosted
-    version: "0.13.6"
+    version: "1.2.2"
   http_multi_server:
     dependency: transitive
     description:
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index dc1eb11ca7..092b0bb75c 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -32,7 +32,7 @@ dependencies:
   flutter_svg: ^2.0.9
   package_info_plus: ^8.0.1
   url_launcher: ^6.2.4
-  http: ^0.13.6
+  http: ^1.1.0
   cancellation_token_http: ^2.0.0
   easy_localization: ^3.0.3
   share_plus: ^10.0.0
@@ -56,6 +56,7 @@ dependencies:
   thumbhash: 0.1.0+1
   async: ^2.11.0
   dynamic_color: ^1.7.0 #package to apply system theme
+  background_downloader: ^8.5.5
 
   #image editing packages
   crop_image: ^1.0.13