diff --git a/android/fastlane/metadata/android/en-US/changelogs/78.txt b/android/fastlane/metadata/android/en-US/changelogs/78.txt new file mode 100644 index 0000000000..df7c84f545 --- /dev/null +++ b/android/fastlane/metadata/android/en-US/changelogs/78.txt @@ -0,0 +1,3 @@ +- Adding a new manifest icon and updating the download icon throughout the apps. +- Fixing an issue with long File Types by wrapping the text. +- Adding full screen image previews. diff --git a/lib/authentication/login/views/login_page.dart b/lib/authentication/login/views/login_page.dart index f9b74975b9..7f8e222c18 100644 --- a/lib/authentication/login/views/login_page.dart +++ b/lib/authentication/login/views/login_page.dart @@ -1835,7 +1835,7 @@ class _DownloadWalletViewState extends State { padding: const EdgeInsets.all(44), child: Column( children: [ - ArDriveIcons.download(size: 40), + ArDriveIcons.download2(size: 40), const SizedBox(height: 4), // TODO: create/update localization key Text('Download Keyfile', diff --git a/lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart b/lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart index 3d153f579b..7d49087ad1 100644 --- a/lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart +++ b/lib/blocs/fs_entry_preview/fs_entry_preview_cubit.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:ardrive/blocs/fs_entry_preview/image_preview_notification.dart'; import 'package:ardrive/blocs/profile/profile_cubit.dart'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/models/models.dart'; @@ -12,6 +13,7 @@ import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:cryptography/cryptography.dart'; import 'package:drift/drift.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; part 'fs_entry_preview_state.dart'; @@ -29,6 +31,8 @@ class FsEntryPreviewCubit extends Cubit { final SecretKey? _fileKey; StreamSubscription? _entrySubscription; + final ValueNotifier imagePreviewNotifier = + ValueNotifier(ImagePreviewNotification()); final previewMaxFileSize = 1024 * 1024 * 100; final allowedPreviewContentTypes = []; @@ -45,8 +49,8 @@ class FsEntryPreviewCubit extends Cubit { bool isSharedFile = false, }) : _driveDao = driveDao, _configService = configService, - _arweave = arweave, _profileCubit = profileCubit, + _arweave = arweave, _crypto = crypto, _fileKey = fileKey, super(FsEntryPreviewInitial()) { @@ -76,19 +80,11 @@ class FsEntryPreviewCubit extends Cubit { switch (previewType) { case 'image': - final data = await _getPreviewData(file, previewUrl); - - if (data != null) { - emit(FsEntryPreviewImage( - imageBytes: data, - previewUrl: previewUrl, - filename: file.name, - contentType: file.contentType, - )); - } else { - emit(FsEntryPreviewUnavailable()); - } - + _previewImage( + fileKey != null, + selectedItem, + previewUrl, + ); break; case 'audio': @@ -113,52 +109,6 @@ class FsEntryPreviewCubit extends Cubit { } else { emit(FsEntryPreviewUnavailable()); } - - return Future.value(); - } - - Future _getPreviewData( - FileDataTableItem file, - String previewUrl, - ) async { - final dataTx = await _getTxDetails(file); - - if (dataTx == null) { - emit(FsEntryPreviewUnavailable()); - return null; - } - - final dataRes = await ArDriveHTTP().getAsBytes(previewUrl); - - final isPinFile = file.pinnedDataOwnerAddress != null; - - if (_fileKey != null && !isPinFile) { - if (file.size! >= previewMaxFileSize) { - emit(FsEntryPreviewUnavailable()); - return null; - } - - try { - final decodedBytes = await _crypto.decryptDataFromTransaction( - dataTx, - dataRes.data, - _fileKey!, - ); - - return decodedBytes; - } catch (e) { - emit(FsEntryPreviewUnavailable()); - return Future.value(); - } - } - - return dataRes.data; - } - - Future _getTxDetails(FileDataTableItem file) async { - final dataTx = await _arweave.getTransactionDetails(file.dataTxId); - - return dataTx; } Future preview() async { @@ -219,6 +169,90 @@ class FsEntryPreviewCubit extends Cubit { } } + void _previewImage( + bool isPrivate, + FileDataTableItem file, + String previewUrl, + ) async { + final isPinFile = file.pinnedDataOwnerAddress != null; + + final Uint8List? dataBytes = await _getBytesFromCache( + dataTxId: file.dataTxId, + dataUrl: previewUrl, + withDriveDao: false, + ); + + if (dataBytes == null) { + emit(FsEntryPreviewUnavailable()); + return; + } + + if (isPrivate && !isPinFile) { + if (file.size! >= previewMaxFileSize) { + emit(FsEntryPreviewUnavailable()); + } + + final fileKey = await _getFileKey( + fileId: file.id, + driveId: driveId, + isPrivate: true, + isPin: false, + ); + final decodedBytes = await _decodePrivateData( + dataBytes, + fileKey!, + file.dataTxId, + ); + imagePreviewNotifier.value = ImagePreviewNotification( + dataBytes: decodedBytes, + ); + } else { + imagePreviewNotifier.value = ImagePreviewNotification( + dataBytes: dataBytes, + ); + } + + emit(FsEntryPreviewImage( + previewUrl: previewUrl, + filename: file.name, + contentType: file.contentType, + )); + } + + Future _getFileKey({ + required String fileId, + required String driveId, + required bool isPrivate, + required bool isPin, + }) async { + if (!isPrivate || isPin) { + return null; + } + + if (_fileKey != null) { + return _fileKey; + } + + final profile = _profileCubit.state; + late SecretKey? driveKey; + + if (profile is ProfileLoggedIn) { + driveKey = await _driveDao.getDriveKey( + driveId, + profile.cipherKey, + ); + } else { + driveKey = await _driveDao.getDriveKeyFromMemory(driveId); + } + + if (driveKey == null) { + return null; + } + + final fileKey = await _driveDao.getFileKey(fileId, driveKey); + return fileKey; + } + void _previewAudio( bool isPrivate, FileDataTableItem selectedItem, previewUrl) { if (_configService.config.enableAudioPreview) { @@ -257,98 +291,124 @@ class FsEntryPreviewCubit extends Cubit { try { emit(const FsEntryPreviewLoading()); - final dataTx = await _arweave.getTransactionDetails(file.dataTxId); - - if (dataTx == null) { - emit(FsEntryPreviewFailure()); - return; - } - - late Uint8List dataBytes; - - final cachedBytes = await _driveDao.getPreviewDataFromMemory(dataTx.id); - - if (cachedBytes == null) { - final dataRes = await ArDriveHTTP().getAsBytes(dataUrl); - dataBytes = dataRes.data; - - await _driveDao.putPreviewDataInMemory( - dataTxId: dataTx.id, - bytes: dataBytes, - ); - } else { - dataBytes = cachedBytes; - } + final Uint8List? dataBytes = await _getBytesFromCache( + dataTxId: file.dataTxId, + dataUrl: dataUrl, + ); final drive = await _driveDao.driveById(driveId: driveId).getSingle(); + final isPinFile = file.pinnedDataOwnerAddress != null; switch (drive.privacy) { case DrivePrivacyTag.public: - emit( - FsEntryPreviewImage( - imageBytes: dataBytes, - previewUrl: dataUrl, - filename: file.name, - contentType: file.dataContentType ?? - lookupMimeTypeWithDefaultType(file.name), - ), - ); + _emitImagePreview(file, dataUrl, dataBytes: dataBytes); break; case DrivePrivacyTag.private: - final profile = _profileCubit.state; - SecretKey? driveKey; - - final isPinFile = file.pinnedDataOwnerAddress != null; - - if (isPinFile) { - emit( - FsEntryPreviewImage( - imageBytes: dataBytes, - previewUrl: dataUrl, - filename: file.name, - contentType: file.dataContentType ?? - lookupMimeTypeWithDefaultType(file.name), - ), - ); - break; - } - - if (profile is ProfileLoggedIn) { - driveKey = await _driveDao.getDriveKey( - drive.id, - profile.cipherKey, - ); - } else { - driveKey = await _driveDao.getDriveKeyFromMemory(driveId); - } + final fileKey = await _getFileKey( + fileId: file.id, + driveId: driveId, + isPrivate: true, + isPin: isPinFile, + ); - if (driveKey == null) { - throw StateError('Drive Key not found'); + if (dataBytes == null || isPinFile) { + _emitImagePreview(file, dataUrl, dataBytes: dataBytes); + return; } - final fileKey = await _driveDao.getFileKey(file.id, driveKey); - final decodedBytes = await _crypto.decryptDataFromTransaction( - dataTx, + final decodedBytes = await _decodePrivateData( dataBytes, - fileKey, - ); - emit( - FsEntryPreviewImage( - imageBytes: decodedBytes, - previewUrl: dataUrl, - filename: file.name, - contentType: file.dataContentType ?? - lookupMimeTypeWithDefaultType(file.name), - ), + fileKey!, + file.dataTxId, ); + _emitImagePreview(file, dataUrl, dataBytes: decodedBytes); break; default: - emit(FsEntryPreviewFailure()); + _emitImagePreview(file, dataUrl, dataBytes: dataBytes); } } catch (err) { - emit(FsEntryPreviewFailure()); + _emitImagePreview(file, dataUrl); + } + } + + Future _getBytesFromCache({ + required String dataTxId, + required String dataUrl, + bool withDriveDao = true, + }) async { + Uint8List? dataBytes; + + final cachedBytes = withDriveDao + ? await _driveDao.getPreviewDataFromMemory( + dataTxId, + ) + : null; + + if (cachedBytes == null) { + try { + final dataRes = await ArDriveHTTP().getAsBytes(dataUrl); + dataBytes = dataRes.data; + + await _driveDao.putPreviewDataInMemory( + dataTxId: dataTxId, + bytes: dataBytes!, + ); + } catch (_) { + dataBytes = null; + } + } else { + dataBytes = cachedBytes; + } + + return dataBytes; + } + + Future _decodePrivateData( + Uint8List dataBytes, + SecretKey fileKey, + String dataTxId, + ) async { + final dataTx = await _getDataTx(dataTxId); + + if (dataTx == null) { + return null; } + + try { + final decodedBytes = await _crypto.decryptDataFromTransaction( + dataTx, + dataBytes, + fileKey, + ); + + return decodedBytes; + } catch (e) { + return null; + } + } + + Future _getDataTx( + String fileDataTxId, + ) async { + final dataTx = await _arweave.getTransactionDetails(fileDataTxId); + return dataTx; + } + + void _emitImagePreview( + FileEntry file, + String dataUrl, { + Uint8List? dataBytes, + }) { + imagePreviewNotifier.value = ImagePreviewNotification( + dataBytes: dataBytes, + ); + emit(FsEntryPreviewImage( + previewUrl: dataUrl, + filename: file.name, + contentType: + file.dataContentType ?? lookupMimeTypeWithDefaultType(file.name), + )); } bool _supportedExtension(String? previewType, String? fileExtension) { @@ -373,6 +433,7 @@ class FsEntryPreviewCubit extends Cubit { @override Future close() { _entrySubscription?.cancel(); + imagePreviewNotifier.dispose(); return super.close(); } } diff --git a/lib/blocs/fs_entry_preview/fs_entry_preview_state.dart b/lib/blocs/fs_entry_preview/fs_entry_preview_state.dart index c2ac51a34f..7fe373aaa7 100644 --- a/lib/blocs/fs_entry_preview/fs_entry_preview_state.dart +++ b/lib/blocs/fs_entry_preview/fs_entry_preview_state.dart @@ -25,19 +25,17 @@ class FsEntryPreviewLoading extends FsEntryPreviewSuccess { } class FsEntryPreviewImage extends FsEntryPreviewSuccess { - final Uint8List imageBytes; final String filename; final String contentType; const FsEntryPreviewImage({ - required this.imageBytes, required this.filename, required this.contentType, - required String previewUrl, - }) : super(previewUrl: previewUrl); + required super.previewUrl, + }); @override - List get props => [imageBytes, previewUrl]; + List get props => [previewUrl]; } class FsEntryPreviewAudio extends FsEntryPreviewSuccess { @@ -79,5 +77,3 @@ class FsEntryPreviewText extends FsEntryPreviewSuccess { @override List get props => [previewUrl]; } - -class FsEntryPreviewFailure extends FsEntryPreviewState {} diff --git a/lib/blocs/fs_entry_preview/image_preview_notification.dart b/lib/blocs/fs_entry_preview/image_preview_notification.dart new file mode 100644 index 0000000000..6eb51f2ff6 --- /dev/null +++ b/lib/blocs/fs_entry_preview/image_preview_notification.dart @@ -0,0 +1,7 @@ +import 'dart:typed_data'; + +class ImagePreviewNotification { + Uint8List? dataBytes; + + ImagePreviewNotification({this.dataBytes}); +} diff --git a/lib/components/details_panel.dart b/lib/components/details_panel.dart index 74e0e5cb78..fc7619136c 100644 --- a/lib/components/details_panel.dart +++ b/lib/components/details_panel.dart @@ -97,6 +97,7 @@ class _DetailsPanelState extends State { mobileView: false, previewState: previewState, infoState: infoState, + context: context, ), ), mobile: (context) => Column( @@ -104,6 +105,7 @@ class _DetailsPanelState extends State { mobileView: true, previewState: previewState, infoState: infoState, + context: context, ), ), ); @@ -117,6 +119,7 @@ class _DetailsPanelState extends State { required bool mobileView, required FsEntryPreviewState previewState, required FsEntryInfoState infoState, + required BuildContext context, }) { final isNotSharePageInMobileView = !(widget.isSharePage && !mobileView); final isPreviewUnavailable = previewState is FsEntryPreviewUnavailable; @@ -133,7 +136,7 @@ class _DetailsPanelState extends State { ), Column( children: [ - Expanded(child: _buildPreview(previewState)), + Expanded(child: _buildPreview(previewState, context: context)), ], ), ), @@ -190,7 +193,7 @@ class _DetailsPanelState extends State { contentPadding: isSharePage ? const EdgeInsets.only() : const EdgeInsets.all(24), - content: _buildPreview(previewState), + content: _buildPreview(previewState, context: context), ), ), ], @@ -327,7 +330,7 @@ class _DetailsPanelState extends State { children: [ Expanded( child: ArDriveButton( - icon: ArDriveIcons.download( + icon: ArDriveIcons.download2( color: Colors.white), onPressed: () { final file = ARFSFactory() @@ -366,8 +369,11 @@ class _DetailsPanelState extends State { ]; } - Widget _buildPreview(FsEntryPreviewState previewState) { - if (previewState is FsEntryPreviewUnavailable) { + Widget _buildPreview( + FsEntryPreviewState previewState, { + required BuildContext context, + }) { + if (previewState is FsEntryPreviewUnavailable && widget.isSharePage) { return Center( child: ConstrainedBox( constraints: const BoxConstraints( @@ -416,7 +422,7 @@ class _DetailsPanelState extends State { ), const SizedBox(height: 24), ArDriveButton( - icon: ArDriveIcons.download(color: Colors.white), + icon: ArDriveIcons.download2(color: Colors.white), onPressed: () { final file = ARFSFactory().getARFSFileFromFileRevision( widget.revisions!.last, @@ -447,6 +453,7 @@ class _DetailsPanelState extends State { key: ValueKey(widget.item.id), state: previewState, isSharePage: widget.isSharePage, + previewCubit: context.read(), ), ); } @@ -1071,7 +1078,7 @@ class _DownloadOrPreview extends StatelessWidget { ); }, tooltip: appLocalizationsOf(context).download, - icon: ArDriveIcons.download(size: 20), + icon: ArDriveIcons.download2(size: 20), ); } } @@ -1150,7 +1157,7 @@ class DetailsPanelToolbar extends StatelessWidget { ), _buildActionIcon( tooltip: appLocalizationsOf(context).download, - icon: ArDriveIcons.download(size: defaultIconSize), + icon: ArDriveIcons.download2(size: defaultIconSize), onTap: () { if (item is FileDataTableItem) { promptToDownloadProfileFile( diff --git a/lib/components/new_button/new_button.dart b/lib/components/new_button/new_button.dart index cd2475c810..e9582701cb 100644 --- a/lib/components/new_button/new_button.dart +++ b/lib/components/new_button/new_button.dart @@ -316,7 +316,7 @@ class NewButton extends StatelessWidget { driveDetailState.driveIsEmpty || !canUpload, name: appLocalizations.createManifest, - icon: ArDriveIcons.tournament(size: defaultIconSize), + icon: ArDriveIcons.manifest(size: defaultIconSize), ), if (context.read().config.enableQuickSyncAuthoring) ArDriveNewButtonItem( diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e5f1d63f5a..3295726fde 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -198,6 +198,10 @@ "@closeEmphasized": { "description": "Close modal. Emphasized with upper case" }, + "collapse": "Collapse", + "@collapse": { + "description": "E.g.: Collapse the image" + }, "collapseSideBar": "Collapse Sidebar", "@collapseSideBar": { "description": "Collapse side bar tooltip" @@ -669,6 +673,10 @@ "@errorFetchingQuote": {}, "estimatedStorage": "Estimated Storage", "@estimatedStorage": {}, + "expand": "Expand", + "@expand": { + "description": "E.g.: Expand the image" + }, "expandSideBar": "Expand sidebar", "@expandSideBar": { "description": "Expand side bar tooltip" diff --git a/lib/main.dart b/lib/main.dart index ba40328522..1ee1028e0e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,6 +20,8 @@ import 'package:ardrive/user/repositories/user_repository.dart'; import 'package:ardrive/utils/app_flavors.dart'; import 'package:ardrive/utils/local_key_value_store.dart'; import 'package:ardrive/utils/logger/logger.dart'; +import 'package:ardrive/utils/mobile_screen_orientation.dart'; +import 'package:ardrive/utils/mobile_status_bar.dart'; import 'package:ardrive/utils/pre_cache_assets.dart'; import 'package:ardrive/utils/secure_key_value_store.dart'; import 'package:ardrive_http/ardrive_http.dart'; @@ -57,6 +59,9 @@ late PaymentService _turboPayment; void main() async { WidgetsFlutterBinding.ensureInitialized(); + MobileStatusBar.show(); + MobileScreenOrientation.lockInPortraitUp(); + final localStore = await LocalKeyValueStore.getInstance(); await AppInfoServices().loadAppInfo(); diff --git a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart index bdd9908ce7..a7ede0094b 100644 --- a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart +++ b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart @@ -141,6 +141,10 @@ ArDriveIcon getIconForContentType(String contentType, {double size = 18}) { return ArDriveIcons.fileOutlined( size: size, ); + } else if (FileTypeHelper.isManifest(contentType)) { + return ArDriveIcons.manifest( + size: size, + ); } else { return ArDriveIcons.fileOutlined( size: size, @@ -254,7 +258,7 @@ class _DriveExplorerItemTileTrailingState }, content: _buildItem( appLocalizationsOf(context).download, - ArDriveIcons.download( + ArDriveIcons.download2( size: defaultIconSize, ), ), @@ -319,7 +323,7 @@ class _DriveExplorerItemTileTrailingState }, content: _buildItem( appLocalizationsOf(context).download, - ArDriveIcons.download( + ArDriveIcons.download2( size: defaultIconSize, ), ), @@ -459,7 +463,7 @@ class EntityActionsMenu extends StatelessWidget { }, content: _buildItem( appLocalizationsOf(context).download, - ArDriveIcons.download( + ArDriveIcons.download2( size: defaultIconSize, ), ), @@ -510,7 +514,7 @@ class EntityActionsMenu extends StatelessWidget { }, content: ArDriveDropdownItemTile( name: appLocalizationsOf(context).download, - icon: ArDriveIcons.download( + icon: ArDriveIcons.download2( size: defaultIconSize, ), )), @@ -552,7 +556,7 @@ class EntityActionsMenu extends StatelessWidget { }, content: ArDriveDropdownItemTile( name: appLocalizationsOf(context).exportDriveContents, - icon: ArDriveIcons.download( + icon: ArDriveIcons.download2( size: defaultIconSize, ), ), @@ -589,7 +593,7 @@ class EntityActionsMenu extends StatelessWidget { }, content: _buildItem( appLocalizationsOf(context).download, - ArDriveIcons.download( + ArDriveIcons.download2( size: defaultIconSize, ), ), diff --git a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart index 4564b5998b..6df229ec37 100644 --- a/lib/pages/drive_detail/components/fs_entry_preview_widget.dart +++ b/lib/pages/drive_detail/components/fs_entry_preview_widget.dart @@ -4,11 +4,13 @@ const List _speedOptions = [.25, .5, .75, 1, 1.25, 1.5, 1.75, 2]; class FsEntryPreviewWidget extends StatefulWidget { final bool isSharePage; + final FsEntryPreviewCubit previewCubit; const FsEntryPreviewWidget({ Key? key, required this.state, required this.isSharePage, + required this.previewCubit, }) : super(key: key); final FsEntryPreviewState state; @@ -20,7 +22,8 @@ class FsEntryPreviewWidget extends StatefulWidget { class _FsEntryPreviewWidgetState extends State { @override Widget build(BuildContext context) { - switch (widget.state.runtimeType) { + final stateType = widget.state.runtimeType; + switch (stateType) { case FsEntryPreviewUnavailable: return const Center( child: Text('Preview unavailable'), @@ -40,9 +43,9 @@ class _FsEntryPreviewWidgetState extends State { return ImagePreviewWidget( filename: (widget.state as FsEntryPreviewImage).filename, contentType: (widget.state as FsEntryPreviewImage).contentType, - imageBytes: (widget.state as FsEntryPreviewImage).imageBytes, isSharePage: widget.isSharePage, isFullScreen: false, + previewCubit: widget.previewCubit, ); case FsEntryPreviewAudio: @@ -166,6 +169,7 @@ class _VideoPlayerWidgetState extends State } Navigator.of(context).push(PageRouteBuilder( + barrierDismissible: true, transitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero, pageBuilder: (context, animation, secondaryAnimation) { @@ -415,6 +419,8 @@ class _VideoPlayerWidgetState extends State ); } else { return IconButton( + tooltip: + appLocalizationsOf(context).expand, onPressed: !controlsEnabled ? null : () { @@ -562,6 +568,7 @@ class _VideoPlayerWidgetState extends State mobile: (context) { if (widget.isSharePage) { return IconButton( + tooltip: appLocalizationsOf(context).expand, onPressed: !controlsEnabled ? null : () { @@ -592,6 +599,7 @@ class _VideoPlayerWidgetState extends State ), ScreenTypeLayout.builder( desktop: (context) => IconButton( + tooltip: appLocalizationsOf(context).expand, onPressed: !controlsEnabled ? null : () { @@ -681,20 +689,50 @@ class _FullScreenVideoPlayerWidgetState }); _videoPlayerController.addListener(_listener); - SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeRight, - DeviceOrientation.landscapeLeft, - ]); + MobileScreenOrientation.lockInLandscape(); _hideControlsTimer = Timer(const Duration(seconds: 3), () { if (mounted) { - setState(() { - _controlsVisible = false; - }); + _hideControls(); + } + }); + } + + void _resetHideControlsTimer() { + _hideControlsTimer?.cancel(); + _hideControlsTimer = Timer(const Duration(seconds: 3), () { + if (mounted) { + _hideControls(); } }); } + void _cancelHideControlsTimer() { + _hideControlsTimer?.cancel(); + } + + void _showControls() { + setState(() { + _controlsVisible = true; + MobileStatusBar.show(); + }); + } + + void _hideControls() { + setState(() { + _controlsVisible = false; + MobileStatusBar.hide(); + }); + } + + void _toggleControls() { + if (_controlsVisible) { + _hideControls(); + } else { + _showControls(); + } + } + void _listener() { setState(() { if (_videoPlayerController.value.hasError) { @@ -708,9 +746,7 @@ class _FullScreenVideoPlayerWidgetState @override void dispose() { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - ]); + MobileScreenOrientation.lockInPortraitUp(); // Calling onClose() here to work when user hits close zoom button or hits // system back button on Android. @@ -738,110 +774,89 @@ class _FullScreenVideoPlayerWidgetState : 0.0; return Scaffold( - body: Center( - child: Stack( - fit: StackFit.expand, - children: [ - Container(color: Colors.black), - Center( - child: AspectRatio( - aspectRatio: _videoPlayerController.value.aspectRatio, - child: _errorMessage != null - ? Padding( - padding: const EdgeInsets.all(20), - child: Text( - _errorMessage ?? '', - textAlign: TextAlign.center, - style: ArDriveTypography.body - .smallBold700(color: colors.themeFgMuted) - .copyWith(fontSize: 13), - )) - : !videoValue.isInitialized - ? const Center( - child: SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator(), - ), - ) - : _videoPlayer ?? const SizedBox.shrink(), - )), - MouseRegion( - onHover: (event) { - if (!AppPlatform.isMobile) { - setState(() { - _controlsVisible = true; - _hideControlsTimer?.cancel(); - _hideControlsTimer = Timer(const Duration(seconds: 3), () { + body: Center( + child: Stack( + fit: StackFit.expand, + children: [ + Container(color: Colors.black), + Center( + child: AspectRatio( + aspectRatio: _videoPlayerController.value.aspectRatio, + child: _errorMessage != null + ? Padding( + padding: const EdgeInsets.all(20), + child: Text( + _errorMessage ?? '', + textAlign: TextAlign.center, + style: ArDriveTypography.body + .smallBold700(color: colors.themeFgMuted) + .copyWith(fontSize: 13), + )) + : !videoValue.isInitialized + ? const Center( + child: SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator(), + ), + ) + : _videoPlayer ?? const SizedBox.shrink(), + )), + MouseRegion( + onHover: (event) { + if (!AppPlatform.isMobile) { + _showControls(); + _resetHideControlsTimer(); + } + }, + onExit: (event) { + if (!AppPlatform.isMobile) { if (mounted) { setState(() { - _controlsVisible = false; + _cancelHideControlsTimer(); }); } - }); - }); - } - }, - onExit: (event) { - if (!AppPlatform.isMobile) { - if (mounted) { - setState(() { - _hideControlsTimer?.cancel(); - }); - } - } - }, - cursor: _controlsVisible - ? SystemMouseCursors.click - : SystemMouseCursors.none, - child: TapRegion( - onTapInside: (event) { - setState(() { - _hideControlsTimer?.cancel(); - _controlsVisible = !_controlsVisible; - - if (_controlsVisible && !AppPlatform.isMobile) { - _hideControlsTimer = Timer(const Duration(seconds: 3), () { - if (mounted) { - setState(() { - _controlsVisible = false; - }); - } - }); } - }); - }, - child: Container(color: Colors.black.withOpacity(0.0)), - ), - ), - AnimatedOpacity( - opacity: _controlsVisible ? 1.0 : 0.0, - duration: const Duration(milliseconds: 200), - child: Column( - children: [ - const Expanded(child: SizedBox.shrink()), - MouseRegion( + }, + cursor: _controlsVisible + ? SystemMouseCursors.click + : SystemMouseCursors.none, + child: TapRegion( + onTapInside: (event) { + _cancelHideControlsTimer(); + _toggleControls(); + if (_controlsVisible && !AppPlatform.isMobile) { + _resetHideControlsTimer(); + } + }, + child: Container(color: Colors.black.withOpacity(0.0)), + ), + ), + AnimatedOpacity( + opacity: _controlsVisible ? 1.0 : 0.0, + duration: const Duration(milliseconds: 200), + child: Column( + children: [ + const Expanded(child: SizedBox.shrink()), + MouseRegion( onHover: (event) { - _hideControlsTimer?.cancel(); + _cancelHideControlsTimer(); if (!AppPlatform.isMobile && !_controlsVisible) { - setState(() { - _controlsVisible = true; - }); + _showControls(); } }, child: TapRegion( - onTapInside: (event) { - if (AppPlatform.isMobile && !_controlsVisible) { - _hideControlsTimer?.cancel(); - setState(() { - _controlsVisible = true; - }); - } - }, - child: Container( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 20), - color: colors.themeBgCanvas, - child: Column(children: [ + onTapInside: (event) { + if (AppPlatform.isMobile && !_controlsVisible) { + _cancelHideControlsTimer(); + _showControls(); + } + }, + child: Container( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 20), + color: colors.themeBgCanvas, + child: Column( + children: [ Text(widget.filename, style: ArDriveTypography.body.smallBold700( color: colors.themeFgDefault)), @@ -851,86 +866,81 @@ class _FullScreenVideoPlayerWidgetState Text(currentTime), const SizedBox(width: 8), Expanded( - child: SliderTheme( - data: SliderThemeData( - trackHeight: 4, - trackShape: - _NoAdditionalHeightRoundedRectSliderTrackShape(), - inactiveTrackColor: - colors.themeBgSubtle, - disabledThumbColor: - colors.themeAccentBrand, - disabledInactiveTrackColor: - colors.themeBgSubtle, - overlayShape: - SliderComponentShape.noOverlay, - thumbShape: - const RoundSliderThumbShape( - enabledThumbRadius: 8, - )), - child: Slider( - value: min( - videoValue - .position.inMilliseconds - .toDouble(), - videoValue - .duration.inMilliseconds - .toDouble()), - secondaryTrackValue: bufferedValue, - min: 0.0, - max: videoValue - .duration.inMilliseconds - .toDouble(), - onChangeStart: (v) async { - if (_videoPlayerController - .value.duration > - Duration.zero) { - _wasPlaying = - _videoPlayerController - .value.isPlaying; - if (_wasPlaying) { - await _lock - .synchronized(() async { - await _videoPlayerController - .pause() - .catchError((e) { - logger.e( - 'Error pausing video: $e'); - }); - - setState(() {}); - }); - } - } - }, - onChanged: (v) async { - if (_videoPlayerController - .value.duration > - Duration.zero) { - _videoPlayerController.seekTo( - Duration( - milliseconds: - v.toInt())); - setState(() {}); - } - }, - onChangeEnd: (v) async { - if (_videoPlayerController - .value.duration > - Duration.zero && - _wasPlaying) { - await _lock - .synchronized(() async { - await _videoPlayerController - .play() - .catchError((e) { - logger.e( - 'Error playing video: $e'); - }); - }); - setState(() {}); - } - }))), + child: SliderTheme( + data: SliderThemeData( + trackHeight: 4, + trackShape: + _NoAdditionalHeightRoundedRectSliderTrackShape(), + inactiveTrackColor: + colors.themeBgSubtle, + disabledThumbColor: + colors.themeAccentBrand, + disabledInactiveTrackColor: + colors.themeBgSubtle, + overlayShape: + SliderComponentShape.noOverlay, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + )), + child: Slider( + value: min( + videoValue.position.inMilliseconds + .toDouble(), + videoValue.duration.inMilliseconds + .toDouble()), + secondaryTrackValue: bufferedValue, + min: 0.0, + max: videoValue.duration.inMilliseconds + .toDouble(), + onChangeStart: (v) async { + if (_videoPlayerController + .value.duration > + Duration.zero) { + _wasPlaying = _videoPlayerController + .value.isPlaying; + if (_wasPlaying) { + await _lock.synchronized(() async { + await _videoPlayerController + .pause() + .catchError((e) { + logger.e( + 'Error pausing video: $e'); + }); + + setState(() {}); + }); + } + } + }, + onChanged: (v) async { + if (_videoPlayerController + .value.duration > + Duration.zero) { + _videoPlayerController.seekTo( + Duration( + milliseconds: v.toInt())); + setState(() {}); + } + }, + onChangeEnd: (v) async { + if (_videoPlayerController + .value.duration > + Duration.zero && + _wasPlaying) { + await _lock.synchronized(() async { + await _videoPlayerController + .play() + .catchError((e) { + logger + .e('Error playing video: $e'); + }); + }); + setState(() {}); + } + }, + ), + ), + ), const SizedBox(width: 8), Text(duration), ], @@ -947,45 +957,52 @@ class _FullScreenVideoPlayerWidgetState crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( + child: SafeArea( child: Align( - alignment: Alignment.centerLeft, - child: ScreenTypeLayout.builder( - mobile: (context) => IconButton( - onPressed: () { - Navigator.of(context).pop(); - }, - icon: const Icon( - Icons - .fullscreen_exit_outlined, - size: 24)), - desktop: (context) => Row( - children: [ - SizedBox( - width: 200, - child: VolumeSliderWidget( - volume: - _videoPlayerController - .value.volume, - setVolume: (v) { - setState(() { - _videoPlayerController - .setVolume(v); - }); - }, - sliderVisible: - _isVolumeSliderVisible, - setSliderVisible: (v) { - setState(() { - _isVolumeSliderVisible = - v; - }); - }, - )), - const Expanded( - child: SizedBox.shrink()), - ], - ), - ))), + alignment: Alignment.centerLeft, + child: ScreenTypeLayout.builder( + mobile: (context) => IconButton( + tooltip: + appLocalizationsOf(context) + .collapse, + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon( + Icons + .fullscreen_exit_outlined, + size: 24)), + desktop: (context) => Row( + children: [ + SizedBox( + width: 200, + child: VolumeSliderWidget( + volume: + _videoPlayerController + .value.volume, + setVolume: (v) { + setState(() { + _videoPlayerController + .setVolume(v); + }); + }, + sliderVisible: + _isVolumeSliderVisible, + setSliderVisible: (v) { + setState(() { + _isVolumeSliderVisible = + v; + }); + }, + )), + const Expanded( + child: SizedBox.shrink()), + ], + ), + ), + ), + ), + ), IconButton.outlined( onPressed: () { setState(() { @@ -1019,13 +1036,18 @@ class _FullScreenVideoPlayerWidgetState _videoPlayerController .seekTo(Duration.zero); } - await _lock.synchronized(() async { - await _videoPlayerController - .play() - .catchError((e) { - logger.e('Error playing video: $e'); - }); - }); + await _lock.synchronized( + () async { + await _videoPlayerController + .play() + .catchError( + (e) { + logger.e( + 'Error playing video: $e'); + }, + ); + }, + ); } setState(() {}); }, @@ -1047,113 +1069,130 @@ class _FullScreenVideoPlayerWidgetState )), ), IconButton.outlined( - onPressed: () { - setState(() { - _videoPlayerController.seekTo( - _videoPlayerController - .value.position + - const Duration(seconds: 10)); - }); - }, - icon: const Icon(Icons.forward_10, - size: 24)), + onPressed: () { + setState(() { + _videoPlayerController.seekTo( + _videoPlayerController + .value.position + + const Duration(seconds: 10)); + }); + }, + icon: const Icon( + Icons.forward_10, + size: 24, + ), + ), Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ScreenTypeLayout.builder( + child: Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + ScreenTypeLayout.builder( desktop: (context) => MenuAnchor( - menuChildren: [ - ..._speedOptions.map((v) { - return ListTile( - tileColor: colors - .themeBgSurface, - onTap: () { - setState(() { - _videoPlayerController - .setPlaybackSpeed( - v); - _menuController - .close(); - }); - }, - title: Text( - v == 1.0 - ? appLocalizationsOf( - context) - .normal - : '$v', - style: ArDriveTypography - .body - .buttonNormalBold( - color: colors - .themeFgDefault), - ), - ); - }) - ], - controller: _menuController, - child: IconButton( - onPressed: () { - _menuController.open(); - }, - icon: const Icon( - Icons - .settings_outlined, - size: 24)), - ), + menuChildren: [ + ..._speedOptions.map((v) { + return ListTile( + tileColor: + colors.themeBgSurface, + onTap: () { + setState(() { + _videoPlayerController + .setPlaybackSpeed( + v); + _menuController.close(); + }); + }, + title: Text( + v == 1.0 + ? appLocalizationsOf( + context) + .normal + : '$v', + style: ArDriveTypography + .body + .buttonNormalBold( + color: colors + .themeFgDefault), + ), + ); + }) + ], + controller: _menuController, + child: IconButton( + onPressed: () { + _menuController.open(); + }, + icon: const Icon( + Icons.settings_outlined, + size: 24)), + ), mobile: (context) => IconButton( - onPressed: () { - _displaySpeedOptionsModal( - context, (v) { - setState(() { - _videoPlayerController - .setPlaybackSpeed(v); - }); - }); - }, - icon: const Icon( - Icons.settings_outlined, - size: 24))), - ScreenTypeLayout.builder( - desktop: (context) => IconButton( onPressed: () { - Navigator.of(context).pop(); + _displaySpeedOptionsModal( + context, (v) { + setState(() { + _videoPlayerController + .setPlaybackSpeed(v); + }); + }); }, icon: const Icon( - Icons - .fullscreen_exit_outlined, - size: 24)), - mobile: (context) => - const SizedBox.shrink(), - ) - ], + Icons.settings_outlined, + size: 24, + ), + ), + ), + SafeArea( + child: ScreenTypeLayout.builder( + desktop: (context) => IconButton( + tooltip: appLocalizationsOf( + context) + .collapse, + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon( + Icons + .fullscreen_exit_outlined, + size: 24)), + mobile: (context) => + const SizedBox.shrink(), + ), + ) + ], + ), ), - )) + ) ], ), ) - ]), - ))), - ], - )), - ], - ))); + ], + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); } } class ImagePreviewFullScreenWidget extends StatefulWidget { - final Uint8List imageBytes; final String filename; final String contentType; + final FsEntryPreviewCubit previewCubit; const ImagePreviewFullScreenWidget({ super.key, required this.filename, required this.contentType, - required this.imageBytes, + required this.previewCubit, }); @override @@ -1168,20 +1207,13 @@ class _ImagePreviewFullScreenWidgetState @override void initState() { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeRight, - DeviceOrientation.landscapeLeft, - ]); - + MobileScreenOrientation.lockInLandscape(); super.initState(); } @override void dispose() { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - ]); - + MobileScreenOrientation.lockInPortraitUp(); super.dispose(); } @@ -1191,25 +1223,25 @@ class _ImagePreviewFullScreenWidgetState body: ImagePreviewWidget( filename: widget.filename, contentType: widget.contentType, - imageBytes: widget.imageBytes, isFullScreen: true, + previewCubit: widget.previewCubit, ), ); } } class ImagePreviewWidget extends StatefulWidget { - final Uint8List imageBytes; final String filename; final String contentType; final bool isSharePage; final bool isFullScreen; + final FsEntryPreviewCubit previewCubit; const ImagePreviewWidget({ super.key, required this.filename, required this.contentType, - required this.imageBytes, + required this.previewCubit, this.isSharePage = false, this.isFullScreen = false, }); @@ -1221,54 +1253,241 @@ class ImagePreviewWidget extends StatefulWidget { } class _ImagePreviewWidgetState extends State { + bool _controlsVisible = true; + Timer? _hideControlsTimer; + @override - Widget build(BuildContext context) { - if (!widget.isSharePage && !widget.isFullScreen) { - return _buildImage(); + void initState() { + super.initState(); + _resetHideControlsTimer(); + } + + @override + void dispose() { + _cancelHideControlsTimer(); + if (widget.isFullScreen) { + MobileStatusBar.show(); + } + super.dispose(); + } + + void _resetHideControlsTimer() { + _hideControlsTimer?.cancel(); + _hideControlsTimer = Timer(const Duration(seconds: 3), () { + if (mounted) { + _hideControls(); + } + }); + } + + void _cancelHideControlsTimer() { + _hideControlsTimer?.cancel(); + } + + void _showControls() { + setState(() { + _controlsVisible = true; + if (widget.isFullScreen) { + MobileStatusBar.show(); + } + }); + } + + void _hideControls() { + setState(() { + _controlsVisible = false; + if (widget.isFullScreen) { + MobileStatusBar.hide(); + } + }); + } + + void _toggleControls() { + if (_controlsVisible) { + _hideControls(); } else { - final theme = ArDriveTheme.of(context); + _showControls(); + } + } + @override + Widget build(BuildContext context) { + if (widget.isFullScreen) { + return Center( + child: Stack( + fit: StackFit.expand, + children: [ + _buildImage(), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: _buildActionBar(), + ), + ], + ), + ); + } else { return Column( children: [ Flexible(child: _buildImage()), - Container( - color: theme.themeData.colors.themeBgCanvas, - child: _buildActionBar(), - ), + _buildActionBar(), ], ); } } Widget _buildImage() { - return ArDriveImage( - fit: BoxFit.contain, - height: double.maxFinite, - width: double.maxFinite, - image: MemoryImage( - widget.imageBytes, + return ValueListenableBuilder( + valueListenable: widget.previewCubit.imagePreviewNotifier, + builder: (context, imagePreview, _) { + if (!widget.isFullScreen) { + if (imagePreview.dataBytes == null) { + return const UnpreviewableContent(); + } + return _buildImageFromBytes( + imagePreview.dataBytes!, + withTapRegion: false, + ); + } else { + if (imagePreview.dataBytes == null) { + return const UnpreviewableContent(); + } + return _buildImageFromBytes( + imagePreview.dataBytes!, + withTapRegion: true, + ); + } + }, + ); + } + + Widget _buildImageFromBytes( + Uint8List imageBytes, { + required bool withTapRegion, + }) { + if (!withTapRegion) { + return ArDriveImage( + fit: BoxFit.contain, + height: double.maxFinite, + width: double.maxFinite, + image: MemoryImage( + imageBytes, + ), + ); + } + return MouseRegion( + onHover: (event) { + if (!AppPlatform.isMobile) { + _showControls(); + _resetHideControlsTimer(); + } + }, + onExit: (event) { + if (!AppPlatform.isMobile) { + if (mounted) { + setState(() { + _cancelHideControlsTimer(); + }); + } + } + }, + cursor: + _controlsVisible ? SystemMouseCursors.click : SystemMouseCursors.none, + child: TapRegion( + onTapInside: (event) { + setState(() { + _cancelHideControlsTimer(); + _toggleControls(); + + if (_controlsVisible && !AppPlatform.isMobile) { + _resetHideControlsTimer(); + } + }); + }, + child: ArDriveImage( + fit: BoxFit.contain, + height: double.maxFinite, + width: double.maxFinite, + image: MemoryImage( + imageBytes, + ), + ), ), ); } Widget _buildActionBar() { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - height: 96, - child: Padding( - padding: const EdgeInsets.only( - left: 24, - top: 24, - bottom: 24, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, + final theme = ArDriveTheme.of(context); + final isFileExplorer = !widget.isSharePage && !widget.isFullScreen; + late Widget actionBar; + + if (isFileExplorer) { + actionBar = Column(children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: _buildNameAndExtension(isFileExplorer: isFileExplorer), + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [_buildFullScreenButton(isFileExplorer: isFileExplorer)], + ), + ]); + } else { + actionBar = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: _buildNameAndExtension(isFileExplorer: isFileExplorer), + ), + _buildFullScreenButton(isFileExplorer: isFileExplorer), + ], + ); + } + + if (widget.isFullScreen) { + return AnimatedOpacity( + opacity: _controlsVisible ? 1.0 : 0.0, + duration: const Duration(milliseconds: 200), + child: Container( + color: theme.themeData.colors.themeBgCanvas, + child: actionBar, + ), + ); + } else { + return Container( + color: theme.themeData.colors.themeBgCanvas, + child: actionBar, + ); + } + } + + Widget _buildNameAndExtension({required bool isFileExplorer}) { + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: isFileExplorer ? 0 : 96, + ), + child: Padding( + padding: EdgeInsets.only( + left: 24, + top: 24, + bottom: isFileExplorer ? 0 : 24, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: isFileExplorer + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + children: [ + Wrap( + direction: Axis.horizontal, children: [ Text( - _getFileNameWithNoExtension(), + getBasenameWithoutExtension(filePath: widget.filename), style: ArDriveTypography.body.smallBold700( color: ArDriveTheme.of(context) .themeData @@ -1276,60 +1495,59 @@ class _ImagePreviewWidgetState extends State { .themeFgDefault, ), ), - Text( - _getFileExtension(), - style: ArDriveTypography.body.smallRegular( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDisabled, - ), - ), ], ), - ), - ), - Padding( - padding: const EdgeInsets.only( - right: 24, - top: 24, - bottom: 24, - ), - child: IconButton( - onPressed: goFullScreen, - icon: widget.isFullScreen - ? const Icon(Icons.fullscreen_exit_outlined) - : const Icon(Icons.fullscreen_outlined, size: 24), - ), + Text( + getFileTypeFromMime( + contentType: widget.contentType, + ).toUpperCase(), + style: ArDriveTypography.body.smallRegular( + color: + ArDriveTheme.of(context).themeData.colors.themeFgDisabled, + ), + ), + ], ), - ], + ), ); } - String _getFileNameWithNoExtension() { - return widget.filename.substring(0, widget.filename.lastIndexOf('.')); - } - - String _getFileExtension() { - return widget.contentType - .substring( - widget.contentType.lastIndexOf('/') + 1, - ) - .toUpperCase(); + Widget _buildFullScreenButton({required bool isFileExplorer}) { + return SafeArea( + child: Padding( + padding: EdgeInsets.only( + left: 24, + top: isFileExplorer ? 0 : 24, + bottom: 24, + ), + child: IconButton( + tooltip: widget.isFullScreen + ? appLocalizationsOf(context).collapse + : appLocalizationsOf(context).expand, + onPressed: _toggleFullScreen, + icon: widget.isFullScreen + ? const Icon(Icons.fullscreen_exit_outlined) + : const Icon(Icons.fullscreen_outlined, size: 24), + ), + ), + ); } - void goFullScreen() { + void _toggleFullScreen() { if (widget.isFullScreen) { Navigator.of(context).pop(); } else { Navigator.of(context).push( PageRouteBuilder( + barrierDismissible: true, transitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero, - pageBuilder: (context, _, __) => ImagePreviewFullScreenWidget( - filename: widget.filename, - contentType: widget.contentType, - imageBytes: widget.imageBytes, + pageBuilder: (context, _, __) => Scaffold( + body: ImagePreviewFullScreenWidget( + filename: widget.filename, + contentType: widget.contentType, + previewCubit: widget.previewCubit, + ), ), ), ); @@ -1493,19 +1711,7 @@ class _AudioPlayerWidgetState extends State child: FittedBox( fit: BoxFit.contain, child: _loadState == LoadState.failed - ? Column( - children: [ - const Icon(Icons.error_outline_outlined, - size: 20), - Text( - appLocalizationsOf(context) - .couldNotLoadFile, - style: ArDriveTypography.body - .smallBold700( - color: colors.themeFgMuted) - .copyWith(fontSize: 13)), - ], - ) + ? const UnpreviewableContent() : ArDriveIcons.music( size: 100, color: colors.themeFgMuted), )), diff --git a/lib/pages/drive_detail/components/unpreviewable_content.dart b/lib/pages/drive_detail/components/unpreviewable_content.dart new file mode 100644 index 0000000000..0542cb7c51 --- /dev/null +++ b/lib/pages/drive_detail/components/unpreviewable_content.dart @@ -0,0 +1,27 @@ +import 'package:ardrive/utils/app_localizations_wrapper.dart'; +import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:flutter/material.dart'; + +class UnpreviewableContent extends StatelessWidget { + const UnpreviewableContent({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: FittedBox( + fit: BoxFit.contain, + child: Column( + children: [ + const Icon(Icons.error_outline_outlined, size: 20), + Text( + appLocalizationsOf(context).couldNotLoadFile, + style: ArDriveTypography.body.smallBold700( + color: ArDriveTheme.of(context).themeData.colors.themeFgMuted, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 4a75464133..eba97fb80c 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -25,12 +25,15 @@ import 'package:ardrive/pages/drive_detail/components/drive_file_drop_zone.dart' import 'package:ardrive/pages/drive_detail/components/dropdown_item.dart'; import 'package:ardrive/pages/drive_detail/components/file_icon.dart'; import 'package:ardrive/pages/drive_detail/components/hover_widget.dart'; +import 'package:ardrive/pages/drive_detail/components/unpreviewable_content.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/compare_alphabetically_and_natural.dart'; import 'package:ardrive/utils/filesize.dart'; import 'package:ardrive/utils/logger/logger.dart'; +import 'package:ardrive/utils/mobile_screen_orientation.dart'; +import 'package:ardrive/utils/mobile_status_bar.dart'; import 'package:ardrive/utils/plausible_event_tracker.dart'; import 'package:ardrive/utils/size_constants.dart'; import 'package:ardrive/utils/user_utils.dart'; @@ -246,7 +249,7 @@ class _DriveDetailPageState extends State { .enableMultipleFileDownload) ...[ ArDriveIconButton( tooltip: 'Download selected files', - icon: ArDriveIcons.download(), + icon: ArDriveIcons.download2(), onPressed: () async { final selectedItems = context .read() @@ -312,7 +315,7 @@ class _DriveDetailPageState extends State { content: ArDriveDropdownItemTile( name: appLocalizationsOf(context) .download, - icon: ArDriveIcons.download( + icon: ArDriveIcons.download2( size: defaultIconSize, ), ), @@ -360,7 +363,7 @@ class _DriveDetailPageState extends State { content: ArDriveDropdownItemTile( name: appLocalizationsOf(context) .exportDriveContents, - icon: ArDriveIcons.download( + icon: ArDriveIcons.download2( size: defaultIconSize, ), ), @@ -810,7 +813,7 @@ class MobileFolderNavigation extends StatelessWidget { }, content: ArDriveDropdownItemTile( name: appLocalizationsOf(context).download, - icon: ArDriveIcons.download( + icon: ArDriveIcons.download2( size: defaultIconSize, ), )), @@ -853,7 +856,7 @@ class MobileFolderNavigation extends StatelessWidget { }, content: _buildItem( appLocalizationsOf(context).exportDriveContents, - ArDriveIcons.download( + ArDriveIcons.download2( size: defaultIconSize, ), ), diff --git a/lib/pages/shared_file/shared_file_page.dart b/lib/pages/shared_file/shared_file_page.dart index 56019617bd..bd110ccc11 100644 --- a/lib/pages/shared_file/shared_file_page.dart +++ b/lib/pages/shared_file/shared_file_page.dart @@ -193,7 +193,7 @@ class SharedFilePage extends StatelessWidget { ), const SizedBox(height: 24), ArDriveButton( - icon: ArDriveIcons.download(color: Colors.white), + icon: ArDriveIcons.download2(color: Colors.white), onPressed: () { final file = ARFSFactory().getARFSFileFromFileRevision( state.fileRevisions.first, diff --git a/lib/utils/file_type_helper.dart b/lib/utils/file_type_helper.dart index 2875a73492..ed42d24301 100644 --- a/lib/utils/file_type_helper.dart +++ b/lib/utils/file_type_helper.dart @@ -1,4 +1,4 @@ -class FileTypeHelper { +abstract class FileTypeHelper { static const List _imageTypes = ['image/']; static const List _audioTypes = ['audio/']; static const List _videoTypes = ['video/']; @@ -36,6 +36,10 @@ class FileTypeHelper { 'application/x-rar-compressed' ]; + static const List _manifestTypes = [ + 'application/x.arweave-manifest+json', + ]; + static bool isImage(String contentType) { return _imageTypes.any((type) => contentType.startsWith(type)); } @@ -59,4 +63,8 @@ class FileTypeHelper { static bool isZip(String contentType) { return _zipTypes.contains(contentType); } + + static bool isManifest(String contentType) { + return _manifestTypes.contains(contentType); + } } diff --git a/lib/utils/mobile_screen_orientation.dart b/lib/utils/mobile_screen_orientation.dart new file mode 100644 index 0000000000..3857199ecf --- /dev/null +++ b/lib/utils/mobile_screen_orientation.dart @@ -0,0 +1,35 @@ +import 'package:flutter/services.dart'; + +abstract class MobileScreenOrientation { + static void freeUpDependingOnScreenSize() { + // if (ScreenSize.isSmall) { + // blockInPortraitUp(); + // } else { + // freeUp(); + // } + throw UnimplementedError(); + } + + static void freeUp() { + // SystemChrome.setPreferredOrientations([ + // DeviceOrientation.portraitUp, + // DeviceOrientation.landscapeRight, + // DeviceOrientation.landscapeLeft, + // DeviceOrientation.portraitDown, + // ]); + throw UnimplementedError(); + } + + static void lockInLandscape() { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeRight, + DeviceOrientation.landscapeLeft, + ]); + } + + static void lockInPortraitUp() { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + ]); + } +} diff --git a/lib/utils/mobile_status_bar.dart b/lib/utils/mobile_status_bar.dart new file mode 100644 index 0000000000..345fc99e2c --- /dev/null +++ b/lib/utils/mobile_status_bar.dart @@ -0,0 +1,11 @@ +import 'package:flutter/services.dart'; + +abstract class MobileStatusBar { + static hide() { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + } + + static show() { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } +} diff --git a/packages/ardrive_uploader/build/unit_test_assets/FontManifest.json b/packages/ardrive_uploader/build/unit_test_assets/FontManifest.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/ardrive_uploader/build/unit_test_assets/FontManifest.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/ardrive_uploader/example/pubspec.yaml b/packages/ardrive_uploader/example/pubspec.yaml index 12dbc29c83..0ca023ef4a 100644 --- a/packages/ardrive_uploader/example/pubspec.yaml +++ b/packages/ardrive_uploader/example/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: ardrive_io: git: url: https://github.com/ar-io/ardrive_io.git - ref: v1.4.1 + ref: v1.4.2 arweave: git: url: https://github.com/ardriveapp/arweave-dart.git diff --git a/packages/ardrive_uploader/pubspec.yaml b/packages/ardrive_uploader/pubspec.yaml index 1de9a67bda..24dd8cf40b 100644 --- a/packages/ardrive_uploader/pubspec.yaml +++ b/packages/ardrive_uploader/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: ardrive_io: git: url: https://github.com/ar-io/ardrive_io.git - ref: v1.4.1 + ref: v1.4.2 arweave: git: url: https://github.com/ardriveapp/arweave-dart.git diff --git a/packages/pst/pubspec.lock b/packages/pst/pubspec.lock index d51e4468ed..41bc607836 100644 --- a/packages/pst/pubspec.lock +++ b/packages/pst/pubspec.lock @@ -45,11 +45,11 @@ packages: dependency: "direct main" description: path: "." - ref: PE-4971-generate-optional-fields-if-missing - resolved-ref: "965224ff551191614d83f6ce01bf2340e9ed9980" + ref: "v3.8.1" + resolved-ref: "6629b72b6e36a1628fd5a74a5deb5cae24d09fe5" url: "https://github.com/ardriveapp/arweave-dart.git" source: git - version: "3.8.0" + version: "3.8.1" async: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index c753356982..e667bc9d7d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -84,20 +84,20 @@ packages: dependency: "direct main" description: path: "." - ref: "v1.4.1" - resolved-ref: "441c37764a1b120057ec2cd1e6eecb47bb35a37a" + ref: "v1.4.2" + resolved-ref: "0a693d0c863a2e7898755278eb862a90cffaa037" url: "https://github.com/ar-io/ardrive_io.git" source: git - version: "1.4.1" + version: "1.4.2" ardrive_ui: dependency: "direct main" description: path: "." - ref: "v1.14.0" - resolved-ref: f2c75067ebb214200e507e269bddbfd7285720ab + ref: "v1.15.0" + resolved-ref: "0c088e5eb93cd2f3434e90080a91ec4ac2362da8" url: "https://github.com/ar-io/ardrive_ui.git" source: git - version: "1.14.0" + version: "1.15.0" ardrive_uploader: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 633a0c3a5e..bcddd5657b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Secure, permanent storage publish_to: 'none' -version: 2.24.2 +version: 2.25.0 environment: sdk: '>=3.0.2 <4.0.0' @@ -35,11 +35,11 @@ dependencies: ardrive_io: git: url: https://github.com/ar-io/ardrive_io.git - ref: v1.4.1 + ref: v1.4.2 ardrive_ui: git: url: https://github.com/ar-io/ardrive_ui.git - ref: v1.14.0 + ref: v1.15.0 ardrive_utils: path: ./packages/ardrive_utils ardrive_uploader: