Skip to content

Commit c234c95

Browse files
authored
websocket upload notification - closed #24 (#25)
* Render when a new asset is uploaded from WebSocket notification * Update Readme
1 parent 7cc7fc0 commit c234c95

23 files changed

+11037
-69
lines changed

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ dev:
22
docker-compose -f ./server/docker-compose.yml up
33

44
dev-update:
5-
docker-compose -f ./server/docker-compose.yml up --build -V
5+
docker-compose -f ./server/docker-compose.yml up --build -V
6+
7+
dev-scale:
8+
docker-compose -f ./server/docker-compose.yml up --build -V --scale immich_server=3 --remove-orphans

README.md

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,13 @@ This project is under heavy development, there will be continous functions, feat
2828

2929
# Features
3030

31-
[x] Upload assets(videos/images)
32-
33-
[x] View assets
34-
35-
[x] Quick navigation with drag scroll bar
36-
37-
[x] Auto Backup
38-
39-
[x] Support HEIC/HEIF Backup
40-
41-
[x] Extract and display EXIF info
31+
- Upload assets(videos/images).
32+
- View assets.
33+
- Quick navigation with drag scroll bar.
34+
- Auto Backup.
35+
- Support HEIC/HEIF Backup.
36+
- Extract and display EXIF info.
37+
- Real-time render from multi-device upload event.
4238

4339
# Development
4440

mobile/lib/main.dart

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import 'package:flutter/material.dart';
22
import 'package:flutter/services.dart';
33
import 'package:hive_flutter/hive_flutter.dart';
44
import 'package:hooks_riverpod/hooks_riverpod.dart';
5+
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
56
import 'package:immich_mobile/routing/router.dart';
67
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
78
import 'package:immich_mobile/shared/providers/backup.provider.dart';
9+
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
810
import 'constants/hive_box.dart';
911
import 'package:google_fonts/google_fonts.dart';
1012

@@ -36,20 +38,23 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
3638
switch (state) {
3739
case AppLifecycleState.resumed:
3840
debugPrint("[APP STATE] resumed");
39-
ref.read(appStateProvider.notifier).state = AppStateEnum.resumed;
40-
ref.read(backupProvider.notifier).resumeBackup();
41+
ref.watch(appStateProvider.notifier).state = AppStateEnum.resumed;
42+
ref.watch(backupProvider.notifier).resumeBackup();
43+
ref.watch(websocketProvider.notifier).connect();
44+
ref.watch(assetProvider.notifier).getAllAsset();
4145
break;
4246
case AppLifecycleState.inactive:
4347
debugPrint("[APP STATE] inactive");
44-
ref.read(appStateProvider.notifier).state = AppStateEnum.inactive;
48+
ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive;
49+
ref.watch(websocketProvider.notifier).disconnect();
4550
break;
4651
case AppLifecycleState.paused:
4752
debugPrint("[APP STATE] paused");
48-
ref.read(appStateProvider.notifier).state = AppStateEnum.paused;
53+
ref.watch(appStateProvider.notifier).state = AppStateEnum.paused;
4954
break;
5055
case AppLifecycleState.detached:
5156
debugPrint("[APP STATE] detached");
52-
ref.read(appStateProvider.notifier).state = AppStateEnum.detached;
57+
ref.watch(appStateProvider.notifier).state = AppStateEnum.detached;
5358
break;
5459
}
5560
}

mobile/lib/modules/home/providers/asset.provider.dart

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import 'package:photo_manager/photo_manager.dart';
1010
class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
1111
final AssetService _assetService = AssetService();
1212
final DeviceInfoService _deviceInfoService = DeviceInfoService();
13+
final Ref ref;
1314

14-
AssetNotifier() : super([]);
15+
AssetNotifier(this.ref) : super([]);
1516

1617
getAllAsset() async {
1718
List<ImmichAsset>? allAssets = await _assetService.getAllAsset();
1819

1920
if (allAssets != null) {
20-
allAssets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
2121
state = allAssets;
2222
}
2323
}
@@ -26,6 +26,10 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
2626
state = [];
2727
}
2828

29+
onNewAssetUploaded(ImmichAsset newAsset) {
30+
state = [...state, newAsset];
31+
}
32+
2933
deleteAssets(Set<ImmichAsset> deleteAssets) async {
3034
var deviceInfo = await _deviceInfoService.getDeviceInfo();
3135
var deviceId = deviceInfo["deviceId"];
@@ -43,7 +47,6 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
4347
}
4448

4549
final List<String> result = await PhotoManager.editor.deleteWithIds(deleteIdList);
46-
print(result);
4750

4851
// Delete asset on server
4952
List<DeleteAssetResponse>? deleteAssetResult = await _assetService.deleteAssets(deleteAssets);
@@ -59,14 +62,13 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
5962
}
6063
}
6164

62-
final currentLocalPageProvider = StateProvider<int>((ref) => 0);
63-
6465
final assetProvider = StateNotifierProvider<AssetNotifier, List<ImmichAsset>>((ref) {
65-
return AssetNotifier();
66+
return AssetNotifier(ref);
6667
});
6768

6869
final assetGroupByDateTimeProvider = StateProvider((ref) {
69-
var assetGroup = ref.watch(assetProvider);
70+
var assets = ref.watch(assetProvider);
7071

71-
return assetGroup.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
72+
assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
73+
return assets.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
7274
});

mobile/lib/modules/home/ui/profile_drawer.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
55
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
66
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
77
import 'package:immich_mobile/shared/providers/backup.provider.dart';
8+
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
89

910
class ProfileDrawer extends ConsumerWidget {
1011
const ProfileDrawer({Key? key}) : super(key: key);
@@ -60,6 +61,7 @@ class ProfileDrawer extends ConsumerWidget {
6061
if (res) {
6162
ref.watch(backupProvider.notifier).cancelBackup();
6263
ref.watch(assetProvider.notifier).clearAllAsset();
64+
ref.watch(websocketProvider.notifier).disconnect();
6365
AutoRouter.of(context).popUntilRoot();
6466
}
6567
},

mobile/lib/modules/home/views/home_page.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
1111
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
1212
import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
1313
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
14+
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
1415
import 'package:sliver_tools/sliver_tools.dart';
1516

1617
class HomePage extends HookConsumerWidget {
@@ -25,12 +26,13 @@ class HomePage extends HookConsumerWidget {
2526
var homePageState = ref.watch(homePageStateProvider);
2627

2728
useEffect(() {
29+
ref.read(websocketProvider.notifier).connect();
2830
ref.read(assetProvider.notifier).getAllAsset();
2931
return null;
3032
}, []);
3133

3234
onPopBackFromBackupPage() {
33-
ref.read(assetProvider.notifier).getAllAsset();
35+
// ref.read(assetProvider.notifier).getAllAsset();
3436
}
3537

3638
Widget _buildBody() {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import 'dart:convert';
2+
3+
import 'package:flutter/foundation.dart';
4+
import 'package:hive/hive.dart';
5+
import 'package:hooks_riverpod/hooks_riverpod.dart';
6+
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
7+
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
8+
import 'package:socket_io_client/socket_io_client.dart';
9+
10+
import 'package:immich_mobile/constants/hive_box.dart';
11+
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
12+
13+
class WebscoketState {
14+
final Socket? socket;
15+
final bool isConnected;
16+
17+
WebscoketState({
18+
this.socket,
19+
required this.isConnected,
20+
});
21+
22+
WebscoketState copyWith({
23+
Socket? socket,
24+
bool? isConnected,
25+
}) {
26+
return WebscoketState(
27+
socket: socket ?? this.socket,
28+
isConnected: isConnected ?? this.isConnected,
29+
);
30+
}
31+
32+
@override
33+
String toString() => 'WebscoketState(socket: $socket, isConnected: $isConnected)';
34+
35+
@override
36+
bool operator ==(Object other) {
37+
if (identical(this, other)) return true;
38+
39+
return other is WebscoketState && other.socket == socket && other.isConnected == isConnected;
40+
}
41+
42+
@override
43+
int get hashCode => socket.hashCode ^ isConnected.hashCode;
44+
}
45+
46+
class WebsocketNotifier extends StateNotifier<WebscoketState> {
47+
WebsocketNotifier(this.ref) : super(WebscoketState(socket: null, isConnected: false)) {
48+
debugPrint("Init websocket instance");
49+
}
50+
51+
final Ref ref;
52+
53+
connect() {
54+
var authenticationState = ref.watch(authenticationProvider);
55+
56+
if (authenticationState.isAuthenticated) {
57+
var accessToken = Hive.box(userInfoBox).get(accessTokenKey);
58+
var endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
59+
try {
60+
debugPrint("[WEBSOCKET] Attempting to connect to ws");
61+
// Configure socket transports must be sepecified
62+
Socket socket = io(
63+
endpoint,
64+
OptionBuilder()
65+
.setTransports(['websocket'])
66+
.enableReconnection()
67+
.enableForceNew()
68+
.enableForceNewConnection()
69+
.enableAutoConnect()
70+
.setExtraHeaders({"Authorization": "Bearer $accessToken"})
71+
.build(),
72+
);
73+
74+
socket.onConnect((_) {
75+
debugPrint("[WEBSOCKET] Established Websocket Connection");
76+
state = WebscoketState(isConnected: true, socket: socket);
77+
});
78+
79+
socket.onDisconnect((_) {
80+
debugPrint("[WEBSOCKET] Disconnect to Websocket Connection");
81+
state = WebscoketState(isConnected: false, socket: null);
82+
});
83+
84+
socket.on('error', (errorMessage) {
85+
debugPrint("Webcoket Error - $errorMessage");
86+
state = WebscoketState(isConnected: false, socket: null);
87+
});
88+
89+
socket.on('on_upload_success', (data) {
90+
var jsonString = jsonDecode(data.toString());
91+
ImmichAsset newAsset = ImmichAsset.fromMap(jsonString);
92+
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
93+
});
94+
} catch (e) {
95+
debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}");
96+
}
97+
}
98+
}
99+
100+
disconnect() {
101+
debugPrint("[WEBSOCKET] Attempting to disconnect");
102+
var socket = state.socket?.disconnect();
103+
if (socket != null) {
104+
if (socket.disconnected) {
105+
state = WebscoketState(isConnected: false, socket: null);
106+
}
107+
}
108+
}
109+
}
110+
111+
final websocketProvider = StateNotifierProvider<WebsocketNotifier, WebscoketState>((ref) {
112+
return WebsocketNotifier(ref);
113+
});

mobile/pubspec.lock

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,20 @@ packages:
742742
url: "https://pub.dartlang.org"
743743
source: hosted
744744
version: "0.2.5"
745+
socket_io_client:
746+
dependency: "direct main"
747+
description:
748+
name: socket_io_client
749+
url: "https://pub.dartlang.org"
750+
source: hosted
751+
version: "2.0.0-beta.4-nullsafety.0"
752+
socket_io_common:
753+
dependency: transitive
754+
description:
755+
name: socket_io_common
756+
url: "https://pub.dartlang.org"
757+
source: hosted
758+
version: "2.0.0"
745759
source_gen:
746760
dependency: transitive
747761
description:

mobile/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies:
3333
sliver_tools: ^0.2.5
3434
badges: ^2.0.2
3535
photo_view: ^0.13.0
36+
socket_io_client: ^2.0.0-beta.4-nullsafety.0
3637

3738
dev_dependencies:
3839
flutter_test:

server/docker-compose.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@ version: '3.8'
22

33

44
services:
5-
server:
6-
container_name: immich_server
5+
immich_server:
76
image: immich-server-dev:1.0.0
87
build:
98
context: .
109
target: development
1110
dockerfile: ./Dockerfile
1211
command: npm run start:dev
13-
ports:
14-
- "3000:3000"
12+
expose:
13+
- "3000"
1514
volumes:
1615
- .:/usr/src/app
1716
- ${UPLOAD_LOCATION}:/usr/src/app/upload
@@ -60,7 +59,7 @@ services:
6059
networks:
6160
- immich_network
6261
depends_on:
63-
- server
62+
- immich_server
6463

6564
networks:
6665
immich_network:

0 commit comments

Comments
 (0)