mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat: drift partners (#20051)
* feat: drift toggle partner in timeline * partners operation * fix: lint
This commit is contained in:
parent
99e5b33969
commit
737e768212
@ -1,3 +1,6 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
|
||||||
|
|
||||||
// TODO: Rename to User once Isar is removed
|
// TODO: Rename to User once Isar is removed
|
||||||
@ -123,3 +126,88 @@ quotaSizeInBytes: $quotaSizeInBytes,
|
|||||||
quotaUsageInBytes.hashCode ^
|
quotaUsageInBytes.hashCode ^
|
||||||
quotaSizeInBytes.hashCode;
|
quotaSizeInBytes.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PartnerUserDto {
|
||||||
|
final String id;
|
||||||
|
final String email;
|
||||||
|
final String name;
|
||||||
|
final bool inTimeline;
|
||||||
|
|
||||||
|
final String? profileImagePath;
|
||||||
|
|
||||||
|
const PartnerUserDto({
|
||||||
|
required this.id,
|
||||||
|
required this.email,
|
||||||
|
required this.name,
|
||||||
|
required this.inTimeline,
|
||||||
|
this.profileImagePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
PartnerUserDto copyWith({
|
||||||
|
String? id,
|
||||||
|
String? email,
|
||||||
|
String? name,
|
||||||
|
bool? inTimeline,
|
||||||
|
String? profileImagePath,
|
||||||
|
}) {
|
||||||
|
return PartnerUserDto(
|
||||||
|
id: id ?? this.id,
|
||||||
|
email: email ?? this.email,
|
||||||
|
name: name ?? this.name,
|
||||||
|
inTimeline: inTimeline ?? this.inTimeline,
|
||||||
|
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': id,
|
||||||
|
'email': email,
|
||||||
|
'name': name,
|
||||||
|
'inTimeline': inTimeline,
|
||||||
|
'profileImagePath': profileImagePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory PartnerUserDto.fromMap(Map<String, dynamic> map) {
|
||||||
|
return PartnerUserDto(
|
||||||
|
id: map['id'] as String,
|
||||||
|
email: map['email'] as String,
|
||||||
|
name: map['name'] as String,
|
||||||
|
inTimeline: map['inTimeline'] as bool,
|
||||||
|
profileImagePath: map['profileImagePath'] != null
|
||||||
|
? map['profileImagePath'] as String
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory PartnerUserDto.fromJson(String source) =>
|
||||||
|
PartnerUserDto.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'PartnerUserDto(id: $id, email: $email, name: $name, inTimeline: $inTimeline, profileImagePath: $profileImagePath)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant PartnerUserDto other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.id == id &&
|
||||||
|
other.email == email &&
|
||||||
|
other.name == name &&
|
||||||
|
other.inTimeline == inTimeline &&
|
||||||
|
other.profileImagePath == profileImagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^
|
||||||
|
email.hashCode ^
|
||||||
|
name.hashCode ^
|
||||||
|
inTimeline.hashCode ^
|
||||||
|
profileImagePath.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
61
mobile/lib/domain/services/partner.service.dart
Normal file
61
mobile/lib/domain/services/partner.service.dart
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/partner_api.repository.dart';
|
||||||
|
|
||||||
|
class DriftPartnerService {
|
||||||
|
final DriftPartnerRepository _driftPartnerRepository;
|
||||||
|
final PartnerApiRepository _partnerApiRepository;
|
||||||
|
|
||||||
|
const DriftPartnerService(
|
||||||
|
this._driftPartnerRepository,
|
||||||
|
this._partnerApiRepository,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<List<PartnerUserDto>> getSharedWith(String userId) {
|
||||||
|
return _driftPartnerRepository.getSharedWith(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<PartnerUserDto>> getSharedBy(String userId) {
|
||||||
|
return _driftPartnerRepository.getSharedBy(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<PartnerUserDto>> getAvailablePartners(
|
||||||
|
String currentUserId,
|
||||||
|
) async {
|
||||||
|
final otherUsers =
|
||||||
|
await _driftPartnerRepository.getAvailablePartners(currentUserId);
|
||||||
|
final currentPartners =
|
||||||
|
await _driftPartnerRepository.getSharedBy(currentUserId);
|
||||||
|
final available = otherUsers.where((user) {
|
||||||
|
return !currentPartners.any((partner) => partner.id == user.id);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleShowInTimeline(String partnerId, String userId) async {
|
||||||
|
final partner = await _driftPartnerRepository.getPartner(partnerId, userId);
|
||||||
|
if (partner == null) {
|
||||||
|
debugPrint("Partner not found: $partnerId for user: $userId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _partnerApiRepository.update(
|
||||||
|
partnerId,
|
||||||
|
inTimeline: !partner.inTimeline,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _driftPartnerRepository.toggleShowInTimeline(partner, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addPartner(String partnerId, String userId) async {
|
||||||
|
await _partnerApiRepository.create(partnerId);
|
||||||
|
await _driftPartnerRepository.create(partnerId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removePartner(String partnerId, String userId) async {
|
||||||
|
await _partnerApiRepository.delete(partnerId);
|
||||||
|
await _driftPartnerRepository.delete(partnerId, userId);
|
||||||
|
}
|
||||||
|
}
|
164
mobile/lib/infrastructure/repositories/partner.repository.dart
Normal file
164
mobile/lib/infrastructure/repositories/partner.repository.dart
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
|
class DriftPartnerRepository extends DriftDatabaseRepository {
|
||||||
|
final Drift _db;
|
||||||
|
const DriftPartnerRepository(this._db) : super(_db);
|
||||||
|
|
||||||
|
Future<List<PartnerUserDto>> getPartners(String userId) {
|
||||||
|
final query = _db.select(_db.partnerEntity).join([
|
||||||
|
innerJoin(
|
||||||
|
_db.userEntity,
|
||||||
|
_db.userEntity.id.equalsExp(_db.partnerEntity.sharedById),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.partnerEntity.sharedWithId.equals(userId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final user = row.readTable(_db.userEntity);
|
||||||
|
final partner = row.readTable(_db.partnerEntity);
|
||||||
|
return PartnerUserDto(
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
inTimeline: partner.inTimeline,
|
||||||
|
);
|
||||||
|
}).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get users who we can share our library with
|
||||||
|
Future<List<PartnerUserDto>> getAvailablePartners(String currentUserId) {
|
||||||
|
final query = _db.select(_db.userEntity)
|
||||||
|
..where((row) => row.id.equals(currentUserId).not());
|
||||||
|
|
||||||
|
return query.map((user) {
|
||||||
|
return PartnerUserDto(
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
inTimeline: false,
|
||||||
|
);
|
||||||
|
}).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get users who are sharing their photos WITH the current user
|
||||||
|
Future<List<PartnerUserDto>> getSharedWith(String partnerId) {
|
||||||
|
final query = _db.select(_db.partnerEntity).join([
|
||||||
|
innerJoin(
|
||||||
|
_db.userEntity,
|
||||||
|
_db.userEntity.id.equalsExp(_db.partnerEntity.sharedById),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.partnerEntity.sharedWithId.equals(partnerId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final user = row.readTable(_db.userEntity);
|
||||||
|
final partner = row.readTable(_db.partnerEntity);
|
||||||
|
return PartnerUserDto(
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
inTimeline: partner.inTimeline,
|
||||||
|
);
|
||||||
|
}).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get users who the current user is sharing their photos TO
|
||||||
|
Future<List<PartnerUserDto>> getSharedBy(String userId) {
|
||||||
|
final query = _db.select(_db.partnerEntity).join([
|
||||||
|
innerJoin(
|
||||||
|
_db.userEntity,
|
||||||
|
_db.userEntity.id.equalsExp(_db.partnerEntity.sharedWithId),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.partnerEntity.sharedById.equals(userId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final user = row.readTable(_db.userEntity);
|
||||||
|
final partner = row.readTable(_db.partnerEntity);
|
||||||
|
return PartnerUserDto(
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
inTimeline: partner.inTimeline,
|
||||||
|
);
|
||||||
|
}).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> getAllPartnerIds(String userId) async {
|
||||||
|
// Get users who are sharing with me (sharedWithId = userId)
|
||||||
|
final sharingWithMeQuery = _db.select(_db.partnerEntity)
|
||||||
|
..where((tbl) => tbl.sharedWithId.equals(userId));
|
||||||
|
final sharingWithMe =
|
||||||
|
await sharingWithMeQuery.map((row) => row.sharedById).get();
|
||||||
|
|
||||||
|
// Get users who I am sharing with (sharedById = userId)
|
||||||
|
final sharingWithThemQuery = _db.select(_db.partnerEntity)
|
||||||
|
..where((tbl) => tbl.sharedById.equals(userId));
|
||||||
|
final sharingWithThem =
|
||||||
|
await sharingWithThemQuery.map((row) => row.sharedWithId).get();
|
||||||
|
|
||||||
|
// Combine both lists and remove duplicates
|
||||||
|
final allPartnerIds =
|
||||||
|
<String>{...sharingWithMe, ...sharingWithThem}.toList();
|
||||||
|
return allPartnerIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<PartnerUserDto?> getPartner(String partnerId, String userId) {
|
||||||
|
final query = _db.select(_db.partnerEntity).join([
|
||||||
|
innerJoin(
|
||||||
|
_db.userEntity,
|
||||||
|
_db.userEntity.id.equalsExp(_db.partnerEntity.sharedById),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.partnerEntity.sharedById.equals(partnerId) &
|
||||||
|
_db.partnerEntity.sharedWithId.equals(userId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final user = row.readTable(_db.userEntity);
|
||||||
|
final partner = row.readTable(_db.partnerEntity);
|
||||||
|
return PartnerUserDto(
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
inTimeline: partner.inTimeline,
|
||||||
|
);
|
||||||
|
}).getSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> toggleShowInTimeline(PartnerUserDto partner, String userId) {
|
||||||
|
return _db.partnerEntity.update().replace(
|
||||||
|
PartnerEntityCompanion(
|
||||||
|
sharedById: Value(partner.id),
|
||||||
|
sharedWithId: Value(userId),
|
||||||
|
inTimeline: Value(!partner.inTimeline),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> create(String partnerId, String userId) {
|
||||||
|
final entity = PartnerEntityCompanion(
|
||||||
|
sharedById: Value(userId),
|
||||||
|
sharedWithId: Value(partnerId),
|
||||||
|
inTimeline: const Value(false),
|
||||||
|
);
|
||||||
|
|
||||||
|
return _db.partnerEntity.insertOne(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> delete(String partnerId, String userId) {
|
||||||
|
return _db.partnerEntity.deleteWhere(
|
||||||
|
(t) => t.sharedById.equals(userId) & t.sharedWithId.equals(partnerId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
161
mobile/lib/pages/library/partner/drift_partner.page.dart
Normal file
161
mobile/lib/pages/library/partner/drift_partner.page.dart
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/partner_user_avatar.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/partner.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftPartnerPage extends HookConsumerWidget {
|
||||||
|
const DriftPartnerPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final potentialPartnersAsync = ref.watch(driftAvailablePartnerProvider);
|
||||||
|
|
||||||
|
addNewUsersHandler() async {
|
||||||
|
final potentialPartners = potentialPartnersAsync.value;
|
||||||
|
if (potentialPartners == null || potentialPartners.isEmpty) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "partner_page_no_more_users".tr(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectedUser = await showDialog<PartnerUserDto>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return SimpleDialog(
|
||||||
|
title: const Text("partner_page_select_partner").tr(),
|
||||||
|
children: [
|
||||||
|
for (PartnerUserDto partner in potentialPartners)
|
||||||
|
SimpleDialogOption(
|
||||||
|
onPressed: () => context.pop(partner),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: PartnerUserAvatar(partner: partner),
|
||||||
|
),
|
||||||
|
Text(partner.name),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (selectedUser != null) {
|
||||||
|
await ref.read(partnerUsersProvider.notifier).addPartner(selectedUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteUser(PartnerUserDto partner) {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return ConfirmDialog(
|
||||||
|
title: "stop_photo_sharing",
|
||||||
|
content: "partner_page_stop_sharing_content"
|
||||||
|
.tr(namedArgs: {'partner': partner.name}),
|
||||||
|
onOk: () =>
|
||||||
|
ref.read(partnerUsersProvider.notifier).removePartner(partner),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("partners").t(context: context),
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: false,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: potentialPartnersAsync.whenOrNull(
|
||||||
|
data: (data) => addNewUsersHandler,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.person_add),
|
||||||
|
tooltip: "add_partner".tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: _SharedToPartnerList(
|
||||||
|
onAddPartner: addNewUsersHandler,
|
||||||
|
onDeletePartner: onDeleteUser,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SharedToPartnerList extends ConsumerWidget {
|
||||||
|
final VoidCallback onAddPartner;
|
||||||
|
final Function(PartnerUserDto partner) onDeletePartner;
|
||||||
|
|
||||||
|
const _SharedToPartnerList({
|
||||||
|
required this.onAddPartner,
|
||||||
|
required this.onDeletePartner,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final partnerAsync = ref.watch(driftSharedByPartnerProvider);
|
||||||
|
|
||||||
|
return partnerAsync.when(
|
||||||
|
data: (partners) {
|
||||||
|
if (partners.isEmpty) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: const Text(
|
||||||
|
"partner_page_empty_message",
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: onAddPartner,
|
||||||
|
icon: const Icon(Icons.person_add),
|
||||||
|
label: const Text("add_partner").tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: partners.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final partner = partners[index];
|
||||||
|
return ListTile(
|
||||||
|
leading: PartnerUserAvatar(partner: partner),
|
||||||
|
title: Text(partner.name),
|
||||||
|
subtitle: Text(partner.email),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.person_remove),
|
||||||
|
onPressed: () => onDeletePartner(partner),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (error, stack) => Center(
|
||||||
|
child: Text("Error loading partners: $error"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -6,15 +6,15 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/local_album_thumbnail.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/images/local_album_thumbnail.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/partner_user_avatar.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/partner.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/partner.provider.dart';
|
||||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
@ -391,7 +391,8 @@ class _QuickAccessButtonList extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final partners = ref.watch(partnerSharedWithProvider);
|
final partnerSharedWithAsync = ref.watch(driftSharedWithPartnerProvider);
|
||||||
|
final partners = partnerSharedWithAsync.valueOrNull ?? [];
|
||||||
|
|
||||||
return SliverPadding(
|
return SliverPadding(
|
||||||
padding: const EdgeInsets.only(left: 16, top: 12, right: 16, bottom: 32),
|
padding: const EdgeInsets.only(left: 16, top: 12, right: 16, bottom: 32),
|
||||||
@ -452,7 +453,6 @@ class _QuickAccessButtonList extends ConsumerWidget {
|
|||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// TODO: PIN code is needed
|
|
||||||
onTap: () => context.pushRoute(const DriftLockedFolderRoute()),
|
onTap: () => context.pushRoute(const DriftLockedFolderRoute()),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
@ -466,7 +466,7 @@ class _QuickAccessButtonList extends ConsumerWidget {
|
|||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () => context.pushRoute(const PartnerRoute()),
|
onTap: () => context.pushRoute(const DriftPartnerRoute()),
|
||||||
),
|
),
|
||||||
_PartnerList(partners: partners),
|
_PartnerList(partners: partners),
|
||||||
],
|
],
|
||||||
@ -480,7 +480,7 @@ class _QuickAccessButtonList extends ConsumerWidget {
|
|||||||
class _PartnerList extends StatelessWidget {
|
class _PartnerList extends StatelessWidget {
|
||||||
const _PartnerList({required this.partners});
|
const _PartnerList({required this.partners});
|
||||||
|
|
||||||
final List<UserDto> partners;
|
final List<PartnerUserDto> partners;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -503,7 +503,9 @@ class _PartnerList extends StatelessWidget {
|
|||||||
left: 12.0,
|
left: 12.0,
|
||||||
right: 18.0,
|
right: 18.0,
|
||||||
),
|
),
|
||||||
leading: userAvatar(context, partner, radius: 16),
|
leading: PartnerUserAvatar(
|
||||||
|
partner: partner,
|
||||||
|
),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"partner_list_user_photos",
|
"partner_list_user_photos",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
@ -6,11 +6,14 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.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/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftPartnerDetailPage extends StatelessWidget {
|
class DriftPartnerDetailPage extends StatelessWidget {
|
||||||
final UserDto partner;
|
final PartnerUserDto partner;
|
||||||
|
|
||||||
const DriftPartnerDetailPage({
|
const DriftPartnerDetailPage({
|
||||||
super.key,
|
super.key,
|
||||||
@ -35,12 +38,7 @@ class DriftPartnerDetailPage extends StatelessWidget {
|
|||||||
title: partner.name,
|
title: partner.name,
|
||||||
icon: Icons.person_outline,
|
icon: Icons.person_outline,
|
||||||
),
|
),
|
||||||
topSliverWidget: _InfoBox(
|
topSliverWidget: _InfoBox(partner: partner),
|
||||||
onTap: () => {
|
|
||||||
// TODO: Create DriftUserProvider/DriftUserService to handle this action
|
|
||||||
},
|
|
||||||
inTimeline: partner.inTimeline,
|
|
||||||
),
|
|
||||||
topSliverWidgetHeight: 110,
|
topSliverWidgetHeight: 110,
|
||||||
bottomSheet: const PartnerDetailBottomSheet(),
|
bottomSheet: const PartnerDetailBottomSheet(),
|
||||||
),
|
),
|
||||||
@ -48,15 +46,53 @@ class DriftPartnerDetailPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InfoBox extends StatelessWidget {
|
class _InfoBox extends ConsumerStatefulWidget {
|
||||||
final VoidCallback onTap;
|
final PartnerUserDto partner;
|
||||||
final bool inTimeline;
|
|
||||||
|
|
||||||
const _InfoBox({
|
const _InfoBox({
|
||||||
required this.onTap,
|
required this.partner,
|
||||||
required this.inTimeline,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<_InfoBox> createState() => _InfoBoxState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InfoBoxState extends ConsumerState<_InfoBox> {
|
||||||
|
bool _inTimeline = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_inTimeline = widget.partner.inTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
_toggleInTimeline() async {
|
||||||
|
final user = ref.read(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ref.read(partnerUsersProvider.notifier).toggleShowInTimeline(
|
||||||
|
widget.partner.id,
|
||||||
|
user.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_inTimeline = !_inTimeline;
|
||||||
|
});
|
||||||
|
} catch (error, stack) {
|
||||||
|
debugPrint("Failed to toggle in timeline: $error $stack");
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
toastType: ToastType.error,
|
||||||
|
durationInSecond: 1,
|
||||||
|
msg: "Failed to toggle the timeline setting",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
@ -96,8 +132,8 @@ class _InfoBox extends StatelessWidget {
|
|||||||
style: context.textTheme.bodyMedium,
|
style: context.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
trailing: Switch(
|
trailing: Switch(
|
||||||
value: inTimeline,
|
value: _inTimeline,
|
||||||
onChanged: (_) => onTap(),
|
onChanged: (_) => _toggleInTimeline(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
|
||||||
|
class PartnerUserAvatar extends StatelessWidget {
|
||||||
|
const PartnerUserAvatar({super.key, required this.partner});
|
||||||
|
|
||||||
|
final PartnerUserDto partner;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final url =
|
||||||
|
"${Store.get(StoreKey.serverEndpoint)}/users/${partner.id}/profile-image";
|
||||||
|
final nameFirstLetter = partner.name.isNotEmpty ? partner.name[0] : "";
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: 16,
|
||||||
|
backgroundColor: context.primaryColor.withAlpha(50),
|
||||||
|
foregroundImage: CachedNetworkImageProvider(
|
||||||
|
url,
|
||||||
|
headers: ApiService.getRequestHeaders(),
|
||||||
|
cacheKey: "user-${partner.id}-profile",
|
||||||
|
),
|
||||||
|
// silence errors if user has no profile image, use initials as fallback
|
||||||
|
onForegroundImageError: (exception, stackTrace) {},
|
||||||
|
child: Text(nameFirstLetter.toUpperCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
92
mobile/lib/providers/infrastructure/partner.provider.dart
Normal file
92
mobile/lib/providers/infrastructure/partner.provider.dart
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/partner.service.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
class PartnerNotifier extends Notifier<List<PartnerUserDto>> {
|
||||||
|
late DriftPartnerService _driftPartnerService;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<PartnerUserDto> build() {
|
||||||
|
_driftPartnerService = ref.read(driftPartnerServiceProvider);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadPartners() async {
|
||||||
|
final currentUser = ref.read(currentUserProvider);
|
||||||
|
if (currentUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = await _driftPartnerService.getSharedWith(currentUser.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<PartnerUserDto>> getPartners(String userId) async {
|
||||||
|
final partners = await _driftPartnerService.getSharedWith(userId);
|
||||||
|
state = partners;
|
||||||
|
return partners;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleShowInTimeline(String partnerId, String userId) async {
|
||||||
|
await _driftPartnerService.toggleShowInTimeline(partnerId, userId);
|
||||||
|
await _loadPartners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addPartner(PartnerUserDto partner) async {
|
||||||
|
final currentUser = ref.read(currentUserProvider);
|
||||||
|
if (currentUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _driftPartnerService.addPartner(partner.id, currentUser.id);
|
||||||
|
await _loadPartners();
|
||||||
|
ref.invalidate(driftAvailablePartnerProvider);
|
||||||
|
ref.invalidate(driftSharedByPartnerProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removePartner(PartnerUserDto partner) async {
|
||||||
|
final currentUser = ref.read(currentUserProvider);
|
||||||
|
if (currentUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _driftPartnerService.removePartner(partner.id, currentUser.id);
|
||||||
|
await _loadPartners();
|
||||||
|
ref.invalidate(driftAvailablePartnerProvider);
|
||||||
|
ref.invalidate(driftSharedByPartnerProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final driftAvailablePartnerProvider =
|
||||||
|
FutureProvider.autoDispose<List<PartnerUserDto>>((ref) {
|
||||||
|
final currentUser = ref.watch(currentUserProvider);
|
||||||
|
if (currentUser == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref
|
||||||
|
.watch(driftPartnerServiceProvider)
|
||||||
|
.getAvailablePartners(currentUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
final driftSharedByPartnerProvider =
|
||||||
|
FutureProvider.autoDispose<List<PartnerUserDto>>((ref) {
|
||||||
|
final currentUser = ref.watch(currentUserProvider);
|
||||||
|
if (currentUser == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.watch(driftPartnerServiceProvider).getSharedBy(currentUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
final driftSharedWithPartnerProvider =
|
||||||
|
FutureProvider.autoDispose<List<PartnerUserDto>>((ref) {
|
||||||
|
final currentUser = ref.watch(currentUserProvider);
|
||||||
|
if (currentUser == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.watch(driftPartnerServiceProvider).getSharedWith(currentUser.id);
|
||||||
|
});
|
@ -1,10 +1,15 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/partner.service.dart';
|
||||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/partner.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/partner_api.repository.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'user.provider.g.dart';
|
part 'user.provider.g.dart';
|
||||||
@ -23,3 +28,20 @@ UserService userService(Ref ref) => UserService(
|
|||||||
userApiRepository: ref.watch(userApiRepositoryProvider),
|
userApiRepository: ref.watch(userApiRepositoryProvider),
|
||||||
storeService: ref.watch(storeServiceProvider),
|
storeService: ref.watch(storeServiceProvider),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Drifts
|
||||||
|
final driftPartnerRepositoryProvider = Provider<DriftPartnerRepository>(
|
||||||
|
(ref) => DriftPartnerRepository(ref.watch(driftProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
final driftPartnerServiceProvider = Provider<DriftPartnerService>(
|
||||||
|
(ref) => DriftPartnerService(
|
||||||
|
ref.watch(driftPartnerRepositoryProvider),
|
||||||
|
ref.watch(partnerApiRepositoryProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final partnerUsersProvider =
|
||||||
|
NotifierProvider<PartnerNotifier, List<PartnerUserDto>>(
|
||||||
|
PartnerNotifier.new,
|
||||||
|
);
|
||||||
|
@ -51,6 +51,7 @@ import 'package:immich_mobile/pages/library/library.page.dart';
|
|||||||
import 'package:immich_mobile/pages/library/local_albums.page.dart';
|
import 'package:immich_mobile/pages/library/local_albums.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/locked/locked.page.dart';
|
import 'package:immich_mobile/pages/library/locked/locked.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/locked/pin_auth.page.dart';
|
import 'package:immich_mobile/pages/library/locked/pin_auth.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/partner/drift_partner.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
|
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
|
import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
|
import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
|
||||||
@ -485,6 +486,11 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: ChangeExperienceRoute.page,
|
page: ChangeExperienceRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftPartnerRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: DriftUploadDetailRoute.page,
|
page: DriftUploadDetailRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
@ -896,7 +896,7 @@ class DriftPartnerDetailRoute
|
|||||||
extends PageRouteInfo<DriftPartnerDetailRouteArgs> {
|
extends PageRouteInfo<DriftPartnerDetailRouteArgs> {
|
||||||
DriftPartnerDetailRoute({
|
DriftPartnerDetailRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
required UserDto partner,
|
required PartnerUserDto partner,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
DriftPartnerDetailRoute.name,
|
DriftPartnerDetailRoute.name,
|
||||||
@ -920,7 +920,7 @@ class DriftPartnerDetailRouteArgs {
|
|||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final UserDto partner;
|
final PartnerUserDto partner;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@ -928,6 +928,22 @@ class DriftPartnerDetailRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftPartnerPage]
|
||||||
|
class DriftPartnerRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftPartnerRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftPartnerRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftPartnerRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftPartnerPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [DriftPlaceDetailPage]
|
/// [DriftPlaceDetailPage]
|
||||||
class DriftPlaceDetailRoute extends PageRouteInfo<DriftPlaceDetailRouteArgs> {
|
class DriftPlaceDetailRoute extends PageRouteInfo<DriftPlaceDetailRouteArgs> {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user