feat(file_viewer): implement file view handler

This commit is contained in:
Muhesh7 2023-02-21 20:02:29 +05:30
parent add5442607
commit 16a82b62d1
11 changed files with 243 additions and 138 deletions

View file

@ -1,107 +1,108 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="io.ente.photos">
xmlns:tools="http://schemas.android.com/tools"
package="io.ente.photos">
<application android:name="${applicationName}"
android:label="@string/app_name"
android:icon="@mipmap/launcher_icon"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
android:allowBackup="false"
android:fullBackupContent="false"
android:largeHeap="true">
android:label="@string/app_name"
android:icon="@mipmap/launcher_icon"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
android:allowBackup="false"
android:fullBackupContent="false"
android:largeHeap="true">
<activity android:name=".MainActivity" android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:exported="true"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- <intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter> -->
<intent-filter>
<action android:name="android.intent.action.PICK" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<!-- file provider to share files having a file:// URI -->
android:theme="@style/LaunchTheme"
android:exported="true"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PICK" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<!--Filter to support sharing images into our app-->
<!-- file provider to share files having a file:// URI -->
<!--Filter
to support sharing images into our app-->
<intent-filter android:label="@string/backup">
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*"/>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter android:label="@string/backup">
<action android:name="android.intent.action.SEND_MULTIPLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*"/>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter android:label="@string/backup">
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="video/*"/>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter android:label="@string/backup">
<action android:name="android.intent.action.SEND_MULTIPLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="video/*"/>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data android:name="flutterEmbedding" android:value="2"/>
<meta-data android:name="flutterEmbedding" android:value="2" />
<meta-data android:name="asset_statements"
android:resource="@string/asset_statements"/>
android:resource="@string/asset_statements" />
<meta-data android:name="io.sentry.dsn"
android:value="https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4"/>
android:value="https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4" />
<meta-data android:name="firebase_analytics_collection_deactivated"
android:value="true"/>
android:value="true" />
</application>
<!-- Android 11: https://developer.android.com/preview/privacy/package-visibility -->
<!-- https://developer.android.com/training/package-visibility/use-cases -->
<queries>
<intent>
<action android:name="android.intent.action.SENDTO"/>
<data android:scheme="mailto"/>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
</intent>
</queries>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MANAGE_MEDIA"/>
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MANAGE_MEDIA" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES"/> <!-- If you want to read images-->
android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- If you want to read images-->
<uses-permission
android:name="android.permission.READ_MEDIA_VIDEO"/> <!-- If you want to read videos-->
android:name="android.permission.READ_MEDIA_VIDEO" /> <!-- If you want to read videos-->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32"/>
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:ignore="ScopedStorage"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.android.vending.BILLING"/>
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="com.android.vending.BILLING" />
</manifest>

View file

@ -4,16 +4,16 @@ import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:background_fetch/background_fetch.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:logging/logging.dart';
import 'package:media_extension/media_extension.dart';
import 'package:media_extension/media_extension_action_types.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/services/app_lifecycle_service.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/ui/home_widget.dart';
import "package:photos/ui/viewer/actions/file_viewer.dart";
import "package:photos/utils/intent_util.dart";
class EnteApp extends StatefulWidget {
final Future<void> Function(String) runBackgroundTask;
@ -31,63 +31,44 @@ class EnteApp extends StatefulWidget {
class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
final _logger = Logger("EnteAppState");
final _mediaExtensionPlugin = MediaExtension();
@override
void initState() {
_logger.info('init App');
super.initState();
setupIntentAction();
WidgetsBinding.instance.addObserver(this);
}
Future<bool> initIntentAction() async {
if (!Platform.isAndroid) {
AppLifecycleService.instance.setIntentAction(IntentAction.main);
return true;
}
IntentAction intentAction = IntentAction.main;
try {
final actionResult = await _mediaExtensionPlugin.getIntentAction();
intentAction = actionResult.action!;
} on PlatformException {
intentAction = IntentAction.main;
} catch (error) {
_logger.info(error);
intentAction = IntentAction.main;
}
if (intentAction == IntentAction.main) {
void setupIntentAction() async {
final mediaExtentionAction = await initIntentAction();
AppLifecycleService.instance.setMediaExtensionAction(mediaExtentionAction);
if (mediaExtentionAction.action == IntentAction.main) {
_configureBackgroundFetch();
}
AppLifecycleService.instance.setIntentAction(intentAction);
return true;
}
@override
Widget build(BuildContext context) {
if (Platform.isAndroid || kDebugMode) {
return FutureBuilder(
future: initIntentAction(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
return snapshot.data != null && snapshot.data == true
? AdaptiveTheme(
light: lightThemeData,
dark: darkThemeData,
initial: AdaptiveThemeMode.system,
builder: (lightTheme, dartTheme) => MaterialApp(
title: "ente",
themeMode: ThemeMode.system,
theme: lightTheme,
darkTheme: dartTheme,
home: const HomeWidget(),
debugShowCheckedModeBanner: false,
builder: EasyLoading.init(),
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates:
AppLocalizations.localizationsDelegates,
),
)
: Container();
},
return AdaptiveTheme(
light: lightThemeData,
dark: darkThemeData,
initial: AdaptiveThemeMode.system,
builder: (lightTheme, dartTheme) => MaterialApp(
title: "ente",
themeMode: ThemeMode.system,
theme: lightTheme,
darkTheme: dartTheme,
home: AppLifecycleService.instance.mediaExtensionAction.action ==
IntentAction.view
? const FileViewer()
: const HomeWidget(),
debugShowCheckedModeBanner: false,
builder: EasyLoading.init(),
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates,
),
);
} else {
return MaterialApp(

View file

@ -5,15 +5,17 @@ class AppLifecycleService {
final _logger = Logger("AppLifecycleService");
bool isForeground = false;
IntentAction intentAction = IntentAction.main;
MediaExtentionAction mediaExtensionAction =
MediaExtentionAction(action: IntentAction.main);
static final AppLifecycleService instance =
AppLifecycleService._privateConstructor();
AppLifecycleService._privateConstructor();
void setIntentAction(IntentAction intentAction) {
this.intentAction = intentAction;
void setMediaExtensionAction(MediaExtentionAction mediaExtensionAction) {
_logger.info("App invoked via ${mediaExtensionAction.action}");
this.mediaExtensionAction = mediaExtensionAction;
}
void onAppInForeground(String reason) {

View file

@ -457,8 +457,11 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
}
Future<bool> _restoreFilesToCollection(int toCollectionID) async {
final dialog = createProgressDialog(context, "Restoring files...",
isDismissible: true);
final dialog = createProgressDialog(
context,
"Restoring files...",
isDismissible: true,
);
await dialog.show();
try {
await CollectionsService.instance

View file

@ -264,7 +264,7 @@ class _HomeWidgetState extends State<HomeWidget> {
_logger.info("Building home_Widget with tab $_selectedTabIndex");
bool isSettingsOpen = false;
final enableDrawer = LocalSyncService.instance.hasCompletedFirstImport();
final action = AppLifecycleService.instance.mediaExtensionAction.action;
return UserDetailsStateWidget(
child: WillPopScope(
child: Scaffold(
@ -297,9 +297,7 @@ class _HomeWidgetState extends State<HomeWidget> {
Navigator.pop(context);
return false;
}
if (Platform.isAndroid &&
AppLifecycleService.instance.intentAction ==
IntentAction.main) {
if (Platform.isAndroid && action == IntentAction.main) {
MoveToBackground.moveTaskToBack();
return false;
} else {

View file

@ -311,7 +311,6 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
bool? _shouldRender;
int? _currentUserID;
late StreamSubscription<ClearSelectionsEvent> _clearSelectionsEvent;
final _mediaExtensionPlugin = MediaExtension();
@override
void initState() {
@ -426,16 +425,18 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
if (widget.selectedFiles.files.isNotEmpty) {
_selectFile(file);
} else {
if (AppLifecycleService.instance.intentAction == IntentAction.pick) {
if (AppLifecycleService.instance.mediaExtensionAction.action ==
IntentAction.pick) {
final ioFile = await getFile(file);
_mediaExtensionPlugin.setResult("file://${ioFile!.path}");
MediaExtension().setResult("file://${ioFile!.path}");
} else {
_routeToDetailPage(file, context);
}
}
},
onLongPress: () {
if (AppLifecycleService.instance.intentAction == IntentAction.main) {
if (AppLifecycleService.instance.mediaExtensionAction.action ==
IntentAction.main) {
HapticFeedback.lightImpact();
_selectFile(file);
}

View file

@ -100,8 +100,9 @@ class _DeleteEmptyAlbumsState extends State<DeleteEmptyAlbums> {
"${collections.length}";
try {
await CollectionsService.instance.trashEmptyCollection(
collections[i].collection,
isBulkDelete: true);
collections[i].collection,
isBulkDelete: true,
);
} catch (_) {
failedCount++;
}

View file

@ -0,0 +1,104 @@
import 'dart:convert';
import "package:chewie/chewie.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:logging/logging.dart";
import "package:media_extension/media_extension_action_types.dart";
import "package:photo_view/photo_view.dart";
import "package:photos/services/app_lifecycle_service.dart";
import "package:video_player/video_player.dart";
class FileViewer extends StatefulWidget {
const FileViewer({super.key});
@override
State<StatefulWidget> createState() {
return FileViewerState();
}
}
class FileViewerState extends State<FileViewer> {
final action = AppLifecycleService.instance.mediaExtensionAction;
ChewieController? controller;
VideoPlayerController? videoController;
final Logger _logger = Logger("FileViewer");
@override
void initState() {
super.initState();
if (action.type == MediaType.video) {
initController();
}
}
@override
void dispose() {
videoController?.dispose();
controller?.dispose();
super.dispose();
}
void initController() async {
videoController = VideoPlayerController.contentUri(
Uri.parse(action.data!),
);
controller = ChewieController(
videoPlayerController: videoController!,
autoInitialize: true,
aspectRatio: 16 / 9,
autoPlay: true,
looping: true,
showOptions: false,
materialProgressColors: ChewieProgressColors(
playedColor: const Color.fromRGBO(45, 194, 98, 1.0),
handleColor: Colors.white,
bufferedColor: Colors.white,
),
);
controller!.addListener(() {
if (!controller!.isFullScreen) {
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp],
);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () {
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
},
icon: const Icon(Icons.arrow_back),
),
),
body: Column(
children: [
Expanded(
child: Center(
child: (() {
if (action.type == MediaType.image) {
return PhotoView(
imageProvider: MemoryImage(base64Decode(action.data!)),
);
} else if (action.type == MediaType.video) {
return controller != null
? Chewie(controller: controller!)
: const CircularProgressIndicator();
} else {
_logger.severe('unsupported file type ${action.type}');
return const Icon(Icons.error);
}
})(),
),
),
],
),
);
}
}

View file

@ -0,0 +1,16 @@
import "package:flutter/services.dart";
import "package:media_extension/media_extension.dart";
import "package:media_extension/media_extension_action_types.dart";
Future<MediaExtentionAction> initIntentAction() async {
final mediaExtensionPlugin = MediaExtension();
MediaExtentionAction mediaExtensionAction;
try {
mediaExtensionAction = await mediaExtensionPlugin.getIntentAction();
} on PlatformException {
mediaExtensionAction = MediaExtentionAction(action: IntentAction.main);
} catch (error) {
mediaExtensionAction = MediaExtentionAction(action: IntentAction.main);
}
return mediaExtensionAction;
}

View file

@ -76,7 +76,7 @@ dependencies:
local_auth: ^1.1.5
logging: ^1.0.1
lottie: ^1.2.2
media_extension: ^1.0.0
media_extension: ^1.0.1
modal_bottom_sheet: ^3.0.0-pre
motionphoto:
git: "https://github.com/ente-io/motionphoto.git"

14
run.sh
View file

@ -2,14 +2,12 @@
FLUTTER_RUN="flutter run --flavor dev "
if [ ! -z "$1" ]
then
SUPPLIED_ENV_FILE="$1"
while IFS= read -r line
do
FLUTTER_RUN="$FLUTTER_RUN --dart-define $line"
SUPPLIED_ENV_FILE=".env"
while IFS= read -r line
do
FLUTTER_RUN="$FLUTTER_RUN --dart-define $line"
done < "$SUPPLIED_ENV_FILE"
done < "$SUPPLIED_ENV_FILE"
fi
echo "Running: $FLUTTER_RUN"
$FLUTTER_RUN