118 - Implement shared album feature (#124)

* New features 
  - Share album. Users can now create albums to share with existing people on the network.
  - Owner can delete the album.
  - Owner can invite the additional users to the album.
  - Shared users and the owner can add additional assets to the album.
* In the asset viewer, the user can swipe up to see detailed information and swip down to dismiss.
* Several UI enhancements.
This commit is contained in:
Alex
2022-04-23 21:08:45 -05:00
committed by GitHub
parent a3b84b3ca7
commit 4309104925
87 changed files with 3717 additions and 199 deletions

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
class AlbumActionOutlinedButton extends StatelessWidget {
final VoidCallback? onPressed;
final String labelText;
final IconData iconData;
const AlbumActionOutlinedButton({Key? key, this.onPressed, required this.labelText, required this.iconData})
: super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: OutlinedButton.icon(
style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.symmetric(vertical: 0, horizontal: 10)),
shape: MaterialStateProperty.resolveWith<OutlinedBorder>(
(_) => RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
side: MaterialStateProperty.resolveWith<BorderSide>(
(_) => const BorderSide(width: 1, color: Color.fromARGB(255, 158, 158, 158)),
),
),
icon: Icon(iconData, size: 15),
label: Text(
labelText,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87),
),
onPressed: onPressed,
),
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/providers/album_title.provider.dart';
class AlbumTitleTextField extends ConsumerWidget {
const AlbumTitleTextField({
Key? key,
required this.isAlbumTitleEmpty,
required this.albumTitleTextFieldFocusNode,
required this.albumTitleController,
required this.isAlbumTitleTextFieldFocus,
}) : super(key: key);
final ValueNotifier<bool> isAlbumTitleEmpty;
final FocusNode albumTitleTextFieldFocusNode;
final TextEditingController albumTitleController;
final ValueNotifier<bool> isAlbumTitleTextFieldFocus;
@override
Widget build(BuildContext context, WidgetRef ref) {
return TextField(
onChanged: (v) {
if (v.isEmpty) {
isAlbumTitleEmpty.value = true;
} else {
isAlbumTitleEmpty.value = false;
}
ref.watch(albumTitleProvider.notifier).setAlbumTitle(v);
},
focusNode: albumTitleTextFieldFocusNode,
style: TextStyle(fontSize: 28, color: Colors.grey[700], fontWeight: FontWeight.bold),
controller: albumTitleController,
onTap: () {
isAlbumTitleTextFieldFocus.value = true;
if (albumTitleController.text == 'Untitled') {
albumTitleController.clear();
}
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
suffixIcon: !isAlbumTitleEmpty.value && isAlbumTitleTextFieldFocus.value
? IconButton(
onPressed: () {
albumTitleController.clear();
isAlbumTitleEmpty.value = true;
},
icon: const Icon(Icons.cancel_rounded),
splashRadius: 10,
)
: null,
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.circular(10),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.circular(10),
),
hintText: 'Add a title',
focusColor: Colors.grey[300],
fillColor: Colors.grey[200],
filled: isAlbumTitleTextFieldFocus.value,
),
);
}
}

View File

@@ -0,0 +1,181 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
const AlbumViewerAppbar({
Key? key,
required AsyncValue<SharedAlbum> albumInfo,
required this.userId,
required this.albumId,
}) : _albumInfo = albumInfo,
super(key: key);
final AsyncValue<SharedAlbum> _albumInfo;
final String userId;
final String albumId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable;
final selectedAssetsInAlbum = ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
void _onDeleteAlbumPressed(String albumId) async {
ImmichLoadingOverlayController.appLoader.show();
bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(albumId);
if (isSuccess) {
AutoRouter.of(context).navigate(const TabControllerRoute(children: [SharingRoute()]));
} else {
ImmichToast.show(
context: context,
msg: "Failed to delete album",
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
}
ImmichLoadingOverlayController.appLoader.hide();
}
void _onLeaveAlbumPressed(String albumId) async {
ImmichLoadingOverlayController.appLoader.show();
bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(albumId);
if (isSuccess) {
AutoRouter.of(context).navigate(const TabControllerRoute(children: [SharingRoute()]));
} else {
Navigator.pop(context);
ImmichToast.show(
context: context,
msg: "Failed to leave album",
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
}
ImmichLoadingOverlayController.appLoader.hide();
}
void _onRemoveFromAlbumPressed(String albumId) async {
ImmichLoadingOverlayController.appLoader.show();
bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
albumId,
selectedAssetsInAlbum.map((a) => a.id).toList(),
);
if (isSuccess) {
Navigator.pop(context);
ref.watch(assetSelectionProvider.notifier).disableMultiselection();
ref.refresh(sharedAlbumDetailProvider(albumId));
} else {
Navigator.pop(context);
ImmichToast.show(
context: context,
msg: "There are problems in removing assets from album",
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
}
ImmichLoadingOverlayController.appLoader.hide();
}
_buildBottomSheetActionButton() {
if (isMultiSelectionEnable) {
if (_albumInfo.asData?.value.ownerId == userId) {
return ListTile(
leading: const Icon(Icons.delete_sweep_rounded),
title: const Text(
'Remove from album',
style: TextStyle(fontWeight: FontWeight.bold),
),
onTap: () => _onRemoveFromAlbumPressed(albumId),
);
} else {
return Container();
}
} else {
if (_albumInfo.asData?.value.ownerId == userId) {
return ListTile(
leading: const Icon(Icons.delete_forever_rounded),
title: const Text(
'Delete album',
style: TextStyle(fontWeight: FontWeight.bold),
),
onTap: () => _onDeleteAlbumPressed(albumId),
);
} else {
return ListTile(
leading: const Icon(Icons.person_remove_rounded),
title: const Text(
'Leave album',
style: TextStyle(fontWeight: FontWeight.bold),
),
onTap: () => _onLeaveAlbumPressed(albumId),
);
}
}
}
void _buildBottomSheet() {
showModalBottomSheet(
backgroundColor: immichBackgroundColor,
isScrollControlled: false,
context: context,
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildBottomSheetActionButton(),
],
);
},
);
}
_buildLeadingButton() {
if (isMultiSelectionEnable) {
return IconButton(
onPressed: () => ref.watch(assetSelectionProvider.notifier).disableMultiselection(),
icon: const Icon(Icons.close_rounded),
splashRadius: 25,
);
} else {
return IconButton(
onPressed: () async => await AutoRouter.of(context).pop(),
icon: const Icon(Icons.arrow_back_ios_rounded),
splashRadius: 25,
);
}
}
return AppBar(
elevation: 0,
leading: _buildLeadingButton(),
title: isMultiSelectionEnable ? Text(selectedAssetsInAlbum.length.toString()) : Container(),
centerTitle: false,
actions: [
IconButton(
splashRadius: 25,
onPressed: _buildBottomSheet,
icon: const Icon(Icons.more_horiz_rounded),
),
],
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

View File

@@ -0,0 +1,181 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/routing/router.dart';
class AlbumViewerThumbnail extends HookConsumerWidget {
final ImmichAsset asset;
const AlbumViewerThumbnail({Key? key, required this.asset}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1);
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
var deviceId = ref.watch(authenticationProvider).deviceId;
final selectedAssetsInAlbumViewer = ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable;
_viewAsset() {
if (asset.type == 'IMAGE') {
AutoRouter.of(context).push(
ImageViewerRoute(
imageUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false',
heroTag: asset.id,
thumbnailUrl: thumbnailRequestUrl,
asset: asset,
),
);
} else {
AutoRouter.of(context).push(
VideoViewerRoute(
videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
asset: asset),
);
}
}
BoxBorder drawBorderColor() {
if (selectedAssetsInAlbumViewer.contains(asset)) {
return Border.all(
color: Theme.of(context).primaryColorLight,
width: 10,
);
} else {
return const Border();
}
}
_enableMultiSelection() {
ref.watch(assetSelectionProvider.notifier).enableMultiselection();
ref.watch(assetSelectionProvider.notifier).addAssetsInAlbumViewer([asset]);
}
_disableMultiSelection() {
ref.watch(assetSelectionProvider.notifier).disableMultiselection();
}
_buildVideoLabel() {
if (asset.type == 'IMAGE') {
return Container();
} else {
return Positioned(
top: 5,
right: 5,
child: Row(
children: [
Text(
asset.duration.toString().substring(0, 7),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
),
);
}
}
_buildAssetStoreLocationIcon() {
return Positioned(
right: 10,
bottom: 5,
child: Icon(
(deviceId != asset.deviceId) ? Icons.cloud_done_outlined : Icons.photo_library_rounded,
color: Colors.white,
size: 18,
),
);
}
_buildAssetSelectionIcon() {
bool isSelected = selectedAssetsInAlbumViewer.contains(asset);
if (isMultiSelectionEnable) {
return Positioned(
left: 10,
top: 5,
child: isSelected
? Icon(
Icons.check_circle_rounded,
color: Theme.of(context).primaryColor,
)
: const Icon(
Icons.check_circle_outline_rounded,
color: Colors.white,
),
);
} else {
return Container();
}
}
_buildThumbnailImage() {
return Container(
decoration: BoxDecoration(border: drawBorderColor()),
child: CachedNetworkImage(
cacheKey: "${asset.id}-${cacheKey.value}",
width: 300,
height: 300,
memCacheHeight: 200,
fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
fadeInDuration: const Duration(milliseconds: 250),
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
scale: 0.2,
child: CircularProgressIndicator(value: downloadProgress.progress),
),
errorWidget: (context, url, error) {
return Icon(
Icons.image_not_supported_outlined,
color: Theme.of(context).primaryColor,
);
},
),
);
}
_handleSelectionGesture() {
if (selectedAssetsInAlbumViewer.contains(asset)) {
ref.watch(assetSelectionProvider.notifier).removeAssetsInAlbumViewer([asset]);
if (selectedAssetsInAlbumViewer.isEmpty) {
_disableMultiSelection();
}
} else {
ref.watch(assetSelectionProvider.notifier).addAssetsInAlbumViewer([asset]);
}
}
return GestureDetector(
onTap: isMultiSelectionEnable ? _handleSelectionGesture : _viewAsset,
onLongPress: _enableMultiSelection,
child: Hero(
tag: asset.id,
child: Stack(
children: [
_buildThumbnailImage(),
_buildAssetStoreLocationIcon(),
_buildVideoLabel(),
_buildAssetSelectionIcon(),
],
),
),
);
}
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/ui/selection_thumbnail_image.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
class AssetGridByMonth extends HookConsumerWidget {
final List<ImmichAsset> assetGroup;
const AssetGridByMonth({Key? key, required this.assetGroup}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return SelectionThumbnailImage(asset: assetGroup[index]);
},
childCount: assetGroup.length,
),
);
}
}

View File

@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
class MonthGroupTitle extends HookConsumerWidget {
final String month;
final List<ImmichAsset> assetGroup;
const MonthGroupTitle({Key? key, required this.month, required this.assetGroup}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedDateGroup = ref.watch(assetSelectionProvider).selectedMonths;
final selectedAssets = ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
_handleTitleIconClick() {
HapticFeedback.heavyImpact();
if (isAlbumExist) {
if (selectedDateGroup.contains(month)) {
ref.watch(assetSelectionProvider.notifier).removeAssetsInMonth(month, []);
ref.watch(assetSelectionProvider.notifier).removeSelectedAdditionalAssets(assetGroup);
} else {
ref.watch(assetSelectionProvider.notifier).addAllAssetsInMonth(month, []);
// Deep clone assetGroup
var assetGroupWithNewItems = [...assetGroup];
for (var selectedAsset in selectedAssets) {
assetGroupWithNewItems.removeWhere((a) => a.id == selectedAsset.id);
}
ref.watch(assetSelectionProvider.notifier).addAdditionalAssets(assetGroupWithNewItems);
}
} else {
if (selectedDateGroup.contains(month)) {
ref.watch(assetSelectionProvider.notifier).removeAssetsInMonth(month, assetGroup);
} else {
ref.watch(assetSelectionProvider.notifier).addAllAssetsInMonth(month, assetGroup);
}
}
}
_getSimplifiedMonth() {
var monthAndYear = month.split(',');
var yearText = monthAndYear[1].trim();
var monthText = monthAndYear[0].trim();
var currentYear = DateTime.now().year.toString();
if (yearText == currentYear) {
return monthText;
} else {
return month;
}
}
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 29.0, bottom: 29.0, left: 14.0, right: 8.0),
child: Row(
children: [
GestureDetector(
onTap: _handleTitleIconClick,
child: selectedDateGroup.contains(month)
? Icon(
Icons.check_circle_rounded,
color: Theme.of(context).primaryColor,
)
: const Icon(
Icons.circle_outlined,
color: Colors.grey,
),
),
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
_getSimplifiedMonth(),
style: TextStyle(
fontSize: 24,
color: Theme.of(context).primaryColor,
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,149 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
class SelectionThumbnailImage extends HookConsumerWidget {
final ImmichAsset asset;
const SelectionThumbnailImage({Key? key, required this.asset}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1);
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
var selectedAsset = ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
var newAssetsForAlbum = ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
Widget _buildSelectionIcon(ImmichAsset asset) {
if (selectedAsset.contains(asset) && !isAlbumExist) {
return Icon(
Icons.check_circle,
color: Theme.of(context).primaryColor,
);
} else if (selectedAsset.contains(asset) && isAlbumExist) {
return const Icon(
Icons.check_circle,
color: Color.fromARGB(255, 233, 233, 233),
);
} else if (newAssetsForAlbum.contains(asset) && isAlbumExist) {
return Icon(
Icons.check_circle,
color: Theme.of(context).primaryColor,
);
} else {
return const Icon(
Icons.circle_outlined,
color: Colors.white,
);
}
}
BoxBorder drawBorderColor() {
if (selectedAsset.contains(asset) && !isAlbumExist) {
return Border.all(
color: Theme.of(context).primaryColorLight,
width: 10,
);
} else if (selectedAsset.contains(asset) && isAlbumExist) {
return Border.all(
color: const Color.fromARGB(255, 190, 190, 190),
width: 10,
);
} else if (newAssetsForAlbum.contains(asset) && isAlbumExist) {
return Border.all(
color: Theme.of(context).primaryColorLight,
width: 10,
);
}
return const Border();
}
return GestureDetector(
onTap: () {
if (isAlbumExist) {
// Operation for existing album
if (!selectedAsset.contains(asset)) {
if (newAssetsForAlbum.contains(asset)) {
ref.watch(assetSelectionProvider.notifier).removeSelectedAdditionalAssets([asset]);
} else {
ref.watch(assetSelectionProvider.notifier).addAdditionalAssets([asset]);
}
}
} else {
// Operation for new album
if (selectedAsset.contains(asset)) {
ref.watch(assetSelectionProvider.notifier).removeSelectedNewAssets([asset]);
} else {
ref.watch(assetSelectionProvider.notifier).addNewAssets([asset]);
}
}
},
child: Hero(
tag: asset.id,
child: Stack(
children: [
Container(
decoration: BoxDecoration(border: drawBorderColor()),
child: CachedNetworkImage(
cacheKey: "${asset.id}-${cacheKey.value}",
width: 150,
height: 150,
memCacheHeight: asset.type == 'IMAGE' ? 150 : 150,
fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
fadeInDuration: const Duration(milliseconds: 250),
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
scale: 0.2,
child: CircularProgressIndicator(value: downloadProgress.progress),
),
errorWidget: (context, url, error) {
return Icon(
Icons.image_not_supported_outlined,
color: Theme.of(context).primaryColor,
);
},
),
),
Padding(
padding: const EdgeInsets.all(3.0),
child: Align(
alignment: Alignment.topLeft,
child: _buildSelectionIcon(asset),
),
),
asset.type == 'IMAGE'
? Container()
: Positioned(
bottom: 5,
right: 5,
child: Row(
children: [
Text(
asset.duration.toString().substring(0, 7),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
),
)
],
),
),
);
}
}

View File

@@ -0,0 +1,55 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
class SharedAlbumThumbnailImage extends HookConsumerWidget {
final ImmichAsset asset;
const SharedAlbumThumbnailImage({Key? key, required this.asset}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1);
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
return GestureDetector(
onTap: () {
// debugPrint("View ${asset.id}");
},
child: Hero(
tag: asset.id,
child: Stack(
children: [
CachedNetworkImage(
cacheKey: "${asset.id}-${cacheKey.value}",
width: 500,
height: 500,
memCacheHeight: asset.type == 'IMAGE' ? 500 : 500,
fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
fadeInDuration: const Duration(milliseconds: 250),
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
scale: 0.2,
child: CircularProgressIndicator(value: downloadProgress.progress),
),
errorWidget: (context, url, error) {
return Icon(
Icons.image_not_supported_outlined,
color: Theme.of(context).primaryColor,
);
},
),
],
),
),
);
}
}

View File

@@ -0,0 +1,83 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/routing/router.dart';
class SharingSliverAppBar extends StatelessWidget {
const SharingSliverAppBar({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SliverAppBar(
centerTitle: true,
floating: false,
pinned: true,
snap: false,
leading: Container(),
// elevation: 0,
title: Text(
'IMMICH',
style: TextStyle(
fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold,
fontSize: 22,
color: Theme.of(context).primaryColor,
),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(50.0),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 4.0),
child: TextButton.icon(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Theme.of(context).primaryColor.withAlpha(20)),
// foregroundColor: MaterialStateProperty.all(Colors.white),
),
onPressed: () {
AutoRouter.of(context).push(const CreateSharedAlbumRoute());
},
icon: const Icon(
Icons.photo_album_outlined,
size: 20,
),
label: const Text(
"Create shared album",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 4.0),
child: TextButton.icon(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Theme.of(context).primaryColor.withAlpha(20)),
// foregroundColor: MaterialStateProperty.all(Colors.white),
),
onPressed: null,
icon: const Icon(
Icons.swap_horizontal_circle_outlined,
size: 20,
),
label: const Text(
"Share with partner",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
),
),
)
],
),
),
),
);
}
}