diff --git a/app/assets/images/icons/google-drive.svg b/app/assets/images/icons/google-drive.svg
new file mode 100644
index 0000000..987eb2c
--- /dev/null
+++ b/app/assets/images/icons/google-drive.svg
@@ -0,0 +1,8 @@
+
diff --git a/app/assets/images/icons/google_photos.svg b/app/assets/images/icons/google_photos.svg
deleted file mode 100644
index 44b3fb3..0000000
--- a/app/assets/images/icons/google_photos.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
diff --git a/app/assets/locales/app_en.arb b/app/assets/locales/app_en.arb
index 81d8549..c467ea3 100644
--- a/app/assets/locales/app_en.arb
+++ b/app/assets/locales/app_en.arb
@@ -20,7 +20,7 @@
"common_delete_from_google_drive": "Delete from Google Drive",
"common_delete_from_device": "Delete from Device",
"common_cancel": "Cancel",
-
+ "common_not_available": "N/A",
"no_internet_connection_error": "No internet connection! Please check your network and try again.",
"something_went_wrong_error": "Something went wrong! Please try again later.",
@@ -71,6 +71,18 @@
"download_in_progress_text": "Download in progress",
"download_require_text": "Download required",
- "download_require_message": "To watch the video, simply download it first. Tap the download button to begin downloading the video."
-
+ "download_require_message": "To watch the video, simply download it first. Tap the download button to begin downloading the video.",
+
+ "name_text": "Name",
+ "size_text": "Size",
+ "created_at_text": "Created at",
+ "modified_at_text": "Modified at",
+ "mimetype_text": "MIME Type",
+ "duration_text": "Duration",
+ "location_text": "Location",
+ "resolution_text": "Resolution",
+ "orientation_text": "Orientation",
+ "path_text": "Path",
+ "display_size_text": "Display Size",
+ "source_text": "Source"
}
\ No newline at end of file
diff --git a/app/lib/components/thumbnail_builder.dart b/app/lib/components/thumbnail_builder.dart
new file mode 100644
index 0000000..0c34678
--- /dev/null
+++ b/app/lib/components/thumbnail_builder.dart
@@ -0,0 +1,119 @@
+import 'dart:typed_data';
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:data/models/media/media.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:style/extensions/context_extensions.dart';
+import 'package:style/indicators/circular_progress_indicator.dart';
+
+class AppMediaThumbnail extends StatelessWidget {
+ final Object? heroTag;
+ final AppMedia media;
+ final Size size;
+ final double radius;
+ final Future? thumbnailByte;
+
+ const AppMediaThumbnail({
+ super.key,
+ required this.size,
+ this.heroTag,
+ this.radius = 4,
+ required this.thumbnailByte,
+ required this.media,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ if (media.sources.contains(AppMediaSource.local)) {
+ return FutureBuilder(
+ future: thumbnailByte,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done &&
+ snapshot.hasData) {
+ return ClipRRect(
+ borderRadius: BorderRadius.circular(radius),
+ child: Hero(
+ tag: heroTag ?? '',
+ child: Image.memory(
+ snapshot.data!,
+ height: size.height,
+ width: size.width,
+ fit: BoxFit.cover,
+ ),
+ ),
+ );
+ } else if (snapshot.hasError) {
+ return AppMediaErrorPlaceHolder(
+ size: size,
+ );
+ } else {
+ return AppMediaPlaceHolder(
+ showLoader: false,
+ size: size,
+ );
+ }
+ },
+ );
+ } else {
+ return Hero(
+ tag: heroTag ?? '',
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(radius),
+ child: CachedNetworkImage(
+ imageUrl: media.thumbnailLink ?? '',
+ width: size.width,
+ height: size.height,
+ fit: BoxFit.cover,
+ errorWidget: (context, url, error) => AppMediaErrorPlaceHolder(
+ size: size,
+ ),
+ progressIndicatorBuilder: (context, url, progress) =>
+ AppMediaPlaceHolder(
+ size: size,
+ value: progress.progress,
+ )),
+ ),
+ );
+ }
+ }
+}
+
+class AppMediaPlaceHolder extends StatelessWidget {
+ final double? value;
+ final Size? size;
+ final bool showLoader;
+
+ const AppMediaPlaceHolder(
+ {super.key, this.value, this.showLoader = true, this.size});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: size?.height,
+ width: size?.width,
+ color: context.colorScheme.containerHighOnSurface,
+ alignment: Alignment.center,
+ child: showLoader ? AppCircularProgressIndicator(value: value) : null,
+ );
+ }
+}
+
+class AppMediaErrorPlaceHolder extends StatelessWidget {
+ final Size? size;
+
+ const AppMediaErrorPlaceHolder({super.key, this.size});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: size?.height,
+ width: size?.width,
+ color: context.colorScheme.containerNormalOnSurface,
+ alignment: Alignment.center,
+ child: Icon(
+ CupertinoIcons.exclamationmark_circle,
+ color: context.colorScheme.onPrimary,
+ size: 32,
+ ),
+ );
+ }
+}
diff --git a/app/lib/domain/assets/assets_paths.dart b/app/lib/domain/assets/assets_paths.dart
index c4bd56a..332d3d7 100644
--- a/app/lib/domain/assets/assets_paths.dart
+++ b/app/lib/domain/assets/assets_paths.dart
@@ -9,5 +9,5 @@ class PathImages {
}
class PathIcons {
- String get googlePhotos => 'assets/images/icons/google_photos.svg';
+ String get googleDrive => 'assets/images/icons/google-drive.svg';
}
diff --git a/app/lib/ui/flow/home/components/app_media_item.dart b/app/lib/ui/flow/home/components/app_media_item.dart
index 12efc36..4cd07c9 100644
--- a/app/lib/ui/flow/home/components/app_media_item.dart
+++ b/app/lib/ui/flow/home/components/app_media_item.dart
@@ -1,6 +1,5 @@
-import 'dart:async';
import 'dart:typed_data';
-import 'package:cached_network_image/cached_network_image.dart';
+import 'package:cloud_gallery/components/thumbnail_builder.dart';
import 'package:cloud_gallery/domain/formatter/duration_formatter.dart';
import 'package:data/models/app_process/app_process.dart';
import 'package:data/models/media/media.dart';
@@ -35,7 +34,7 @@ class AppMediaItem extends StatefulWidget {
class _AppMediaItemState extends State
with AutomaticKeepAliveClientMixin {
- late Future thumbnailByte;
+ Future? thumbnailByte;
@override
void initState() {
@@ -93,64 +92,14 @@ class _AppMediaItemState extends State
Widget _buildMediaView(
{required BuildContext context, required BoxConstraints constraints}) {
- if (widget.media.sources.contains(AppMediaSource.local)) {
- return FutureBuilder(
- future: thumbnailByte,
- builder: (context, snapshot) {
- if (snapshot.connectionState == ConnectionState.done &&
- snapshot.hasData) {
- return Hero(
- tag: widget.media,
- child: Image.memory(
- snapshot.data!,
- width: constraints.maxWidth,
- height: constraints.maxHeight,
- fit: BoxFit.cover,
- ),
- );
- } else {
- return _buildPlaceholder(context: context, showLoader: false);
- }
- },
- );
- } else {
- return Hero(
- tag: widget.media,
- child: CachedNetworkImage(
- imageUrl: widget.media.thumbnailLink ?? '',
- width: constraints.maxWidth,
- height: constraints.maxHeight,
- fit: BoxFit.cover,
- errorWidget: (context, url, error) => _buildErrorWidget(context),
- progressIndicatorBuilder: (context, url, progress) =>
- _buildPlaceholder(
- context: context,
- value: progress.progress,
- )),
- );
- }
+ return AppMediaThumbnail(
+ size: constraints.biggest,
+ thumbnailByte: thumbnailByte,
+ media: widget.media,
+ heroTag: widget.media,
+ );
}
- Widget _buildPlaceholder(
- {required BuildContext context,
- double? value,
- bool showLoader = true}) =>
- Container(
- color: context.colorScheme.containerHighOnSurface,
- alignment: Alignment.center,
- child: showLoader ? AppCircularProgressIndicator(value: value) : null,
- );
-
- Widget _buildErrorWidget(BuildContext context) => Container(
- color: context.colorScheme.containerNormalOnSurface,
- alignment: Alignment.center,
- child: Icon(
- CupertinoIcons.exclamationmark_circle,
- color: context.colorScheme.onPrimary,
- size: 32,
- ),
- );
-
Widget _sourceIndicators({required BuildContext context}) {
return Row(
children: [
@@ -161,7 +110,7 @@ class _AppMediaItemState extends State
children: [
if (widget.media.sources.contains(AppMediaSource.googleDrive))
SvgPicture.asset(
- Assets.images.icons.googlePhotos,
+ Assets.images.icons.googleDrive,
height: 14,
width: 14,
),
diff --git a/app/lib/ui/flow/home/components/multi_selection_done_button.dart b/app/lib/ui/flow/home/components/multi_selection_done_button.dart
index f806095..32909b8 100644
--- a/app/lib/ui/flow/home/components/multi_selection_done_button.dart
+++ b/app/lib/ui/flow/home/components/multi_selection_done_button.dart
@@ -39,7 +39,7 @@ class MultiSelectionDoneButton extends ConsumerWidget {
if (showUploadToDriveButton)
AppSheetAction(
icon: SvgPicture.asset(
- Assets.images.icons.googlePhotos,
+ Assets.images.icons.googleDrive,
height: 24,
width: 24,
),
diff --git a/app/lib/ui/flow/media_metadata_details/media_metadata_details.dart b/app/lib/ui/flow/media_metadata_details/media_metadata_details.dart
new file mode 100644
index 0000000..8f53390
--- /dev/null
+++ b/app/lib/ui/flow/media_metadata_details/media_metadata_details.dart
@@ -0,0 +1,175 @@
+import 'dart:typed_data';
+import 'package:cloud_gallery/components/app_page.dart';
+import 'package:cloud_gallery/components/thumbnail_builder.dart';
+import 'package:cloud_gallery/domain/assets/assets_paths.dart';
+import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
+import 'package:cloud_gallery/domain/formatter/byte_formatter.dart';
+import 'package:cloud_gallery/domain/formatter/date_formatter.dart';
+import 'package:cloud_gallery/domain/formatter/duration_formatter.dart';
+import 'package:data/models/media/media.dart';
+import 'package:data/models/media/media_extension.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:style/extensions/context_extensions.dart';
+import 'package:style/text/app_text_style.dart';
+
+class MediaMetadataDetailsScreen extends StatefulWidget {
+ final AppMedia media;
+
+ const MediaMetadataDetailsScreen({super.key, required this.media});
+
+ @override
+ State createState() =>
+ _MediaMetadataDetailsScreenState();
+}
+
+class _MediaMetadataDetailsScreenState
+ extends State {
+ Future? thumbnailByte;
+
+ @override
+ void initState() {
+ if (widget.media.sources.contains(AppMediaSource.local)) {
+ thumbnailByte = widget.media.loadThumbnail();
+ }
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AppPage(
+ title: '',
+ body: Builder(builder: (context) {
+ return Material(
+ color: Colors.transparent,
+ child: ListView(
+ padding: context.systemPadding,
+ children: [
+ Stack(
+ alignment: Alignment.center,
+ children: [
+ AppMediaThumbnail(
+ size: Size(context.mediaQuerySize.width, 200),
+ thumbnailByte: thumbnailByte,
+ media: widget.media,
+ radius: 0,
+ ),
+ if (widget.media.type.isVideo)
+ Icon(Icons.play_arrow_rounded,
+ size: 50, color: context.colorScheme.onPrimary),
+ ],
+ ),
+ const SizedBox(height: 16),
+ DetailsTile(
+ title: context.l10n.name_text,
+ subtitle: (widget.media.name?.trim().isNotEmpty ?? false)
+ ? widget.media.name!
+ : context.l10n.common_not_available,
+ ),
+ DetailsTile(
+ title: context.l10n.path_text,
+ subtitle: widget.media.path,
+ ),
+ DetailsTile(
+ title: context.l10n.created_at_text,
+ subtitle: widget.media.createdTime == null
+ ? context.l10n.common_not_available
+ : "${widget.media.createdTime?.format(context, DateFormatType.dayMonthYear)}, ${widget.media.createdTime?.format(context, DateFormatType.time)}",
+ ),
+ DetailsTile(
+ title: context.l10n.modified_at_text,
+ subtitle: widget.media.modifiedTime == null
+ ? context.l10n.common_not_available
+ : "${widget.media.modifiedTime?.format(context, DateFormatType.dayMonthYear)}, ${widget.media.modifiedTime?.format(context, DateFormatType.time)}",
+ ),
+ DetailsTile(
+ title: context.l10n.mimetype_text,
+ subtitle: widget.media.mimeType ??
+ context.l10n.common_not_available,
+ ),
+ DetailsTile(
+ title: context.l10n.size_text,
+ subtitle:
+ int.tryParse(widget.media.size ?? '')?.formatBytes ??
+ context.l10n.common_not_available,
+ ),
+ if (widget.media.type.isVideo)
+ DetailsTile(
+ title: context.l10n.duration_text,
+ subtitle: widget.media.videoDuration?.format ??
+ context.l10n.common_not_available,
+ ),
+ DetailsTile(
+ title: context.l10n.location_text,
+ subtitle: widget.media.latitude == null ||
+ widget.media.longitude == null
+ ? context.l10n.common_not_available
+ : '${widget.media.latitude}, ${widget.media.longitude}',
+ ),
+ DetailsTile(
+ title: context.l10n.orientation_text,
+ subtitle: widget.media.orientation?.name ?? 'N/A',
+ ),
+ DetailsTile(
+ title: context.l10n.resolution_text,
+ subtitle: widget.media.displayHeight == null ||
+ widget.media.displayWidth == null
+ ? context.l10n.common_not_available
+ : '${widget.media.displayWidth?.toInt()} x ${widget.media.displayHeight?.toInt()}',
+ ),
+ ListTile(
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16),
+ dense: true,
+ title: Text(
+ context.l10n.source_text,
+ style: AppTextStyles.body.copyWith(
+ color: context.colorScheme.textPrimary,
+ ),
+ ),
+ subtitle: Row(
+ children: [
+ if (widget.media.sources.contains(AppMediaSource.local))
+ Icon(Icons.phone_android_rounded,
+ color: context.colorScheme.textSecondary,
+ size: 20),
+ if (widget.media.sources
+ .contains(AppMediaSource.googleDrive))
+ SvgPicture.asset(
+ Assets.images.icons.googleDrive,
+ width: 20,
+ )
+ ],
+ ))
+ ],
+ ),
+ );
+ }));
+ }
+}
+
+class DetailsTile extends StatelessWidget {
+ final String title;
+ final String subtitle;
+
+ const DetailsTile({super.key, required this.title, required this.subtitle});
+
+ @override
+ Widget build(BuildContext context) {
+ return ListTile(
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16),
+ dense: true,
+ title: Text(
+ title,
+ style: AppTextStyles.body.copyWith(
+ color: context.colorScheme.textPrimary,
+ ),
+ ),
+ subtitle: Text(
+ subtitle,
+ style: AppTextStyles.body2.copyWith(
+ color: context.colorScheme.textSecondary,
+ ),
+ ),
+ );
+ }
+}
diff --git a/app/lib/ui/flow/media_preview/components/download_require_view.dart b/app/lib/ui/flow/media_preview/components/download_require_view.dart
index 9dafe1e..8981bec 100644
--- a/app/lib/ui/flow/media_preview/components/download_require_view.dart
+++ b/app/lib/ui/flow/media_preview/components/download_require_view.dart
@@ -30,7 +30,7 @@ class DownloadRequireView extends StatelessWidget {
child: Image.network(
height: double.infinity,
width: double.infinity,
- media.thumbnailLink!,
+ media.thumbnailLink ?? '',
fit: BoxFit.cover,
),
),
diff --git a/app/lib/ui/flow/media_preview/components/top_bar.dart b/app/lib/ui/flow/media_preview/components/top_bar.dart
index 2c5cc58..58cea83 100644
--- a/app/lib/ui/flow/media_preview/components/top_bar.dart
+++ b/app/lib/ui/flow/media_preview/components/top_bar.dart
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
+import 'package:cloud_gallery/ui/navigation/app_router.dart';
import 'package:data/models/media/media_extension.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -39,11 +40,11 @@ class PreviewTopBar extends StatelessWidget {
actions: [
ActionButton(
onPressed: () {
- ///TODO: media details
+ AppRouter.mediaMetaDataDetails(media: media).push(context);
},
icon: Icon(
CupertinoIcons.info,
- color: context.colorScheme.textSecondary,
+ color: context.colorScheme.textPrimary,
size: 22,
),
),
@@ -76,7 +77,7 @@ class PreviewTopBar extends StatelessWidget {
child: Row(
children: [
SvgPicture.asset(
- Assets.images.icons.googlePhotos,
+ Assets.images.icons.googleDrive,
width: 20,
height: 20,
),
diff --git a/app/lib/ui/flow/media_transfer/components/transfer_item.dart b/app/lib/ui/flow/media_transfer/components/transfer_item.dart
index aff7a6a..017336f 100644
--- a/app/lib/ui/flow/media_transfer/components/transfer_item.dart
+++ b/app/lib/ui/flow/media_transfer/components/transfer_item.dart
@@ -1,5 +1,4 @@
import 'dart:typed_data';
-import 'package:cached_network_image/cached_network_image.dart';
import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
import 'package:cloud_gallery/domain/formatter/byte_formatter.dart';
import 'package:data/models/app_process/app_process.dart';
@@ -10,8 +9,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:style/buttons/action_button.dart';
import 'package:style/extensions/context_extensions.dart';
-import 'package:style/indicators/circular_progress_indicator.dart';
import 'package:style/text/app_text_style.dart';
+import '../../../../components/thumbnail_builder.dart';
class ProcessItem extends StatefulWidget {
final AppProcess process;
@@ -25,6 +24,16 @@ class ProcessItem extends StatefulWidget {
}
class _ProcessItemState extends State {
+ Future? thumbnailByte;
+
+ @override
+ void initState() {
+ if (widget.process.media.sources.contains(AppMediaSource.local)) {
+ thumbnailByte =
+ widget.process.media.loadThumbnail(size: const Size(80, 80));
+ }
+ super.initState();
+ }
@override
Widget build(BuildContext context) {
@@ -56,7 +65,8 @@ class _ProcessItemState extends State {
color: context.colorScheme.textSecondary,
),
),
- if (widget.process.progress != null && widget.process.status.isProcessing) ...[
+ if (widget.process.progress != null &&
+ widget.process.status.isProcessing) ...[
LinearProgressIndicator(
value: widget.process.progress?.percentageInPoint,
backgroundColor: context.colorScheme.outline,
@@ -96,100 +106,10 @@ class _ProcessItemState extends State {
}
Widget _buildThumbnailView({required BuildContext context}) {
- if (widget.process.media.sources.contains(AppMediaSource.local)) {
- return FutureByteLoader(
- bytes: widget.process.media.loadThumbnail(size: const Size(100, 100)),
- builder: (context, bytes) => Container(
- width: 60,
- height: 60,
- decoration: BoxDecoration(
- color: context.colorScheme.containerHighOnSurface,
- borderRadius: BorderRadius.circular(4),
- image: DecorationImage(
- image: MemoryImage(bytes!),
- fit: BoxFit.cover,
- ),
- ),
- ),
- errorWidget: (context, error) => _buildErrorWidget(context),
- placeholder: (context) => _buildPlaceholder(context: context),
- );
- } else {
- return CachedNetworkImage(
- imageUrl: widget.process.media.thumbnailLink!,
- width: 80,
- height: 80,
- fit: BoxFit.cover,
- errorWidget: (context, url, error) => _buildErrorWidget(context),
- progressIndicatorBuilder: (context, url, progress) =>
- _buildPlaceholder(
- context: context,
- value: progress.progress,
- ));
- }
- }
-
- Widget _buildPlaceholder(
- {required BuildContext context,
- double? value,
- bool showLoader = true}) =>
- Container(
- color: context.colorScheme.containerHighOnSurface,
- alignment: Alignment.center,
- child: showLoader ? AppCircularProgressIndicator(value: value) : null,
- );
-
- Widget _buildErrorWidget(BuildContext context) => Container(
- color: context.colorScheme.containerNormalOnSurface,
- alignment: Alignment.center,
- child: Icon(
- CupertinoIcons.exclamationmark_circle,
- color: context.colorScheme.onPrimary,
- size: 32,
- ),
- );
-}
-
-class FutureByteLoader extends StatefulWidget {
- final Future bytes;
- final Widget Function(BuildContext context, Uint8List? bytes) builder;
- final Widget Function(BuildContext context) placeholder;
- final Widget Function(BuildContext context, Object? error) errorWidget;
-
- const FutureByteLoader(
- {super.key,
- required this.bytes,
- required this.builder,
- required this.placeholder,
- required this.errorWidget});
-
- @override
- State createState() => _FutureByteLoaderState();
-}
-
-class _FutureByteLoaderState extends State {
- late Future bytes;
-
- @override
- void initState() {
- bytes = widget.bytes;
- super.initState();
- }
-
- @override
- Widget build(BuildContext context) {
- return FutureBuilder(
- future: bytes,
- builder: (context, snapshot) {
- if (snapshot.connectionState == ConnectionState.done &&
- snapshot.hasData) {
- return widget.builder(context, snapshot.data);
- } else if (snapshot.hasError) {
- return widget.errorWidget(context, snapshot.error);
- } else {
- return widget.placeholder(context);
- }
- },
+ return AppMediaThumbnail(
+ size: const Size(80, 80),
+ thumbnailByte: thumbnailByte,
+ media: widget.process.media,
);
}
}
diff --git a/app/lib/ui/navigation/app_router.dart b/app/lib/ui/navigation/app_router.dart
index 4fc3f77..0524095 100644
--- a/app/lib/ui/navigation/app_router.dart
+++ b/app/lib/ui/navigation/app_router.dart
@@ -5,6 +5,7 @@ import 'package:data/models/media/media.dart';
import 'package:flutter/cupertino.dart';
import 'package:go_router/go_router.dart';
import '../flow/home/home_screen.dart';
+import '../flow/media_metadata_details/media_metadata_details.dart';
import '../flow/media_preview/media_preview_screen.dart';
import 'app_route.dart';
@@ -39,11 +40,24 @@ class AppRouter {
),
);
+ static AppRoute mediaMetaDataDetails(
+ {required AppMedia media}) =>
+ AppRoute(
+ AppRoutePath.metaDataDetails,
+ builder: (context) => MediaMetadataDetailsScreen(
+ media: media,
+ ),
+ );
+
static final routes = [
home.goRoute,
onBoard.goRoute,
accounts.goRoute,
mediaTransfer.goRoute,
+ GoRoute(
+ path: AppRoutePath.metaDataDetails,
+ builder: (context, state) => state.widget(context),
+ ),
GoRoute(
path: AppRoutePath.preview,
pageBuilder: (context, state) {
@@ -66,4 +80,5 @@ class AppRoutePath {
static const accounts = '/accounts';
static const preview = '/preview';
static const transfer = '/transfer';
+ static const metaDataDetails = '/metadata-details';
}