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

View file

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

View file

@ -47,6 +47,7 @@ extension ExtraEntryActionView on EntryAction {
EntryAction.open || EntryAction.openVideo => l10n.entryActionOpen,
EntryAction.openMap => l10n.entryActionOpenMap,
EntryAction.setAs => l10n.entryActionSetAs,
EntryAction.cast => l10n.entryActionCast,
// platform
EntryAction.rotateScreen => l10n.entryActionRotateScreen,
// metadata
@ -120,6 +121,7 @@ extension ExtraEntryActionView on EntryAction {
EntryAction.open || EntryAction.openVideo => AIcons.openOutside,
EntryAction.openMap => AIcons.map,
EntryAction.setAs => AIcons.setAs,
EntryAction.cast => AIcons.cast,
// platform
EntryAction.rotateScreen => AIcons.rotateScreen,
// 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.open:
case EntryAction.setAs:
case EntryAction.cast:
return !settings.useTvLayout;
case EntryAction.info:
case EntryAction.share:
@ -256,6 +257,8 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
appService.setAs(targetEntry.uri, targetEntry.mimeType).then((success) {
if (!success) showNoMatchingAppDialog(context);
});
case EntryAction.cast:
const CastNotification(true).dispatch(context);
// platform
case EntryAction.rotateScreen:
_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/theme/durations.dart';
import 'package:aves/widgets/viewer/controls/cast.dart';
import 'package:aves/widgets/viewer/controls/events.dart';
import 'package:aves_magnifier/aves_magnifier.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
class ViewerController {
class ViewerController with CastMixin {
final ValueNotifier<AvesEntry?> entryNotifier = ValueNotifier(null);
final ViewerTransition transition;
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
class FilterSelectedNotification extends Notification with EquatableMixin {
final CollectionFilter filter;

View file

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

View file

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

View file

@ -302,6 +302,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@ -920,6 +928,22 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -1314,7 +1338,7 @@ packages:
source: hosted
version: "2.3.2"
shelf:
dependency: transitive
dependency: "direct main"
description:
name: shelf
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4

View file

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

View file

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