mirror of
https://github.com/immich-app/immich.git
synced 2025-01-04 02:46:47 +01:00
feat(mobile): Transparent bottom Android navigation bar (#1953)
* transparent system overlay * immersive view to gallery viewer, as well * comments
This commit is contained in:
parent
a4c215751e
commit
950989a85e
2 changed files with 150 additions and 133 deletions
|
@ -211,6 +211,9 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||||
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
||||||
|
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent),
|
||||||
|
);
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
localizationsDelegates: context.localizationDelegates,
|
localizationsDelegates: context.localizationDelegates,
|
||||||
|
|
|
@ -247,6 +247,13 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
(showAppBar.value && !isZoomed.value)) &&
|
(showAppBar.value && !isZoomed.value)) &&
|
||||||
!isPlayingVideo.value;
|
!isPlayingVideo.value;
|
||||||
|
|
||||||
|
// Change to and from immersive mode, hiding navigation and app bar
|
||||||
|
if (show) {
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
} else {
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
|
}
|
||||||
|
|
||||||
return AnimatedOpacity(
|
return AnimatedOpacity(
|
||||||
duration: const Duration(milliseconds: 100),
|
duration: const Duration(milliseconds: 100),
|
||||||
opacity: show ? 1.0 : 0.0,
|
opacity: show ? 1.0 : 0.0,
|
||||||
|
@ -291,145 +298,152 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
body: Stack(
|
body: WillPopScope(
|
||||||
children: [
|
onWillPop: () async {
|
||||||
PhotoViewGallery.builder(
|
// Change immersive mode back to normal "edgeToEdge" mode
|
||||||
scaleStateChangedCallback: (state) {
|
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
isZoomed.value = state != PhotoViewScaleState.initial;
|
return true;
|
||||||
showAppBar.value = !isZoomed.value;
|
},
|
||||||
},
|
child: Stack(
|
||||||
pageController: controller,
|
children: [
|
||||||
scrollPhysics: isZoomed.value
|
PhotoViewGallery.builder(
|
||||||
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
|
scaleStateChangedCallback: (state) {
|
||||||
: (Platform.isIOS
|
isZoomed.value = state != PhotoViewScaleState.initial;
|
||||||
? const BouncingScrollPhysics() // Use bouncing physics for iOS
|
showAppBar.value = !isZoomed.value;
|
||||||
: const ClampingScrollPhysics() // Use heavy physics for Android
|
},
|
||||||
),
|
pageController: controller,
|
||||||
itemCount: assetList.length,
|
scrollPhysics: isZoomed.value
|
||||||
scrollDirection: Axis.horizontal,
|
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
|
||||||
onPageChanged: (value) {
|
: (Platform.isIOS
|
||||||
// Precache image
|
? const BouncingScrollPhysics() // Use bouncing physics for iOS
|
||||||
if (indexOfAsset.value < value) {
|
: const ClampingScrollPhysics() // Use heavy physics for Android
|
||||||
// Moving forwards, so precache the next asset
|
),
|
||||||
precacheNextImage(value + 1);
|
itemCount: assetList.length,
|
||||||
} else {
|
scrollDirection: Axis.horizontal,
|
||||||
// Moving backwards, so precache previous asset
|
onPageChanged: (value) {
|
||||||
precacheNextImage(value - 1);
|
// Precache image
|
||||||
}
|
if (indexOfAsset.value < value) {
|
||||||
indexOfAsset.value = value;
|
// Moving forwards, so precache the next asset
|
||||||
HapticFeedback.selectionClick();
|
precacheNextImage(value + 1);
|
||||||
},
|
} else {
|
||||||
loadingBuilder: isLoadPreview.value
|
// Moving backwards, so precache previous asset
|
||||||
? (context, event) {
|
precacheNextImage(value - 1);
|
||||||
final asset = assetList[indexOfAsset.value];
|
}
|
||||||
if (!asset.isLocal) {
|
indexOfAsset.value = value;
|
||||||
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve
|
HapticFeedback.selectionClick();
|
||||||
// Three-Stage Loading (WEBP -> JPEG -> Original)
|
},
|
||||||
final webPThumbnail = CachedNetworkImage(
|
loadingBuilder: isLoadPreview.value
|
||||||
imageUrl: getThumbnailUrl(
|
? (context, event) {
|
||||||
asset,
|
final asset = assetList[indexOfAsset.value];
|
||||||
type: api.ThumbnailFormat.WEBP,
|
if (!asset.isLocal) {
|
||||||
),
|
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve
|
||||||
cacheKey: getThumbnailCacheKey(
|
// Three-Stage Loading (WEBP -> JPEG -> Original)
|
||||||
asset,
|
final webPThumbnail = CachedNetworkImage(
|
||||||
type: api.ThumbnailFormat.WEBP,
|
imageUrl: getThumbnailUrl(
|
||||||
),
|
asset,
|
||||||
httpHeaders: {'Authorization': authToken},
|
type: api.ThumbnailFormat.WEBP,
|
||||||
progressIndicatorBuilder: (_, __, ___) => const Center(
|
),
|
||||||
child: ImmichLoadingIndicator(),
|
cacheKey: getThumbnailCacheKey(
|
||||||
),
|
asset,
|
||||||
fadeInDuration: const Duration(milliseconds: 0),
|
type: api.ThumbnailFormat.WEBP,
|
||||||
fit: BoxFit.contain,
|
),
|
||||||
);
|
httpHeaders: {'Authorization': authToken},
|
||||||
|
progressIndicatorBuilder: (_, __, ___) => const Center(
|
||||||
|
child: ImmichLoadingIndicator(),
|
||||||
|
),
|
||||||
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
);
|
||||||
|
|
||||||
return CachedNetworkImage(
|
return CachedNetworkImage(
|
||||||
imageUrl: getThumbnailUrl(
|
imageUrl: getThumbnailUrl(
|
||||||
asset,
|
asset,
|
||||||
type: api.ThumbnailFormat.JPEG,
|
type: api.ThumbnailFormat.JPEG,
|
||||||
),
|
),
|
||||||
cacheKey: getThumbnailCacheKey(
|
cacheKey: getThumbnailCacheKey(
|
||||||
asset,
|
asset,
|
||||||
type: api.ThumbnailFormat.JPEG,
|
type: api.ThumbnailFormat.JPEG,
|
||||||
),
|
),
|
||||||
httpHeaders: {'Authorization': authToken},
|
httpHeaders: {'Authorization': authToken},
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
fadeInDuration: const Duration(milliseconds: 0),
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
placeholder: (_, __) => webPThumbnail,
|
placeholder: (_, __) => webPThumbnail,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return Image(
|
||||||
|
image: localThumbnailImageProvider(asset),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
builder: (context, index) {
|
||||||
|
getAssetExif();
|
||||||
|
if (assetList[index].isImage && !isPlayingMotionVideo.value) {
|
||||||
|
// Show photo
|
||||||
|
final ImageProvider provider;
|
||||||
|
if (assetList[index].isLocal) {
|
||||||
|
provider = localImageProvider(assetList[index]);
|
||||||
|
} else {
|
||||||
|
if (isLoadOriginal.value) {
|
||||||
|
provider = originalImageProvider(assetList[index]);
|
||||||
} else {
|
} else {
|
||||||
return Image(
|
provider = remoteThumbnailImageProvider(
|
||||||
image: localThumbnailImageProvider(asset),
|
assetList[index],
|
||||||
fit: BoxFit.contain,
|
api.ThumbnailFormat.JPEG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: null,
|
return PhotoViewGalleryPageOptions(
|
||||||
builder: (context, index) {
|
onDragStart: (_, details, __) =>
|
||||||
getAssetExif();
|
localPosition = details.localPosition,
|
||||||
if (assetList[index].isImage && !isPlayingMotionVideo.value) {
|
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
|
||||||
// Show photo
|
onTapDown: (_, __, ___) =>
|
||||||
final ImageProvider provider;
|
showAppBar.value = !showAppBar.value,
|
||||||
if (assetList[index].isLocal) {
|
imageProvider: provider,
|
||||||
provider = localImageProvider(assetList[index]);
|
heroAttributes: PhotoViewHeroAttributes(
|
||||||
} else {
|
tag: assetList[index].id,
|
||||||
if (isLoadOriginal.value) {
|
|
||||||
provider = originalImageProvider(assetList[index]);
|
|
||||||
} else {
|
|
||||||
provider = remoteThumbnailImageProvider(
|
|
||||||
assetList[index],
|
|
||||||
api.ThumbnailFormat.JPEG,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return PhotoViewGalleryPageOptions(
|
|
||||||
onDragStart: (_, details, __) =>
|
|
||||||
localPosition = details.localPosition,
|
|
||||||
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
|
|
||||||
onTapDown: (_, __, ___) =>
|
|
||||||
showAppBar.value = !showAppBar.value,
|
|
||||||
imageProvider: provider,
|
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
|
||||||
tag: assetList[index].id,
|
|
||||||
),
|
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
tightMode: true,
|
|
||||||
minScale: PhotoViewComputedScale.contained,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return PhotoViewGalleryPageOptions.customChild(
|
|
||||||
onDragStart: (_, details, __) =>
|
|
||||||
localPosition = details.localPosition,
|
|
||||||
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
|
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
|
||||||
tag: assetList[index].id,
|
|
||||||
),
|
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
maxScale: 1.0,
|
|
||||||
minScale: 1.0,
|
|
||||||
child: SafeArea(
|
|
||||||
child: VideoViewerPage(
|
|
||||||
onPlaying: () => isPlayingVideo.value = true,
|
|
||||||
onPaused: () => isPlayingVideo.value = false,
|
|
||||||
asset: assetList[index],
|
|
||||||
isMotionVideo: isPlayingMotionVideo.value,
|
|
||||||
onVideoEnded: () {
|
|
||||||
if (isPlayingMotionVideo.value) {
|
|
||||||
isPlayingMotionVideo.value = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
filterQuality: FilterQuality.high,
|
||||||
);
|
tightMode: true,
|
||||||
}
|
minScale: PhotoViewComputedScale.contained,
|
||||||
},
|
);
|
||||||
),
|
} else {
|
||||||
Positioned(
|
return PhotoViewGalleryPageOptions.customChild(
|
||||||
top: 0,
|
onDragStart: (_, details, __) =>
|
||||||
left: 0,
|
localPosition = details.localPosition,
|
||||||
right: 0,
|
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
|
||||||
child: buildAppBar(),
|
heroAttributes: PhotoViewHeroAttributes(
|
||||||
),
|
tag: assetList[index].id,
|
||||||
],
|
),
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
maxScale: 1.0,
|
||||||
|
minScale: 1.0,
|
||||||
|
child: SafeArea(
|
||||||
|
child: VideoViewerPage(
|
||||||
|
onPlaying: () => isPlayingVideo.value = true,
|
||||||
|
onPaused: () => isPlayingVideo.value = false,
|
||||||
|
asset: assetList[index],
|
||||||
|
isMotionVideo: isPlayingMotionVideo.value,
|
||||||
|
onVideoEnded: () {
|
||||||
|
if (isPlayingMotionVideo.value) {
|
||||||
|
isPlayingMotionVideo.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: buildAppBar(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue