mirror of
https://github.com/immich-app/immich.git
synced 2026-04-29 20:48:48 -07:00
Compare commits
9 Commits
debug/back
...
fix/shared
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
880ff7af5e | ||
|
|
39cfad7136 | ||
|
|
350056dd1a | ||
|
|
57e1b172dd | ||
|
|
7efafd9db1 | ||
|
|
f597ebb672 | ||
|
|
b9694d9b52 | ||
|
|
925fabff27 | ||
|
|
33ec6ee0e8 |
@@ -17,7 +17,6 @@
|
||||
A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A01DD6982F7F43B40049AB63 /* ImageRequest.swift */; };
|
||||
B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */; };
|
||||
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; };
|
||||
B21E34B02E5B09190031FDB9 /* FileLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34B12E5B09100031FDB9 /* FileLogger.swift */; };
|
||||
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; };
|
||||
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */; };
|
||||
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; };
|
||||
@@ -104,7 +103,6 @@
|
||||
B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = "<group>"; };
|
||||
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
|
||||
B21E34B12E5B09100031FDB9 /* FileLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLogger.swift; sourceTree = "<group>"; };
|
||||
B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = "<group>"; };
|
||||
B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityApiImpl.swift; sourceTree = "<group>"; };
|
||||
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; };
|
||||
@@ -306,7 +304,6 @@
|
||||
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */,
|
||||
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */,
|
||||
B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */,
|
||||
B21E34B12E5B09100031FDB9 /* FileLogger.swift */,
|
||||
);
|
||||
path = Background;
|
||||
sourceTree = "<group>";
|
||||
@@ -617,7 +614,6 @@
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */,
|
||||
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */,
|
||||
B21E34B02E5B09190031FDB9 /* FileLogger.swift in Sources */,
|
||||
FE5499F32F1197D8006016CB /* LocalImages.g.swift in Sources */,
|
||||
FE5499F62F11980E006016CB /* LocalImagesImpl.swift in Sources */,
|
||||
FE5499F42F1197D8006016CB /* RemoteImages.g.swift in Sources */,
|
||||
|
||||
@@ -80,34 +80,29 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
* starts the engine, and sets up a timeout timer if specified.
|
||||
*/
|
||||
func run() {
|
||||
FileLogger.log("BackgroundWorker:run Starting Flutter engine for taskType=\(taskType) maxSeconds=\(maxSeconds.map(String.init) ?? "nil")")
|
||||
// Start the Flutter engine with the specified callback as the entry point
|
||||
let isRunning = engine.run(
|
||||
withEntrypoint: "backgroundSyncNativeEntrypoint",
|
||||
libraryURI: "package:immich_mobile/domain/services/background_worker.service.dart"
|
||||
)
|
||||
|
||||
|
||||
// Verify that the Flutter engine started successfully
|
||||
if !isRunning {
|
||||
FileLogger.log("BackgroundWorker:run Flutter engine failed to start, completing with success=false")
|
||||
complete(success: false)
|
||||
return
|
||||
}
|
||||
FileLogger.log("BackgroundWorker:run Flutter engine started")
|
||||
|
||||
|
||||
// Register plugins in the new engine
|
||||
GeneratedPluginRegistrant.register(with: engine)
|
||||
// Register custom plugins
|
||||
AppDelegate.registerPlugins(with: engine, messenger: engine.binaryMessenger)
|
||||
flutterApi = BackgroundWorkerFlutterApi(binaryMessenger: engine.binaryMessenger)
|
||||
BackgroundWorkerBgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: self)
|
||||
FileLogger.log("BackgroundWorker:run Plugins registered, waiting for Flutter onInitialized")
|
||||
|
||||
|
||||
// Set up a timeout timer if maxSeconds was specified to prevent runaway background tasks
|
||||
if maxSeconds != nil {
|
||||
// Schedule a timer to cancel the task after the specified timeout period
|
||||
Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), repeats: false) { _ in
|
||||
FileLogger.log("BackgroundWorker:run maxSeconds=\(self.maxSeconds!) timer fired, closing task")
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
@@ -119,7 +114,6 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
* This method acts as a bridge between the native iOS background task system and Flutter.
|
||||
*/
|
||||
func onInitialized() throws {
|
||||
FileLogger.log("BackgroundWorker:onInitialized Flutter ready, calling onIosUpload isRefresh=\(self.taskType == .refresh)")
|
||||
flutterApi?.onIosUpload(isRefresh: self.taskType == .refresh, maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
||||
self.handleHostResult(result: result)
|
||||
})
|
||||
@@ -132,22 +126,16 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
*/
|
||||
func close() {
|
||||
if isComplete {
|
||||
FileLogger.log("BackgroundWorker:close Already complete, ignoring close()")
|
||||
return
|
||||
}
|
||||
FileLogger.log("BackgroundWorker:close Cancel requested, signaling Flutter (taskType=\(taskType))")
|
||||
|
||||
flutterApi?.cancel { result in
|
||||
FileLogger.log("BackgroundWorker:close Flutter cancel acknowledged")
|
||||
self.complete(success: false)
|
||||
}
|
||||
|
||||
// Fallback safety mechanism: ensure completion is called within 2 seconds
|
||||
// This prevents the background task from hanging indefinitely if Flutter doesn't respond
|
||||
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
|
||||
if !self.isComplete {
|
||||
FileLogger.log("BackgroundWorker:close 2s fallback fired, Flutter did not acknowledge cancel")
|
||||
}
|
||||
self.complete(success: false)
|
||||
}
|
||||
}
|
||||
@@ -161,12 +149,8 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
*/
|
||||
private func handleHostResult(result: Result<Void, PigeonError>) {
|
||||
switch result {
|
||||
case .success():
|
||||
FileLogger.log("BackgroundWorker:handleHostResult Flutter onIosUpload succeeded (taskType=\(taskType))")
|
||||
self.complete(success: true)
|
||||
case .failure(let error):
|
||||
FileLogger.log("BackgroundWorker:handleHostResult Flutter onIosUpload failed: \(error.localizedDescription) (taskType=\(taskType))")
|
||||
self.close()
|
||||
case .success(): self.complete(success: true)
|
||||
case .failure(_): self.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,8 +166,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
if(isComplete) {
|
||||
return
|
||||
}
|
||||
|
||||
FileLogger.log("BackgroundWorker:complete Tearing down engine, success=\(success) (taskType=\(taskType))")
|
||||
|
||||
isComplete = true
|
||||
AppDelegate.cancelPlugins(with: engine)
|
||||
engine.destroyContext()
|
||||
|
||||
@@ -5,7 +5,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
func enable() throws {
|
||||
BackgroundWorkerApiImpl.scheduleRefreshWorker()
|
||||
BackgroundWorkerApiImpl.scheduleProcessingWorker()
|
||||
FileLogger.log("BackgroundWorkerApiImpl:enable Background worker scheduled")
|
||||
print("BackgroundWorkerApiImpl:enable Background worker scheduled")
|
||||
}
|
||||
|
||||
func configure(settings: BackgroundWorkerSettings) throws {
|
||||
@@ -19,7 +19,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
func disable() throws {
|
||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID);
|
||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID);
|
||||
FileLogger.log("BackgroundWorkerApiImpl:disableUploadWorker Disabled background workers")
|
||||
print("BackgroundWorkerApiImpl:disableUploadWorker Disabled background workers")
|
||||
}
|
||||
|
||||
private static let refreshTaskID = "app.alextran.immich.background.refreshUpload"
|
||||
@@ -30,7 +30,6 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
BGTaskScheduler.shared.register(
|
||||
forTaskWithIdentifier: processingTaskID, using: nil) { task in
|
||||
if task is BGProcessingTask {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:BGProcessingTask Background Processing task received")
|
||||
handleBackgroundProcessing(task: task as! BGProcessingTask)
|
||||
}
|
||||
}
|
||||
@@ -38,11 +37,9 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
BGTaskScheduler.shared.register(
|
||||
forTaskWithIdentifier: refreshTaskID, using: nil) { task in
|
||||
if task is BGAppRefreshTask {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:BGAppRefreshTask Background Refresh task received")
|
||||
handleBackgroundRefresh(task: task as! BGAppRefreshTask)
|
||||
}
|
||||
}
|
||||
FileLogger.log("BackgroundWorkerApiImpl:registerBackgroundWorkers Background workers registered")
|
||||
}
|
||||
|
||||
private static func scheduleRefreshWorker() {
|
||||
@@ -51,9 +48,8 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
|
||||
do {
|
||||
try BGTaskScheduler.shared.submit(backgroundRefresh)
|
||||
FileLogger.log("BackgroundWorkerApiImpl:scheduleRefreshWorker Scheduled Refresh task")
|
||||
} catch {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:scheduleRefreshWorker Could not schedule the refresh upload task \(error.localizedDescription)")
|
||||
print("Could not schedule the refresh upload task \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,32 +61,25 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
|
||||
do {
|
||||
try BGTaskScheduler.shared.submit(backgroundProcessing)
|
||||
FileLogger.log("BackgroundWorkerApiImpl:scheduleProcessingWorker Scheduled Processing task")
|
||||
} catch {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:scheduleProcessingWorker Could not schedule the processing upload task \(error.localizedDescription)")
|
||||
print("Could not schedule the processing upload task \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private static func handleBackgroundRefresh(task: BGAppRefreshTask) {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundRefresh Entered, re-queuing next refresh task")
|
||||
scheduleRefreshWorker()
|
||||
// If another task is running, cede the background time back to the OS
|
||||
if taskSemaphore.wait(timeout: .now()) == .success {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundRefresh Starting background worker")
|
||||
// Restrict the refresh task to run only for a maximum of (maxSeconds) seconds
|
||||
runBackgroundWorker(task: task, taskType: .refresh, maxSeconds: 20)
|
||||
} else {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundRefresh Processing task is in progress")
|
||||
task.setTaskCompleted(success: true)
|
||||
task.setTaskCompleted(success: false)
|
||||
}
|
||||
}
|
||||
|
||||
private static func handleBackgroundProcessing(task: BGProcessingTask) {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundProcessing Entered, re-queuing next processing task")
|
||||
scheduleProcessingWorker()
|
||||
FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundProcessing Waiting for taskSemaphore")
|
||||
taskSemaphore.wait()
|
||||
FileLogger.log("BackgroundWorkerApiImpl:handleBackgroundProcessing Semaphore acquired, starting background worker")
|
||||
// There are no restrictions for processing tasks. Although, the OS could signal expiration at any time
|
||||
runBackgroundWorker(task: task, taskType: .processing, maxSeconds: nil)
|
||||
}
|
||||
@@ -116,12 +105,11 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
}
|
||||
|
||||
task.expirationHandler = {
|
||||
FileLogger.log("BackgroundWorkerApiImpl:runBackgroundWorker iOS signaled expiration (taskType=\(taskType)), closing worker")
|
||||
DispatchQueue.main.async {
|
||||
backgroundWorker.close()
|
||||
}
|
||||
isSuccess = false
|
||||
|
||||
|
||||
// Schedule a timer to signal the semaphore after 2 seconds
|
||||
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
|
||||
semaphore.signal()
|
||||
@@ -134,6 +122,6 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
|
||||
semaphore.wait()
|
||||
task.setTaskCompleted(success: isSuccess)
|
||||
FileLogger.log("BackgroundWorkerApiImpl:runBackgroundWorker Background task completed with success: \(isSuccess)")
|
||||
print("Background task completed with success: \(isSuccess)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
enum FileLogger {
|
||||
private static let queue = DispatchQueue(label: "app.alextran.immich.FileLogger")
|
||||
private static let isoFormatter: ISO8601DateFormatter = {
|
||||
let f = ISO8601DateFormatter()
|
||||
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
return f
|
||||
}()
|
||||
|
||||
private static var logFileURL: URL? {
|
||||
guard let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
|
||||
else { return nil }
|
||||
return docs.appendingPathComponent("background_log.txt")
|
||||
}
|
||||
|
||||
static func log(_ message: String) {
|
||||
let line = "[\(isoFormatter.string(from: Date()))] \(message)\n"
|
||||
print(line, terminator: "")
|
||||
queue.async {
|
||||
guard let url = logFileURL, let data = line.data(using: .utf8) else { return }
|
||||
if FileManager.default.fileExists(atPath: url.path) {
|
||||
if let handle = try? FileHandle(forWritingTo: url) {
|
||||
defer { try? handle.close() }
|
||||
try? handle.seekToEnd()
|
||||
try? handle.write(contentsOf: data)
|
||||
}
|
||||
} else {
|
||||
try? data.write(to: url, options: .atomic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,9 +115,7 @@
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<string>No</string>
|
||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
||||
@@ -2,17 +2,21 @@ 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/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
||||
class MapBottomSheet extends StatelessWidget {
|
||||
const MapBottomSheet({super.key});
|
||||
final Key? sheetKey;
|
||||
|
||||
const MapBottomSheet({super.key, this.sheetKey});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BaseBottomSheet(
|
||||
key: sheetKey,
|
||||
initialChildSize: 0.25,
|
||||
maxChildSize: 0.75,
|
||||
shouldCloseOnMinExtent: false,
|
||||
@@ -49,7 +53,7 @@ class _ScopedMapTimeline extends StatelessWidget {
|
||||
return timelineService;
|
||||
}),
|
||||
],
|
||||
child: const Timeline(appBar: null, bottomSheet: null, withScrubber: false),
|
||||
child: const Timeline(appBar: null, bottomSheet: GeneralBottomSheet(minChildSize: 0.23), withScrubber: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/map/map_utils.dart';
|
||||
@@ -53,6 +54,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
final _reloadMutex = AsyncMutex();
|
||||
final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2));
|
||||
final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25);
|
||||
final GlobalKey _bottomSheetKey = GlobalKey();
|
||||
StreamSubscription? _eventSubscription;
|
||||
|
||||
@override
|
||||
@@ -184,7 +186,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
return Stack(
|
||||
children: [
|
||||
_Map(initialLocation: widget.initialLocation, onMapCreated: onMapCreated, onMapReady: onMapReady),
|
||||
_DynamicBottomSheet(bottomSheetOffset: bottomSheetOffset),
|
||||
_DynamicBottomSheet(bottomSheetOffset: bottomSheetOffset, sheetKey: _bottomSheetKey),
|
||||
_DynamicMyLocationButton(onZoomToLocation: onZoomToLocation, bottomSheetOffset: bottomSheetOffset),
|
||||
],
|
||||
);
|
||||
@@ -224,8 +226,9 @@ class _Map extends StatelessWidget {
|
||||
|
||||
class _DynamicBottomSheet extends StatefulWidget {
|
||||
final ValueNotifier<double> bottomSheetOffset;
|
||||
final GlobalKey sheetKey;
|
||||
|
||||
const _DynamicBottomSheet({required this.bottomSheetOffset});
|
||||
const _DynamicBottomSheet({required this.bottomSheetOffset, required this.sheetKey});
|
||||
|
||||
@override
|
||||
State<_DynamicBottomSheet> createState() => _DynamicBottomSheetState();
|
||||
@@ -236,10 +239,13 @@ class _DynamicBottomSheetState extends State<_DynamicBottomSheet> {
|
||||
Widget build(BuildContext context) {
|
||||
return NotificationListener<DraggableScrollableNotification>(
|
||||
onNotification: (notification) {
|
||||
widget.bottomSheetOffset.value = notification.extent;
|
||||
return true;
|
||||
final sheet = notification.context.findAncestorWidgetOfExactType<BaseBottomSheet>();
|
||||
if (sheet?.key == widget.sheetKey) {
|
||||
widget.bottomSheetOffset.value = notification.extent;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: const MapBottomSheet(),
|
||||
child: MapBottomSheet(sheetKey: widget.sheetKey),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,6 +469,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
ref.read(timelineStateProvider.notifier).setScrolling(true);
|
||||
},
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
timeline,
|
||||
if (isBottomWidgetVisible)
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -570,8 +570,8 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.9
|
||||
uuid:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0
|
||||
validator:
|
||||
specifier: ^13.12.0
|
||||
version: 13.15.35
|
||||
@@ -12110,6 +12110,10 @@ packages:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
hasBin: true
|
||||
|
||||
uuid@14.0.0:
|
||||
resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==}
|
||||
hasBin: true
|
||||
|
||||
uuid@8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
@@ -25779,6 +25783,8 @@ snapshots:
|
||||
|
||||
uuid@11.1.0: {}
|
||||
|
||||
uuid@14.0.0: {}
|
||||
|
||||
uuid@8.3.2: {}
|
||||
|
||||
validator@13.15.35: {}
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
"thumbhash": "^0.1.1",
|
||||
"transformation-matrix": "^3.1.0",
|
||||
"ua-parser-js": "^2.0.0",
|
||||
"uuid": "^11.1.0",
|
||||
"uuid": "^14.0.0",
|
||||
"validator": "^13.12.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
let isOwned = $derived(authManager.authenticated && authManager.user.id === sharedLink?.userId);
|
||||
let password = $state('');
|
||||
|
||||
if (passwordRequired) {
|
||||
assetViewerManager.showAssetViewer(false);
|
||||
}
|
||||
|
||||
const handlePasswordSubmit = async () => {
|
||||
try {
|
||||
sharedLink = await sharedLinkLogin({ key, slug, sharedLinkLoginDto: { password } });
|
||||
@@ -101,10 +105,10 @@
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Album}
|
||||
{#if !passwordRequired && sharedLink?.type === SharedLinkType.Album}
|
||||
<AlbumViewer {sharedLink} />
|
||||
{/if}
|
||||
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Individual}
|
||||
{#if !passwordRequired && sharedLink?.type === SharedLinkType.Individual}
|
||||
<div class="immich-scrollbar">
|
||||
<IndividualSharedViewer {sharedLink} {isOwned} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import type { RouteId } from '$app/types';
|
||||
import { assetCacheManager } from '$lib/managers/AssetCacheManager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
@@ -13,8 +13,9 @@ export const isExternalUrl = (url: string): boolean => {
|
||||
};
|
||||
|
||||
export const isPhotosRoute = (route?: string | null) => !!route?.startsWith('/(user)/photos/[[assetId=id]]');
|
||||
const isSharedLinkSlugRoute = (route?: string | null) => !!route?.startsWith('/(user)/s/[slug]');
|
||||
export const isSharedLinkRoute = (route?: string | null) =>
|
||||
!!route?.startsWith('/(user)/share/[key]') || !!route?.startsWith('/(user)/s/[slug]');
|
||||
!!route?.startsWith('/(user)/share/[key]') || isSharedLinkSlugRoute(route);
|
||||
export const isSearchRoute = (route?: string | null) => !!route?.startsWith('/(user)/search');
|
||||
export const isAlbumsRoute = (route?: string | null) => !!route?.startsWith('/(user)/albums/[albumId=id]');
|
||||
export const isPeopleRoute = (route?: string | null) => !!route?.startsWith('/(user)/people/[personId]');
|
||||
@@ -29,31 +30,32 @@ export function getAssetInfoFromParam({ assetId, slug, key }: { assetId?: string
|
||||
}
|
||||
|
||||
function currentUrlWithoutAsset() {
|
||||
const $page = get(page);
|
||||
// This contains special casing for the /photos/:assetId route, which hangs directly
|
||||
// off / instead of a subpath, unlike every other asset-containing route.
|
||||
return isPhotosRoute($page.route.id)
|
||||
? Route.photos() + $page.url.search
|
||||
: $page.url.pathname.replace(/(\/photos.*)$/, '') + $page.url.search;
|
||||
if (isPhotosRoute(page.route.id)) {
|
||||
return Route.photos() + page.url.search;
|
||||
} else if (isSharedLinkSlugRoute(page.route.id)) {
|
||||
return Route.viewSharedLink({ slug: page.data.slug, key: page.data.key }) + page.url.search;
|
||||
} else {
|
||||
return page.url.pathname.replace(/(\/photos.*)$/, '') + page.url.search;
|
||||
}
|
||||
}
|
||||
|
||||
export function currentUrlReplaceAssetId(assetId: string) {
|
||||
const $page = get(page);
|
||||
const params = new URLSearchParams($page.url.search);
|
||||
const params = new URLSearchParams(page.url.search);
|
||||
// always remove the assetGridScrollTargetParams
|
||||
params.delete('at');
|
||||
const paramsString = params.toString();
|
||||
const searchparams = paramsString == '' ? '' : '?' + params.toString();
|
||||
// this contains special casing for the /photos/:assetId photos route, which hangs directly
|
||||
// off / instead of a subpath, unlike every other asset-containing route.
|
||||
return isPhotosRoute($page.route.id)
|
||||
return isPhotosRoute(page.route.id)
|
||||
? `${Route.viewAsset({ id: assetId })}${searchparams}`
|
||||
: `${$page.url.pathname.replace(/\/photos\/[^/]+$/, '')}/photos/${assetId}${searchparams}`;
|
||||
: `${page.url.pathname.replace(/\/photos\/[^/]+$/, '')}/photos/${assetId}${searchparams}`;
|
||||
}
|
||||
|
||||
function replaceScrollTarget(url: string, searchParams?: AssetGridRouteSearchParams | null) {
|
||||
const $page = get(page);
|
||||
const parsed = new URL(url, $page.url);
|
||||
const parsed = new URL(url, page.url);
|
||||
|
||||
const { at: assetId } = searchParams || { at: null };
|
||||
|
||||
@@ -61,7 +63,7 @@ function replaceScrollTarget(url: string, searchParams?: AssetGridRouteSearchPar
|
||||
return parsed.pathname;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams($page.url.search);
|
||||
const params = new URLSearchParams(page.url.search);
|
||||
if (assetId) {
|
||||
params.set('at', assetId);
|
||||
}
|
||||
@@ -69,8 +71,7 @@ function replaceScrollTarget(url: string, searchParams?: AssetGridRouteSearchPar
|
||||
}
|
||||
|
||||
function currentUrl() {
|
||||
const $page = get(page);
|
||||
const current = $page.url;
|
||||
const current = page.url;
|
||||
return current.pathname + current.search + current.hash;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user