diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fbb12dc1..a83e49cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file. ## [v1.8.9] - 2023-06-04 +### Added + +- About: data usage + ### Changed - target Android 14 (API 34) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt index 1dcc4bd32..ed5d4a2ad 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt @@ -9,11 +9,13 @@ import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.PermissionManager import deckers.thibault.aves.utils.StorageUtils +import deckers.thibault.aves.utils.StorageUtils.getFolderSize import deckers.thibault.aves.utils.StorageUtils.getPrimaryVolumePath import deckers.thibault.aves.utils.StorageUtils.getVolumePaths import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.util.PathUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -25,6 +27,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { + "getDataUsage" -> ioScope.launch { safe(call, result, ::getDataUsage) } "getStorageVolumes" -> ioScope.launch { safe(call, result, ::getStorageVolumes) } "getVaultRoot" -> ioScope.launch { safe(call, result, ::getVaultRoot) } "getFreeSpace" -> ioScope.launch { safe(call, result, ::getFreeSpace) } @@ -39,6 +42,37 @@ class StorageHandler(private val context: Context) : MethodCallHandler { } } + private fun getDataUsage(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + var internalCache = getFolderSize(context.cacheDir) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + internalCache += getFolderSize(context.codeCacheDir) + } + val externalCache = context.externalCacheDirs.map(::getFolderSize).sum() + + val dataDir = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) context.dataDir else File(context.applicationInfo.dataDir) + + val database = getFolderSize(File(dataDir, "databases")) + val flutter = getFolderSize(File(PathUtils.getDataDirectory(context))) + val vaults = getFolderSize(File(StorageUtils.getVaultRoot(context))) + val trash = context.getExternalFilesDirs(null).mapNotNull { StorageUtils.trashDirFor(context, it.path) }.map(::getFolderSize).sum() + + val internalData = getFolderSize(dataDir) - internalCache + val externalData = context.getExternalFilesDirs(null).map(::getFolderSize).sum() + val miscData = internalData + externalData - (database + flutter + vaults + trash) + + result.success( + hashMapOf( + "database" to database, + "flutter" to flutter, + "vaults" to vaults, + "trash" to trash, + "miscData" to miscData, + "internalCache" to internalCache, + "externalCache" to externalCache, + ) + ) + } + private fun getStorageVolumes(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { val volumes = ArrayList>() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt index f1385bdd9..0675a30f6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt @@ -716,6 +716,18 @@ object StorageUtils { // convenience methods + fun getFolderSize(f: File): Long { + var size: Long = 0 + if (f.isDirectory) { + for (file in f.listFiles()!!) { + size += getFolderSize(file) + } + } else { + size = f.length() + } + return size + } + fun ensureTrailingSeparator(dirPath: String): String { return if (dirPath.endsWith(File.separator)) dirPath else dirPath + File.separator } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bcd049bbd..486b494e7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -533,6 +533,14 @@ "aboutBugReportInstruction": "Report on GitHub with the logs and system information", "aboutBugReportButton": "Report", + "aboutDataUsageSectionTitle": "Data Usage", + "aboutDataUsageData": "Data", + "aboutDataUsageCache": "Cache", + "aboutDataUsageDatabase": "Database", + "aboutDataUsageMisc": "Misc", + "aboutDataUsageInternal": "Internal", + "aboutDataUsageExternal": "External", + "aboutCreditsSectionTitle": "Credits", "aboutCreditsWorldAtlas1": "This app uses a TopoJSON file from", "aboutCreditsWorldAtlas2": "under ISC License.", diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 674f0433e..268076c27 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -8,6 +8,8 @@ import 'package:flutter/services.dart'; import 'package:streams_channel/streams_channel.dart'; abstract class StorageService { + Future> getDataUsage(); + Future> getStorageVolumes(); Future getVaultRoot(); @@ -45,6 +47,17 @@ class PlatformStorageService implements StorageService { static const _platform = MethodChannel('deckers.thibault/aves/storage'); static final _stream = StreamsChannel('deckers.thibault/aves/activity_result_stream'); + @override + Future> getDataUsage() async { + try { + final result = await _platform.invokeMethod('getDataUsage'); + if (result != null) return (result as Map).cast(); + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return {}; + } + @override Future> getStorageVolumes() async { try { diff --git a/lib/widgets/about/about_mobile_page.dart b/lib/widgets/about/about_mobile_page.dart index 995c49bf7..53aee3388 100644 --- a/lib/widgets/about/about_mobile_page.dart +++ b/lib/widgets/about/about_mobile_page.dart @@ -1,6 +1,7 @@ import 'package:aves/widgets/about/app_ref.dart'; import 'package:aves/widgets/about/bug_report.dart'; import 'package:aves/widgets/about/credits.dart'; +import 'package:aves/widgets/about/data_usage.dart'; import 'package:aves/widgets/about/licenses.dart'; import 'package:aves/widgets/about/translators.dart'; import 'package:aves/widgets/common/basic/insets.dart'; @@ -31,6 +32,8 @@ class AboutMobilePage extends StatelessWidget { const Divider(), const BugReport(), const Divider(), + const AboutDataUsage(), + const Divider(), const AboutCredits(), const Divider(), const AboutTranslators(), diff --git a/lib/widgets/about/data_usage.dart b/lib/widgets/about/data_usage.dart new file mode 100644 index 000000000..1e6025964 --- /dev/null +++ b/lib/widgets/about/data_usage.dart @@ -0,0 +1,161 @@ +import 'package:aves/ref/brand_colors.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/theme/colors.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/styles.dart'; +import 'package:aves/utils/file_utils.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_donut.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AboutDataUsage extends StatefulWidget { + const AboutDataUsage({super.key}); + + @override + State createState() => _AboutDataUsageState(); +} + +class _AboutDataUsageState extends State with FeedbackMixin { + late Future> _loader; + bool _isExpanded = false; + + @override + void initState() { + super.initState(); + _loader = storageService.getDataUsage(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final animationDuration = context.select((v) => v.expansionTileAnimation); + return ExpansionPanelList( + expansionCallback: (index, isExpanded) { + setState(() => _isExpanded = !isExpanded); + }, + animationDuration: animationDuration, + expandedHeaderPadding: EdgeInsets.zero, + elevation: 0, + children: [ + ExpansionPanel( + headerBuilder: (context, isExpanded) => ConstrainedBox( + constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + alignment: AlignmentDirectional.centerStart, + child: Text(l10n.aboutDataUsageSectionTitle, style: AStyles.knownTitleText), + ), + ), + body: FutureBuilder>( + future: _loader, + builder: (context, snapshot) { + final data = snapshot.data; + if (data == null) return const SizedBox(); + + final dataMap = { + DataUsageDonut.bin: data['trash'] ?? 0, + DataUsageDonut.database: data['database'] ?? 0, + DataUsageDonut.vaults: data['vaults'] ?? 0, + DataUsageDonut.misc: data['miscData'] ?? 0, + }; + final flutter = data['flutter'] ?? 0; + if (flutter > 0) { + dataMap[DataUsageDonut.flutter] = flutter; + } + final cacheMap = { + DataUsageDonut.internal: data['internalCache'] ?? 0, + DataUsageDonut.external: data['externalCache'] ?? 0, + }; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DataUsageDonut( + title: l10n.aboutDataUsageData, + byTypes: dataMap, + animationDuration: animationDuration, + ), + DataUsageDonut( + title: l10n.aboutDataUsageCache, + byTypes: cacheMap, + animationDuration: animationDuration, + ), + ], + ); + }, + ), + isExpanded: _isExpanded, + canTapOnHeader: true, + backgroundColor: Colors.transparent, + ), + ], + ); + } +} + +class DataUsageDonut extends StatelessWidget { + final String title; + final Map byTypes; + final Duration animationDuration; + + // data + static const String bin = 'bin'; + static const String database = 'database'; + static const String flutter = 'flutter'; + static const String vaults = 'vaults'; + static const String misc = 'misc'; + + // cache + static const String internal = 'internal'; + static const String external = 'external'; + + const DataUsageDonut({ + super.key, + required this.title, + required this.byTypes, + required this.animationDuration, + }); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final locale = l10n.localeName; + final colors = context.watch(); + + return AvesDonut( + title: Text(title), + byTypes: byTypes, + animationDuration: animationDuration, + formatKey: (d) { + switch (d.key) { + case bin: + return l10n.binPageTitle; + case database: + return l10n.aboutDataUsageDatabase; + case flutter: + return 'Flutter'; + case vaults: + return l10n.albumTierVaults; + case misc: + return l10n.aboutDataUsageMisc; + case internal: + return l10n.aboutDataUsageInternal; + case external: + return l10n.aboutDataUsageExternal; + default: + return d.key; + } + }, + formatValue: (v) => formatFileSize(locale, v, round: 0), + colorize: (d) { + Color? color; + switch (d.key) { + case flutter: + color = colors.fromBrandColor(BrandColors.flutter); + } + return color ?? colors.fromString(d.key); + }, + ); + } +} diff --git a/lib/widgets/common/action_mixins/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart index 10723ce6f..d8a7c32c5 100644 --- a/lib/widgets/common/action_mixins/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -30,7 +30,7 @@ mixin SizeAwareMixin { if (free == null) return true; late int needed; - int sumSize(sum, entry) => sum + (entry.sizeBytes ?? 0); + int sumSize(int sum, AvesEntry entry) => sum + (entry.sizeBytes ?? 0); switch (moveType) { case MoveType.copy: case MoveType.export: diff --git a/lib/widgets/common/identity/aves_donut.dart b/lib/widgets/common/identity/aves_donut.dart new file mode 100644 index 000000000..46f7f9208 --- /dev/null +++ b/lib/widgets/common/identity/aves_donut.dart @@ -0,0 +1,186 @@ +import 'dart:math'; + +import 'package:aves/model/settings/enums/accessibility_animations.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +typedef DatumKeyFormatter = String Function(AvesDonutDatum d); +typedef DatumValueFormatter = String Function(int d); +typedef DatumColorizer = Color Function(AvesDonutDatum d); +typedef DatumCallback = void Function(AvesDonutDatum d); + +class AvesDonut extends StatefulWidget { + final Widget title; + final Map byTypes; + final Duration animationDuration; + final DatumKeyFormatter formatKey; + final DatumValueFormatter formatValue; + final DatumColorizer colorize; + final DatumCallback? onTap; + + const AvesDonut({ + super.key, + required this.title, + required this.byTypes, + required this.animationDuration, + required this.formatKey, + required this.formatValue, + required this.colorize, + this.onTap, + }); + + @override + State createState() => _AvesDonutState(); +} + +class _AvesDonutState extends State with AutomaticKeepAliveClientMixin { + Map get byTypes => widget.byTypes; + + DatumKeyFormatter get formatKey => widget.formatKey; + + DatumValueFormatter get formatValue => widget.formatValue; + + DatumColorizer get colorize => widget.colorize; + + static const avesDonutMinWidth = 124.0; + + @override + Widget build(BuildContext context) { + super.build(context); + + if (byTypes.isEmpty) return const SizedBox(); + + final sum = byTypes.values.sum; + + final seriesData = byTypes.entries.map((kv) { + final type = kv.key; + return AvesDonutDatum( + key: type, + value: kv.value, + ); + }).toList(); + seriesData.sort((d1, d2) { + final c = d2.value.compareTo(d1.value); + return c != 0 ? c : compareAsciiUpperCase(formatKey(d1), formatKey(d2)); + }); + + final series = [ + charts.Series( + id: 'type', + colorFn: (d, i) => charts.ColorUtil.fromDartColor(colorize(d)), + domainFn: (d, i) => formatKey(d), + measureFn: (d, i) => d.value, + data: seriesData, + labelAccessorFn: (d, _) => '${formatKey(d)}: ${d.value}', + ), + ]; + + return LayoutBuilder(builder: (context, constraints) { + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + final minWidth = avesDonutMinWidth * textScaleFactor; + final availableWidth = constraints.maxWidth; + final dim = max(minWidth, availableWidth / (availableWidth > 4 * minWidth ? 4 : (availableWidth > 2 * minWidth ? 2 : 1))); + + final donut = SizedBox( + width: dim, + height: dim, + child: Stack( + children: [ + charts.PieChart( + series, + animate: context.select((v) => v.accessibilityAnimations.animate), + animationDuration: widget.animationDuration, + defaultRenderer: charts.ArcRendererConfig( + arcWidth: 16, + ), + ), + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + widget.title, + Text( + formatValue(sum), + textAlign: TextAlign.center, + ), + ], + ), + ), + ], + ), + ); + final onTap = widget.onTap; + final legend = SizedBox( + width: dim, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: seriesData + .map((d) => InkWell( + onTap: onTap != null ? () => onTap(d) : null, + borderRadius: const BorderRadius.all(Radius.circular(123)), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(AIcons.disc, color: colorize(d)), + const SizedBox(width: 8), + Flexible( + child: Text( + formatKey(d), + overflow: TextOverflow.fade, + softWrap: false, + maxLines: 1, + ), + ), + const SizedBox(width: 8), + Text( + formatValue(d.value), + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall!.color, + ), + ), + const SizedBox(width: 4), + ], + ), + )) + .toList(), + ), + ); + final children = [ + donut, + legend, + ]; + return availableWidth > minWidth * 2 + ? Row( + mainAxisSize: MainAxisSize.min, + children: children, + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: children, + ); + }); + } + + @override + bool get wantKeepAlive => true; +} + +@immutable +class AvesDonutDatum extends Equatable { + final String key; + final int value; + + @override + List get props => [key, value]; + + const AvesDonutDatum({ + required this.key, + required this.value, + }); +} diff --git a/lib/widgets/stats/mime_donut.dart b/lib/widgets/stats/mime_donut.dart index 82066c751..0c2de30e4 100644 --- a/lib/widgets/stats/mime_donut.dart +++ b/lib/widgets/stats/mime_donut.dart @@ -1,21 +1,14 @@ -import 'dart:math'; - import 'package:aves/model/filters/mime.dart'; -import 'package:aves/model/settings/enums/accessibility_animations.dart'; -import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/utils/mime_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_donut.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; -import 'package:charts_flutter/flutter.dart' as charts; -import 'package:collection/collection.dart'; -import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -class MimeDonut extends StatefulWidget { +class MimeDonut extends StatelessWidget { final IconData icon; final Map byMimeTypes; final Duration animationDuration; @@ -29,157 +22,21 @@ class MimeDonut extends StatefulWidget { required this.onFilterSelection, }); - @override - State createState() => _MimeDonutState(); -} - -class _MimeDonutState extends State with AutomaticKeepAliveClientMixin { - Map get byMimeTypes => widget.byMimeTypes; - - static const mimeDonutMinWidth = 124.0; - @override Widget build(BuildContext context) { - super.build(context); - - if (byMimeTypes.isEmpty) return const SizedBox(); - - final l10n = context.l10n; - final locale = l10n.localeName; + final locale = context.l10n.localeName; final numberFormat = NumberFormat.decimalPattern(locale); - - final sum = byMimeTypes.values.sum; - final colors = context.watch(); - final seriesData = byMimeTypes.entries.map((kv) { - final mimeType = kv.key; - final displayText = MimeUtils.displayType(mimeType); - return EntryByMimeDatum( - mimeType: mimeType, - displayText: displayText, - color: colors.fromString(displayText), - entryCount: kv.value, - ); - }).toList(); - seriesData.sort((d1, d2) { - final c = d2.entryCount.compareTo(d1.entryCount); - return c != 0 ? c : compareAsciiUpperCase(d1.displayText, d2.displayText); - }); - final series = [ - charts.Series( - id: 'mime', - colorFn: (d, i) => charts.ColorUtil.fromDartColor(d.color), - domainFn: (d, i) => d.displayText, - measureFn: (d, i) => d.entryCount, - data: seriesData, - labelAccessorFn: (d, _) => '${d.displayText}: ${d.entryCount}', - ), - ]; - - return LayoutBuilder(builder: (context, constraints) { - final textScaleFactor = MediaQuery.textScaleFactorOf(context); - final minWidth = mimeDonutMinWidth * textScaleFactor; - final availableWidth = constraints.maxWidth; - final dim = max(minWidth, availableWidth / (availableWidth > 4 * minWidth ? 4 : (availableWidth > 2 * minWidth ? 2 : 1))); - - final donut = SizedBox( - width: dim, - height: dim, - child: Stack( - children: [ - charts.PieChart( - series, - animate: context.select((v) => v.accessibilityAnimations.animate), - animationDuration: widget.animationDuration, - defaultRenderer: charts.ArcRendererConfig( - arcWidth: 16, - ), - ), - Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(widget.icon), - Text( - numberFormat.format(sum), - textAlign: TextAlign.center, - ), - ], - ), - ), - ], - ), - ); - final legend = SizedBox( - width: dim, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: seriesData - .map((d) => InkWell( - onTap: () => widget.onFilterSelection(MimeFilter(d.mimeType)), - borderRadius: const BorderRadius.all(Radius.circular(123)), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(AIcons.disc, color: d.color), - const SizedBox(width: 8), - Flexible( - child: Text( - d.displayText, - overflow: TextOverflow.fade, - softWrap: false, - maxLines: 1, - ), - ), - const SizedBox(width: 8), - Text( - numberFormat.format(d.entryCount), - style: TextStyle( - color: Theme.of(context).textTheme.bodySmall!.color, - ), - ), - const SizedBox(width: 4), - ], - ), - )) - .toList(), - ), - ); - final children = [ - donut, - legend, - ]; - return availableWidth > minWidth * 2 - ? Row( - mainAxisSize: MainAxisSize.min, - children: children, - ) - : Column( - mainAxisSize: MainAxisSize.min, - children: children, - ); - }); + String formatKey(d) => MimeUtils.displayType(d.key); + return AvesDonut( + title: Icon(icon), + byTypes: byMimeTypes, + animationDuration: animationDuration, + formatKey: formatKey, + formatValue: numberFormat.format, + colorize: (d) => colors.fromString(formatKey(d)), + onTap: (d) => onFilterSelection(MimeFilter(d.key)), + ); } - - @override - bool get wantKeepAlive => true; -} - -@immutable -class EntryByMimeDatum extends Equatable { - final String mimeType, displayText; - final Color color; - final int entryCount; - - @override - List get props => [mimeType, displayText, color, entryCount]; - - const EntryByMimeDatum({ - required this.mimeType, - required this.displayText, - required this.color, - required this.entryCount, - }); } diff --git a/untranslated.json b/untranslated.json index 125ecde2f..eb90da68e 100644 --- a/untranslated.json +++ b/untranslated.json @@ -298,6 +298,13 @@ "aboutBugCopyInfoButton", "aboutBugReportInstruction", "aboutBugReportButton", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "aboutCreditsSectionTitle", "aboutCreditsWorldAtlas1", "aboutCreditsWorldAtlas2", @@ -902,6 +909,13 @@ "aboutBugCopyInfoButton", "aboutBugReportInstruction", "aboutBugReportButton", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "aboutCreditsSectionTitle", "aboutCreditsWorldAtlas1", "aboutCreditsWorldAtlas2", @@ -1503,6 +1517,13 @@ "aboutBugCopyInfoButton", "aboutBugReportInstruction", "aboutBugReportButton", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "aboutCreditsSectionTitle", "aboutCreditsWorldAtlas1", "aboutCreditsWorldAtlas2", @@ -1854,7 +1875,54 @@ "cs": [ "editorActionTransform", "cropAspectRatioFree", - "cropAspectRatioSquare" + "cropAspectRatioSquare", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" + ], + + "de": [ + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" + ], + + "el": [ + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" + ], + + "es": [ + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" + ], + + "eu": [ + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" ], "fa": [ @@ -2030,6 +2098,13 @@ "aboutBugSaveLogInstruction", "aboutBugCopyInfoInstruction", "aboutBugReportInstruction", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "aboutCreditsSectionTitle", "aboutCreditsWorldAtlas1", "aboutCreditsWorldAtlas2", @@ -2361,6 +2436,16 @@ "filePickerUseThisFolder" ], + "fr": [ + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" + ], + "gl": [ "columnCount", "saveCopyButtonLabel", @@ -2549,6 +2634,13 @@ "aboutBugCopyInfoButton", "aboutBugReportInstruction", "aboutBugReportButton", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "aboutCreditsSectionTitle", "aboutCreditsWorldAtlas1", "aboutCreditsWorldAtlas2", @@ -3214,6 +3306,13 @@ "aboutBugCopyInfoButton", "aboutBugReportInstruction", "aboutBugReportButton", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "aboutCreditsSectionTitle", "aboutCreditsWorldAtlas1", "aboutCreditsWorldAtlas2", @@ -3859,6 +3958,13 @@ "aboutBugCopyInfoButton", "aboutBugReportInstruction", "aboutBugReportButton", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "aboutCreditsSectionTitle", "aboutCreditsWorldAtlas1", "aboutCreditsWorldAtlas2", @@ -4207,6 +4313,26 @@ "filePickerUseThisFolder" ], + "hu": [ + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" + ], + + "id": [ + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" + ], + "it": [ "saveCopyButtonLabel", "applyTooltip", @@ -4216,7 +4342,14 @@ "cropAspectRatioFree", "cropAspectRatioOriginal", "cropAspectRatioSquare", - "widgetTapUpdateWidget" + "widgetTapUpdateWidget", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" ], "ja": [ @@ -4246,6 +4379,13 @@ "vaultBinUsageDialogMessage", "exportEntryDialogQuality", "exportEntryDialogWriteMetadata", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "stateEmpty", "placeEmpty", "searchStatesSectionTitle", @@ -4270,6 +4410,16 @@ "tagEditorDiscardDialogMessage" ], + "ko": [ + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" + ], + "lt": [ "columnCount", "saveCopyButtonLabel", @@ -4319,6 +4469,13 @@ "exportEntryDialogQuality", "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "drawerPlacePage", "statePageTitle", "stateEmpty", @@ -4667,6 +4824,13 @@ "aboutBugCopyInfoButton", "aboutBugReportInstruction", "aboutBugReportButton", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "aboutCreditsSectionTitle", "aboutCreditsWorldAtlas1", "aboutCreditsWorldAtlas2", @@ -5037,6 +5201,13 @@ "patternDialogEnter", "patternDialogConfirm", "exportEntryDialogQuality", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "statePageTitle", "stateEmpty", "searchStatesSectionTitle", @@ -5096,6 +5267,13 @@ "exportEntryDialogQuality", "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "drawerPlacePage", "statePageTitle", "stateEmpty", @@ -5135,6 +5313,13 @@ "authenticateToUnlockVault", "viewDialogSortSectionTitle", "viewDialogReverseSortOrder", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "aboutCreditsSectionTitle", "aboutLicensesBanner", "aboutLicensesAndroidLibrariesSectionTitle", @@ -5432,6 +5617,13 @@ "aboutBugCopyInfoButton", "aboutBugReportInstruction", "aboutBugReportButton", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "aboutCreditsWorldAtlas1", "aboutCreditsWorldAtlas2", "aboutLicensesSectionTitle", @@ -5751,6 +5943,16 @@ "filePickerUseThisFolder" ], + "pl": [ + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" + ], + "pt": [ "saveCopyButtonLabel", "applyTooltip", @@ -5760,7 +5962,14 @@ "cropAspectRatioFree", "cropAspectRatioOriginal", "cropAspectRatioSquare", - "widgetTapUpdateWidget" + "widgetTapUpdateWidget", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" ], "ro": [ @@ -5778,6 +5987,13 @@ "videoResumptionModeAlways", "widgetTapUpdateWidget", "exportEntryDialogQuality", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "settingsAskEverytime", "settingsVideoPlaybackTile", "settingsVideoPlaybackPageTitle", @@ -5786,6 +6002,16 @@ "tagEditorDiscardDialogMessage" ], + "ru": [ + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" + ], + "sk": [ "itemCount", "columnCount", @@ -5891,6 +6117,13 @@ "aboutBugCopyInfoButton", "aboutBugReportInstruction", "aboutBugReportButton", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "aboutCreditsSectionTitle", "aboutCreditsWorldAtlas1", "aboutCreditsWorldAtlas2", @@ -6294,6 +6527,13 @@ "editEntryDateDialogShift", "removeEntryMetadataDialogTitle", "tooManyItemsErrorDialogMessage", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "collectionActionShowTitleSearch", "collectionActionHideTitleSearch", "collectionActionAddShortcut", @@ -6671,6 +6911,13 @@ "vaultBinUsageDialogMessage", "exportEntryDialogQuality", "exportEntryDialogWriteMetadata", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "drawerPlacePage", "statePageTitle", "stateEmpty", @@ -6693,6 +6940,16 @@ "tagPlaceholderState" ], + "uk": [ + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" + ], + "zh": [ "saveCopyButtonLabel", "chipActionGoToPlacePage", @@ -6725,6 +6982,13 @@ "exportEntryDialogQuality", "exportEntryDialogWriteMetadata", "tooManyItemsErrorDialogMessage", + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal", "drawerPlacePage", "statePageTitle", "stateEmpty", @@ -6748,5 +7012,15 @@ "settingsAccessibilityShowPinchGestureAlternatives", "statsTopStatesSectionTitle", "tagPlaceholderState" + ], + + "zh_Hant": [ + "aboutDataUsageSectionTitle", + "aboutDataUsageData", + "aboutDataUsageCache", + "aboutDataUsageDatabase", + "aboutDataUsageMisc", + "aboutDataUsageInternal", + "aboutDataUsageExternal" ] }