poc with dlna_dart / shelf

This commit is contained in:
Thibault Deckers 2023-10-27 15:51:24 +03:00
parent d19b9ad4c6
commit 59582483e3
13 changed files with 322 additions and 2 deletions

View file

@ -112,6 +112,7 @@
"entryActionEdit": "Edit", "entryActionEdit": "Edit",
"entryActionOpen": "Open with", "entryActionOpen": "Open with",
"entryActionSetAs": "Set as", "entryActionSetAs": "Set as",
"entryActionCast": "Cast",
"entryActionOpenMap": "Show in map app", "entryActionOpenMap": "Show in map app",
"entryActionRotateScreen": "Rotate screen", "entryActionRotateScreen": "Rotate screen",
"entryActionAddFavourite": "Add to favorites", "entryActionAddFavourite": "Add to favorites",
@ -518,6 +519,8 @@
"tileLayoutGrid": "Grid", "tileLayoutGrid": "Grid",
"tileLayoutList": "List", "tileLayoutList": "List",
"castDialogTitle": "Cast Devices",
"coverDialogTabCover": "Cover", "coverDialogTabCover": "Cover",
"coverDialogTabApp": "App", "coverDialogTabApp": "App",
"coverDialogTabColor": "Color", "coverDialogTabColor": "Color",

View file

@ -76,6 +76,7 @@ class AIcons {
static const addShortcut = Icons.add_to_home_screen_outlined; static const addShortcut = Icons.add_to_home_screen_outlined;
static const cancel = Icons.cancel_outlined; static const cancel = Icons.cancel_outlined;
static const captureFrame = Icons.screenshot_outlined; static const captureFrame = Icons.screenshot_outlined;
static const cast = Icons.cast_outlined;
static const clear = Icons.clear_outlined; static const clear = Icons.clear_outlined;
static const clipboard = Icons.content_copy_outlined; static const clipboard = Icons.content_copy_outlined;
static const convert = Icons.transform_outlined; static const convert = Icons.transform_outlined;

View file

@ -47,6 +47,7 @@ extension ExtraEntryActionView on EntryAction {
EntryAction.open || EntryAction.openVideo => l10n.entryActionOpen, EntryAction.open || EntryAction.openVideo => l10n.entryActionOpen,
EntryAction.openMap => l10n.entryActionOpenMap, EntryAction.openMap => l10n.entryActionOpenMap,
EntryAction.setAs => l10n.entryActionSetAs, EntryAction.setAs => l10n.entryActionSetAs,
EntryAction.cast => l10n.entryActionCast,
// platform // platform
EntryAction.rotateScreen => l10n.entryActionRotateScreen, EntryAction.rotateScreen => l10n.entryActionRotateScreen,
// metadata // metadata
@ -120,6 +121,7 @@ extension ExtraEntryActionView on EntryAction {
EntryAction.open || EntryAction.openVideo => AIcons.openOutside, EntryAction.open || EntryAction.openVideo => AIcons.openOutside,
EntryAction.openMap => AIcons.map, EntryAction.openMap => AIcons.map,
EntryAction.setAs => AIcons.setAs, EntryAction.setAs => AIcons.setAs,
EntryAction.cast => AIcons.cast,
// platform // platform
EntryAction.rotateScreen => AIcons.rotateScreen, EntryAction.rotateScreen => AIcons.rotateScreen,
// metadata // metadata

View file

@ -0,0 +1,61 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:dlna_dart/dlna.dart';
import 'package:flutter/material.dart';
class CastDialog extends StatefulWidget {
static const routeName = '/dialog/cast';
const CastDialog({super.key});
@override
State<CastDialog> createState() => _CastDialogState();
}
class _CastDialogState extends State<CastDialog> {
final DLNAManager _dlnaManager = DLNAManager();
final Map<String, DLNADevice> _seenRenderers = {};
static const String upnpDeviceTypeMediaRenderer = 'urn:schemas-upnp-org:device:MediaRenderer:1';
@override
void initState() {
super.initState();
_dlnaManager.start().then((deviceManager) {
deviceManager.devices.stream.listen((devices) {
_seenRenderers.addAll(Map.fromEntries(devices.entries.where((kv) => kv.value.info.deviceType == upnpDeviceTypeMediaRenderer)));
setState(() {});
});
});
}
@override
void dispose() {
_dlnaManager.stop();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AvesDialog(
title: context.l10n.castDialogTitle,
scrollableContent: [
if (_seenRenderers.isEmpty)
const Padding(
padding: EdgeInsets.all(16),
child: Center(
child: CircularProgressIndicator(),
),
),
..._seenRenderers.values.map((dev) => ListTile(
title: Text(dev.info.friendlyName),
onTap: () => Navigator.maybeOf(context)?.pop<DLNADevice>(dev),
)),
],
actions: const [
CancelButton(),
],
);
}
}

View file

@ -108,6 +108,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
case EntryAction.copyToClipboard: case EntryAction.copyToClipboard:
case EntryAction.open: case EntryAction.open:
case EntryAction.setAs: case EntryAction.setAs:
case EntryAction.cast:
return !settings.useTvLayout; return !settings.useTvLayout;
case EntryAction.info: case EntryAction.info:
case EntryAction.share: case EntryAction.share:
@ -256,6 +257,8 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
appService.setAs(targetEntry.uri, targetEntry.mimeType).then((success) { appService.setAs(targetEntry.uri, targetEntry.mimeType).then((success) {
if (!success) showNoMatchingAppDialog(context); if (!success) showNoMatchingAppDialog(context);
}); });
case EntryAction.cast:
const CastNotification(true).dispatch(context);
// platform // platform
case EntryAction.rotateScreen: case EntryAction.rotateScreen:
_rotateScreen(context); _rotateScreen(context);

View file

@ -0,0 +1,103 @@
import 'dart:async';
import 'dart:io';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/dialogs/cast_dialog.dart';
import 'package:collection/collection.dart';
import 'package:dlna_dart/dlna.dart';
import 'package:dlna_dart/xmlParser.dart';
import 'package:flutter/material.dart';
import 'package:network_info_plus/network_info_plus.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
mixin CastMixin {
DLNADevice? _renderer;
HttpServer? _mediaServer;
bool get isCasting => _renderer != null && _mediaServer != null;
Future<void> initCast(BuildContext context, List<AvesEntry> entries) async {
await stopCast();
final renderer = await _selectRenderer(context);
_renderer = renderer;
if (renderer == null) return;
debugPrint('cast: select renderer `${renderer.info.friendlyName}` at ${renderer.info.URLBase}');
final ip = await NetworkInfo().getWifiIP();
if (ip == null) return;
final handler = const Pipeline().addHandler((request) async {
final id = int.tryParse(request.url.path);
if (id != null) {
final entry = entries.firstWhereOrNull((v) => v.id == id);
if (entry != null) {
final bytes = await mediaFetchService.getImage(
entry.uri,
entry.mimeType,
rotationDegrees: entry.rotationDegrees,
isFlipped: entry.isFlipped,
pageId: entry.pageId,
sizeBytes: entry.sizeBytes,
);
debugPrint('cast: send ${bytes.length} bytes for entry=$entry');
return Response.ok(
bytes,
headers: {
'Content-Type': entry.mimeType,
},
);
}
}
return Response.notFound('no resource for url=${request.url}');
});
_mediaServer = await shelf_io.serve(handler, ip, 8080);
debugPrint('cast: serving media on $_serverBaseUrl}');
}
Future<void> stopCast() async {
if (isCasting) {
debugPrint('cast: stop');
}
await _mediaServer?.close();
_mediaServer = null;
await _renderer?.stop();
_renderer = null;
}
Future<DLNADevice?> _selectRenderer(BuildContext context) async {
return await showDialog<DLNADevice?>(
context: context,
builder: (context) => const CastDialog(),
routeSettings: const RouteSettings(name: CastDialog.routeName),
);
}
Future<void> castEntry(AvesEntry entry) async {
final server = _mediaServer;
final renderer = _renderer;
if (server == null || renderer == null) return;
debugPrint('cast: set entry=$entry');
try {
await renderer.setUrl(
'$_serverBaseUrl/${entry.id}',
title: entry.bestTitle ?? '',
type: entry.isVideo ? PlayType.Video : PlayType.Image,
);
await renderer.play();
} catch (error, stack) {
await reportService.recordError(error, stack);
}
}
String? get _serverBaseUrl {
final server = _mediaServer;
return server != null ? 'http://${server.address.host}:${server.port}' : null;
}
}

View file

@ -3,13 +3,14 @@ import 'dart:math';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/viewer/controls/cast.dart';
import 'package:aves/widgets/viewer/controls/events.dart'; import 'package:aves/widgets/viewer/controls/events.dart';
import 'package:aves_magnifier/aves_magnifier.dart'; import 'package:aves_magnifier/aves_magnifier.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class ViewerController { class ViewerController with CastMixin {
final ValueNotifier<AvesEntry?> entryNotifier = ValueNotifier(null); final ValueNotifier<AvesEntry?> entryNotifier = ValueNotifier(null);
final ViewerTransition transition; final ViewerTransition transition;
final Duration? autopilotInterval; final Duration? autopilotInterval;

View file

@ -78,6 +78,16 @@ class VideoActionNotification extends Notification {
}); });
} }
@immutable
class CastNotification extends Notification with EquatableMixin {
final bool enabled;
@override
List<Object?> get props => [enabled];
const CastNotification(this.enabled);
}
@immutable @immutable
class FilterSelectedNotification extends Notification with EquatableMixin { class FilterSelectedNotification extends Notification with EquatableMixin {
final CollectionFilter filter; final CollectionFilter filter;

View file

@ -516,6 +516,8 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
bool _handleNotification(dynamic notification) { bool _handleNotification(dynamic notification) {
if (notification is FilterSelectedNotification) { if (notification is FilterSelectedNotification) {
_goToCollection(notification.filter); _goToCollection(notification.filter);
} else if (notification is CastNotification) {
_cast(notification.enabled);
} else if (notification is FullImageLoadedNotification) { } else if (notification is FullImageLoadedNotification) {
final viewStateController = context.read<ViewStateConductor>().getOrCreateController(notification.entry); final viewStateController = context.read<ViewStateConductor>().getOrCreateController(notification.entry);
// microtask so that listeners do not trigger during build // microtask so that listeners do not trigger during build
@ -581,6 +583,21 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
return true; return true;
} }
Future<void> _cast(bool enabled) async {
if (enabled) {
final entries = collection?.sortedEntries;
if (entries != null) {
await viewerController.initCast(context, entries);
final entry = entryNotifier.value;
if (entry != null) {
await viewerController.castEntry(entry);
}
}
} else {
await viewerController.stopCast();
}
}
Future<void> _onVideoAction({ Future<void> _onVideoAction({
required BuildContext context, required BuildContext context,
required AvesEntry entry, required AvesEntry entry,
@ -756,6 +773,13 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
_isEntryTracked = false; _isEntryTracked = false;
await pauseVideoControllers(); await pauseVideoControllers();
await initEntryControllers(newEntry); await initEntryControllers(newEntry);
if (viewerController.isCasting) {
final entry = entryNotifier.value;
if (entry != null) {
await viewerController.castEntry(entry);
}
}
} }
void _onWillPop() { void _onWillPop() {
@ -817,6 +841,8 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
// to be unmounted after the other async steps // to be unmounted after the other async steps
final theme = Theme.of(context); final theme = Theme.of(context);
await viewerController.stopCast();
switch (settings.maxBrightness) { switch (settings.maxBrightness) {
case MaxBrightness.never: case MaxBrightness.never:
case MaxBrightness.viewerOnly: case MaxBrightness.viewerOnly:

View file

@ -33,6 +33,7 @@ enum EntryAction {
openVideo, openVideo,
openMap, openMap,
setAs, setAs,
cast,
// platform // platform
rotateScreen, rotateScreen,
// metadata // metadata
@ -82,6 +83,7 @@ class EntryActions {
EntryAction.open, EntryAction.open,
EntryAction.openMap, EntryAction.openMap,
EntryAction.setAs, EntryAction.setAs,
EntryAction.cast,
]; ];
static const pageActions = { static const pageActions = {

View file

@ -302,6 +302,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0" version: "7.0.0"
dlna_dart:
dependency: "direct main"
description:
name: dlna_dart
sha256: ae07c1c53077bbf58756fa589f936968719b0085441981d33e74f82f89d1d281
url: "https://pub.dev"
source: hosted
version: "0.0.8"
dynamic_color: dynamic_color:
dependency: "direct main" dependency: "direct main"
description: description:
@ -920,6 +928,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
network_info_plus:
dependency: "direct main"
description:
name: network_info_plus
sha256: "2d9e88b9a459e5d4e224f828d26cc38ea140511e89b943116939994324be5c96"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
network_info_plus_platform_interface:
dependency: transitive
description:
name: network_info_plus_platform_interface
sha256: "881f5029c5edaf19c616c201d3d8b366c5b1384afd5c1da5a49e4345de82fb8b"
url: "https://pub.dev"
source: hosted
version: "1.1.3"
nm: nm:
dependency: transitive dependency: transitive
description: description:
@ -1314,7 +1338,7 @@ packages:
source: hosted source: hosted
version: "2.3.2" version: "2.3.2"
shelf: shelf:
dependency: transitive dependency: "direct main"
description: description:
name: shelf name: shelf
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4

View file

@ -117,6 +117,10 @@ dependencies:
volume_controller: volume_controller:
xml: xml:
dlna_dart:
network_info_plus:
shelf:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter

View file

@ -51,6 +51,7 @@
"entryActionEdit", "entryActionEdit",
"entryActionOpen", "entryActionOpen",
"entryActionSetAs", "entryActionSetAs",
"entryActionCast",
"entryActionOpenMap", "entryActionOpenMap",
"entryActionRotateScreen", "entryActionRotateScreen",
"entryActionAddFavourite", "entryActionAddFavourite",
@ -286,6 +287,7 @@
"tileLayoutMosaic", "tileLayoutMosaic",
"tileLayoutGrid", "tileLayoutGrid",
"tileLayoutList", "tileLayoutList",
"castDialogTitle",
"coverDialogTabCover", "coverDialogTabCover",
"coverDialogTabApp", "coverDialogTabApp",
"coverDialogTabColor", "coverDialogTabColor",
@ -644,6 +646,7 @@
], ],
"be": [ "be": [
"entryActionCast",
"binEntriesConfirmationDialogMessage", "binEntriesConfirmationDialogMessage",
"deleteEntriesConfirmationDialogMessage", "deleteEntriesConfirmationDialogMessage",
"setCoverDialogCustom", "setCoverDialogCustom",
@ -712,6 +715,7 @@
"tileLayoutMosaic", "tileLayoutMosaic",
"tileLayoutGrid", "tileLayoutGrid",
"tileLayoutList", "tileLayoutList",
"castDialogTitle",
"coverDialogTabCover", "coverDialogTabCover",
"coverDialogTabApp", "coverDialogTabApp",
"coverDialogTabColor", "coverDialogTabColor",
@ -1077,6 +1081,7 @@
"entryActionEdit", "entryActionEdit",
"entryActionOpen", "entryActionOpen",
"entryActionSetAs", "entryActionSetAs",
"entryActionCast",
"entryActionOpenMap", "entryActionOpenMap",
"entryActionRotateScreen", "entryActionRotateScreen",
"entryActionAddFavourite", "entryActionAddFavourite",
@ -1312,6 +1317,7 @@
"tileLayoutMosaic", "tileLayoutMosaic",
"tileLayoutGrid", "tileLayoutGrid",
"tileLayoutList", "tileLayoutList",
"castDialogTitle",
"coverDialogTabCover", "coverDialogTabCover",
"coverDialogTabApp", "coverDialogTabApp",
"coverDialogTabColor", "coverDialogTabColor",
@ -1689,6 +1695,7 @@
"entryActionFlip", "entryActionFlip",
"entryActionShowGeoTiffOnMap", "entryActionShowGeoTiffOnMap",
"entryActionConvertMotionPhotoToStillImage", "entryActionConvertMotionPhotoToStillImage",
"entryActionCast",
"videoActionCaptureFrame", "videoActionCaptureFrame",
"videoActionSelectStreams", "videoActionSelectStreams",
"viewerActionLock", "viewerActionLock",
@ -1846,6 +1853,7 @@
"tileLayoutMosaic", "tileLayoutMosaic",
"tileLayoutGrid", "tileLayoutGrid",
"tileLayoutList", "tileLayoutList",
"castDialogTitle",
"coverDialogTabCover", "coverDialogTabCover",
"coverDialogTabApp", "coverDialogTabApp",
"coverDialogTabColor", "coverDialogTabColor",
@ -2218,22 +2226,28 @@
], ],
"cs": [ "cs": [
"entryActionCast",
"overlayHistogramLuminance", "overlayHistogramLuminance",
"castDialogTitle",
"aboutDataUsageClearCache" "aboutDataUsageClearCache"
], ],
"de": [ "de": [
"entryActionCast",
"overlayHistogramNone", "overlayHistogramNone",
"overlayHistogramRGB", "overlayHistogramRGB",
"overlayHistogramLuminance", "overlayHistogramLuminance",
"castDialogTitle",
"aboutDataUsageClearCache", "aboutDataUsageClearCache",
"settingsViewerShowHistogram" "settingsViewerShowHistogram"
], ],
"el": [ "el": [
"entryActionCast",
"overlayHistogramNone", "overlayHistogramNone",
"overlayHistogramRGB", "overlayHistogramRGB",
"overlayHistogramLuminance", "overlayHistogramLuminance",
"castDialogTitle",
"aboutDataUsageSectionTitle", "aboutDataUsageSectionTitle",
"aboutDataUsageData", "aboutDataUsageData",
"aboutDataUsageCache", "aboutDataUsageCache",
@ -2246,10 +2260,14 @@
], ],
"es": [ "es": [
"entryActionCast",
"castDialogTitle",
"aboutDataUsageClearCache" "aboutDataUsageClearCache"
], ],
"eu": [ "eu": [
"entryActionCast",
"castDialogTitle",
"aboutDataUsageClearCache" "aboutDataUsageClearCache"
], ],
@ -2262,6 +2280,7 @@
"chipActionShowCountryStates", "chipActionShowCountryStates",
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"entryActionCast",
"videoActionPause", "videoActionPause",
"videoActionPlay", "videoActionPlay",
"videoActionSelectStreams", "videoActionSelectStreams",
@ -2423,6 +2442,7 @@
"tileLayoutMosaic", "tileLayoutMosaic",
"tileLayoutGrid", "tileLayoutGrid",
"tileLayoutList", "tileLayoutList",
"castDialogTitle",
"coverDialogTabCover", "coverDialogTabCover",
"appPickDialogNone", "appPickDialogNone",
"aboutLinkLicense", "aboutLinkLicense",
@ -2774,6 +2794,7 @@
"clearTooltip", "clearTooltip",
"chipActionFilterIn", "chipActionFilterIn",
"entryActionSetAs", "entryActionSetAs",
"entryActionCast",
"videoActionUnmute", "videoActionUnmute",
"videoActionSelectStreams", "videoActionSelectStreams",
"filterTypeRawLabel", "filterTypeRawLabel",
@ -2937,6 +2958,7 @@
"tileLayoutMosaic", "tileLayoutMosaic",
"tileLayoutGrid", "tileLayoutGrid",
"tileLayoutList", "tileLayoutList",
"castDialogTitle",
"coverDialogTabCover", "coverDialogTabCover",
"coverDialogTabApp", "coverDialogTabApp",
"coverDialogTabColor", "coverDialogTabColor",
@ -3309,6 +3331,8 @@
], ],
"fr": [ "fr": [
"entryActionCast",
"castDialogTitle",
"aboutDataUsageClearCache" "aboutDataUsageClearCache"
], ],
@ -3323,6 +3347,7 @@
"chipActionConfigureVault", "chipActionConfigureVault",
"entryActionShareImageOnly", "entryActionShareImageOnly",
"entryActionShareVideoOnly", "entryActionShareVideoOnly",
"entryActionCast",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
"entryInfoActionExportMetadata", "entryInfoActionExportMetadata",
@ -3489,6 +3514,7 @@
"tileLayoutMosaic", "tileLayoutMosaic",
"tileLayoutGrid", "tileLayoutGrid",
"tileLayoutList", "tileLayoutList",
"castDialogTitle",
"coverDialogTabCover", "coverDialogTabCover",
"coverDialogTabApp", "coverDialogTabApp",
"coverDialogTabColor", "coverDialogTabColor",
@ -3930,6 +3956,7 @@
"entryActionEdit", "entryActionEdit",
"entryActionOpen", "entryActionOpen",
"entryActionSetAs", "entryActionSetAs",
"entryActionCast",
"entryActionOpenMap", "entryActionOpenMap",
"entryActionRotateScreen", "entryActionRotateScreen",
"entryActionAddFavourite", "entryActionAddFavourite",
@ -4165,6 +4192,7 @@
"tileLayoutMosaic", "tileLayoutMosaic",
"tileLayoutGrid", "tileLayoutGrid",
"tileLayoutList", "tileLayoutList",
"castDialogTitle",
"coverDialogTabCover", "coverDialogTabCover",
"coverDialogTabApp", "coverDialogTabApp",
"coverDialogTabColor", "coverDialogTabColor",
@ -4586,6 +4614,7 @@
"entryActionEdit", "entryActionEdit",
"entryActionOpen", "entryActionOpen",
"entryActionSetAs", "entryActionSetAs",
"entryActionCast",
"entryActionOpenMap", "entryActionOpenMap",
"entryActionRotateScreen", "entryActionRotateScreen",
"entryActionAddFavourite", "entryActionAddFavourite",
@ -4821,6 +4850,7 @@
"tileLayoutMosaic", "tileLayoutMosaic",
"tileLayoutGrid", "tileLayoutGrid",
"tileLayoutList", "tileLayoutList",
"castDialogTitle",
"coverDialogTabCover", "coverDialogTabCover",
"coverDialogTabApp", "coverDialogTabApp",
"coverDialogTabColor", "coverDialogTabColor",
@ -5193,14 +5223,20 @@
], ],
"hu": [ "hu": [
"entryActionCast",
"castDialogTitle",
"aboutDataUsageClearCache" "aboutDataUsageClearCache"
], ],
"id": [ "id": [
"entryActionCast",
"castDialogTitle",
"aboutDataUsageClearCache" "aboutDataUsageClearCache"
], ],
"it": [ "it": [
"entryActionCast",
"castDialogTitle",
"aboutDataUsageClearCache" "aboutDataUsageClearCache"
], ],
@ -5211,6 +5247,7 @@
"chipActionShowCountryStates", "chipActionShowCountryStates",
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"entryActionCast",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
"editorActionTransform", "editorActionTransform",
@ -5234,6 +5271,7 @@
"vaultBinUsageDialogMessage", "vaultBinUsageDialogMessage",
"exportEntryDialogQuality", "exportEntryDialogQuality",
"exportEntryDialogWriteMetadata", "exportEntryDialogWriteMetadata",
"castDialogTitle",
"aboutDataUsageSectionTitle", "aboutDataUsageSectionTitle",
"aboutDataUsageData", "aboutDataUsageData",
"aboutDataUsageCache", "aboutDataUsageCache",
@ -5310,6 +5348,7 @@
"entryActionEdit", "entryActionEdit",
"entryActionOpen", "entryActionOpen",
"entryActionSetAs", "entryActionSetAs",
"entryActionCast",
"entryActionOpenMap", "entryActionOpenMap",
"entryActionRotateScreen", "entryActionRotateScreen",
"entryActionAddFavourite", "entryActionAddFavourite",
@ -5545,6 +5584,7 @@
"tileLayoutMosaic", "tileLayoutMosaic",
"tileLayoutGrid", "tileLayoutGrid",
"tileLayoutList", "tileLayoutList",
"castDialogTitle",
"coverDialogTabCover", "coverDialogTabCover",
"coverDialogTabApp", "coverDialogTabApp",
"coverDialogTabColor", "coverDialogTabColor",
@ -5917,6 +5957,8 @@
], ],
"ko": [ "ko": [
"entryActionCast",
"castDialogTitle",
"aboutDataUsageClearCache" "aboutDataUsageClearCache"
], ],
@ -5929,6 +5971,7 @@
"chipActionShowCountryStates", "chipActionShowCountryStates",
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"entryActionCast",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
"editorActionTransform", "editorActionTransform",
@ -5972,6 +6015,7 @@
"exportEntryDialogQuality", "exportEntryDialogQuality",
"exportEntryDialogWriteMetadata", "exportEntryDialogWriteMetadata",
"tooManyItemsErrorDialogMessage", "tooManyItemsErrorDialogMessage",
"castDialogTitle",
"aboutDataUsageSectionTitle", "aboutDataUsageSectionTitle",
"aboutDataUsageData", "aboutDataUsageData",
"aboutDataUsageCache", "aboutDataUsageCache",
@ -6082,6 +6126,7 @@
"entryActionEdit", "entryActionEdit",
"entryActionOpen", "entryActionOpen",
"entryActionSetAs", "entryActionSetAs",
"entryActionCast",
"entryActionOpenMap", "entryActionOpenMap",
"entryActionRotateScreen", "entryActionRotateScreen",
"entryActionAddFavourite", "entryActionAddFavourite",
@ -6317,6 +6362,7 @@
"tileLayoutMosaic", "tileLayoutMosaic",
"tileLayoutGrid", "tileLayoutGrid",
"tileLayoutList", "tileLayoutList",
"castDialogTitle",
"coverDialogTabCover", "coverDialogTabCover",
"coverDialogTabApp", "coverDialogTabApp",
"coverDialogTabColor", "coverDialogTabColor",
@ -6689,6 +6735,7 @@
], ],
"my": [ "my": [
"entryActionCast",
"accessibilityAnimationsRemove", "accessibilityAnimationsRemove",
"accessibilityAnimationsKeep", "accessibilityAnimationsKeep",
"overlayHistogramLuminance", "overlayHistogramLuminance",
@ -6696,6 +6743,7 @@
"widgetOpenPageCollection", "widgetOpenPageCollection",
"widgetOpenPageViewer", "widgetOpenPageViewer",
"menuActionConfigureView", "menuActionConfigureView",
"castDialogTitle",
"aboutDataUsageClearCache", "aboutDataUsageClearCache",
"newFilterBanner", "newFilterBanner",
"settingsDefault", "settingsDefault",
@ -6799,6 +6847,7 @@
], ],
"nb": [ "nb": [
"entryActionCast",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
"editorActionTransform", "editorActionTransform",
@ -6810,6 +6859,7 @@
"settingsVideoEnablePip", "settingsVideoEnablePip",
"widgetTapUpdateWidget", "widgetTapUpdateWidget",
"patternDialogEnter", "patternDialogEnter",
"castDialogTitle",
"aboutDataUsageInternal", "aboutDataUsageInternal",
"aboutDataUsageExternal", "aboutDataUsageExternal",
"aboutDataUsageClearCache", "aboutDataUsageClearCache",
@ -6826,6 +6876,7 @@
"chipActionShowCountryStates", "chipActionShowCountryStates",
"entryActionShareImageOnly", "entryActionShareImageOnly",
"entryActionShareVideoOnly", "entryActionShareVideoOnly",
"entryActionCast",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
"editorActionTransform", "editorActionTransform",
@ -6864,6 +6915,7 @@
"exportEntryDialogQuality", "exportEntryDialogQuality",
"exportEntryDialogWriteMetadata", "exportEntryDialogWriteMetadata",
"tooManyItemsErrorDialogMessage", "tooManyItemsErrorDialogMessage",
"castDialogTitle",
"aboutDataUsageSectionTitle", "aboutDataUsageSectionTitle",
"aboutDataUsageData", "aboutDataUsageData",
"aboutDataUsageCache", "aboutDataUsageCache",
@ -6905,6 +6957,7 @@
"nn": [ "nn": [
"sourceStateCataloguing", "sourceStateCataloguing",
"entryActionCast",
"accessibilityAnimationsKeep", "accessibilityAnimationsKeep",
"overlayHistogramNone", "overlayHistogramNone",
"overlayHistogramRGB", "overlayHistogramRGB",
@ -6915,6 +6968,7 @@
"authenticateToUnlockVault", "authenticateToUnlockVault",
"viewDialogSortSectionTitle", "viewDialogSortSectionTitle",
"viewDialogReverseSortOrder", "viewDialogReverseSortOrder",
"castDialogTitle",
"aboutDataUsageSectionTitle", "aboutDataUsageSectionTitle",
"aboutDataUsageData", "aboutDataUsageData",
"aboutDataUsageCache", "aboutDataUsageCache",
@ -7006,6 +7060,7 @@
"entryActionViewMotionPhotoVideo", "entryActionViewMotionPhotoVideo",
"entryActionOpen", "entryActionOpen",
"entryActionSetAs", "entryActionSetAs",
"entryActionCast",
"entryActionOpenMap", "entryActionOpenMap",
"entryActionRotateScreen", "entryActionRotateScreen",
"entryActionAddFavourite", "entryActionAddFavourite",
@ -7213,6 +7268,7 @@
"tileLayoutMosaic", "tileLayoutMosaic",
"tileLayoutGrid", "tileLayoutGrid",
"tileLayoutList", "tileLayoutList",
"castDialogTitle",
"coverDialogTabCover", "coverDialogTabCover",
"appPickDialogTitle", "appPickDialogTitle",
"appPickDialogNone", "appPickDialogNone",
@ -7552,16 +7608,21 @@
], ],
"pl": [ "pl": [
"entryActionCast",
"castDialogTitle",
"aboutDataUsageClearCache" "aboutDataUsageClearCache"
], ],
"pt": [ "pt": [
"entryActionCast",
"castDialogTitle",
"aboutDataUsageClearCache" "aboutDataUsageClearCache"
], ],
"ro": [ "ro": [
"saveCopyButtonLabel", "saveCopyButtonLabel",
"applyTooltip", "applyTooltip",
"entryActionCast",
"editorActionTransform", "editorActionTransform",
"editorTransformCrop", "editorTransformCrop",
"editorTransformRotate", "editorTransformRotate",
@ -7577,6 +7638,7 @@
"videoResumptionModeAlways", "videoResumptionModeAlways",
"widgetTapUpdateWidget", "widgetTapUpdateWidget",
"exportEntryDialogQuality", "exportEntryDialogQuality",
"castDialogTitle",
"aboutDataUsageSectionTitle", "aboutDataUsageSectionTitle",
"aboutDataUsageData", "aboutDataUsageData",
"aboutDataUsageCache", "aboutDataUsageCache",
@ -7595,10 +7657,14 @@
], ],
"ru": [ "ru": [
"entryActionCast",
"castDialogTitle",
"aboutDataUsageClearCache" "aboutDataUsageClearCache"
], ],
"sk": [ "sk": [
"entryActionCast",
"castDialogTitle",
"aboutDataUsageClearCache" "aboutDataUsageClearCache"
], ],
@ -7672,6 +7738,7 @@
"entryActionEdit", "entryActionEdit",
"entryActionOpen", "entryActionOpen",
"entryActionSetAs", "entryActionSetAs",
"entryActionCast",
"entryActionOpenMap", "entryActionOpenMap",
"entryActionRotateScreen", "entryActionRotateScreen",
"entryActionAddFavourite", "entryActionAddFavourite",
@ -7907,6 +7974,7 @@
"tileLayoutMosaic", "tileLayoutMosaic",
"tileLayoutGrid", "tileLayoutGrid",
"tileLayoutList", "tileLayoutList",
"castDialogTitle",
"coverDialogTabCover", "coverDialogTabCover",
"coverDialogTabApp", "coverDialogTabApp",
"coverDialogTabColor", "coverDialogTabColor",
@ -8293,6 +8361,7 @@
"chipActionShowCountryStates", "chipActionShowCountryStates",
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"entryActionCast",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
"editorActionTransform", "editorActionTransform",
@ -8336,6 +8405,7 @@
"editEntryDateDialogShift", "editEntryDateDialogShift",
"removeEntryMetadataDialogTitle", "removeEntryMetadataDialogTitle",
"tooManyItemsErrorDialogMessage", "tooManyItemsErrorDialogMessage",
"castDialogTitle",
"aboutDataUsageSectionTitle", "aboutDataUsageSectionTitle",
"aboutDataUsageData", "aboutDataUsageData",
"aboutDataUsageCache", "aboutDataUsageCache",
@ -8686,6 +8756,7 @@
"chipActionShowCountryStates", "chipActionShowCountryStates",
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"entryActionCast",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
"editorActionTransform", "editorActionTransform",
@ -8725,6 +8796,7 @@
"vaultBinUsageDialogMessage", "vaultBinUsageDialogMessage",
"exportEntryDialogQuality", "exportEntryDialogQuality",
"exportEntryDialogWriteMetadata", "exportEntryDialogWriteMetadata",
"castDialogTitle",
"aboutDataUsageSectionTitle", "aboutDataUsageSectionTitle",
"aboutDataUsageData", "aboutDataUsageData",
"aboutDataUsageCache", "aboutDataUsageCache",
@ -8757,16 +8829,21 @@
], ],
"uk": [ "uk": [
"entryActionCast",
"castDialogTitle",
"aboutDataUsageClearCache" "aboutDataUsageClearCache"
], ],
"vi": [ "vi": [
"entryActionCast",
"castDialogTitle",
"aboutDataUsageClearCache" "aboutDataUsageClearCache"
], ],
"zh": [ "zh": [
"saveCopyButtonLabel", "saveCopyButtonLabel",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"entryActionCast",
"editorTransformCrop", "editorTransformCrop",
"cropAspectRatioFree", "cropAspectRatioFree",
"cropAspectRatioOriginal", "cropAspectRatioOriginal",
@ -8798,6 +8875,7 @@
"exportEntryDialogQuality", "exportEntryDialogQuality",
"exportEntryDialogWriteMetadata", "exportEntryDialogWriteMetadata",
"tooManyItemsErrorDialogMessage", "tooManyItemsErrorDialogMessage",
"castDialogTitle",
"aboutDataUsageSectionTitle", "aboutDataUsageSectionTitle",
"aboutDataUsageData", "aboutDataUsageData",
"aboutDataUsageCache", "aboutDataUsageCache",
@ -8833,8 +8911,10 @@
], ],
"zh_Hant": [ "zh_Hant": [
"entryActionCast",
"overlayHistogramNone", "overlayHistogramNone",
"overlayHistogramLuminance", "overlayHistogramLuminance",
"castDialogTitle",
"aboutDataUsageSectionTitle", "aboutDataUsageSectionTitle",
"aboutDataUsageData", "aboutDataUsageData",
"aboutDataUsageCache", "aboutDataUsageCache",