[mob][photo] Show file caption/description in file viewer. (#5279)
## Description - Tapping on description/caption will open file info. <img src="https://github.com/user-attachments/assets/0f9422ec-49bb-43d8-9568-b57748587866" width="300px"> <img src="https://github.com/user-attachments/assets/43b704b4-6fc4-44ed-8d7a-97b7d27c90b0" width="300px"> <img src="https://github.com/user-attachments/assets/65fca334-14a7-4f01-95c4-46b231687438" width="300px"> <img src="https://github.com/user-attachments/assets/8e56cb29-7af6-439e-8627-3badc60aa383" width="300px">
This commit is contained in:
7
mobile/lib/events/file_caption_updated_event.dart
Normal file
7
mobile/lib/events/file_caption_updated_event.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
import "package:photos/events/event.dart";
|
||||
|
||||
class FileCaptionUpdatedEvent extends Event {
|
||||
final int fileGeneratedID;
|
||||
|
||||
FileCaptionUpdatedEvent(this.fileGeneratedID);
|
||||
}
|
||||
37
mobile/lib/states/detail_page_state.dart
Normal file
37
mobile/lib/states/detail_page_state.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
|
||||
class InheritedDetailPageState extends InheritedWidget {
|
||||
final enableFullScreenNotifier = ValueNotifier(false);
|
||||
InheritedDetailPageState({
|
||||
super.key,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static InheritedDetailPageState of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<InheritedDetailPageState>()!;
|
||||
|
||||
void toggleFullScreen({bool? shouldEnable}) {
|
||||
if (shouldEnable != null) {
|
||||
if (enableFullScreenNotifier.value == shouldEnable) return;
|
||||
}
|
||||
enableFullScreenNotifier.value = !enableFullScreenNotifier.value;
|
||||
if (enableFullScreenNotifier.value) {
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: [],
|
||||
);
|
||||
});
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.edgeToEdge,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedDetailPageState oldWidget) =>
|
||||
oldWidget.enableFullScreenNotifier != enableFullScreenNotifier;
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/models/file/file_type.dart";
|
||||
import "package:photos/service_locator.dart";
|
||||
import "package:photos/services/local_authentication_service.dart";
|
||||
import "package:photos/states/detail_page_state.dart";
|
||||
import "package:photos/ui/common/fast_scroll_physics.dart";
|
||||
import 'package:photos/ui/notification/toast.dart';
|
||||
import 'package:photos/ui/tools/editor/image_editor_page.dart';
|
||||
@@ -77,7 +78,6 @@ class _DetailPageState extends State<DetailPage> {
|
||||
List<EnteFile>? _files;
|
||||
late PageController _pageController;
|
||||
final _selectedIndexNotifier = ValueNotifier(0);
|
||||
final _enableFullScreenNotifier = ValueNotifier(false);
|
||||
bool _isFirstOpened = true;
|
||||
bool isGuestView = false;
|
||||
bool swipeLocked = false;
|
||||
@@ -103,7 +103,6 @@ class _DetailPageState extends State<DetailPage> {
|
||||
void dispose() {
|
||||
_guestViewEventSubscription.cancel();
|
||||
_pageController.dispose();
|
||||
_enableFullScreenNotifier.dispose();
|
||||
_selectedIndexNotifier.dispose();
|
||||
super.dispose();
|
||||
|
||||
@@ -137,96 +136,102 @@ class _DetailPageState extends State<DetailPage> {
|
||||
_files!.length.toString() +
|
||||
" files .",
|
||||
);
|
||||
return PopScope(
|
||||
canPop: !isGuestView,
|
||||
onPopInvoked: (didPop) async {
|
||||
if (isGuestView) {
|
||||
final authenticated = await _requestAuthentication();
|
||||
if (authenticated) {
|
||||
Bus.instance.fire(GuestViewEvent(false, false));
|
||||
await localSettings.setOnGuestView(false);
|
||||
return InheritedDetailPageState(
|
||||
child: PopScope(
|
||||
canPop: !isGuestView,
|
||||
onPopInvoked: (didPop) async {
|
||||
if (isGuestView) {
|
||||
final authenticated = await _requestAuthentication();
|
||||
if (authenticated) {
|
||||
Bus.instance.fire(GuestViewEvent(false, false));
|
||||
await localSettings.setOnGuestView(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(80),
|
||||
child: ValueListenableBuilder(
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
return FileAppBar(
|
||||
_files![selectedIndex],
|
||||
_onFileRemoved,
|
||||
widget.config.mode == DetailPageMode.full,
|
||||
enableFullScreenNotifier: _enableFullScreenNotifier,
|
||||
);
|
||||
},
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(80),
|
||||
child: ValueListenableBuilder(
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
return FileAppBar(
|
||||
_files![selectedIndex],
|
||||
_onFileRemoved,
|
||||
widget.config.mode == DetailPageMode.full,
|
||||
enableFullScreenNotifier: InheritedDetailPageState.of(context)
|
||||
.enableFullScreenNotifier,
|
||||
);
|
||||
},
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
),
|
||||
),
|
||||
),
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: false,
|
||||
backgroundColor: Colors.black,
|
||||
body: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildPageView(context),
|
||||
ValueListenableBuilder(
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
return FileBottomBar(
|
||||
_files![selectedIndex],
|
||||
_onEditFileRequested,
|
||||
widget.config.mode == DetailPageMode.minimalistic &&
|
||||
!isGuestView,
|
||||
onFileRemoved: _onFileRemoved,
|
||||
userID: Configuration.instance.getUserID(),
|
||||
enableFullScreenNotifier: _enableFullScreenNotifier,
|
||||
);
|
||||
},
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
if (_files![selectedIndex].isPanorama() == true) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _enableFullScreenNotifier,
|
||||
builder: (context, value, child) {
|
||||
return IgnorePointer(
|
||||
ignoring: value,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: !value ? 1.0 : 0.0,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Tooltip(
|
||||
message: S.of(context).panorama,
|
||||
child: IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: const Color(0xAA252525),
|
||||
fixedSize: const Size(44, 44),
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: false,
|
||||
backgroundColor: Colors.black,
|
||||
body: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildPageView(),
|
||||
ValueListenableBuilder(
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
return FileBottomBar(
|
||||
_files![selectedIndex],
|
||||
_onEditFileRequested,
|
||||
widget.config.mode == DetailPageMode.minimalistic &&
|
||||
!isGuestView,
|
||||
onFileRemoved: _onFileRemoved,
|
||||
userID: Configuration.instance.getUserID(),
|
||||
enableFullScreenNotifier:
|
||||
InheritedDetailPageState.of(context)
|
||||
.enableFullScreenNotifier,
|
||||
);
|
||||
},
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
if (_files![selectedIndex].isPanorama() == true) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: InheritedDetailPageState.of(context)
|
||||
.enableFullScreenNotifier,
|
||||
builder: (context, value, child) {
|
||||
return IgnorePointer(
|
||||
ignoring: value,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: !value ? 1.0 : 0.0,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Tooltip(
|
||||
message: S.of(context).panorama,
|
||||
child: IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: const Color(0xAA252525),
|
||||
fixedSize: const Size(44, 44),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.threesixty,
|
||||
color: Colors.white,
|
||||
size: 26,
|
||||
),
|
||||
onPressed: () async {
|
||||
await openPanoramaViewerPage(
|
||||
_files![selectedIndex],
|
||||
);
|
||||
},
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.threesixty,
|
||||
color: Colors.white,
|
||||
size: 26,
|
||||
),
|
||||
onPressed: () async {
|
||||
await openPanoramaViewerPage(
|
||||
_files![selectedIndex],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -251,7 +256,7 @@ class _DetailPageState extends State<DetailPage> {
|
||||
).ignore();
|
||||
}
|
||||
|
||||
Widget _buildPageView(BuildContext context) {
|
||||
Widget _buildPageView() {
|
||||
return PageView.builder(
|
||||
clipBehavior: Clip.none,
|
||||
itemBuilder: (context, index) {
|
||||
@@ -271,14 +276,17 @@ class _DetailPageState extends State<DetailPage> {
|
||||
},
|
||||
playbackCallback: (isPlaying) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_toggleFullScreen(shouldEnable: isPlaying);
|
||||
InheritedDetailPageState.of(context)
|
||||
.toggleFullScreen(shouldEnable: isPlaying);
|
||||
});
|
||||
},
|
||||
backgroundDecoration: const BoxDecoration(color: Colors.black),
|
||||
);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
file.fileType != FileType.video ? _toggleFullScreen() : null;
|
||||
file.fileType != FileType.video
|
||||
? InheritedDetailPageState.of(context).toggleFullScreen()
|
||||
: null;
|
||||
},
|
||||
child: fileContent,
|
||||
);
|
||||
@@ -313,26 +321,6 @@ class _DetailPageState extends State<DetailPage> {
|
||||
return false;
|
||||
}
|
||||
|
||||
void _toggleFullScreen({bool? shouldEnable}) {
|
||||
if (shouldEnable != null) {
|
||||
if (_enableFullScreenNotifier.value == shouldEnable) return;
|
||||
}
|
||||
_enableFullScreenNotifier.value = !_enableFullScreenNotifier.value;
|
||||
if (_enableFullScreenNotifier.value) {
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: [],
|
||||
);
|
||||
});
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.edgeToEdge,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _preloadFiles(int index) {
|
||||
if (index > 0) {
|
||||
preloadFile(_files![index - 1]);
|
||||
|
||||
@@ -100,11 +100,6 @@ class FileBottomBarState extends State<FileBottomBar> {
|
||||
),
|
||||
onPressed: () async {
|
||||
await _displayDetails(widget.file);
|
||||
safeRefresh(); //to instantly show the new caption if keypad is closed after pressing 'done' - here the caption will be updated before the bottom sheet is closed
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
); //Waiting for some time till the caption gets updated in db if the user closes the bottom sheet without pressing 'done'
|
||||
safeRefresh();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/file_caption_updated_event.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import 'package:photos/ui/components/keyboard/keyboard_oveylay.dart';
|
||||
import 'package:photos/ui/components/keyboard/keyboard_top_button.dart';
|
||||
import "package:photos/ui/notification/toast.dart";
|
||||
import 'package:photos/utils/magic_util.dart';
|
||||
|
||||
class FileCaptionReadyOnly extends StatelessWidget {
|
||||
@@ -71,18 +74,19 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode.addListener(_focusNodeListener);
|
||||
editedCaption = widget.file.caption;
|
||||
if (editedCaption != null && editedCaption!.isNotEmpty) {
|
||||
hintText = editedCaption!;
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (editedCaption != null) {
|
||||
editFileCaption(null, widget.file, editedCaption!);
|
||||
editFileCaption(null, widget.file, editedCaption!)
|
||||
.then((isSuccess) => _onEditFileFinish(isSuccess));
|
||||
}
|
||||
_textController.dispose();
|
||||
_focusNode.removeListener(_focusNodeListener);
|
||||
@@ -148,7 +152,8 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
||||
Future<void> _onDoneClick(BuildContext context) async {
|
||||
if (editedCaption != null) {
|
||||
final isSuccesful =
|
||||
await editFileCaption(context, widget.file, editedCaption!);
|
||||
await editFileCaption(context, widget.file, editedCaption!)
|
||||
.then((isSuccess) => _onEditFileFinish(isSuccess));
|
||||
if (isSuccesful) {
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
@@ -185,4 +190,15 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
||||
KeyboardOverlay.removeOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
bool _onEditFileFinish(bool isSuccess) {
|
||||
if (isSuccess) {
|
||||
widget.file.pubMagicMetadata?.caption = editedCaption;
|
||||
Bus.instance.fire(FileCaptionUpdatedEvent(widget.file.generatedID!));
|
||||
return true;
|
||||
} else {
|
||||
showShortToast(context, S.of(context).somethingWentWrong);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import "package:media_kit_video/media_kit_video.dart";
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/actions/file/file_actions.dart";
|
||||
import "package:photos/ui/common/loading_widget.dart";
|
||||
import "package:photos/ui/viewer/file/preview_status_widget.dart";
|
||||
import "package:photos/utils/standalone/date_time.dart";
|
||||
@@ -44,6 +45,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isPlayingStreamSubscription =
|
||||
widget.controller.player.stream.playing.listen((isPlaying) {
|
||||
if (isPlaying && !_isSeekingNotifier.value) {
|
||||
@@ -55,7 +57,6 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
});
|
||||
|
||||
_isSeekingNotifier.addListener(isSeekingListener);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -131,27 +132,6 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
widget.file.caption != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
16,
|
||||
12,
|
||||
16,
|
||||
8,
|
||||
),
|
||||
child: Text(
|
||||
widget.file.caption!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: getEnteTextTheme(context)
|
||||
.mini
|
||||
.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
PreviewStatusWidget(
|
||||
showControls: value,
|
||||
file: widget.file,
|
||||
@@ -161,6 +141,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
SeekBarAndDuration(
|
||||
controller: widget.controller,
|
||||
isSeekingNotifier: _isSeekingNotifier,
|
||||
file: widget.file,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -272,11 +253,13 @@ class _PlayPauseButtonState extends State<PlayPauseButtonMediaKit> {
|
||||
class SeekBarAndDuration extends StatelessWidget {
|
||||
final VideoController? controller;
|
||||
final ValueNotifier<bool> isSeekingNotifier;
|
||||
final EnteFile file;
|
||||
|
||||
const SeekBarAndDuration({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.isSeekingNotifier,
|
||||
required this.file,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -302,46 +285,73 @@ class SeekBarAndDuration extends StatelessWidget {
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
child: Column(
|
||||
children: [
|
||||
StreamBuilder(
|
||||
stream: controller?.player.stream.position,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data == null) {
|
||||
return Text(
|
||||
"0:00",
|
||||
style: getEnteTextTheme(
|
||||
context,
|
||||
).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
secondsToDuration(snapshot.data!.inSeconds),
|
||||
file.caption != null && file.caption!.isNotEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
12,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showDetailsSheet(context, file);
|
||||
},
|
||||
child: Text(
|
||||
file.caption!,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: getEnteTextTheme(context)
|
||||
.mini
|
||||
.copyWith(color: textBaseDark),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Row(
|
||||
children: [
|
||||
StreamBuilder(
|
||||
stream: controller?.player.stream.position,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data == null) {
|
||||
return Text(
|
||||
"0:00",
|
||||
style: getEnteTextTheme(
|
||||
context,
|
||||
).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
secondsToDuration(snapshot.data!.inSeconds),
|
||||
style: getEnteTextTheme(
|
||||
context,
|
||||
).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: SeekBar(
|
||||
controller!,
|
||||
isSeekingNotifier,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_secondsToDuration(
|
||||
controller!.player.state.duration.inSeconds,
|
||||
),
|
||||
style: getEnteTextTheme(
|
||||
context,
|
||||
).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: SeekBar(
|
||||
controller!,
|
||||
isSeekingNotifier,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_secondsToDuration(
|
||||
controller!.player.state.duration.inSeconds,
|
||||
),
|
||||
style: getEnteTextTheme(
|
||||
context,
|
||||
).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -7,6 +7,7 @@ import "package:media_kit/media_kit.dart";
|
||||
import "package:media_kit_video/media_kit_video.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/file_caption_updated_event.dart";
|
||||
import "package:photos/events/guest_view_event.dart";
|
||||
import "package:photos/events/pause_video_event.dart";
|
||||
import "package:photos/events/stream_switched_event.dart";
|
||||
@@ -60,6 +61,8 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
|
||||
late final StreamSubscription<GuestViewEvent> _guestViewEventSubscription;
|
||||
bool _isGuestView = false;
|
||||
StreamSubscription<StreamSwitchedEvent>? _streamSwitchedSubscription;
|
||||
late final StreamSubscription<FileCaptionUpdatedEvent>
|
||||
_captionUpdatedSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -84,6 +87,7 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
|
||||
_isGuestView = event.isGuestView;
|
||||
});
|
||||
});
|
||||
|
||||
_streamSwitchedSubscription =
|
||||
Bus.instance.on<StreamSwitchedEvent>().listen((event) {
|
||||
if (event.type != PlayerType.mediaKit || !mounted) return;
|
||||
@@ -93,6 +97,15 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
|
||||
loadOriginal();
|
||||
}
|
||||
});
|
||||
|
||||
_captionUpdatedSubscription =
|
||||
Bus.instance.on<FileCaptionUpdatedEvent>().listen((event) {
|
||||
if (event.fileGeneratedID == widget.file.generatedID) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void loadPreview() {
|
||||
@@ -147,6 +160,7 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
|
||||
_progressNotifier.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
player.dispose();
|
||||
_captionUpdatedSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import "package:logging/logging.dart";
|
||||
import "package:native_video_player/native_video_player.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/file_caption_updated_event.dart";
|
||||
import "package:photos/events/guest_view_event.dart";
|
||||
import "package:photos/events/pause_video_event.dart";
|
||||
import "package:photos/events/seekbar_triggered_event.dart";
|
||||
@@ -80,6 +81,8 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
final _elTooltipController = ElTooltipController();
|
||||
StreamSubscription<PlaybackEvent>? _subscription;
|
||||
StreamSubscription<StreamSwitchedEvent>? _streamSwitchedSubscription;
|
||||
late final StreamSubscription<FileCaptionUpdatedEvent>
|
||||
_captionUpdatedSubscription;
|
||||
int position = 0;
|
||||
|
||||
@override
|
||||
@@ -114,6 +117,15 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
loadOriginal(update: true);
|
||||
}
|
||||
});
|
||||
|
||||
_captionUpdatedSubscription =
|
||||
Bus.instance.on<FileCaptionUpdatedEvent>().listen((event) {
|
||||
if (event.fileGeneratedID == widget.file.generatedID) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> setVideoSource() async {
|
||||
@@ -207,6 +219,7 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
_isSeeking.dispose();
|
||||
_debouncer.cancelDebounceTimer();
|
||||
_elTooltipController.dispose();
|
||||
_captionUpdatedSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -357,6 +370,7 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
showControls: _showControls,
|
||||
isSeeking: _isSeeking,
|
||||
position: position,
|
||||
file: widget.file,
|
||||
)
|
||||
: const SizedBox();
|
||||
},
|
||||
@@ -644,6 +658,7 @@ class _SeekBarAndDuration extends StatelessWidget {
|
||||
final ValueNotifier<bool> showControls;
|
||||
final ValueNotifier<bool> isSeeking;
|
||||
final int position;
|
||||
final EnteFile file;
|
||||
|
||||
const _SeekBarAndDuration({
|
||||
required this.controller,
|
||||
@@ -651,6 +666,7 @@ class _SeekBarAndDuration extends StatelessWidget {
|
||||
required this.showControls,
|
||||
required this.isSeeking,
|
||||
required this.position,
|
||||
required this.file,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -691,34 +707,61 @@ class _SeekBarAndDuration extends StatelessWidget {
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedSize(
|
||||
duration: const Duration(
|
||||
seconds: 5,
|
||||
),
|
||||
curve: Curves.easeInOut,
|
||||
child: Text(
|
||||
secondsToDuration(position ~/ 1000),
|
||||
style: getEnteTextTheme(
|
||||
context,
|
||||
).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
file.caption != null && file.caption!.isNotEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
12,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SeekBar(
|
||||
controller!,
|
||||
durationToSeconds(duration),
|
||||
isSeeking,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
duration ?? "0:00",
|
||||
style: getEnteTextTheme(context).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showDetailsSheet(context, file);
|
||||
},
|
||||
child: Text(
|
||||
file.caption!,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: getEnteTextTheme(context)
|
||||
.mini
|
||||
.copyWith(color: textBaseDark),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Row(
|
||||
children: [
|
||||
AnimatedSize(
|
||||
duration: const Duration(
|
||||
seconds: 5,
|
||||
),
|
||||
curve: Curves.easeInOut,
|
||||
child: Text(
|
||||
secondsToDuration(position ~/ 1000),
|
||||
style: getEnteTextTheme(
|
||||
context,
|
||||
).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SeekBar(
|
||||
controller!,
|
||||
durationToSeconds(duration),
|
||||
isSeeking,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
duration ?? "0:00",
|
||||
style: getEnteTextTheme(context).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -748,115 +791,85 @@ class _VideoDescriptionAndSwitchToMediaKitButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: Platform.isAndroid
|
||||
? MainAxisAlignment.spaceBetween
|
||||
: MainAxisAlignment.center,
|
||||
children: [
|
||||
file.caption?.isNotEmpty ?? false
|
||||
? Expanded(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: showControls,
|
||||
builder: (context, value, _) {
|
||||
return AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInQuad,
|
||||
opacity: value ? 1 : 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||
child: Text(
|
||||
file.caption!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: getEnteTextTheme(context)
|
||||
.mini
|
||||
.copyWith(color: textBaseDark),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
return Platform.isAndroid && !selectedPreview
|
||||
? Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: showControls,
|
||||
builder: (context, value, _) {
|
||||
return IgnorePointer(
|
||||
ignoring: !value,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInQuad,
|
||||
opacity: value ? 1 : 0,
|
||||
child: ElTooltip(
|
||||
padding: const EdgeInsets.all(12),
|
||||
distance: 4,
|
||||
controller: elTooltipController,
|
||||
content: GestureDetector(
|
||||
onLongPress: () {
|
||||
Bus.instance.fire(
|
||||
UseMediaKitForVideo(),
|
||||
);
|
||||
HapticFeedback.vibrate();
|
||||
elTooltipController.hide();
|
||||
},
|
||||
child: Text(S.of(context).useDifferentPlayerInfo),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Platform.isAndroid && !selectedPreview
|
||||
? ValueListenableBuilder(
|
||||
valueListenable: showControls,
|
||||
builder: (context, value, _) {
|
||||
return IgnorePointer(
|
||||
ignoring: !value,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInQuad,
|
||||
opacity: value ? 1 : 0,
|
||||
child: ElTooltip(
|
||||
padding: const EdgeInsets.all(12),
|
||||
distance: 4,
|
||||
controller: elTooltipController,
|
||||
content: GestureDetector(
|
||||
onLongPress: () {
|
||||
Bus.instance.fire(
|
||||
UseMediaKitForVideo(),
|
||||
);
|
||||
HapticFeedback.vibrate();
|
||||
position: ElTooltipPosition.topEnd,
|
||||
color: backgroundElevatedDark,
|
||||
appearAnimationDuration: const Duration(
|
||||
milliseconds: 200,
|
||||
),
|
||||
disappearAnimationDuration: const Duration(
|
||||
milliseconds: 200,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (elTooltipController.value ==
|
||||
ElTooltipStatus.hidden) {
|
||||
elTooltipController.show();
|
||||
} else {
|
||||
elTooltipController.hide();
|
||||
},
|
||||
child: Text(S.of(context).useDifferentPlayerInfo),
|
||||
),
|
||||
position: ElTooltipPosition.topEnd,
|
||||
color: backgroundElevatedDark,
|
||||
appearAnimationDuration: const Duration(
|
||||
milliseconds: 200,
|
||||
),
|
||||
disappearAnimationDuration: const Duration(
|
||||
milliseconds: 200,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (elTooltipController.value ==
|
||||
ElTooltipStatus.hidden) {
|
||||
elTooltipController.show();
|
||||
} else {
|
||||
elTooltipController.hide();
|
||||
}
|
||||
controller?.pause();
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onLongPress: () {
|
||||
Bus.instance.fire(
|
||||
UseMediaKitForVideo(),
|
||||
);
|
||||
HapticFeedback.vibrate();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 0, 4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.play_arrow_outlined,
|
||||
size: 24,
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
Icon(
|
||||
Icons.question_mark_rounded,
|
||||
size: 10,
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
}
|
||||
controller?.pause();
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onLongPress: () {
|
||||
Bus.instance.fire(
|
||||
UseMediaKitForVideo(),
|
||||
);
|
||||
HapticFeedback.vibrate();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 0, 4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.play_arrow_outlined,
|
||||
size: 24,
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
Icon(
|
||||
Icons.question_mark_rounded,
|
||||
size: 10,
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
);
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,14 @@ import 'package:photos/core/cache/thumbnail_in_memory_cache.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import "package:photos/events/file_caption_updated_event.dart";
|
||||
import "package:photos/events/files_updated_event.dart";
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import "package:photos/models/file/extensions/file_props.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/states/detail_page_state.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/actions/file/file_actions.dart";
|
||||
import 'package:photos/ui/common/loading_widget.dart';
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
@@ -55,6 +59,8 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
bool _isZooming = false;
|
||||
PhotoViewController _photoViewController = PhotoViewController();
|
||||
final _scaleStateController = PhotoViewScaleStateController();
|
||||
late final StreamSubscription<FileCaptionUpdatedEvent>
|
||||
_captionUpdatedSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -70,12 +76,22 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
debugPrint("isZooming = $_isZooming, currentState $value");
|
||||
// _logger.info('is reakky zooming $_isZooming with state $value');
|
||||
};
|
||||
|
||||
_captionUpdatedSubscription =
|
||||
Bus.instance.on<FileCaptionUpdatedEvent>().listen((event) {
|
||||
if (event.fileGeneratedID == _photo.generatedID) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_photoViewController.dispose();
|
||||
_scaleStateController.dispose();
|
||||
_captionUpdatedSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -167,7 +183,68 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
};
|
||||
return GestureDetector(
|
||||
onVerticalDragUpdate: verticalDragCallback,
|
||||
child: content,
|
||||
child: widget.photo.caption?.isNotEmpty ?? false
|
||||
? Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
content,
|
||||
Positioned(
|
||||
bottom: 72 + MediaQuery.paddingOf(context).bottom,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: ValueListenableBuilder<bool>(
|
||||
valueListenable: InheritedDetailPageState.of(context)
|
||||
.enableFullScreenNotifier,
|
||||
builder: (context, doNotShowCaption, _) {
|
||||
return AnimatedOpacity(
|
||||
opacity: doNotShowCaption ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: IgnorePointer(
|
||||
ignoring: doNotShowCaption,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showDetailsSheet(context, widget.photo);
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: SizedBox(
|
||||
width:
|
||||
MediaQuery.sizeOf(context).width - 16,
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.photo.caption!,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: getEnteTextTheme(context)
|
||||
.mini
|
||||
.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: content,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -237,11 +237,9 @@ Future<bool> editFileCaption(
|
||||
caption,
|
||||
showDoneToast: false,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (context != null) {
|
||||
showShortToast(context, S.of(context).somethingWentWrong);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user