From a47d82ebfcb8cdac1b8b80803c90967012112b5f Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 9 Mar 2021 12:36:49 +0900 Subject: [PATCH] l10n --- .../app/src/debug/res/values-ko/strings.xml | 4 + .../app/src/main/res/values-ko/strings.xml | 6 + .../app/src/profile/res/values-ko/strings.xml | 4 + l10n.yaml | 8 + lib/l10n/app_en.arb | 659 ++++++++++++++++++ lib/l10n/app_ko.arb | 8 + lib/main.dart | 34 +- lib/model/actions/chip_actions.dart | 20 +- lib/model/actions/collection_actions.dart | 1 - lib/model/actions/entry_actions.dart | 31 +- lib/model/filters/album.dart | 9 +- lib/model/filters/favourite.dart | 17 +- lib/model/filters/filters.dart | 21 +- lib/model/filters/location.dart | 12 +- lib/model/filters/mime.dart | 31 +- lib/model/filters/query.dart | 7 +- lib/model/filters/tag.dart | 12 +- lib/model/filters/type.dart | 58 +- lib/model/settings/coordinate_format.dart | 10 +- lib/model/settings/entry_background.dart | 2 +- lib/model/settings/enums.dart | 10 + lib/model/settings/home_page.dart | 10 +- lib/model/settings/map_style.dart | 20 +- lib/model/settings/screen_on.dart | 12 +- lib/model/settings/settings.dart | 37 +- lib/model/source/album.dart | 11 +- lib/model/source/collection_lens.dart | 2 +- lib/model/source/collection_source.dart | 3 +- lib/model/source/enums.dart | 2 + lib/model/source/location.dart | 3 +- lib/model/source/media_store_source.dart | 1 + lib/model/source/tag.dart | 1 + lib/services/android_app_service.dart | 5 - lib/theme/icons.dart | 1 - lib/utils/android_file_utils.dart | 24 +- lib/utils/constants.dart | 285 ++++---- lib/widgets/about/about_page.dart | 7 +- lib/widgets/about/app_ref.dart | 5 +- lib/widgets/about/credits.dart | 7 +- lib/widgets/about/licenses.dart | 43 +- .../about/{new_version.dart => update.dart} | 29 +- lib/widgets/collection/app_bar.dart | 71 +- .../collection/entry_set_action_delegate.dart | 14 +- lib/widgets/collection/filter_bar.dart | 2 +- .../collection/grid/headers/album.dart | 9 +- lib/widgets/collection/grid/headers/any.dart | 24 +- lib/widgets/collection/grid/headers/date.dart | 74 +- lib/widgets/collection/thumbnail/overlay.dart | 2 +- lib/widgets/collection/thumbnail/raster.dart | 3 +- lib/widgets/collection/thumbnail/vector.dart | 1 + .../collection/thumbnail_collection.dart | 10 +- .../action_mixins/permission_aware.dart | 19 +- .../common/action_mixins/size_aware.dart | 10 +- lib/widgets/common/app_bar_subtitle.dart | 8 +- lib/widgets/common/app_bar_title.dart | 4 +- lib/widgets/common/basic/query_bar.dart | 3 +- .../common/behaviour/double_back_pop.dart | 3 +- .../common/extensions/build_context.dart | 3 + lib/widgets/common/grid/header.dart | 3 +- lib/widgets/common/grid/sliver.dart | 1 + .../common/identity/aves_filter_chip.dart | 13 +- .../identity}/empty.dart | 0 lib/widgets/debug/settings.dart | 2 + lib/widgets/debug/storage.dart | 2 +- lib/widgets/dialogs/add_shortcut_dialog.dart | 21 +- lib/widgets/dialogs/aves_dialog.dart | 7 +- .../dialogs/aves_selection_dialog.dart | 9 +- lib/widgets/dialogs/create_album_dialog.dart | 21 +- lib/widgets/dialogs/rename_album_dialog.dart | 9 +- lib/widgets/dialogs/rename_entry_dialog.dart | 7 +- lib/widgets/drawer/album_tile.dart | 2 +- lib/widgets/drawer/app_drawer.dart | 19 +- lib/widgets/filter_grids/album_pick.dart | 23 +- lib/widgets/filter_grids/albums_page.dart | 36 +- .../common/chip_action_delegate.dart | 22 +- .../common/chip_set_action_delegate.dart | 21 +- .../filter_grids/common/filter_nav_page.dart | 16 +- .../filter_grids/common/section_keys.dart | 26 +- lib/widgets/filter_grids/countries_page.dart | 7 +- lib/widgets/filter_grids/tags_page.dart | 7 +- lib/widgets/search/expandable_filter_row.dart | 2 +- lib/widgets/search/search_button.dart | 2 +- lib/widgets/search/search_delegate.dart | 164 +++-- lib/widgets/search/search_page.dart | 7 +- lib/widgets/settings/access_grants.dart | 13 +- lib/widgets/settings/entry_background.dart | 1 + lib/widgets/settings/hidden_filters.dart | 11 +- lib/widgets/settings/language.dart | 52 ++ lib/widgets/settings/settings_page.dart | 72 +- lib/widgets/stats/filter_table.dart | 2 +- lib/widgets/stats/stats.dart | 26 +- lib/widgets/viewer/entry_action_delegate.dart | 24 +- lib/widgets/viewer/entry_viewer_stack.dart | 2 +- lib/widgets/viewer/info/basic_section.dart | 37 +- lib/widgets/viewer/info/common.dart | 4 +- lib/widgets/viewer/info/info_app_bar.dart | 10 +- lib/widgets/viewer/info/info_search.dart | 80 ++- lib/widgets/viewer/info/location_section.dart | 6 +- lib/widgets/viewer/info/maps/common.dart | 14 +- lib/widgets/viewer/info/maps/google_map.dart | 2 +- lib/widgets/viewer/info/maps/leaflet_map.dart | 7 +- .../info/metadata/metadata_dir_tile.dart | 3 +- .../viewer/info/metadata/xmp_ns/google.dart | 3 +- .../viewer/info/metadata/xmp_ns/mwg.dart | 22 +- .../viewer/info/metadata/xmp_ns/xmp.dart | 3 +- .../viewer/info/metadata/xmp_structs.dart | 5 +- .../viewer/info/metadata/xmp_tile.dart | 3 +- lib/widgets/viewer/overlay/bottom.dart | 10 +- lib/widgets/viewer/overlay/common.dart | 6 +- lib/widgets/viewer/overlay/panorama.dart | 3 +- lib/widgets/viewer/overlay/top.dart | 31 +- lib/widgets/viewer/overlay/video.dart | 5 +- lib/widgets/viewer/panorama_page.dart | 5 +- lib/widgets/viewer/printer.dart | 5 +- lib/widgets/viewer/source_viewer_page.dart | 3 +- .../viewer/visual/entry_page_view.dart | 1 + lib/widgets/viewer/visual/error.dart | 5 +- lib/widgets/viewer/visual/raster.dart | 1 + lib/widgets/welcome_page.dart | 9 +- pubspec.lock | 12 + pubspec.yaml | 179 +++-- test_driver/app.dart | 2 +- 122 files changed, 1955 insertions(+), 905 deletions(-) create mode 100644 android/app/src/debug/res/values-ko/strings.xml create mode 100644 android/app/src/main/res/values-ko/strings.xml create mode 100644 android/app/src/profile/res/values-ko/strings.xml create mode 100644 l10n.yaml create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_ko.arb create mode 100644 lib/model/settings/enums.dart rename lib/widgets/about/{new_version.dart => update.dart} (73%) rename lib/widgets/{collection => common/identity}/empty.dart (100%) create mode 100644 lib/widgets/settings/language.dart diff --git a/android/app/src/debug/res/values-ko/strings.xml b/android/app/src/debug/res/values-ko/strings.xml new file mode 100644 index 000000000..4fff58c6e --- /dev/null +++ b/android/app/src/debug/res/values-ko/strings.xml @@ -0,0 +1,4 @@ + + + 아베스 [Debug] + \ No newline at end of file diff --git a/android/app/src/main/res/values-ko/strings.xml b/android/app/src/main/res/values-ko/strings.xml new file mode 100644 index 000000000..2c44e7463 --- /dev/null +++ b/android/app/src/main/res/values-ko/strings.xml @@ -0,0 +1,6 @@ + + + 아베스 + 검색 + 동영상 + \ No newline at end of file diff --git a/android/app/src/profile/res/values-ko/strings.xml b/android/app/src/profile/res/values-ko/strings.xml new file mode 100644 index 000000000..37f84623f --- /dev/null +++ b/android/app/src/profile/res/values-ko/strings.xml @@ -0,0 +1,4 @@ + + + 아베스 [Profile] + \ No newline at end of file diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 000000000..d8250ba3e --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,8 @@ +# cf guide: http://flutter.dev/go/i18n-user-guide + +# use defaults to: +# - parse ARB files from `lib/l10n` +# - generate class `AppLocalizations` in `app_localizations.dart` + +preferred-supported-locales: + - en diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 000000000..80435191d --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,659 @@ +{ + "appName": "Aves", + "@appName": {}, + "welcomeMessage": "Welcome to Aves", + "@welcomeMessage": {}, + "welcomeAnalyticsToggle": "Allow anonymous analytics and crash reporting (optional)", + "@welcomeAnalyticsToggle": {}, + "welcomeTermsToggle": "I agree to the terms and conditions", + "@welcomeTermsToggle": {}, + + "applyButtonLabel": "APPLY", + "@applyButtonLabel": {}, + "deleteButtonLabel": "DELETE", + "@deleteButtonLabel": {}, + "hideButtonLabel": "HIDE", + "@hideButtonLabel": {}, + "continueButtonLabel": "CONTINUE", + "@continueButtonLabel": {}, + "clearTooltip": "Clear", + "@clearTooltip": {}, + "previousTooltip": "Previous", + "@previousTooltip": {}, + "nextTooltip": "Next", + "@nextTooltip": {}, + + "doubleBackExitMessage": "Tap “back” again to exit.", + "@doubleBackExitMessage": {}, + + "sourceStateLoading": "Loading", + "@sourceStateLoading": {}, + "sourceStateCataloguing": "Cataloguing", + "@sourceStateCataloguing": {}, + "sourceStateLocating": "Locating", + "@sourceStateLocating": {}, + + "chipActionDelete": "Delete", + "@chipActionDelete": {}, + "chipActionGoToAlbumPage": "Show in Albums", + "@chipActionGoToAlbumPage": {}, + "chipActionGoToCountryPage": "Show in Countries", + "@chipActionGoToCountryPage": {}, + "chipActionGoToTagPage": "Show in Tags", + "@chipActionGoToTagPage": {}, + "chipActionHide": "Hide", + "@chipActionHide": {}, + "chipActionPin": "Pin to top", + "@chipActionPin": {}, + "chipActionUnpin": "Unpin from top", + "@chipActionUnpin": {}, + "chipActionRename": "Rename", + "@chipActionRename": {}, + + "entryActionDelete": "Delete", + "@entryActionDelete": {}, + "entryActionExport": "Export", + "@entryActionExport": {}, + "entryActionInfo": "Info", + "@entryActionInfo": {}, + "entryActionRename": "Rename", + "@entryActionRename": {}, + "entryActionRotateCCW": "Rotate counterclockwise", + "@entryActionRotateCCW": {}, + "entryActionRotateCW": "Rotate clockwise", + "@entryActionRotateCW": {}, + "entryActionFlip": "Flip horizontally", + "@entryActionFlip": {}, + "entryActionPrint": "Print", + "@entryActionPrint": {}, + "entryActionShare": "Share", + "@entryActionShare": {}, + "entryActionViewSource": "View source", + "@entryActionViewSource": {}, + "entryActionEdit": "Edit with…", + "@entryActionEdit": {}, + "entryActionOpen": "Open with…", + "@entryActionOpen": {}, + "entryActionSetAs": "Set as…", + "@entryActionSetAs": {}, + "entryActionOpenMap": "Show on map…", + "@entryActionOpenMap": {}, + "entryActionAddFavourite": "Add to favourites", + "@entryActionAddFavourite": {}, + "entryActionRemoveFavourite": "Remove from favourites", + "@entryActionRemoveFavourite": {}, + + "filterFavouriteLabel": "Favourite", + "@filterFavouriteLabel": {}, + "filterLocationEmptyLabel": "Unlocated", + "@filterLocationEmptyLabel": {}, + "filterTagEmptyLabel": "Untagged", + "@filterTagEmptyLabel": {}, + "filterTypeAnimatedLabel": "Animated", + "@filterTypeAnimatedLabel": {}, + "filterTypePanoramaLabel": "Panorama", + "@filterTypePanoramaLabel": {}, + "filterTypeSphericalVideoLabel": "360° Video", + "@filterTypeSphericalVideoLabel": {}, + "filterTypeGeotiffLabel": "GeoTIFF", + "@filterTypeGeotiffLabel": {}, + "filterMimeImageLabel": "Image", + "@filterMimeImageLabel": {}, + "filterMimeVideoLabel": "Video", + "@filterMimeVideoLabel": {}, + + "coordinateFormatDms": "DMS", + "@coordinateFormatDms": {}, + "coordinateFormatDecimal": "Decimal degrees", + "@coordinateFormatDecimal": {}, + + "mapStyleGoogleNormal": "Google Maps", + "@mapStyleGoogleNormal": {}, + "mapStyleGoogleHybrid": "Google Maps (Hybrid)", + "@mapStyleGoogleHybrid": {}, + "mapStyleGoogleTerrain": "Google Maps (Terrain)", + "@mapStyleGoogleTerrain": {}, + "mapStyleOsmHot": "Humanitarian OSM", + "@mapStyleOsmHot": {}, + "mapStyleStamenToner": "Stamen Toner", + "@mapStyleStamenToner": {}, + "mapStyleStamenWatercolor": "Stamen Watercolor", + "@mapStyleStamenWatercolor": {}, + + "keepScreenOnNever": "Never", + "@keepScreenOnNever": {}, + "keepScreenOnViewerOnly": "Viewer page only", + "@keepScreenOnViewerOnly": {}, + "keepScreenOnAlways": "Always", + "@keepScreenOnAlways": {}, + + "albumTierPinned": "Pinned", + "@albumTierPinned": {}, + "albumTierSpecial": "Common", + "@albumTierSpecial": {}, + "albumTierApps": "Apps", + "@albumTierApps": {}, + "albumTierRegular": "Others", + "@albumTierRegular": {}, + + "storageVolumeDescriptionFallbackPrimary": "Internal storage", + "@storageVolumeDescriptionFallbackPrimary": {}, + "storageVolumeDescriptionFallbackNonPrimary": "SD card", + "@storageVolumeDescriptionFallbackNonPrimary": {}, + "rootDirectoryDescription": "root directory", + "@rootDirectoryDescription": {}, + "otherDirectoryDescription": "“{name}” directory", + "@otherDirectoryDescription": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "storageAccessDialogTitle": "Storage Access", + "@storageAccessDialogTitle": {}, + "storageVolumeAccessDialogMessage": "Please select the {directory} of “{volume}” in the next screen to give this app access to it.", + "@storageVolumeAccessDialogMessage": { + "placeholders": { + "directory": { + "type": "String" + }, + "volume": { + "type": "String" + } + } + }, + "restrictedAccessDialogTitle": "Restricted Access", + "@restrictedAccessDialogTitle": {}, + "restrictedAccessDialogMessage": "This app is not allowed to modify files in the {directory} of “{volume}”.\n\nPlease use a pre-installed file manager or gallery app to move the items to another directory.", + "@restrictedAccessDialogMessage": { + "placeholders": { + "directory": { + "type": "String" + }, + "volume": { + "type": "String" + } + } + }, + "notEnoughSpaceDialogTitle": "Not Enough Space", + "@notEnoughSpaceDialogTitle": {}, + "notEnoughSpaceDialogMessage": "This operation needs {neededSize} of free space on “{volume}” to complete, but there is only {freeSize} left.", + "@notEnoughSpaceDialogMessage": { + "placeholders": { + "neededSize": { + "type": "String" + }, + "freeSize": { + "type": "String" + }, + "volume": { + "type": "String" + } + } + }, + + "addShortcutDialogLabel": "Shortcut label", + "@addShortcutDialogLabel": {}, + "addShortcutButtonLabel": "ADD", + "@addShortcutButtonLabel": {}, + + "noMatchingAppDialogTitle": "No Matching App", + "@noMatchingAppDialogTitle": {}, + "noMatchingAppDialogMessage": "There are no apps that can handle this.", + "@noMatchingAppDialogMessage": {}, + + "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Are you sure you want to delete this item?} other{Are you sure you want to delete these {count} items?}}", + "@deleteEntriesConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + + "hideFilterConfirmationDialogMessage": "Matching photos and videos will be hidden from your collection. You can show them again from the “Privacy” settings.\n\nAre you sure you want to hide them?", + "@hideFilterConfirmationDialogMessage": {}, + + "newAlbumDialogTitle": "New Album", + "@newAlbumDialogTitle": {}, + "newAlbumDialogNameLabel": "Album name", + "@newAlbumDialogNameLabel": {}, + "newAlbumDialogNameLabelAlreadyExistsHelper": "Directory already exists", + "@newAlbumDialogNameLabelAlreadyExistsHelper": {}, + "newAlbumDialogStorageLabel": "Storage:", + "@newAlbumDialogStorageLabel": {}, + + "renameAlbumDialogLabel": "New name", + "@renameAlbumDialogLabel": {}, + "renameAlbumDialogLabelAlreadyExistsHelper": "Directory already exists", + "@renameAlbumDialogLabelAlreadyExistsHelper": {}, + + "deleteAlbumConfirmationDialogMessage": "{count, plural, =1{Are you sure you want to delete this album and its item?} other{Are you sure you want to delete this album and its {count} items?}}", + "@deleteAlbumConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + + "renameEntryDialogLabel": "New name", + "@renameEntryDialogLabel": {}, + + "genericSuccessFeedback": "Done!", + "@genericSuccessFeedback": {}, + "genericFailureFeedback": "Failed", + "@genericFailureFeedback": {}, + + "menuActionSort": "Sort", + "@menuActionSort": {}, + "menuActionGroup": "Group", + "@menuActionGroup": {}, + "menuActionStats": "Stats", + "@menuActionStats": {}, + + "aboutPageTitle": "About", + "@aboutPageTitle": {}, + "aboutFlutter": "Flutter", + "@aboutFlutter": {}, + "aboutUpdate": "New Version Available", + "@aboutUpdate": {}, + "aboutUpdateLinks1": "A new version of Aves is available on", + "@aboutUpdateLinks1": {}, + "aboutUpdateLinks2": "and", + "@aboutUpdateLinks2": {}, + "aboutUpdateLinks3": ".", + "@aboutUpdateLinks3": {}, + "aboutUpdateGithub": "Github", + "@aboutUpdateGithub": {}, + "aboutUpdateGooglePlay": "Google Play", + "@aboutUpdateGooglePlay": {}, + "aboutCredits": "Credits", + "@aboutCredits": {}, + "aboutCreditsWorldAtlas1": "This app uses a TopoJSON file from", + "@aboutCreditsWorldAtlas1": {}, + "aboutCreditsWorldAtlas2": "under ISC License.", + "@aboutCreditsWorldAtlas2": {}, + "aboutLicenses": "Open-Source Licenses", + "@aboutLicenses": {}, + "aboutLicensesBanner": "The following sets forth attribution notices for third-party software that may be contained in this application.", + "@aboutLicensesBanner": {}, + "aboutLicensesSortTooltip": "Sort", + "@aboutLicensesSortTooltip": {}, + "aboutLicensesSortByName": "Sort by name", + "@aboutLicensesSortByName": {}, + "aboutLicensesSortByLicense": "Sort by license", + "@aboutLicensesSortByLicense": {}, + "aboutLicensesAndroidLibraries": "Android Libraries", + "@aboutLicensesAndroidLibraries": {}, + "aboutLicensesFlutterPlugins": "Flutter Plugins", + "@aboutLicensesFlutterPlugins": {}, + "aboutLicensesFlutterPackages": "Flutter Packages", + "@aboutLicensesFlutterPackages": {}, + "aboutLicensesDartPackages": "Dart Packages", + "@aboutLicensesDartPackages": {}, + "aboutLicensesShowAllButtonLabel": "SHOW ALL LICENSES", + "@aboutLicensesShowAllButtonLabel": {}, + + "collectionPageTitle": "Collection", + "@collectionPageTitle": {}, + "collectionPickPageTitle": "Pick", + "@collectionPickPageTitle": {}, + "collectionSelectionPageTitle": "{count, plural, =0{Select items} =1{1 item} other{{count} items}}", + "@collectionSelectionPageTitle": { + "placeholders": { + "count": {} + } + }, + + "collectionActionAddShortcut": "Add shortcut", + "@collectionActionAddShortcut": {}, + "collectionActionSelect": "Select", + "@collectionActionSelect": {}, + "collectionActionSelectAll": "Select all", + "@collectionActionSelectAll": {}, + "collectionActionSelectNone": "Select none", + "@collectionActionSelectNone": {}, + "collectionActionCopy": "Copy to album", + "@collectionActionCopy": {}, + "collectionActionMove": "Move to album", + "@collectionActionMove": {}, + "collectionActionRefreshMetadata": "Refresh metadata", + "@collectionActionRefreshMetadata": {}, + + "collectionSortTitle": "Sort", + "@collectionSortTitle": {}, + "collectionSortDate": "By date", + "@collectionSortDate": {}, + "collectionSortSize": "By size", + "@collectionSortSize": {}, + "collectionSortName": "By album & file name", + "@collectionSortName": {}, + + "collectionGroupTitle": "Group", + "@collectionGroupTitle": {}, + "collectionGroupAlbum": "By album", + "@collectionGroupAlbum": {}, + "collectionGroupMonth": "By month", + "@collectionGroupMonth": {}, + "collectionGroupDay": "By day", + "@collectionGroupDay": {}, + "collectionGroupNone": "Do not group", + "@collectionGroupNone": {}, + + "sectionUnknown": "Unknown", + "@sectionUnknown": {}, + "dateToday": "Today", + "@dateToday": {}, + "dateYesterday": "Yesterday", + "@dateYesterday": {}, + "dateThisMonth": "This month", + "@dateThisMonth": {}, + "errorUnsupportedMimeType": "{mimeType} not supported", + "@errorUnsupportedMimeType": { + "placeholders": { + "mimeType": { + "type": "String" + } + } + }, + "collectionDeleteFailureFeedback": "{count, plural, =1{Failed to delete 1 item} other{Failed to delete {count} items}}", + "@collectionDeleteFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionCopyFailureFeedback": "{count, plural, =1{Failed to copy 1 item} other{Failed to copy {count} items}}", + "@collectionCopyFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionMoveFailureFeedback": "{count, plural, =1{Failed to move 1 item} other{Failed to move {count} items}}", + "@collectionMoveFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionExportFailureFeedback": "{count, plural, =1{Failed to export 1 page} other{Failed to export {count} pages}}", + "@collectionExportFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionCopySuccessFeedback": "{count, plural, =1{Copied 1 item} other{Copied {count} items}}", + "@collectionCopySuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionMoveSuccessFeedback": "{count, plural, =1{Moved 1 item} other{Moved {count} items}}", + "@collectionMoveSuccessFeedback": { + "placeholders": { + "count": {} + } + }, + + "collectionEmptyFavourites": "No favourites", + "@collectionEmptyFavourites": {}, + "collectionEmptyVideos": "No videos", + "@collectionEmptyVideos": {}, + "collectionEmptyImages": "No images", + "@collectionEmptyImages": {}, + + "collectionSelectSectionTooltip": "Select section", + "@collectionSelectSectionTooltip": {}, + "collectionDeselectSectionTooltip": "Deselect section", + "@collectionDeselectSectionTooltip": {}, + + "drawerCollectionAll": "All collection", + "@drawerCollectionAll": {}, + "drawerCollectionVideos": "Videos", + "@drawerCollectionVideos": {}, + "drawerCollectionFavourites": "Favourites", + "@drawerCollectionFavourites": {}, + + "chipSortTitle": "Sort", + "@chipSortTitle": {}, + "chipSortDate": "By date", + "@chipSortDate": {}, + "chipSortName": "By name", + "@chipSortName": {}, + "chipSortCount": "By item count", + "@chipSortCount": {}, + + "albumGroupTitle": "Group", + "@albumGroupTitle": {}, + "albumGroupTier": "By tier", + "@albumGroupTier": {}, + "albumGroupVolume": "By storage volume", + "@albumGroupVolume": {}, + "albumGroupNone": "Do not group", + "@albumGroupNone": {}, + + "albumPickPageTitleCopy": "Copy to Album", + "@albumPickPageTitleCopy": {}, + "albumPickPageTitleExport": "Export to Album", + "@albumPickPageTitleExport": {}, + "albumPickPageTitleMove": "Move to Album", + "@albumPickPageTitleMove": {}, + + "albumPageTitle": "Albums", + "@albumPageTitle": {}, + "albumEmpty": "No albums", + "@albumEmpty": {}, + "createAlbumTooltip": "Create album", + "@createAlbumTooltip": {}, + "createAlbumButtonLabel": "CREATE", + "@createAlbumButtonLabel": {}, + + "countryPageTitle": "Countries", + "@countryPageTitle": {}, + "countryEmpty": "No countries", + "@countryEmpty": {}, + + "tagPageTitle": "Tags", + "@tagPageTitle": {}, + "tagEmpty": "No tags", + "@tagEmpty": {}, + + "searchCollectionFieldHint": "Search collection", + "@searchCollectionFieldHint": {}, + "searchSectionRecent": "Recent", + "@searchSectionRecent": {}, + "searchSectionAlbums": "Albums", + "@searchSectionAlbums": {}, + "searchSectionCountries": "Countries", + "@searchSectionCountries": {}, + "searchSectionPlaces": "Places", + "@searchSectionPlaces": {}, + "searchSectionTags": "Tags", + "@searchSectionTags": {}, + + "settingsPageTitle": "Settings", + "@settingsPageTitle": {}, + "settingsSystemDefault": "System", + "@settingsSystemDefault": {}, + + "settingsSectionNavigation": "Navigation", + "@settingsSectionNavigation": {}, + "settingsHome": "Home", + "@settingsHome": {}, + "settingsDoubleBackExit": "Tap “back” twice to exit", + "@settingsDoubleBackExit": {}, + + "settingsSectionDisplay": "Display", + "@settingsSectionDisplay": {}, + "settingsLanguage": "Language", + "@settingsLanguage": {}, + "settingsKeepScreenOnTile": "Keep screen on", + "@settingsKeepScreenOnTile": {}, + "settingsKeepScreenOnTitle": "Keep Screen On", + "@settingsKeepScreenOnTitle": {}, + "settingsRasterImageBackground": "Raster image background", + "@settingsRasterImageBackground": {}, + "settingsVectorImageBackground": "Vector image background", + "@settingsVectorImageBackground": {}, + "settingsCoordinateFormatTile": "Coordinate format", + "@settingsCoordinateFormatTile": {}, + "settingsCoordinateFormatTitle": "Coordinate Format", + "@settingsCoordinateFormatTitle": {}, + + "settingsSectionThumbnails": "Thumbnails", + "@settingsSectionThumbnails": {}, + "settingsThumbnailShowLocationIcon": "Show location icon", + "@settingsThumbnailShowLocationIcon": {}, + "settingsThumbnailShowRawIcon": "Show raw icon", + "@settingsThumbnailShowRawIcon": {}, + "settingsThumbnailShowVideoDuration": "Show video duration", + "@settingsThumbnailShowVideoDuration": {}, + + "settingsSectionViewer": "Viewer", + "@settingsSectionViewer": {}, + "settingsViewerShowMinimap": "Show minimap", + "@settingsViewerShowMinimap": {}, + "settingsViewerShowInformation": "Show information", + "@settingsViewerShowInformation": {}, + "settingsViewerShowInformationSubtitle": "Show title, date, location, etc.", + "@settingsViewerShowInformationSubtitle": {}, + "settingsViewerShowShootingDetails": "Show shooting details", + "@settingsViewerShowShootingDetails": {}, + + "settingsSectionSearch": "Search", + "@settingsSectionSearch": {}, + "settingsSaveSearchHistory": "Save search history", + "@settingsSaveSearchHistory": {}, + + "settingsSectionPrivacy": "Privacy", + "@settingsSectionPrivacy": {}, + "settingsEnableAnalytics": "Allow anonymous analytics and crash reporting", + "@settingsEnableAnalytics": {}, + + "settingsHiddenFiltersTile": "Hidden filters", + "@settingsHiddenFiltersTile": {}, + "settingsHiddenFiltersTitle": "Hidden Filters", + "@settingsHiddenFiltersTitle": {}, + "settingsHiddenFiltersBanner": "Photos and videos matching hidden filters will not appear in your collection.", + "@settingsHiddenFiltersBanner": {}, + "settingsHiddenFiltersEmpty": "No hidden filters", + "@settingsHiddenFiltersEmpty": {}, + + "settingsStorageAccessTile": "Storage access", + "@settingsStorageAccessTile": {}, + "settingsStorageAccessTitle": "Storage Access", + "@settingsStorageAccessTitle": {}, + "settingsStorageAccessBanner": "Some directories require an explicit access grant to modify files in them. You can review here directories to which you previously gave access.", + "@settingsStorageAccessBanner": {}, + "settingsStorageAccessEmpty": "No access grants", + "@settingsStorageAccessEmpty": {}, + "settingsStorageAccessRevokeTooltip": "Revoke", + "@settingsStorageAccessRevokeTooltip": {}, + + "statsPageTitle": "Stats", + "@statsPageTitle": {}, + "statsImage": "{count, plural, =1{image} other{images}}", + "@statsImage": { + "placeholders": { + "count": {} + } + }, + "statsVideo": "{count, plural, =1{video} other{videos}}", + "@statsVideo": { + "placeholders": { + "count": {} + } + }, + "statsWithGps": "{count, plural, =1{1 item with location} other{{count} items with location}}", + "@statsWithGps": { + "placeholders": { + "count": {} + } + }, + "statsTopCountries": "Top Countries", + "@statsTopCountries": {}, + "statsTopPlaces": "Top Places", + "@statsTopPlaces": {}, + "statsTopTags": "Top Tags", + "@statsTopTags": {}, + + "viewerOpenPanoramaButtonLabel": "OPEN PANORAMA", + "@viewerOpenPanoramaButtonLabel": {}, + "viewerOpenTooltip": "Open", + "@viewerOpenTooltip": {}, + "viewerPauseTooltip": "Pause", + "@viewerPauseTooltip": {}, + "viewerPlayTooltip": "Play", + "@viewerPlayTooltip": {}, + "viewerErrorUnknown": "Oops!", + "@viewerErrorUnknown": {}, + "viewerErrorDoesNotExist": "The file no longer exists.", + "@viewerErrorDoesNotExist": {}, + + "viewerInfoPageTitle": "Info", + "@viewerInfoPageTitle": {}, + "viewerInfoBackToViewerTooltip": "Back to viewer", + "@viewerInfoBackToViewerTooltip": {}, + + "viewerInfoUnknown": "unknown", + "@viewerInfoUnknown": {}, + "viewerInfoLabelTitle": "Title", + "@viewerInfoLabelTitle": {}, + "viewerInfoLabelDate": "Date", + "@viewerInfoLabelDate": {}, + "viewerInfoLabelResolution": "Resolution", + "@viewerInfoLabelResolution": {}, + "viewerInfoLabelSize": "Size", + "@viewerInfoLabelSize": {}, + "viewerInfoLabelUri": "URI", + "@viewerInfoLabelUri": {}, + "viewerInfoLabelPath": "Path", + "@viewerInfoLabelPath": {}, + "viewerInfoLabelDuration": "Duration", + "@viewerInfoLabelDuration": {}, + "viewerInfoLabelOwner": "Owned by", + "@viewerInfoLabelOwner": {}, + "viewerInfoLabelCoordinates": "Coordinates", + "@viewerInfoLabelCoordinates": {}, + "viewerInfoLabelAddress": "Address", + "@viewerInfoLabelAddress": {}, + + "viewerInfoMapStyleTitle": "Map Style", + "@viewerInfoMapStyleTitle": {}, + "viewerInfoMapStyleTooltip": "Select map style", + "@viewerInfoMapStyleTooltip": {}, + "viewerInfoMapZoomInTooltip": "Zoom in", + "@viewerInfoMapZoomInTooltip": {}, + "viewerInfoMapZoomOutTooltip": "Zoom out", + "@viewerInfoMapZoomOutTooltip": {}, + "mapAttributionOsmHot": "Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors • Tiles by [HOT](https://www.hotosm.org/) • Hosted by [OSM France](https://openstreetmap.fr/)", + "@mapAttributionOsmHot": {}, + "mapAttributionStamen": "Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors • Tiles by [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0)", + "@mapAttributionStamen": {}, + + "viewerInfoOpenEmbeddedFailureFeedback": "Failed to extract embedded data", + "@viewerInfoOpenEmbeddedFailureFeedback": {}, + "viewerInfoOpenLinkText": "Open", + "@viewerInfoOpenLinkText": {}, + "viewerInfoViewXmlLinkText": "View XML", + "@viewerInfoViewXmlLinkText": {}, + + "viewerInfoSearchFieldLabel": "Search metadata", + "@viewerInfoSearchFieldLabel": {}, + "viewerInfoSearchEmpty": "No matching keys", + "@viewerInfoSearchEmpty": {}, + "viewerInfoSearchSuggestionDate": "Date & time", + "@viewerInfoSearchSuggestionDate": {}, + "viewerInfoSearchSuggestionDescription": "Description", + "@viewerInfoSearchSuggestionDescription": {}, + "viewerInfoSearchSuggestionDimensions": "Dimensions", + "@viewerInfoSearchSuggestionDimensions": {}, + "viewerInfoSearchSuggestionResolution": "Resolution", + "@viewerInfoSearchSuggestionResolution": {}, + "viewerInfoSearchSuggestionRights": "Rights", + "@viewerInfoSearchSuggestionRights": {}, + + "panoramaEnableSensorControl": "Enable sensor control", + "@panoramaEnableSensorControl": {}, + "panoramaDisableSensorControl": "Disable sensor control", + "@panoramaDisableSensorControl": {}, + + "sourceViewerPageTitle": "Source", + "@sourceViewerPageTitle": {} +} diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb new file mode 100644 index 000000000..cb78fe901 --- /dev/null +++ b/lib/l10n/app_ko.arb @@ -0,0 +1,8 @@ +{ + "appName": "아베스", + + "collectionSelectionPageTitle": "{count, plural, =0{항목 선택} other{{count}개}}", + + "settingsLanguage": "언어", + "settingsSystemDefault": "시스템" +} diff --git a/lib/main.dart b/lib/main.dart index a9bad4218..948167cd2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/utils/debouncer.dart'; import 'package:aves/widgets/common/behaviour/route_tracker.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/welcome_page.dart'; @@ -19,6 +20,8 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:overlay_support/overlay_support.dart'; import 'package:provider/provider.dart'; @@ -120,19 +123,30 @@ class _AvesAppState extends State { child: FutureBuilder( future: _appSetup, builder: (context, snapshot) { - final home = (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) + final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done; + final home = initialized ? getFirstPage() : Scaffold( - body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(), + body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox(), ); - return MaterialApp( - navigatorKey: _navigatorKey, - home: home, - navigatorObservers: _navigatorObservers, - title: 'Aves', - darkTheme: darkTheme, - themeMode: ThemeMode.dark, - ); + return Selector( + selector: (context, s) => s.locale, + builder: (context, settingsLocale, child) { + return MaterialApp( + navigatorKey: _navigatorKey, + home: home, + navigatorObservers: _navigatorObservers, + onGenerateTitle: (context) => context.l10n.appName, + darkTheme: darkTheme, + themeMode: ThemeMode.dark, + locale: settingsLocale, + localizationsDelegates: [ + ...AppLocalizations.localizationsDelegates, + LocaleNamesLocalizationsDelegate(), + ], + supportedLocales: AppLocalizations.supportedLocales, + ); + }); }, ), ), diff --git a/lib/model/actions/chip_actions.dart b/lib/model/actions/chip_actions.dart index 27ea9d6c2..aada4fc4f 100644 --- a/lib/model/actions/chip_actions.dart +++ b/lib/model/actions/chip_actions.dart @@ -1,10 +1,10 @@ import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart'; enum ChipSetAction { group, sort, - refresh, stats, } @@ -20,24 +20,24 @@ enum ChipAction { } extension ExtraChipAction on ChipAction { - String getText() { + String getText(BuildContext context) { switch (this) { case ChipAction.delete: - return 'Delete'; + return context.l10n.chipActionDelete; case ChipAction.goToAlbumPage: - return 'Show in Albums'; + return context.l10n.chipActionGoToAlbumPage; case ChipAction.goToCountryPage: - return 'Show in Countries'; + return context.l10n.chipActionGoToCountryPage; case ChipAction.goToTagPage: - return 'Show in Tags'; + return context.l10n.chipActionGoToTagPage; case ChipAction.hide: - return 'Hide'; + return context.l10n.chipActionHide; case ChipAction.pin: - return 'Pin to top'; + return context.l10n.chipActionPin; case ChipAction.unpin: - return 'Unpin from top'; + return context.l10n.chipActionUnpin; case ChipAction.rename: - return 'Rename'; + return context.l10n.chipActionRename; } return null; } diff --git a/lib/model/actions/collection_actions.dart b/lib/model/actions/collection_actions.dart index 531c8e5f9..360fd00b9 100644 --- a/lib/model/actions/collection_actions.dart +++ b/lib/model/actions/collection_actions.dart @@ -2,7 +2,6 @@ enum CollectionAction { addShortcut, sort, group, - refresh, select, selectAll, selectNone, diff --git a/lib/model/actions/entry_actions.dart b/lib/model/actions/entry_actions.dart index 5cd6f62c3..ad47ac452 100644 --- a/lib/model/actions/entry_actions.dart +++ b/lib/model/actions/entry_actions.dart @@ -1,4 +1,5 @@ import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart'; enum EntryAction { @@ -46,41 +47,41 @@ class EntryActions { } extension ExtraEntryAction on EntryAction { - String getText() { + String getText(BuildContext context) { switch (this) { // in app actions case EntryAction.toggleFavourite: // different data depending on toggle state return null; case EntryAction.delete: - return 'Delete'; + return context.l10n.entryActionDelete; case EntryAction.export: - return 'Export'; + return context.l10n.entryActionExport; case EntryAction.info: - return 'Info'; + return context.l10n.entryActionInfo; case EntryAction.rename: - return 'Rename'; + return context.l10n.entryActionRename; case EntryAction.rotateCCW: - return 'Rotate counterclockwise'; + return context.l10n.entryActionRotateCCW; case EntryAction.rotateCW: - return 'Rotate clockwise'; + return context.l10n.entryActionRotateCW; case EntryAction.flip: - return 'Flip horizontally'; + return context.l10n.entryActionFlip; case EntryAction.print: - return 'Print'; + return context.l10n.entryActionPrint; case EntryAction.share: - return 'Share'; + return context.l10n.entryActionShare; case EntryAction.viewSource: - return 'View source'; + return context.l10n.entryActionViewSource; // external app actions case EntryAction.edit: - return 'Edit with…'; + return context.l10n.entryActionEdit; case EntryAction.open: - return 'Open with…'; + return context.l10n.entryActionOpen; case EntryAction.setAs: - return 'Set as…'; + return context.l10n.entryActionSetAs; case EntryAction.openMap: - return 'Show on map…'; + return context.l10n.entryActionOpenMap; case EntryAction.debug: return 'Debug'; } diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index ec48d627b..d2bbdb56c 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -35,10 +35,10 @@ class AlbumFilter extends CollectionFilter { EntryFilter get test => (entry) => entry.directory == album; @override - String get label => uniqueName ?? album.split(separator).last; + String get universalLabel => uniqueName ?? album.split(separator).last; @override - String get tooltip => album; + String getTooltip(BuildContext context) => album; @override Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) { @@ -74,7 +74,10 @@ class AlbumFilter extends CollectionFilter { } @override - String get typeKey => type; + String get category => type; + + @override + String get key => '$type-$album'; @override bool operator ==(Object other) { diff --git a/lib/model/filters/favourite.dart b/lib/model/filters/favourite.dart index 348bb4aeb..0c27c9c72 100644 --- a/lib/model/filters/favourite.dart +++ b/lib/model/filters/favourite.dart @@ -1,11 +1,15 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; class FavouriteFilter extends CollectionFilter { static const type = 'favourite'; + const FavouriteFilter(); + @override Map toMap() => { 'type': type, @@ -15,13 +19,22 @@ class FavouriteFilter extends CollectionFilter { EntryFilter get test => (entry) => entry.isFavourite; @override - String get label => 'Favourite'; + String get universalLabel => type; + + @override + String getLabel(BuildContext context) => context.l10n.filterFavouriteLabel; @override Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.favourite, size: size); @override - String get typeKey => type; + Future color(BuildContext context) => SynchronousFuture(Colors.red); + + @override + String get category => type; + + @override + String get key => type; @override bool operator ==(Object other) { diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index 6584497e6..fdfbf371d 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -14,7 +14,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; abstract class CollectionFilter implements Comparable { - static const List collectionFilterOrder = [ + static const List categoryOrder = [ QueryFilter.type, FavouriteFilter.type, MimeFilter.type, @@ -57,25 +57,28 @@ abstract class CollectionFilter implements Comparable { bool get isUnique => true; - String get label; + String get universalLabel; - String get tooltip => label; + String getLabel(BuildContext context) => universalLabel; + + String getTooltip(BuildContext context) => getLabel(context); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}); - Future color(BuildContext context) => SynchronousFuture(stringToColor(label)); + Future color(BuildContext context) => SynchronousFuture(stringToColor(getLabel(context))); - String get typeKey; - - int get displayPriority => collectionFilterOrder.indexOf(typeKey); + String get category; // to be used as widget key - String get key => '$typeKey-$label'; + String get key; + + int get displayPriority => categoryOrder.indexOf(category); @override int compareTo(CollectionFilter other) { final c = displayPriority.compareTo(other.displayPriority); - return c != 0 ? c : compareAsciiUpperCase(label, other.label); + // assume we compare context-independent labels + return c != 0 ? c : compareAsciiUpperCase(universalLabel, other.universalLabel); } } diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index 7b3e66c35..b92a0073b 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -1,11 +1,11 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; class LocationFilter extends CollectionFilter { static const type = 'location'; - static const emptyLabel = 'unlocated'; static const locationSeparator = ';'; final LocationLevel level; @@ -48,7 +48,10 @@ class LocationFilter extends CollectionFilter { EntryFilter get test => _test; @override - String get label => _location.isEmpty ? emptyLabel : _location; + String get universalLabel => _location; + + @override + String getLabel(BuildContext context) => _location.isEmpty ? context.l10n.filterLocationEmptyLabel : _location; @override Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) { @@ -66,7 +69,10 @@ class LocationFilter extends CollectionFilter { } @override - String get typeKey => type; + String get category => type; + + @override + String get key => '$type-$level-$_location'; @override bool operator ==(Object other) { diff --git a/lib/model/filters/mime.dart b/lib/model/filters/mime.dart index 7b9fa56b7..5b1388589 100644 --- a/lib/model/filters/mime.dart +++ b/lib/model/filters/mime.dart @@ -1,6 +1,8 @@ import 'package:aves/model/filters/filters.dart'; +import 'package:aves/ref/mime_types.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:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -17,14 +19,12 @@ class MimeFilter extends CollectionFilter { if (lowMime.endsWith('/*')) { lowMime = lowMime.substring(0, lowMime.length - 2); _test = (entry) => entry.mimeType.startsWith(lowMime); - if (lowMime == 'video') { - _label = 'Video'; - _icon = AIcons.video; - } else if (lowMime == 'image') { - _label = 'Image'; + _label = lowMime.toUpperCase(); + if (mime == MimeTypes.anyImage) { _icon = AIcons.image; + } else if (mime == MimeTypes.anyVideo) { + _icon = AIcons.video; } - _label ??= lowMime.split('/')[0].toUpperCase(); } else { _test = (entry) => entry.mimeType == lowMime; _label = MimeUtils.displayType(lowMime); @@ -47,13 +47,28 @@ class MimeFilter extends CollectionFilter { EntryFilter get test => _test; @override - String get label => _label; + String get universalLabel => _label; + + @override + String getLabel(BuildContext context) { + switch (mime) { + case MimeTypes.anyImage: + return context.l10n.filterMimeImageLabel; + case MimeTypes.anyVideo: + return context.l10n.filterMimeVideoLabel; + default: + return _label; + } + } @override Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(_icon, size: size); @override - String get typeKey => type; + String get category => type; + + @override + String get key => '$type-$mime'; @override bool operator ==(Object other) { diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index dcbf6064e..cab4778e7 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -50,7 +50,7 @@ class QueryFilter extends CollectionFilter { bool get isUnique => false; @override - String get label => '$query'; + String get universalLabel => query; @override Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.text, size: size); @@ -59,7 +59,10 @@ class QueryFilter extends CollectionFilter { Future color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(AvesFilterChip.defaultOutlineColor); @override - String get typeKey => type; + String get category => type; + + @override + String get key => '$type-$query'; @override bool operator ==(Object other) { diff --git a/lib/model/filters/tag.dart b/lib/model/filters/tag.dart index bec9dbe74..3b39b82ac 100644 --- a/lib/model/filters/tag.dart +++ b/lib/model/filters/tag.dart @@ -1,11 +1,11 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; class TagFilter extends CollectionFilter { static const type = 'tag'; - static const emptyLabel = 'untagged'; final String tag; EntryFilter _test; @@ -36,13 +36,19 @@ class TagFilter extends CollectionFilter { bool get isUnique => false; @override - String get label => tag.isEmpty ? emptyLabel : tag; + String get universalLabel => tag; + + @override + String getLabel(BuildContext context) => tag.isEmpty ? context.l10n.filterTagEmptyLabel : tag; @override Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagOff : AIcons.tag, size: size) : null; @override - String get typeKey => type; + String get category => type; + + @override + String get key => '$type-$tag'; @override bool operator ==(Object other) { diff --git a/lib/model/filters/type.dart b/lib/model/filters/type.dart index 189e97164..f79dc6630 100644 --- a/lib/model/filters/type.dart +++ b/lib/model/filters/type.dart @@ -1,5 +1,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -13,26 +14,26 @@ class TypeFilter extends CollectionFilter { final String itemType; EntryFilter _test; - String _label; IconData _icon; TypeFilter(this.itemType) { - if (itemType == animated) { - _test = (entry) => entry.isAnimated; - _label = 'Animated'; - _icon = AIcons.animated; - } else if (itemType == panorama) { - _test = (entry) => entry.isImage && entry.is360; - _label = 'Panorama'; - _icon = AIcons.threesixty; - } else if (itemType == sphericalVideo) { - _test = (entry) => entry.isVideo && entry.is360; - _label = '360° Video'; - _icon = AIcons.threesixty; - } else if (itemType == geotiff) { - _test = (entry) => entry.isGeotiff; - _label = 'GeoTIFF'; - _icon = AIcons.geo; + switch (itemType) { + case animated: + _test = (entry) => entry.isAnimated; + _icon = AIcons.animated; + break; + case panorama: + _test = (entry) => entry.isImage && entry.is360; + _icon = AIcons.threesixty; + break; + case sphericalVideo: + _test = (entry) => entry.isVideo && entry.is360; + _icon = AIcons.threesixty; + break; + case geotiff: + _test = (entry) => entry.isGeotiff; + _icon = AIcons.geo; + break; } } @@ -51,13 +52,32 @@ class TypeFilter extends CollectionFilter { EntryFilter get test => _test; @override - String get label => _label; + String get universalLabel => itemType; + + @override + String getLabel(BuildContext context) { + switch (itemType) { + case animated: + return context.l10n.filterTypeAnimatedLabel; + case panorama: + return context.l10n.filterTypePanoramaLabel; + case sphericalVideo: + return context.l10n.filterTypeSphericalVideoLabel; + case geotiff: + return context.l10n.filterTypeGeotiffLabel; + default: + return itemType; + } + } @override Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(_icon, size: size); @override - String get typeKey => type; + String get category => type; + + @override + String get key => '$type-$itemType'; @override bool operator ==(Object other) { diff --git a/lib/model/settings/coordinate_format.dart b/lib/model/settings/coordinate_format.dart index 7e0518537..159991ab8 100644 --- a/lib/model/settings/coordinate_format.dart +++ b/lib/model/settings/coordinate_format.dart @@ -1,15 +1,17 @@ import 'package:aves/geo/format.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/widgets.dart'; import 'package:latlong/latlong.dart'; -enum CoordinateFormat { dms, decimal } +import 'enums.dart'; extension ExtraCoordinateFormat on CoordinateFormat { - String get name { + String getName(BuildContext context) { switch (this) { case CoordinateFormat.dms: - return 'DMS'; + return context.l10n.coordinateFormatDms; case CoordinateFormat.decimal: - return 'Decimal degrees'; + return context.l10n.coordinateFormatDecimal; default: return toString(); } diff --git a/lib/model/settings/entry_background.dart b/lib/model/settings/entry_background.dart index ee0ffe4c1..14f83f071 100644 --- a/lib/model/settings/entry_background.dart +++ b/lib/model/settings/entry_background.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -enum EntryBackground { black, white, transparent, checkered } +import 'enums.dart'; extension ExtraEntryBackground on EntryBackground { bool get isColor { diff --git a/lib/model/settings/enums.dart b/lib/model/settings/enums.dart new file mode 100644 index 000000000..c6d32dde6 --- /dev/null +++ b/lib/model/settings/enums.dart @@ -0,0 +1,10 @@ +enum CoordinateFormat { dms, decimal } + +enum EntryBackground { black, white, transparent, checkered } + +enum HomePageSetting { collection, albums } + +// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ +enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor } + +enum KeepScreenOn { never, viewerOnly, always } diff --git a/lib/model/settings/home_page.dart b/lib/model/settings/home_page.dart index bc5c29b75..8c4aa09a6 100644 --- a/lib/model/settings/home_page.dart +++ b/lib/model/settings/home_page.dart @@ -1,15 +1,17 @@ import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; +import 'package:flutter/widgets.dart'; -enum HomePageSetting { collection, albums } +import 'enums.dart'; extension ExtraHomePageSetting on HomePageSetting { - String get name { + String getName(BuildContext context) { switch (this) { case HomePageSetting.collection: - return 'Collection'; + return context.l10n.collectionPageTitle; case HomePageSetting.albums: - return 'Albums'; + return context.l10n.albumPageTitle; default: return toString(); } diff --git a/lib/model/settings/map_style.dart b/lib/model/settings/map_style.dart index 25559107a..954edb2f2 100644 --- a/lib/model/settings/map_style.dart +++ b/lib/model/settings/map_style.dart @@ -1,21 +1,23 @@ -// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ -enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor } +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/widgets.dart'; + +import 'enums.dart'; extension ExtraEntryMapStyle on EntryMapStyle { - String get name { + String getName(BuildContext context) { switch (this) { case EntryMapStyle.googleNormal: - return 'Google Maps'; + return context.l10n.mapStyleGoogleNormal; case EntryMapStyle.googleHybrid: - return 'Google Maps (Hybrid)'; + return context.l10n.mapStyleGoogleHybrid; case EntryMapStyle.googleTerrain: - return 'Google Maps (Terrain)'; + return context.l10n.mapStyleGoogleTerrain; case EntryMapStyle.osmHot: - return 'Humanitarian OSM'; + return context.l10n.mapStyleOsmHot; case EntryMapStyle.stamenToner: - return 'Stamen Toner'; + return context.l10n.mapStyleStamenToner; case EntryMapStyle.stamenWatercolor: - return 'Stamen Watercolor'; + return context.l10n.mapStyleStamenWatercolor; default: return toString(); } diff --git a/lib/model/settings/screen_on.dart b/lib/model/settings/screen_on.dart index a11d3d7e4..ff7e851f0 100644 --- a/lib/model/settings/screen_on.dart +++ b/lib/model/settings/screen_on.dart @@ -1,16 +1,18 @@ import 'package:aves/services/window_service.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/widgets.dart'; -enum KeepScreenOn { never, viewerOnly, always } +import 'enums.dart'; extension ExtraKeepScreenOn on KeepScreenOn { - String get name { + String getName(BuildContext context) { switch (this) { case KeepScreenOn.never: - return 'Never'; + return context.l10n.keepScreenOnNever; case KeepScreenOn.viewerOnly: - return 'Viewer page only'; + return context.l10n.keepScreenOnViewerOnly; case KeepScreenOn.always: - return 'Always'; + return context.l10n.keepScreenOnAlways; default: return toString(); } diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index d436d7ca9..e8d50a8b0 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -1,18 +1,14 @@ import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/settings/coordinate_format.dart'; -import 'package:aves/model/settings/entry_background.dart'; -import 'package:aves/model/settings/home_page.dart'; -import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:pedantic/pedantic.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../source/enums.dart'; +import 'enums.dart'; final Settings settings = Settings._private(); @@ -24,6 +20,7 @@ class Settings extends ChangeNotifier { // app static const hasAcceptedTermsKey = 'has_accepted_terms'; static const isCrashlyticsEnabledKey = 'is_crashlytics_enabled'; + static const localeKey = 'locale'; static const mustBackTwiceToExitKey = 'must_back_twice_to_exit'; static const keepScreenOnKey = 'keep_screen_on'; static const homePageKey = 'home_page'; @@ -99,6 +96,34 @@ class Settings extends ChangeNotifier { unawaited(initFirebase()); } + static const localeSeparator = '-'; + + Locale get locale { + // exceptionally allow getting locale before settings are initialized + final tag = _prefs?.getString(localeKey); + if (tag != null) { + final codes = tag.split(localeSeparator); + return Locale.fromSubtags( + languageCode: codes[0], + scriptCode: codes[1] == '' ? null : codes[1], + countryCode: codes[2] == '' ? null : codes[2], + ); + } + return null; + } + + set locale(Locale newValue) { + String tag; + if (newValue != null) { + tag = [ + newValue.languageCode ?? '', + newValue.scriptCode ?? '', + newValue.countryCode ?? '', + ].join(localeSeparator); + } + setAndNotify(localeKey, tag); + } + bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, true); set mustBackTwiceToExit(bool newValue) => setAndNotify(mustBackTwiceToExitKey, newValue); @@ -182,7 +207,7 @@ class Settings extends ChangeNotifier { set showOverlayInfo(bool newValue) => setAndNotify(showOverlayInfoKey, newValue); - bool get showOverlayShootingDetails => getBoolOrDefault(showOverlayShootingDetailsKey, true); + bool get showOverlayShootingDetails => getBoolOrDefault(showOverlayShootingDetailsKey, false); set showOverlayShootingDetails(bool newValue) => setAndNotify(showOverlayShootingDetailsKey, newValue); diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index 8f78d6c18..699aaa54c 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.dart @@ -4,6 +4,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; import 'package:path/path.dart'; mixin AlbumMixin on SourceBase { @@ -12,8 +13,8 @@ mixin AlbumMixin on SourceBase { List get rawAlbums => List.unmodifiable(_directories); int compareAlbumsByName(String a, String b) { - final ua = getUniqueAlbumName(a); - final ub = getUniqueAlbumName(b); + final ua = getUniqueAlbumName(null, a); + final ub = getUniqueAlbumName(null, b); final c = compareAsciiUpperCase(ua, ub); if (c != 0) return c; final va = androidFileUtils.getStorageVolume(a)?.path ?? ''; @@ -23,7 +24,7 @@ mixin AlbumMixin on SourceBase { void _notifyAlbumChange() => eventBus.fire(AlbumsChangedEvent()); - String getUniqueAlbumName(String dirPath) { + String getUniqueAlbumName(BuildContext context, String dirPath) { String unique(String dirPath, [bool Function(String) test]) { final otherAlbums = _directories.where(test ?? (_) => true).where((item) => item != dirPath); final parts = dirPath.split(separator); @@ -51,7 +52,7 @@ mixin AlbumMixin on SourceBase { if (volume.isPrimary) { return uniqueNameInVolume; } else { - return '$uniqueNameInVolume (${volume.description})'; + return '$uniqueNameInVolume (${volume.getDescription(context)})'; } } } @@ -99,7 +100,7 @@ mixin AlbumMixin on SourceBase { invalidateAlbumFilterSummary(directories: emptyAlbums); final pinnedFilters = settings.pinnedFilters; - emptyAlbums.forEach((album) => pinnedFilters.remove(AlbumFilter(album, getUniqueAlbumName(album)))); + emptyAlbums.forEach((album) => pinnedFilters.removeWhere((filter) => filter is AlbumFilter && filter.album == album)); settings.pinnedFilters = pinnedFilters; } } diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index 4c15ee0e8..9531ed566 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -92,7 +92,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin { void addFilter(CollectionFilter filter) { if (filter == null || filters.contains(filter)) return; if (filter.isUnique) { - filters.removeWhere((old) => old.typeKey == filter.typeKey); + filters.removeWhere((old) => old.category == filter.category); } filters.add(filter); onFilterChanged(); diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index ead56b9a5..485129ecf 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -10,6 +10,7 @@ import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/album.dart'; +import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/services/image_op_events.dart'; @@ -256,8 +257,6 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM } } -enum SourceState { loading, cataloguing, locating, ready } - class EntryAddedEvent { final Set entries; diff --git a/lib/model/source/enums.dart b/lib/model/source/enums.dart index a1ba59bb3..3721deeeb 100644 --- a/lib/model/source/enums.dart +++ b/lib/model/source/enums.dart @@ -1,5 +1,7 @@ enum Activity { browse, select } +enum SourceState { loading, cataloguing, locating, ready } + enum ChipSortFactor { date, name, count } enum AlbumChipGroupFactor { none, importance, volume } diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index 124c9a8d1..5a57d24fd 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -7,6 +7,7 @@ import 'package:aves/model/filters/location.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/enums.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:tuple/tuple.dart'; @@ -138,7 +139,7 @@ mixin LocationMixin on SourceBase { } // the same country code could be found with different country names - // e.g. if the locale changed between geolocating calls + // e.g. if the locale changed between geocoding calls // so we merge countries by code, keeping only one name for each code final countriesByCode = Map.fromEntries(locations.map((address) => MapEntry(address.countryCode, address.countryName)).where((kv) => kv.key != null && kv.key.isNotEmpty)); final updatedCountries = countriesByCode.entries.map((kv) => '${kv.value}${LocationFilter.locationSeparator}${kv.key}').toList()..sort(compareAsciiUpperCase); diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 83b310368..fa164526d 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -6,6 +6,7 @@ import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/enums.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/media_store_service.dart'; import 'package:aves/services/time_service.dart'; diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index 897ba577e..fe27a3c8c 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -3,6 +3,7 @@ import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/enums.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index 9a379334d..38ae05445 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.dart @@ -41,7 +41,6 @@ class AndroidAppService { static Future edit(String uri, String mimeType) async { try { return await platform.invokeMethod('edit', { - 'title': 'Edit with:', 'uri': uri, 'mimeType': mimeType, }); @@ -54,7 +53,6 @@ class AndroidAppService { static Future open(String uri, String mimeType) async { try { return await platform.invokeMethod('open', { - 'title': 'Open with:', 'uri': uri, 'mimeType': mimeType, }); @@ -78,7 +76,6 @@ class AndroidAppService { static Future setAs(String uri, String mimeType) async { try { return await platform.invokeMethod('setAs', { - 'title': 'Set as:', 'uri': uri, 'mimeType': mimeType, }); @@ -94,7 +91,6 @@ class AndroidAppService { final urisByMimeType = groupBy(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())); try { return await platform.invokeMethod('share', { - 'title': 'Share via:', 'urisByMimeType': urisByMimeType, }); } on PlatformException catch (e) { @@ -106,7 +102,6 @@ class AndroidAppService { static Future shareSingle(String uri, String mimeType) async { try { return await platform.invokeMethod('share', { - 'title': 'Share via:', 'urisByMimeType': { mimeType: [uri] }, diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 2e7745856..4a844c018 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -43,7 +43,6 @@ class AIcons { static const IconData openOutside = Icons.open_in_new_outlined; static const IconData pin = Icons.push_pin_outlined; static const IconData print = Icons.print_outlined; - static const IconData refresh = Icons.refresh_outlined; static const IconData rename = Icons.title_outlined; static const IconData rotateLeft = Icons.rotate_left_outlined; static const IconData rotateRight = Icons.rotate_right_outlined; diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index f0de8f962..3a5ca800b 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -1,6 +1,7 @@ import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/android_file_service.dart'; import 'package:aves/utils/change_notifier.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:path/path.dart'; @@ -115,21 +116,30 @@ class Package { @immutable class StorageVolume { - final String description, path, state; + final String _description, path, state; final bool isPrimary, isRemovable; const StorageVolume({ - this.description, + String description, this.isPrimary, this.isRemovable, this.path, this.state, - }); + }) : _description = description; + + String getDescription(BuildContext context) { + if (_description != null) return _description; + // ideally, the context should always be provided, but in some cases (e.g. album comparison), + // this would require numerous additional methods to have the context as argument + // for such a minor benefit: fallback volume description on Android < N + if (isPrimary) return context?.l10n?.storageVolumeDescriptionFallbackPrimary ?? 'Internal Storage'; + return context?.l10n?.storageVolumeDescriptionFallbackNonPrimary ?? 'SD card'; + } factory StorageVolume.fromMap(Map map) { final isPrimary = map['isPrimary'] ?? false; return StorageVolume( - description: map['description'] ?? (isPrimary ? 'Internal storage' : 'SD card'), + description: map['description'], isPrimary: isPrimary, isRemovable: map['isRemovable'] ?? false, path: map['path'] ?? '', @@ -167,11 +177,9 @@ class VolumeRelativeDirectory { ); } - String get directoryDescription => relativeDir.isEmpty ? 'root' : '“$relativeDir”'; - - String get volumeDescription { + String getVolumeDescription(BuildContext context) { final volume = androidFileUtils.storageVolumes.firstWhere((volume) => volume.path == volumePath, orElse: () => null); - return volume?.description ?? volumePath; + return volume?.getDescription(context) ?? volumePath; } @override diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index bde527b73..d265a608c 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -21,7 +21,6 @@ class Constants { ); static const overlayUnknown = '—'; // em dash - static const infoUnknown = 'unknown'; static final pointNemo = LatLng(-48.876667, -123.393333); @@ -66,61 +65,175 @@ class Constants { ), ]; - static const List flutterPackages = [ - Dependency( - name: 'Flutter', - license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/flutter/flutter/blob/master/LICENSE', - sourceUrl: 'https://github.com/flutter/flutter', - ), - Dependency( - name: 'Charts', - license: 'Apache 2.0', - licenseUrl: 'https://github.com/google/charts/blob/master/LICENSE', - sourceUrl: 'https://github.com/google/charts', - ), - Dependency( - name: 'Collection', - license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/dart-lang/collection/blob/master/LICENSE', - sourceUrl: 'https://github.com/dart-lang/collection', - ), + static const List flutterPlugins = [ Dependency( name: 'Connectivity', license: 'BSD 3-Clause', licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/connectivity/connectivity/LICENSE', sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/connectivity/connectivity', ), + Dependency( + name: 'FlutterFire (Core, Analytics, Crashlytics)', + license: 'BSD 3-Clause', + licenseUrl: 'https://github.com/FirebaseExtended/flutterfire/blob/master/LICENSE', + sourceUrl: 'https://github.com/FirebaseExtended/flutterfire', + ), + Dependency( + name: 'Flutter ijkplayer', + license: 'MIT', + licenseUrl: 'https://github.com/CaiJingLong/flutter_ijkplayer/blob/master/LICENSE', + sourceUrl: 'https://github.com/CaiJingLong/flutter_ijkplayer', + ), + Dependency( + name: 'Geocoder', + license: 'MIT', + licenseUrl: 'https://github.com/aloisdeniel/flutter_geocoder/blob/master/LICENSE', + sourceUrl: 'https://github.com/aloisdeniel/flutter_geocoder', + ), + Dependency( + name: 'Google API Availability', + license: 'MIT', + licenseUrl: 'https://github.com/Baseflow/flutter-google-api-availability/blob/master/LICENSE', + sourceUrl: 'https://github.com/Baseflow/flutter-google-api-availability', + ), + Dependency( + name: 'Google Maps for Flutter', + license: 'BSD 3-Clause', + licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter/LICENSE', + sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter', + ), + Dependency( + name: 'Package Info', + license: 'BSD 3-Clause', + licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/package_info/LICENSE', + sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/package_info', + ), + Dependency( + name: 'Permission Handler', + license: 'MIT', + licenseUrl: 'https://github.com/Baseflow/flutter-permission-handler/blob/develop/permission_handler/LICENSE', + sourceUrl: 'https://github.com/Baseflow/flutter-permission-handler', + ), + Dependency( + name: 'Printing', + license: 'Apache 2.0', + licenseUrl: 'https://github.com/DavBfr/dart_pdf/blob/master/LICENSE', + sourceUrl: 'https://github.com/DavBfr/dart_pdf', + ), + Dependency( + name: 'Shared Preferences', + license: 'BSD 3-Clause', + licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/shared_preferences/shared_preferences/LICENSE', + sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences', + ), + Dependency( + name: 'sqflite', + license: 'MIT', + licenseUrl: 'https://github.com/tekartik/sqflite/blob/master/sqflite/LICENSE', + sourceUrl: 'https://github.com/tekartik/sqflite', + ), + Dependency( + name: 'Streams Channel', + license: 'Apache 2.0', + licenseUrl: 'https://github.com/loup-v/streams_channel/blob/master/LICENSE', + sourceUrl: 'https://github.com/loup-v/streams_channel', + ), + Dependency( + name: 'URL Launcher', + license: 'BSD 3-Clause', + licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/LICENSE', + sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher', + ), + ]; + + static const List dartPackages = [ + Dependency( + name: 'Collection', + license: 'BSD 3-Clause', + licenseUrl: 'https://github.com/dart-lang/collection/blob/master/LICENSE', + sourceUrl: 'https://github.com/dart-lang/collection', + ), Dependency( name: 'Country Code', license: 'MIT', licenseUrl: 'https://github.com/denixport/dart.country/blob/master/LICENSE', sourceUrl: 'https://github.com/denixport/dart.country', ), - Dependency( - name: 'Decorated Icon', - license: 'MIT', - licenseUrl: 'https://github.com/benPesso/flutter_decorated_icon/blob/master/LICENSE', - sourceUrl: 'https://github.com/benPesso/flutter_decorated_icon', - ), Dependency( name: 'Event Bus', license: 'MIT', licenseUrl: 'https://github.com/marcojakob/dart-event-bus/blob/master/LICENSE', sourceUrl: 'https://github.com/marcojakob/dart-event-bus', ), + Dependency( + name: 'Github', + license: 'MIT', + licenseUrl: 'https://github.com/SpinlockLabs/github.dart/blob/master/LICENSE', + sourceUrl: 'https://github.com/SpinlockLabs/github.dart', + ), + Dependency( + name: 'Intl', + license: 'BSD 3-Clause', + licenseUrl: 'https://github.com/dart-lang/intl/blob/master/LICENSE', + sourceUrl: 'https://github.com/dart-lang/intl', + ), + Dependency( + name: 'LatLong', + license: 'Apache 2.0', + licenseUrl: 'https://github.com/MikeMitterer/dart-latlong/blob/master/LICENSE', + sourceUrl: 'https://github.com/MikeMitterer/dart-latlong', + ), + Dependency( + name: 'PDF for Dart and Flutter', + license: 'Apache 2.0', + licenseUrl: 'https://github.com/DavBfr/dart_pdf/blob/master/LICENSE', + sourceUrl: 'https://github.com/DavBfr/dart_pdf', + ), + Dependency( + name: 'Pedantic', + license: 'BSD 3-Clause', + licenseUrl: 'https://github.com/dart-lang/pedantic/blob/master/LICENSE', + sourceUrl: 'https://github.com/dart-lang/pedantic', + ), + Dependency( + name: 'Tuple', + license: 'BSD 2-Clause', + licenseUrl: 'https://github.com/dart-lang/tuple/blob/master/LICENSE', + sourceUrl: 'https://github.com/dart-lang/tuple', + ), + Dependency( + name: 'Version', + license: 'BSD 3-Clause', + licenseUrl: 'https://github.com/dartninja/version/blob/master/LICENSE', + sourceUrl: 'https://github.com/dartninja/version', + ), + Dependency( + name: 'XML', + license: 'MIT', + licenseUrl: 'https://github.com/renggli/dart-xml/blob/master/LICENSE', + sourceUrl: 'https://github.com/renggli/dart-xml', + ), + ]; + + static const List flutterPackages = [ + Dependency( + name: 'Charts', + license: 'Apache 2.0', + licenseUrl: 'https://github.com/google/charts/blob/master/LICENSE', + sourceUrl: 'https://github.com/google/charts', + ), + Dependency( + name: 'Decorated Icon', + license: 'MIT', + licenseUrl: 'https://github.com/benPesso/flutter_decorated_icon/blob/master/LICENSE', + sourceUrl: 'https://github.com/benPesso/flutter_decorated_icon', + ), Dependency( name: 'Expansion Tile Card', license: 'BSD 3-Clause', licenseUrl: 'https://github.com/Skylled/expansion_tile_card/blob/master/LICENSE', sourceUrl: 'https://github.com/Skylled/expansion_tile_card', ), - Dependency( - name: 'FlutterFire (Core, Analytics, Crashlytics)', - license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/FirebaseExtended/flutterfire/blob/master/LICENSE', - sourceUrl: 'https://github.com/FirebaseExtended/flutterfire', - ), Dependency( name: 'Flushbar', license: 'Apache 2.0', @@ -134,10 +247,10 @@ class Constants { sourceUrl: 'https://github.com/git-touch/highlight', ), Dependency( - name: 'Flutter ijkplayer', + name: 'Flutter Localized Locales', license: 'MIT', - licenseUrl: 'https://github.com/CaiJingLong/flutter_ijkplayer/blob/master/LICENSE', - sourceUrl: 'https://github.com/CaiJingLong/flutter_ijkplayer', + licenseUrl: 'https://github.com/guidezpl/flutter-localized-locales/blob/master/LICENSE', + sourceUrl: 'https://github.com/guidezpl/flutter-localized-locales', ), Dependency( name: 'Flutter Map', @@ -163,36 +276,6 @@ class Constants { licenseUrl: 'https://github.com/dnfield/flutter_svg/blob/master/LICENSE', sourceUrl: 'https://github.com/dnfield/flutter_svg', ), - Dependency( - name: 'Geocoder', - license: 'MIT', - licenseUrl: 'https://github.com/aloisdeniel/flutter_geocoder/blob/master/LICENSE', - sourceUrl: 'https://github.com/aloisdeniel/flutter_geocoder', - ), - Dependency( - name: 'Github', - license: 'MIT', - licenseUrl: 'https://github.com/SpinlockLabs/github.dart/blob/master/LICENSE', - sourceUrl: 'https://github.com/SpinlockLabs/github.dart', - ), - Dependency( - name: 'Google Maps for Flutter', - license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter', - ), - Dependency( - name: 'Intl', - license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/dart-lang/intl/blob/master/LICENSE', - sourceUrl: 'https://github.com/dart-lang/intl', - ), - Dependency( - name: 'LatLong', - license: 'Apache 2.0', - licenseUrl: 'https://github.com/MikeMitterer/dart-latlong/blob/master/LICENSE', - sourceUrl: 'https://github.com/MikeMitterer/dart-latlong', - ), Dependency( name: 'Material Design Icons Flutter', license: 'MIT', @@ -205,12 +288,6 @@ class Constants { licenseUrl: 'https://github.com/boyan01/overlay_support/blob/master/LICENSE', sourceUrl: 'https://github.com/boyan01/overlay_support', ), - Dependency( - name: 'Package Info', - license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/package_info/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/package_info', - ), Dependency( name: 'Palette Generator', license: 'BSD 3-Clause', @@ -223,84 +300,18 @@ class Constants { licenseUrl: 'https://github.com/zesage/panorama/blob/master/LICENSE', sourceUrl: 'https://github.com/zesage/panorama', ), - Dependency( - name: 'PDF for Dart and Flutter', - license: 'Apache 2.0', - licenseUrl: 'https://github.com/DavBfr/dart_pdf/blob/master/LICENSE', - sourceUrl: 'https://github.com/DavBfr/dart_pdf', - ), - Dependency( - name: 'Pedantic', - license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/dart-lang/pedantic/blob/master/LICENSE', - sourceUrl: 'https://github.com/dart-lang/pedantic', - ), Dependency( name: 'Percent Indicator', license: 'BSD 2-Clause', licenseUrl: 'https://github.com/diegoveloper/flutter_percent_indicator/blob/master/LICENSE', sourceUrl: 'https://github.com/diegoveloper/flutter_percent_indicator/', ), - Dependency( - name: 'Permission Handler', - license: 'MIT', - licenseUrl: 'https://github.com/Baseflow/flutter-permission-handler/blob/develop/permission_handler/LICENSE', - sourceUrl: 'https://github.com/Baseflow/flutter-permission-handler', - ), - Dependency( - name: 'Printing', - license: 'Apache 2.0', - licenseUrl: 'https://github.com/DavBfr/dart_pdf/blob/master/LICENSE', - sourceUrl: 'https://github.com/DavBfr/dart_pdf', - ), Dependency( name: 'Provider', license: 'MIT', licenseUrl: 'https://github.com/rrousselGit/provider/blob/master/LICENSE', sourceUrl: 'https://github.com/rrousselGit/provider', ), - Dependency( - name: 'Shared Preferences', - license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/shared_preferences/shared_preferences/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences', - ), - Dependency( - name: 'sqflite', - license: 'MIT', - licenseUrl: 'https://github.com/tekartik/sqflite/blob/master/sqflite/LICENSE', - sourceUrl: 'https://github.com/tekartik/sqflite', - ), - Dependency( - name: 'Streams Channel', - license: 'Apache 2.0', - licenseUrl: 'https://github.com/loup-v/streams_channel/blob/master/LICENSE', - sourceUrl: 'https://github.com/loup-v/streams_channel', - ), - Dependency( - name: 'Tuple', - license: 'BSD 2-Clause', - licenseUrl: 'https://github.com/dart-lang/tuple/blob/master/LICENSE', - sourceUrl: 'https://github.com/dart-lang/tuple', - ), - Dependency( - name: 'URL Launcher', - license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher', - ), - Dependency( - name: 'Version', - license: 'BSD 3-Clause', - licenseUrl: 'https://github.com/dartninja/version/blob/master/LICENSE', - sourceUrl: 'https://github.com/dartninja/version', - ), - Dependency( - name: 'XML', - license: 'MIT', - licenseUrl: 'https://github.com/renggli/dart-xml/blob/master/LICENSE', - sourceUrl: 'https://github.com/renggli/dart-xml', - ), ]; } diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart index c65d2e6e0..d368bf332 100644 --- a/lib/widgets/about/about_page.dart +++ b/lib/widgets/about/about_page.dart @@ -1,7 +1,8 @@ import 'package:aves/widgets/about/app_ref.dart'; import 'package:aves/widgets/about/credits.dart'; import 'package:aves/widgets/about/licenses.dart'; -import 'package:aves/widgets/about/new_version.dart'; +import 'package:aves/widgets/about/update.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; class AboutPage extends StatelessWidget { @@ -11,7 +12,7 @@ class AboutPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('About'), + title: Text(context.l10n.aboutPageTitle), ), body: SafeArea( child: CustomScrollView( @@ -23,7 +24,7 @@ class AboutPage extends StatelessWidget { [ AppReference(), Divider(), - AboutNewVersion(), + AboutUpdate(), AboutCredits(), Divider(), ], diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart index 4a7f4b91c..0e383a192 100644 --- a/lib/widgets/about/app_ref.dart +++ b/lib/widgets/about/app_ref.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:aves/flutter_version.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:flutter/material.dart'; import 'package:package_info/package_info.dart'; @@ -48,7 +49,7 @@ class _AppReferenceState extends State { leading: AvesLogo( size: style.fontSize * MediaQuery.textScaleFactorOf(context) * 1.25, ), - text: 'Aves ${snapshot.data?.version}', + text: '${context.l10n.appName} ${snapshot.data?.version}', url: 'https://github.com/deckerst/aves', textStyle: style, ); @@ -71,7 +72,7 @@ class _AppReferenceState extends State { ), ), ), - TextSpan(text: 'Flutter ${version['frameworkVersion']}'), + TextSpan(text: '${context.l10n.aboutFlutter} ${version['frameworkVersion']}'), ], ), style: TextStyle(color: subColor), diff --git a/lib/widgets/about/credits.dart b/lib/widgets/about/credits.dart index 55f2a5406..8cf4f1a5e 100644 --- a/lib/widgets/about/credits.dart +++ b/lib/widgets/about/credits.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; class AboutCredits extends StatelessWidget { @@ -16,13 +17,13 @@ class AboutCredits extends StatelessWidget { constraints: BoxConstraints(minHeight: 48), child: Align( alignment: AlignmentDirectional.centerStart, - child: Text('Credits', style: Constants.titleTextStyle), + child: Text(context.l10n.aboutCredits, style: Constants.titleTextStyle), ), ), Text.rich( TextSpan( children: [ - TextSpan(text: 'This app uses a TopoJSON file from'), + TextSpan(text: context.l10n.aboutCreditsWorldAtlas1), WidgetSpan( child: LinkChip( text: 'World Atlas', @@ -31,7 +32,7 @@ class AboutCredits extends StatelessWidget { ), alignment: PlaceholderAlignment.middle, ), - TextSpan(text: 'under ISC License.'), + TextSpan(text: context.l10n.aboutCreditsWorldAtlas2), ], ), ), diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index 7b1415212..7d4558417 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -3,6 +3,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/basic/menu_row.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -15,13 +16,15 @@ class Licenses extends StatefulWidget { class _LicensesState extends State { final ValueNotifier _expandedNotifier = ValueNotifier(null); LicenseSort _sort = LicenseSort.name; - List _platform, _flutter; + List _platform, _flutterPlugins, _flutterPackages, _dartPackages; @override void initState() { super.initState(); - _platform = List.from(Constants.androidDependencies); - _flutter = List.from(Constants.flutterPackages); + _platform = List.from(Constants.androidDependencies); + _flutterPlugins = List.from(Constants.flutterPlugins); + _flutterPackages = List.from(Constants.flutterPackages); + _dartPackages = List.from(Constants.dartPackages); _sortPackages(); } @@ -38,7 +41,9 @@ class _LicensesState extends State { } _platform.sort(compare); - _flutter.sort(compare); + _flutterPlugins.sort(compare); + _flutterPackages.sort(compare); + _dartPackages.sort(compare); } @override @@ -51,16 +56,28 @@ class _LicensesState extends State { _buildHeader(), SizedBox(height: 16), AvesExpansionTile( - title: 'Android Libraries', + title: context.l10n.aboutLicensesAndroidLibraries, color: BrandColors.android, expandedNotifier: _expandedNotifier, children: _platform.map((package) => LicenseRow(package)).toList(), ), AvesExpansionTile( - title: 'Flutter Packages', + title: context.l10n.aboutLicensesFlutterPlugins, color: BrandColors.flutter, expandedNotifier: _expandedNotifier, - children: _flutter.map((package) => LicenseRow(package)).toList(), + children: _flutterPlugins.map((package) => LicenseRow(package)).toList(), + ), + AvesExpansionTile( + title: context.l10n.aboutLicensesFlutterPackages, + color: BrandColors.flutter, + expandedNotifier: _expandedNotifier, + children: _flutterPackages.map((package) => LicenseRow(package)).toList(), + ), + AvesExpansionTile( + title: context.l10n.aboutLicensesDartPackages, + color: BrandColors.flutter, + expandedNotifier: _expandedNotifier, + children: _dartPackages.map((package) => LicenseRow(package)).toList(), ), Center( child: TextButton( @@ -76,7 +93,7 @@ class _LicensesState extends State { ), ), ), - child: Text('Show All Licenses'.toUpperCase()), + child: Text(context.l10n.aboutLicensesShowAllButtonLabel), ), ), ], @@ -94,17 +111,17 @@ class _LicensesState extends State { child: Row( children: [ Expanded( - child: Text('Open-Source Licenses', style: Constants.titleTextStyle), + child: Text(context.l10n.aboutLicenses, style: Constants.titleTextStyle), ), PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( value: LicenseSort.name, - child: MenuRow(text: 'Sort by name', checked: _sort == LicenseSort.name), + child: MenuRow(text: context.l10n.aboutLicensesSortByName, checked: _sort == LicenseSort.name), ), PopupMenuItem( value: LicenseSort.license, - child: MenuRow(text: 'Sort by license', checked: _sort == LicenseSort.license), + child: MenuRow(text: context.l10n.aboutLicensesSortByLicense, checked: _sort == LicenseSort.license), ), ], onSelected: (newSort) { @@ -112,7 +129,7 @@ class _LicensesState extends State { _sortPackages(); setState(() {}); }, - tooltip: 'Sort', + tooltip: context.l10n.aboutLicensesSortTooltip, icon: Icon(AIcons.sort), ), ], @@ -121,7 +138,7 @@ class _LicensesState extends State { SizedBox(height: 8), Padding( padding: EdgeInsets.symmetric(horizontal: 8), - child: Text('The following sets forth attribution notices for third party software that may be contained in this application.'), + child: Text(context.l10n.aboutLicensesBanner), ), ], ); diff --git a/lib/widgets/about/new_version.dart b/lib/widgets/about/update.dart similarity index 73% rename from lib/widgets/about/new_version.dart rename to lib/widgets/about/update.dart index a5cb22fc7..94e4ecef0 100644 --- a/lib/widgets/about/new_version.dart +++ b/lib/widgets/about/update.dart @@ -2,29 +2,30 @@ import 'package:aves/model/availability.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/about/news_badge.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; -class AboutNewVersion extends StatefulWidget { +class AboutUpdate extends StatefulWidget { @override - _AboutNewVersionState createState() => _AboutNewVersionState(); + _AboutUpdateState createState() => _AboutUpdateState(); } -class _AboutNewVersionState extends State { - Future _newVersionLoader; +class _AboutUpdateState extends State { + Future _updateChecker; @override void initState() { super.initState(); - _newVersionLoader = availability.isNewVersionAvailable; + _updateChecker = availability.isNewVersionAvailable; } @override Widget build(BuildContext context) { return FutureBuilder( - future: _newVersionLoader, + future: _updateChecker, builder: (context, snapshot) { - final newVersion = snapshot.data == true; - if (!newVersion) return SizedBox(); + final newVersionAvailable = snapshot.data == true; + if (!newVersionAvailable) return SizedBox(); return Column( children: [ Padding( @@ -46,7 +47,7 @@ class _AboutNewVersionState extends State { ), alignment: PlaceholderAlignment.middle, ), - TextSpan(text: 'New Version Available', style: Constants.titleTextStyle), + TextSpan(text: context.l10n.aboutUpdate, style: Constants.titleTextStyle), ], ), ), @@ -55,25 +56,25 @@ class _AboutNewVersionState extends State { Text.rich( TextSpan( children: [ - TextSpan(text: 'A new version of Aves is available on '), + TextSpan(text: context.l10n.aboutUpdateLinks1), WidgetSpan( child: LinkChip( - text: 'Github', + text: context.l10n.aboutUpdateGithub, url: 'https://github.com/deckerst/aves/releases', textStyle: TextStyle(fontWeight: FontWeight.bold), ), alignment: PlaceholderAlignment.middle, ), - TextSpan(text: ' and '), + TextSpan(text: context.l10n.aboutUpdateLinks2), WidgetSpan( child: LinkChip( - text: 'Google Play', + text: context.l10n.aboutUpdateGooglePlay, url: 'https://play.google.com/store/apps/details?id=deckers.thibault.aves', textStyle: TextStyle(fontWeight: FontWeight.bold), ), alignment: PlaceholderAlignment.middle, ), - TextSpan(text: '.'), + TextSpan(text: context.l10n.aboutUpdateLinks3), ], ), ), diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 4cc51796f..919822ec0 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:aves/main.dart'; import 'package:aves/model/actions/collection_actions.dart'; import 'package:aves/model/actions/entry_actions.dart'; +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -15,6 +16,7 @@ import 'package:aves/widgets/collection/filter_bar.dart'; import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/basic/menu_row.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/search/search_button.dart'; @@ -23,7 +25,6 @@ import 'package:aves/widgets/stats/stats.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:intl/intl.dart'; import 'package:pedantic/pedantic.dart'; class CollectionAppBar extends StatefulWidget { @@ -141,7 +142,7 @@ class _CollectionAppBarState extends State with SingleTickerPr Widget _buildAppBarTitle() { if (collection.isBrowsing) { Widget title = Text( - AvesApp.mode == AppMode.pick ? 'Pick' : 'Collection', + AvesApp.mode == AppMode.pick ? context.l10n.collectionPickPageTitle : context.l10n.collectionPageTitle, key: Key('appbar-title'), ); if (AvesApp.mode == AppMode.main) { @@ -150,7 +151,7 @@ class _CollectionAppBarState extends State with SingleTickerPr source: source, ); } - return TappableAppBarTitle( + return InteractiveAppBarTitle( onTap: _goToSearch, child: title, ); @@ -159,7 +160,7 @@ class _CollectionAppBarState extends State with SingleTickerPr animation: collection.selectionChangeNotifier, builder: (context, child) { final count = collection.selection.length; - return Text(Intl.plural(count, zero: 'Select items', one: '$count item', other: '$count items')); + return Text(context.l10n.collectionSelectionPageTitle(count)); }, ); } @@ -180,7 +181,7 @@ class _CollectionAppBarState extends State with SingleTickerPr return IconButton( icon: Icon(action.getIcon()), onPressed: collection.selection.isEmpty ? null : () => _actionDelegate.onEntryActionSelected(context, action), - tooltip: action.getText(), + tooltip: action.getText(context), ); }, )), @@ -197,35 +198,30 @@ class _CollectionAppBarState extends State with SingleTickerPr PopupMenuItem( key: Key('menu-sort'), value: CollectionAction.sort, - child: MenuRow(text: 'Sort…', icon: AIcons.sort), + child: MenuRow(text: context.l10n.menuActionSort, icon: AIcons.sort), ), if (collection.sortFactor == EntrySortFactor.date) PopupMenuItem( key: Key('menu-group'), value: CollectionAction.group, - child: MenuRow(text: 'Group…', icon: AIcons.group), + child: MenuRow(text: context.l10n.menuActionGroup, icon: AIcons.group), ), if (collection.isBrowsing) ...[ - if (kDebugMode) - PopupMenuItem( - value: CollectionAction.refresh, - child: MenuRow(text: 'Refresh', icon: AIcons.refresh), - ), if (AvesApp.mode == AppMode.main) PopupMenuItem( value: CollectionAction.select, enabled: isNotEmpty, - child: MenuRow(text: 'Select', icon: AIcons.select), + child: MenuRow(text: context.l10n.collectionActionSelect, icon: AIcons.select), ), PopupMenuItem( value: CollectionAction.stats, enabled: isNotEmpty, - child: MenuRow(text: 'Stats', icon: AIcons.stats), + child: MenuRow(text: context.l10n.menuActionStats, icon: AIcons.stats), ), if (AvesApp.mode == AppMode.main && canAddShortcuts) PopupMenuItem( value: CollectionAction.addShortcut, - child: MenuRow(text: 'Add shortcut…', icon: AIcons.addShortcut), + child: MenuRow(text: context.l10n.collectionActionAddShortcut, icon: AIcons.addShortcut), ), ], if (collection.isSelecting) ...[ @@ -233,28 +229,28 @@ class _CollectionAppBarState extends State with SingleTickerPr PopupMenuItem( value: CollectionAction.copy, enabled: hasSelection, - child: MenuRow(text: 'Copy to album'), + child: MenuRow(text: context.l10n.collectionActionCopy), ), PopupMenuItem( value: CollectionAction.move, enabled: hasSelection, - child: MenuRow(text: 'Move to album'), + child: MenuRow(text: context.l10n.collectionActionMove), ), PopupMenuItem( value: CollectionAction.refreshMetadata, enabled: hasSelection, - child: MenuRow(text: 'Refresh metadata'), + child: MenuRow(text: context.l10n.collectionActionRefreshMetadata), ), PopupMenuDivider(), PopupMenuItem( value: CollectionAction.selectAll, enabled: collection.selection.length < collection.entryCount, - child: MenuRow(text: 'Select all'), + child: MenuRow(text: context.l10n.collectionActionSelectAll), ), PopupMenuItem( value: CollectionAction.selectNone, enabled: hasSelection, - child: MenuRow(text: 'Select none'), + child: MenuRow(text: context.l10n.collectionActionSelectNone), ), ] ]; @@ -289,9 +285,6 @@ class _CollectionAppBarState extends State with SingleTickerPr case CollectionAction.refreshMetadata: _actionDelegate.onCollectionActionSelected(context, action); break; - case CollectionAction.refresh: - unawaited(source.refresh()); - break; case CollectionAction.select: collection.select(); break; @@ -313,12 +306,12 @@ class _CollectionAppBarState extends State with SingleTickerPr builder: (context) => AvesSelectionDialog( initialValue: settings.collectionGroupFactor, options: { - EntryGroupFactor.album: 'By album', - EntryGroupFactor.month: 'By month', - EntryGroupFactor.day: 'By day', - EntryGroupFactor.none: 'Do not group', + EntryGroupFactor.album: context.l10n.collectionGroupAlbum, + EntryGroupFactor.month: context.l10n.collectionGroupMonth, + EntryGroupFactor.day: context.l10n.collectionGroupDay, + EntryGroupFactor.none: context.l10n.collectionGroupNone, }, - title: 'Group', + title: context.l10n.collectionGroupTitle, ), ); if (value != null) { @@ -332,11 +325,11 @@ class _CollectionAppBarState extends State with SingleTickerPr builder: (context) => AvesSelectionDialog( initialValue: settings.collectionSortFactor, options: { - EntrySortFactor.date: 'By date', - EntrySortFactor.size: 'By size', - EntrySortFactor.name: 'By album & file name', + EntrySortFactor.date: context.l10n.collectionSortDate, + EntrySortFactor.size: context.l10n.collectionSortSize, + EntrySortFactor.name: context.l10n.collectionSortName, }, - title: 'Sort', + title: context.l10n.collectionSortTitle, ), ); if (value != null) { @@ -348,14 +341,24 @@ class _CollectionAppBarState extends State with SingleTickerPr } Future _showShortcutDialog(BuildContext context) async { + final filters = collection.filters; + var defaultName; + if (filters.isEmpty) { + defaultName = context.l10n.collectionPageTitle; + } else { + final sortedFilters = List.from(filters)..sort(); + defaultName = sortedFilters.first.getLabel(context); + } final name = await showDialog( context: context, - builder: (context) => AddShortcutDialog(collection.filters), + builder: (context) { + return AddShortcutDialog(defaultName: defaultName); + }, ); if (name == null || name.isEmpty) return; final iconEntry = collection.sortedEntries.isNotEmpty ? collection.sortedEntries.first : null; - unawaited(AppShortcutService.pin(name, iconEntry, collection.filters)); + unawaited(AppShortcutService.pin(name, iconEntry, filters)); } void _goToSearch() { diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index d8a4544fd..2c3a0404c 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -14,12 +14,12 @@ import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/filter_grids/album_pick.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:intl/intl.dart'; class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final CollectionLens collection; @@ -112,10 +112,10 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware final movedCount = movedOps.length; if (movedCount < todoCount) { final count = todoCount - movedCount; - showFeedback(context, 'Failed to ${copy ? 'copy' : 'move'} ${Intl.plural(count, one: '$count item', other: '$count items')}'); + showFeedback(context, copy ? context.l10n.collectionCopyFailureFeedback(count) : context.l10n.collectionMoveFailureFeedback(count)); } else { final count = movedCount; - showFeedback(context, '${copy ? 'Copied' : 'Moved'} ${Intl.plural(count, one: '$count item', other: '$count items')}'); + showFeedback(context, copy ? context.l10n.collectionCopySuccessFeedback(count) : context.l10n.collectionMoveSuccessFeedback(count)); } await source.updateAfterMove( todoEntries: todoEntries, @@ -138,15 +138,15 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware builder: (context) { return AvesDialog( context: context, - content: Text('Are you sure you want to delete ${Intl.plural(count, one: 'this item', other: 'these $count items')}?'), + content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(count)), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('Cancel'.toUpperCase()), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), TextButton( onPressed: () => Navigator.pop(context, true), - child: Text('Delete'.toUpperCase()), + child: Text(context.l10n.deleteButtonLabel), ), ], ); @@ -167,7 +167,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware final deletedCount = deletedUris.length; if (deletedCount < selectionCount) { final count = selectionCount - deletedCount; - showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}'); + showFeedback(context, context.l10n.collectionDeleteFailureFeedback(count)); } source.removeEntries(deletedUris); collection.browse(); diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart index 10e922443..1cde65a07 100644 --- a/lib/widgets/collection/filter_bar.dart +++ b/lib/widgets/collection/filter_bar.dart @@ -14,7 +14,7 @@ class FilterBar extends StatefulWidget implements PreferredSizeWidget { Key key, @required Set filters, @required this.onPressed, - }) : filters = List.from(filters)..sort(), + }) : filters = List.from(filters)..sort(), super(key: key); @override diff --git a/lib/widgets/collection/grid/headers/album.dart b/lib/widgets/collection/grid/headers/album.dart index cf67b3b6d..5dcfe766c 100644 --- a/lib/widgets/collection/grid/headers/album.dart +++ b/lib/widgets/collection/grid/headers/album.dart @@ -9,12 +9,11 @@ import 'package:flutter/material.dart'; class AlbumSectionHeader extends StatelessWidget { final String directory, albumName; - AlbumSectionHeader({ + const AlbumSectionHeader({ Key key, - @required CollectionSource source, @required this.directory, - }) : albumName = source.getUniqueAlbumName(directory), - super(key: key); + @required this.albumName, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -47,7 +46,7 @@ class AlbumSectionHeader extends StatelessWidget { return SectionHeader.getPreferredHeight( context: context, maxWidth: maxWidth, - title: source.getUniqueAlbumName(directory), + title: source.getUniqueAlbumName(context, directory), hasLeading: androidFileUtils.getAlbumType(directory) != AlbumType.regular, hasTrailing: androidFileUtils.isOnRemovableStorage(directory), ); diff --git a/lib/widgets/collection/grid/headers/any.dart b/lib/widgets/collection/grid/headers/any.dart index 7ed5bf63c..e323ba087 100644 --- a/lib/widgets/collection/grid/headers/any.dart +++ b/lib/widgets/collection/grid/headers/any.dart @@ -23,7 +23,7 @@ class CollectionSectionHeader extends StatelessWidget { @override Widget build(BuildContext context) { - final header = _buildHeader(); + final header = _buildHeader(context); return header != null ? SizedBox( height: height, @@ -32,18 +32,12 @@ class CollectionSectionHeader extends StatelessWidget { : SizedBox.shrink(); } - Widget _buildHeader() { - Widget _buildAlbumHeader() => AlbumSectionHeader( - key: ValueKey(sectionKey), - source: collection.source, - directory: (sectionKey as EntryAlbumSectionKey).directory, - ); - + Widget _buildHeader(BuildContext context) { switch (collection.sortFactor) { case EntrySortFactor.date: switch (collection.groupFactor) { case EntryGroupFactor.album: - return _buildAlbumHeader(); + return _buildAlbumHeader(context); case EntryGroupFactor.month: return MonthSectionHeader(key: ValueKey(sectionKey), date: (sectionKey as EntryDateSectionKey).date); case EntryGroupFactor.day: @@ -53,13 +47,23 @@ class CollectionSectionHeader extends StatelessWidget { } break; case EntrySortFactor.name: - return _buildAlbumHeader(); + return _buildAlbumHeader(context); case EntrySortFactor.size: break; } return null; } + Widget _buildAlbumHeader(BuildContext context) { + final source = collection.source; + final directory = (sectionKey as EntryAlbumSectionKey).directory; + return AlbumSectionHeader( + key: ValueKey(sectionKey), + directory: directory, + albumName: source.getUniqueAlbumName(context, directory), + ); + } + static double getPreferredHeight(BuildContext context, double maxWidth, CollectionSource source, SectionKey sectionKey) { var headerExtent = 0.0; if (sectionKey is EntryAlbumSectionKey) { diff --git a/lib/widgets/collection/grid/headers/date.dart b/lib/widgets/collection/grid/headers/date.dart index 8de36ce61..2e09e7874 100644 --- a/lib/widgets/collection/grid/headers/date.dart +++ b/lib/widgets/collection/grid/headers/date.dart @@ -1,73 +1,79 @@ import 'package:aves/model/source/section_keys.dart'; import 'package:aves/utils/time_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/grid/header.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class DaySectionHeader extends StatelessWidget { final DateTime date; - final String text; - DaySectionHeader({ + const DaySectionHeader({ Key key, @required this.date, - }) : text = _formatDate(date), - super(key: key); + }) : super(key: key); // Examples (en_US): - // `MMMMd`: `April 15` - // `yMMMMd`: `April 15, 2020` - // `MMMEd`: `Wed, Apr 15` - // `yMMMEd`: `Wed, Apr 15, 2020` - // `MMMMEEEEd`: `Wednesday, April 15` - // `yMMMMEEEEd`: `Wednesday, April 15, 2020` - // `MEd`: `Wed, 4/15` - // `yMEd`: `Wed, 4/15/2020` - static DateFormat md = DateFormat.MMMMd(); - static DateFormat ymd = DateFormat.yMMMMd(); - static DateFormat day = DateFormat.E(); + // `MMMMd`: `April 15` + // `yMMMMd`: `April 15, 2020` + // `MMMEd`: `Wed, Apr 15` + // `yMMMEd`: `Wed, Apr 15, 2020` + // `MMMMEEEEd`: `Wednesday, April 15` + // `yMMMMEEEEd`: `Wednesday, April 15, 2020` + // `MEd`: `Wed, 4/15` + // `yMEd`: `Wed, 4/15/2020` - static String _formatDate(DateTime date) { - if (date.isToday) return 'Today'; - if (date.isYesterday) return 'Yesterday'; - if (date.isThisYear) return '${md.format(date)} (${day.format(date)})'; - return '${ymd.format(date)} (${day.format(date)})'; + // Examples (ko): + // `MMMMd`: `1월 26일` + // `yMMMMd`: `2021년 1월 26일` + // `MMMEd`: `1월 26일 (화)` + // `yMMMEd`: `2021년 1월 26일 (화)` + // `MMMMEEEEd`: `1월 26일 화요일` + // `yMMMMEEEEd`: `2021년 1월 26일 화요일` + // `MEd`: `1. 26. (화)` + // `yMEd`: `2021. 1. 26. (화)` + + static String _formatDate(BuildContext context, DateTime date) { + final l10n = context.l10n; + if (date == null) return l10n.sectionUnknown; + if (date.isToday) return l10n.dateToday; + if (date.isYesterday) return l10n.dateYesterday; + final locale = l10n.localeName; + if (date.isThisYear) return '${DateFormat.MMMMd(locale).format(date)} (${DateFormat.E(locale).format(date)})'; + return '${DateFormat.yMMMMd(locale).format(date)} (${DateFormat.E(locale).format(date)})'; } @override Widget build(BuildContext context) { return SectionHeader( sectionKey: EntryDateSectionKey(date), - title: text, + title: _formatDate(context, date), ); } } class MonthSectionHeader extends StatelessWidget { final DateTime date; - final String text; - MonthSectionHeader({ + const MonthSectionHeader({ Key key, @required this.date, - }) : text = _formatDate(date), - super(key: key); + }) : super(key: key); - static DateFormat m = DateFormat.MMMM(); - static DateFormat ym = DateFormat.yMMMM(); - - static String _formatDate(DateTime date) { - if (date == null) return 'Unknown'; - if (date.isThisMonth) return 'This month'; - if (date.isThisYear) return m.format(date); - return ym.format(date); + static String _formatDate(BuildContext context, DateTime date) { + final l10n = context.l10n; + if (date == null) return l10n.sectionUnknown; + if (date.isThisMonth) return l10n.dateThisMonth; + final locale = l10n.localeName; + if (date.isThisYear) return DateFormat.MMMM(locale).format(date); + return DateFormat.yMMMM(locale).format(date); } @override Widget build(BuildContext context) { return SectionHeader( sectionKey: EntryDateSectionKey(date), - title: text, + title: _formatDate(context, date), ); } } diff --git a/lib/widgets/collection/thumbnail/overlay.dart b/lib/widgets/collection/thumbnail/overlay.dart index 86dc5edb7..5a539790c 100644 --- a/lib/widgets/collection/thumbnail/overlay.dart +++ b/lib/widgets/collection/thumbnail/overlay.dart @@ -1,7 +1,7 @@ import 'dart:math'; -import 'package:aves/model/highlight.dart'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/highlight.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/enums.dart'; diff --git a/lib/widgets/collection/thumbnail/raster.dart b/lib/widgets/collection/thumbnail/raster.dart index e7ac7c516..400fa5b8d 100644 --- a/lib/widgets/collection/thumbnail/raster.dart +++ b/lib/widgets/collection/thumbnail/raster.dart @@ -3,6 +3,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/thumbnail/error.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/transition_image.dart'; import 'package:flutter/material.dart'; @@ -79,7 +80,7 @@ class _RasterImageThumbnailState extends State { @override Widget build(BuildContext context) { if (!entry.canDecode) { - return _buildError(context, '${entry.mimeType} not supported', null); + return _buildError(context, context.l10n.errorUnsupportedMimeType(entry.mimeType), null); } final fastImage = Image( diff --git a/lib/widgets/collection/thumbnail/vector.dart b/lib/widgets/collection/thumbnail/vector.dart index d9d379ff5..099780779 100644 --- a/lib/widgets/collection/thumbnail/vector.dart +++ b/lib/widgets/collection/thumbnail/vector.dart @@ -1,6 +1,7 @@ import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/entry_background.dart'; +import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index 709a56686..4989688fc 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -6,12 +6,11 @@ import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/highlight.dart'; import 'package:aves/model/source/collection_lens.dart'; -import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/enums.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/app_bar.dart'; -import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/collection/grid/section_layout.dart'; import 'package:aves/widgets/collection/grid/selector.dart'; import 'package:aves/widgets/collection/grid/thumbnail.dart'; @@ -23,6 +22,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/sliver.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:aves/widgets/common/scaling.dart'; import 'package:aves/widgets/common/tile_extent_manager.dart'; @@ -265,18 +265,18 @@ class _CollectionScrollViewState extends State { if (collection.filters.any((filter) => filter is FavouriteFilter)) { return EmptyContent( icon: AIcons.favourite, - text: 'No favourites', + text: context.l10n.collectionEmptyFavourites, ); } if (collection.filters.any((filter) => filter is MimeFilter && filter.mime == MimeTypes.anyVideo)) { return EmptyContent( icon: AIcons.video, - text: 'No videos', + text: context.l10n.collectionEmptyVideos, ); } return EmptyContent( icon: AIcons.image, - text: 'No images', + text: context.l10n.collectionEmptyImages, ); }, ); diff --git a/lib/widgets/common/action_mixins/permission_aware.dart b/lib/widgets/common/action_mixins/permission_aware.dart index cbd8ce7c9..92b51c639 100644 --- a/lib/widgets/common/action_mixins/permission_aware.dart +++ b/lib/widgets/common/action_mixins/permission_aware.dart @@ -1,6 +1,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/services/android_file_service.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:flutter/material.dart'; @@ -26,18 +27,20 @@ mixin PermissionAwareMixin { final confirmed = await showDialog( context: context, builder: (context) { + final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir); + final volume = dir.getVolumeDescription(context); return AvesDialog( context: context, - title: 'Storage Volume Access', - content: Text('Please select the ${dir.directoryDescription} directory of “${dir.volumeDescription}” in the next screen, so that this app can access it and complete your request.'), + title: context.l10n.storageAccessDialogTitle, + content: Text(context.l10n.storageVolumeAccessDialogMessage(directory, volume)), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('Cancel'.toUpperCase()), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), TextButton( onPressed: () => Navigator.pop(context, true), - child: Text('OK'.toUpperCase()), + child: Text(MaterialLocalizations.of(context).okButtonLabel), ), ], ); @@ -58,14 +61,16 @@ mixin PermissionAwareMixin { return showDialog( context: context, builder: (context) { + final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir); + final volume = dir.getVolumeDescription(context); return AvesDialog( context: context, - title: 'Restricted Access', - content: Text('This app is not allowed to modify files in the ${dir.directoryDescription} directory of “${dir.volumeDescription}”.\n\nPlease use a pre-installed file manager or gallery app to move the items to another directory.'), + title: context.l10n.restrictedAccessDialogTitle, + content: Text(context.l10n.restrictedAccessDialogMessage(directory, volume)), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('OK'.toUpperCase()), + child: Text(MaterialLocalizations.of(context).okButtonLabel), ), ], ); diff --git a/lib/widgets/common/action_mixins/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart index bf6e34b44..b3584fa37 100644 --- a/lib/widgets/common/action_mixins/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -6,6 +6,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/services/android_file_service.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/file_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -43,14 +44,17 @@ mixin SizeAwareMixin { await showDialog( context: context, builder: (context) { + final neededSize = formatFilesize(needed); + final freeSize = formatFilesize(free); + final volume = destinationVolume.getDescription(context); return AvesDialog( context: context, - title: 'Not Enough Space', - content: Text('This operation needs ${formatFilesize(needed)} of free space on “${destinationVolume.description}” to complete, but there is only ${formatFilesize(free)} left.'), + title: context.l10n.notEnoughSpaceDialogTitle, + content: Text(context.l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('OK'.toUpperCase()), + child: Text(MaterialLocalizations.of(context).okButtonLabel), ), ], ); diff --git a/lib/widgets/common/app_bar_subtitle.dart b/lib/widgets/common/app_bar_subtitle.dart index 8b44a7520..575a92d95 100644 --- a/lib/widgets/common/app_bar_subtitle.dart +++ b/lib/widgets/common/app_bar_subtitle.dart @@ -1,5 +1,7 @@ import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/enums.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; class SourceStateAwareAppBarTitle extends StatelessWidget { @@ -54,13 +56,13 @@ class SourceStateSubtitle extends StatelessWidget { String subtitle; switch (source.stateNotifier.value) { case SourceState.loading: - subtitle = 'Loading'; + subtitle = context.l10n.sourceStateLoading; break; case SourceState.cataloguing: - subtitle = 'Cataloguing'; + subtitle = context.l10n.sourceStateCataloguing; break; case SourceState.locating: - subtitle = 'Locating'; + subtitle = context.l10n.sourceStateLocating; break; case SourceState.ready: default: diff --git a/lib/widgets/common/app_bar_title.dart b/lib/widgets/common/app_bar_title.dart index d00745b8c..ed285d341 100644 --- a/lib/widgets/common/app_bar_title.dart +++ b/lib/widgets/common/app_bar_title.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -class TappableAppBarTitle extends StatelessWidget { +class InteractiveAppBarTitle extends StatelessWidget { final GestureTapCallback onTap; final Widget child; - const TappableAppBarTitle({ + const InteractiveAppBarTitle({ this.onTap, @required this.child, }); diff --git a/lib/widgets/common/basic/query_bar.dart b/lib/widgets/common/basic/query_bar.dart index 570e4589f..aa3834857 100644 --- a/lib/widgets/common/basic/query_bar.dart +++ b/lib/widgets/common/basic/query_bar.dart @@ -1,6 +1,7 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/debouncer.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -34,7 +35,7 @@ class _QueryBarState extends State { _controller.clear(); filterNotifier.value = ''; }, - tooltip: 'Clear', + tooltip: context.l10n.clearTooltip, ); return Row( diff --git a/lib/widgets/common/behaviour/double_back_pop.dart b/lib/widgets/common/behaviour/double_back_pop.dart index 4a844fdda..e3aa98d5c 100644 --- a/lib/widgets/common/behaviour/double_back_pop.dart +++ b/lib/widgets/common/behaviour/double_back_pop.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:overlay_support/overlay_support.dart'; @@ -37,7 +38,7 @@ class _DoubleBackPopScopeState extends State with FeedbackMi _stopBackTimer(); _backTimer = Timer(Durations.doubleBackTimerDelay, () => _backOnce = false); toast( - 'Tap “back” again to exit.', + context.l10n.doubleBackExitMessage, duration: Durations.doubleBackTimerDelay, ); return SynchronousFuture(false); diff --git a/lib/widgets/common/extensions/build_context.dart b/lib/widgets/common/extensions/build_context.dart index 3f06767b8..bd79ec90f 100644 --- a/lib/widgets/common/extensions/build_context.dart +++ b/lib/widgets/common/extensions/build_context.dart @@ -1,5 +1,8 @@ import 'package:flutter/widgets.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; extension ExtraContext on BuildContext { String get currentRouteName => ModalRoute.of(this)?.settings?.name; + + AppLocalizations get l10n => AppLocalizations.of(this); } diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart index da4b4eca5..2ca2500e4 100644 --- a/lib/widgets/common/grid/header.dart +++ b/lib/widgets/common/grid/header.dart @@ -4,6 +4,7 @@ import 'package:aves/model/source/section_keys.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; @@ -161,7 +162,7 @@ class _SectionSelectableLeading extends StatelessWidget { alignment: AlignmentDirectional.topStart, icon: Icon(selected ? AIcons.selected : AIcons.unselected), onPressed: onPressed, - tooltip: selected ? 'Deselect section' : 'Select section', + tooltip: selected ? context.l10n.collectionDeselectSectionTooltip : context.l10n.collectionSelectSectionTooltip, constraints: BoxConstraints( minHeight: leadingDimension, minWidth: leadingDimension, diff --git a/lib/widgets/common/grid/sliver.dart b/lib/widgets/common/grid/sliver.dart index 85ba6b917..1b1ce521f 100644 --- a/lib/widgets/common/grid/sliver.dart +++ b/lib/widgets/common/grid/sliver.dart @@ -10,6 +10,7 @@ import 'package:provider/provider.dart'; // Use a `SliverList` instead of multiple `SliverGrid` because having one `SliverGrid` per section does not scale up. // With the multiple `SliverGrid` solution, thumbnails at the beginning of each sections are built even though they are offscreen // because of `RenderSliverMultiBoxAdaptor.addInitialChild` called by `RenderSliverGrid.performLayout` (line 547), as of Flutter v1.17.0. +// cf https://github.com/flutter/flutter/issues/49027 class SectionedListSliver extends StatelessWidget { const SectionedListSliver(); diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 69c955659..e81ac3f52 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -73,7 +73,7 @@ class AvesFilterChip extends StatefulWidget { items: actions .map((action) => PopupMenuItem( value: action, - child: MenuRow(text: action.getText(), icon: action.getIcon()), + child: MenuRow(text: action.getText(context), icon: action.getIcon()), )) .toList(), ); @@ -103,10 +103,15 @@ class _AvesFilterChipState extends State { @override void initState() { super.initState(); - _initColorLoader(); _tapped = false; } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _initColorLoader(); + } + @override void didUpdateWidget(covariant AvesFilterChip oldWidget) { super.didUpdateWidget(oldWidget); @@ -146,7 +151,7 @@ class _AvesFilterChipState extends State { ], Flexible( child: Text( - filter.label, + filter.getLabel(context), softWrap: false, overflow: TextOverflow.fade, maxLines: 1, @@ -203,7 +208,7 @@ class _AvesFilterChipState extends State { child: widget.background, ), Tooltip( - message: filter.tooltip, + message: filter.getTooltip(context), preferBelow: false, child: Material( color: hasBackground ? Colors.transparent : Theme.of(context).scaffoldBackgroundColor, diff --git a/lib/widgets/collection/empty.dart b/lib/widgets/common/identity/empty.dart similarity index 100% rename from lib/widgets/collection/empty.dart rename to lib/widgets/common/identity/empty.dart diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart index 9a1759feb..d7bf2a394 100644 --- a/lib/widgets/debug/settings.dart +++ b/lib/widgets/debug/settings.dart @@ -40,6 +40,8 @@ class DebugSettingsSection extends StatelessWidget { 'pinnedFilters': toMultiline(settings.pinnedFilters), 'searchHistory': toMultiline(settings.searchHistory), 'lastVersionCheckDate': '${settings.lastVersionCheckDate}', + 'locale': '${settings.locale}', + 'systemLocale': '${WidgetsBinding.instance.window.locale}', }), ), ], diff --git a/lib/widgets/debug/storage.dart b/lib/widgets/debug/storage.dart index b3a1ac803..fb29874df 100644 --- a/lib/widgets/debug/storage.dart +++ b/lib/widgets/debug/storage.dart @@ -39,7 +39,7 @@ class _DebugStorageSectionState extends State with Automati Padding( padding: EdgeInsets.symmetric(horizontal: 8), child: InfoRowGroup({ - 'description': '${v.description}', + 'description': '${v.getDescription(context)}', 'isPrimary': '${v.isPrimary}', 'isRemovable': '${v.isRemovable}', 'state': '${v.state}', diff --git a/lib/widgets/dialogs/add_shortcut_dialog.dart b/lib/widgets/dialogs/add_shortcut_dialog.dart index b321ec0c1..a12edb60c 100644 --- a/lib/widgets/dialogs/add_shortcut_dialog.dart +++ b/lib/widgets/dialogs/add_shortcut_dialog.dart @@ -1,12 +1,14 @@ -import 'package:aves/model/filters/filters.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; import 'aves_dialog.dart'; class AddShortcutDialog extends StatefulWidget { - final Set filters; + final String defaultName; - const AddShortcutDialog(this.filters); + const AddShortcutDialog({ + @required this.defaultName, + }); @override _AddShortcutDialogState createState() => _AddShortcutDialogState(); @@ -19,12 +21,7 @@ class _AddShortcutDialogState extends State { @override void initState() { super.initState(); - final filters = List.from(widget.filters)..sort(); - if (filters.isEmpty) { - _nameController.text = 'Collection'; - } else { - _nameController.text = filters.first.label; - } + _nameController.text = widget.defaultName; _validate(); } @@ -41,7 +38,7 @@ class _AddShortcutDialogState extends State { content: TextField( controller: _nameController, decoration: InputDecoration( - labelText: 'Shortcut label', + labelText: context.l10n.addShortcutDialogLabel, ), autofocus: true, maxLength: 25, @@ -51,14 +48,14 @@ class _AddShortcutDialogState extends State { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('Cancel'.toUpperCase()), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), ValueListenableBuilder( valueListenable: _isValidNotifier, builder: (context, isValid, child) { return TextButton( onPressed: isValid ? () => _submit(context) : null, - child: Text('Add'.toUpperCase()), + child: Text(context.l10n.addShortcutButtonLabel), ); }, ) diff --git a/lib/widgets/dialogs/aves_dialog.dart b/lib/widgets/dialogs/aves_dialog.dart index 8427c6af3..1542726bd 100644 --- a/lib/widgets/dialogs/aves_dialog.dart +++ b/lib/widgets/dialogs/aves_dialog.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -92,12 +93,12 @@ void showNoMatchingAppDialog(BuildContext context) { builder: (context) { return AvesDialog( context: context, - title: 'No Matching App', - content: Text('There are no apps that can handle this.'), + title: context.l10n.noMatchingAppDialogTitle, + content: Text(context.l10n.noMatchingAppDialogMessage), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('OK'.toUpperCase()), + child: Text(MaterialLocalizations.of(context).okButtonLabel), ), ], ); diff --git a/lib/widgets/dialogs/aves_selection_dialog.dart b/lib/widgets/dialogs/aves_selection_dialog.dart index c4e5c7e4b..d2c8b9185 100644 --- a/lib/widgets/dialogs/aves_selection_dialog.dart +++ b/lib/widgets/dialogs/aves_selection_dialog.dart @@ -23,7 +23,7 @@ class AvesSelectionDialog extends StatefulWidget { _AvesSelectionDialogState createState() => _AvesSelectionDialogState(); } -class _AvesSelectionDialogState extends State { +class _AvesSelectionDialogState extends State> { T _selectedValue; @override @@ -41,13 +41,14 @@ class _AvesSelectionDialogState extends State { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('Cancel'.toUpperCase()), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), ], ); } Widget _buildRadioListTile(T value, String title) { + final subtitle = widget.optionSubtitleBuilder?.call(value); return ReselectableRadioListTile( key: Key(value.toString()), value: value, @@ -64,9 +65,9 @@ class _AvesSelectionDialogState extends State { overflow: TextOverflow.fade, maxLines: 1, ), - subtitle: widget.optionSubtitleBuilder != null + subtitle: subtitle != null ? Text( - widget.optionSubtitleBuilder(value), + subtitle, softWrap: false, overflow: TextOverflow.fade, maxLines: 1, diff --git a/lib/widgets/dialogs/create_album_dialog.dart b/lib/widgets/dialogs/create_album_dialog.dart index 26c41dad1..afc19ca6f 100644 --- a/lib/widgets/dialogs/create_album_dialog.dart +++ b/lib/widgets/dialogs/create_album_dialog.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -50,17 +51,17 @@ class _CreateAlbumDialogState extends State { volumeTiles.addAll([ Padding( padding: AvesDialog.contentHorizontalPadding + EdgeInsets.only(top: 20), - child: Text('Storage:'), + child: Text(context.l10n.newAlbumDialogStorageLabel), ), - ...primaryVolumes.map(_buildVolumeTile), - ...otherVolumes.map(_buildVolumeTile), + ...primaryVolumes.map((volume) => _buildVolumeTile(context, volume)), + ...otherVolumes.map((volume) => _buildVolumeTile(context, volume)), SizedBox(height: 8), ]); } return AvesDialog( context: context, - title: 'New Album', + title: context.l10n.newAlbumDialogTitle, scrollController: _scrollController, scrollableContent: [ ...volumeTiles, @@ -73,8 +74,8 @@ class _CreateAlbumDialogState extends State { controller: _nameController, focusNode: _nameFieldFocusNode, decoration: InputDecoration( - labelText: 'Album name', - helperText: exists ? 'Directory already exists' : '', + labelText: context.l10n.newAlbumDialogNameLabel, + helperText: exists ? context.l10n.newAlbumDialogNameLabelAlreadyExistsHelper : '', ), autofocus: _allVolumes.length == 1, onChanged: (_) => _validate(), @@ -86,14 +87,14 @@ class _CreateAlbumDialogState extends State { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('Cancel'.toUpperCase()), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), ValueListenableBuilder( valueListenable: _isValidNotifier, builder: (context, isValid, child) { return TextButton( onPressed: isValid ? () => _submit(context) : null, - child: Text('Create'.toUpperCase()), + child: Text(context.l10n.createAlbumButtonLabel), ); }, ), @@ -101,7 +102,7 @@ class _CreateAlbumDialogState extends State { ); } - Widget _buildVolumeTile(StorageVolume volume) => RadioListTile( + Widget _buildVolumeTile(BuildContext context, StorageVolume volume) => RadioListTile( value: volume, groupValue: _selectedVolume, onChanged: (volume) { @@ -110,7 +111,7 @@ class _CreateAlbumDialogState extends State { setState(() {}); }, title: Text( - volume.description, + volume.getDescription(context), softWrap: false, overflow: TextOverflow.fade, maxLines: 1, diff --git a/lib/widgets/dialogs/rename_album_dialog.dart b/lib/widgets/dialogs/rename_album_dialog.dart index 7997464fb..6d16f74fb 100644 --- a/lib/widgets/dialogs/rename_album_dialog.dart +++ b/lib/widgets/dialogs/rename_album_dialog.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as path; @@ -46,8 +47,8 @@ class _RenameAlbumDialogState extends State { return TextField( controller: _nameController, decoration: InputDecoration( - labelText: 'New name', - helperText: exists ? 'Directory already exists' : '', + labelText: context.l10n.renameAlbumDialogLabel, + helperText: exists ? context.l10n.renameAlbumDialogLabelAlreadyExistsHelper : '', ), autofocus: true, onChanged: (_) => _validate(), @@ -57,14 +58,14 @@ class _RenameAlbumDialogState extends State { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('Cancel'.toUpperCase()), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), ValueListenableBuilder( valueListenable: _isValidNotifier, builder: (context, isValid, child) { return TextButton( onPressed: isValid ? () => _submit(context) : null, - child: Text('Apply'.toUpperCase()), + child: Text(context.l10n.applyButtonLabel), ); }, ) diff --git a/lib/widgets/dialogs/rename_entry_dialog.dart b/lib/widgets/dialogs/rename_entry_dialog.dart index d73410ba9..c72028759 100644 --- a/lib/widgets/dialogs/rename_entry_dialog.dart +++ b/lib/widgets/dialogs/rename_entry_dialog.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:aves/model/entry.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as path; @@ -41,7 +42,7 @@ class _RenameEntryDialogState extends State { content: TextField( controller: _nameController, decoration: InputDecoration( - labelText: 'New name', + labelText: context.l10n.renameEntryDialogLabel, suffixText: entry.extension, ), autofocus: true, @@ -51,14 +52,14 @@ class _RenameEntryDialogState extends State { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('Cancel'.toUpperCase()), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), ValueListenableBuilder( valueListenable: _isValidNotifier, builder: (context, isValid, child) { return TextButton( onPressed: isValid ? () => _submit(context) : null, - child: Text('Apply'.toUpperCase()), + child: Text(context.l10n.applyButtonLabel), ); }, ) diff --git a/lib/widgets/drawer/album_tile.dart b/lib/widgets/drawer/album_tile.dart index ca8b17fd5..6bb8b2199 100644 --- a/lib/widgets/drawer/album_tile.dart +++ b/lib/widgets/drawer/album_tile.dart @@ -15,7 +15,7 @@ class AlbumTile extends StatelessWidget { @override Widget build(BuildContext context) { final source = context.read(); - final uniqueName = source.getUniqueAlbumName(album); + final uniqueName = source.getUniqueAlbumName(context, album); return CollectionNavTile( leading: IconUtils.getAlbumIcon( context: context, diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index c3322e4dd..8e6cb704e 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -12,6 +12,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/about/about_page.dart'; import 'package:aves/widgets/about/news_badge.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/debug/app_debug_page.dart'; @@ -105,7 +106,7 @@ class _AppDrawerState extends State { children: [ AvesLogo(size: 64), Text( - 'Aves', + context.l10n.appName, style: TextStyle( fontSize: 44, fontWeight: FontWeight.w300, @@ -146,25 +147,25 @@ class _AppDrawerState extends State { Widget get allCollectionTile => CollectionNavTile( leading: Icon(AIcons.allCollection), - title: 'All collection', + title: context.l10n.drawerCollectionAll, filter: null, ); Widget get videoTile => CollectionNavTile( leading: Icon(AIcons.video), - title: 'Videos', + title: context.l10n.drawerCollectionVideos, filter: MimeFilter(MimeTypes.anyVideo), ); Widget get favouriteTile => CollectionNavTile( leading: Icon(AIcons.favourite), - title: 'Favourites', + title: context.l10n.drawerCollectionFavourites, filter: FavouriteFilter(), ); Widget get albumListTile => NavTile( icon: AIcons.album, - title: 'Albums', + title: context.l10n.albumPageTitle, trailing: StreamBuilder( stream: source.eventBus.on(), builder: (context, _) => Text('${source.rawAlbums.length}'), @@ -175,7 +176,7 @@ class _AppDrawerState extends State { Widget get countryListTile => NavTile( icon: AIcons.location, - title: 'Countries', + title: context.l10n.countryPageTitle, trailing: StreamBuilder( stream: source.eventBus.on(), builder: (context, _) => Text('${source.sortedCountries.length}'), @@ -186,7 +187,7 @@ class _AppDrawerState extends State { Widget get tagListTile => NavTile( icon: AIcons.tag, - title: 'Tags', + title: context.l10n.tagPageTitle, trailing: StreamBuilder( stream: source.eventBus.on(), builder: (context, _) => Text('${source.sortedTags.length}'), @@ -197,7 +198,7 @@ class _AppDrawerState extends State { Widget get settingsTile => NavTile( icon: AIcons.settings, - title: 'Settings', + title: context.l10n.settingsPageTitle, topLevel: false, routeName: SettingsPage.routeName, pageBuilder: (_) => SettingsPage(), @@ -209,7 +210,7 @@ class _AppDrawerState extends State { final newVersion = snapshot.data == true; return NavTile( icon: AIcons.info, - title: 'About', + title: context.l10n.aboutPageTitle, trailing: newVersion ? AboutNewsBadge() : null, topLevel: false, routeName: AboutPage.routeName, diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index e21fc62a3..d9006329c 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -7,10 +7,11 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/basic/menu_row.dart'; import 'package:aves/widgets/common/basic/query_bar.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/dialogs/create_album_dialog.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; @@ -58,7 +59,7 @@ class _AlbumPickPageState extends State { stream: source.eventBus.on(), builder: (context, snapshot) => FilterGridPage( appBar: appBar, - filterSections: AlbumListPage.getAlbumEntries(source), + filterSections: AlbumListPage.getAlbumEntries(context, source), showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, applyQuery: (filters, query) { if (query == null || query.isEmpty) return filters; @@ -68,7 +69,7 @@ class _AlbumPickPageState extends State { queryNotifier: _queryNotifier, emptyBuilder: () => EmptyContent( icon: AIcons.album, - text: 'No albums', + text: context.l10n.albumEmpty, ), settingsRouteKey: AlbumListPage.routeName, appBarHeight: AlbumPickAppBar.preferredHeight, @@ -100,11 +101,11 @@ class AlbumPickAppBar extends StatelessWidget { String title() { switch (moveType) { case MoveType.copy: - return 'Copy to Album'; + return context.l10n.albumPickPageTitleCopy; case MoveType.export: - return 'Export to Album'; + return context.l10n.albumPickPageTitleExport; case MoveType.move: - return 'Move to Album'; + return context.l10n.albumPickPageTitleMove; default: return null; } @@ -131,22 +132,26 @@ class AlbumPickAppBar extends StatelessWidget { Navigator.pop(context, newAlbum); } }, - tooltip: 'Create album', + tooltip: context.l10n.createAlbumTooltip, ), PopupMenuButton( itemBuilder: (context) { return [ PopupMenuItem( value: ChipSetAction.sort, - child: MenuRow(text: 'Sort…', icon: AIcons.sort), + child: MenuRow(text: context.l10n.menuActionSort, icon: AIcons.sort), ), PopupMenuItem( value: ChipSetAction.group, - child: MenuRow(text: 'Group…', icon: AIcons.group), + child: MenuRow(text: context.l10n.menuActionGroup, icon: AIcons.group), ), ]; }, onSelected: (action) { + // remove focus, if any, to prevent the keyboard from showing up + // after the user is done with the popup menu + FocusManager.instance.primaryFocus?.unfocus(); + // wait for the popup menu to hide before proceeding with the action Future.delayed(Durations.popupMenuAnimation * timeDilation, () => actionDelegate.onActionSelected(context, action)); }, diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index 10350bea2..baa5e3145 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -7,7 +7,8 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/widgets/collection/empty.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; @@ -32,7 +33,7 @@ class AlbumListPage extends StatelessWidget { stream: source.eventBus.on(), builder: (context, snapshot) => FilterNavigationPage( source: source, - title: 'Albums', + title: context.l10n.albumPageTitle, groupable: true, showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, chipSetActionDelegate: AlbumChipSetActionDelegate(), @@ -43,10 +44,10 @@ class AlbumListPage extends StatelessWidget { ChipAction.delete, ChipAction.hide, ], - filterSections: getAlbumEntries(source), + filterSections: getAlbumEntries(context, source), emptyBuilder: () => EmptyContent( icon: AIcons.album, - text: 'No albums', + text: context.l10n.albumEmpty, ), ), ), @@ -57,14 +58,14 @@ class AlbumListPage extends StatelessWidget { // common with album selection page to move/copy entries - static Map>> getAlbumEntries(CollectionSource source) { - final filters = source.rawAlbums.map((album) => AlbumFilter(album, source.getUniqueAlbumName(album))).toSet(); + static Map>> getAlbumEntries(BuildContext context, CollectionSource source) { + final filters = source.rawAlbums.map((album) => AlbumFilter(album, source.getUniqueAlbumName(context, album))).toSet(); final sorted = FilterNavigationPage.sort(settings.albumSortFactor, source, filters); - return _group(sorted); + return _group(context, sorted); } - static Map>> _group(Iterable> sortedMapEntries) { + static Map>> _group(BuildContext context, Iterable> sortedMapEntries) { final pinned = settings.pinnedFilters.whereType(); final byPin = groupBy, bool>(sortedMapEntries, (e) => pinned.contains(e.filter)); final pinnedMapEntries = (byPin[true] ?? []); @@ -73,25 +74,28 @@ class AlbumListPage extends StatelessWidget { var sections = >>{}; switch (settings.albumGroupFactor) { case AlbumChipGroupFactor.importance: + final specialKey = AlbumImportanceSectionKey.special(context); + final appsKey = AlbumImportanceSectionKey.apps(context); + final regularKey = AlbumImportanceSectionKey.regular(context); sections = groupBy, ChipSectionKey>(unpinnedMapEntries, (kv) { switch (androidFileUtils.getAlbumType(kv.filter.album)) { case AlbumType.regular: - return AlbumImportanceSectionKey.regular; + return regularKey; case AlbumType.app: - return AlbumImportanceSectionKey.apps; + return appsKey; default: - return AlbumImportanceSectionKey.special; + return specialKey; } }); sections = { - AlbumImportanceSectionKey.special: sections[AlbumImportanceSectionKey.special], - AlbumImportanceSectionKey.apps: sections[AlbumImportanceSectionKey.apps], - AlbumImportanceSectionKey.regular: sections[AlbumImportanceSectionKey.regular], + specialKey: sections[specialKey], + appsKey: sections[appsKey], + regularKey: sections[regularKey], }..removeWhere((key, value) => value == null); break; case AlbumChipGroupFactor.volume: sections = groupBy, ChipSectionKey>(unpinnedMapEntries, (kv) { - return StorageVolumeSectionKey(androidFileUtils.getStorageVolume(kv.filter.album)); + return StorageVolumeSectionKey(context, androidFileUtils.getStorageVolume(kv.filter.album)); }); break; case AlbumChipGroupFactor.none: @@ -106,7 +110,7 @@ class AlbumListPage extends StatelessWidget { if (pinnedMapEntries.isNotEmpty) { sections = Map.fromEntries([ - MapEntry(AlbumImportanceSectionKey.pinned, pinnedMapEntries), + MapEntry(AlbumImportanceSectionKey.pinned(context), pinnedMapEntries), ...sections.entries, ]); } diff --git a/lib/widgets/filter_grids/common/chip_action_delegate.dart b/lib/widgets/filter_grids/common/chip_action_delegate.dart index 00f4e67d7..07e5c0e5c 100644 --- a/lib/widgets/filter_grids/common/chip_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_action_delegate.dart @@ -10,13 +10,13 @@ import 'package:aves/services/image_op_events.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/rename_album_dialog.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:path/path.dart' as path; import 'package:provider/provider.dart'; @@ -52,15 +52,15 @@ class ChipActionDelegate { builder: (context) { return AvesDialog( context: context, - content: Text('Matching photos and videos will be hidden from your collection. You can show them again from the “Privacy” settings.\n\nAre you sure you want to hide them?'), + content: Text(context.l10n.hideFilterConfirmationDialogMessage), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('Cancel'.toUpperCase()), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), TextButton( onPressed: () => Navigator.pop(context, true), - child: Text('Hide'.toUpperCase()), + child: Text(context.l10n.hideButtonLabel), ), ], ); @@ -116,15 +116,15 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per builder: (context) { return AvesDialog( context: context, - content: Text('Are you sure you want to delete this album and its ${Intl.plural(count, one: 'item', other: '$count items')}?'), + content: Text(context.l10n.deleteAlbumConfirmationDialogMessage(count)), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('Cancel'.toUpperCase()), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), TextButton( onPressed: () => Navigator.pop(context, true), - child: Text('Delete'.toUpperCase()), + child: Text(context.l10n.deleteButtonLabel), ), ], ); @@ -145,7 +145,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per final deletedCount = deletedUris.length; if (deletedCount < selectionCount) { final count = selectionCount - deletedCount; - showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}'); + showFeedback(context, context.l10n.collectionDeleteFailureFeedback(count)); } source.removeEntries(deletedUris); source.resumeMonitoring(); @@ -183,9 +183,9 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per final movedCount = movedOps.length; if (movedCount < todoCount) { final count = todoCount - movedCount; - showFeedback(context, 'Failed to move ${Intl.plural(count, one: '$count item', other: '$count items')}'); + showFeedback(context, context.l10n.collectionMoveFailureFeedback(count)); } else { - showFeedback(context, 'Done!'); + showFeedback(context, context.l10n.genericSuccessFeedback); } final pinned = settings.pinnedFilters.contains(filter); await source.updateAfterMove( @@ -197,7 +197,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per ); // repin new album after obsolete album got removed and unpinned if (pinned) { - final newFilter = AlbumFilter(destinationAlbum, source.getUniqueAlbumName(destinationAlbum)); + final newFilter = AlbumFilter(destinationAlbum, source.getUniqueAlbumName(context, destinationAlbum)); settings.pinnedFilters = settings.pinnedFilters..add(newFilter); } source.resumeMonitoring(); diff --git a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart index 1ea75bcab..299a47744 100644 --- a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart @@ -2,6 +2,7 @@ import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/stats/stats.dart'; import 'package:flutter/material.dart'; @@ -13,14 +14,10 @@ abstract class ChipSetActionDelegate { set sortFactor(ChipSortFactor factor); void onActionSelected(BuildContext context, ChipSetAction action) { - final source = context.read(); switch (action) { case ChipSetAction.sort: _showSortDialog(context); break; - case ChipSetAction.refresh: - source.refresh(); - break; case ChipSetAction.stats: _goToStats(context); break; @@ -35,11 +32,11 @@ abstract class ChipSetActionDelegate { builder: (context) => AvesSelectionDialog( initialValue: sortFactor, options: { - ChipSortFactor.date: 'By date', - ChipSortFactor.name: 'By name', - ChipSortFactor.count: 'By item count', + ChipSortFactor.date: context.l10n.chipSortDate, + ChipSortFactor.name: context.l10n.chipSortName, + ChipSortFactor.count: context.l10n.chipSortCount, }, - title: 'Sort', + title: context.l10n.chipSortTitle, ), ); if (factor != null) { @@ -86,11 +83,11 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate { builder: (context) => AvesSelectionDialog( initialValue: settings.albumGroupFactor, options: { - AlbumChipGroupFactor.importance: 'By tier', - AlbumChipGroupFactor.volume: 'By storage volume', - AlbumChipGroupFactor.none: 'Do not group', + AlbumChipGroupFactor.importance: context.l10n.albumGroupTier, + AlbumChipGroupFactor.volume: context.l10n.albumGroupVolume, + AlbumChipGroupFactor.none: context.l10n.albumGroupNone, }, - title: 'Group', + title: context.l10n.albumGroupTitle, ), ); if (factor != null) { diff --git a/lib/widgets/filter_grids/common/filter_nav_page.dart b/lib/widgets/filter_grids/common/filter_nav_page.dart index 5ac7e20da..86db06aa2 100644 --- a/lib/widgets/filter_grids/common/filter_nav_page.dart +++ b/lib/widgets/filter_grids/common/filter_nav_page.dart @@ -12,6 +12,7 @@ import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/basic/menu_row.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; @@ -49,7 +50,7 @@ class FilterNavigationPage extends StatelessWidget { return FilterGridPage( key: ValueKey('filter-grid-page'), appBar: SliverAppBar( - title: TappableAppBarTitle( + title: InteractiveAppBarTitle( onTap: () => _goToSearch(context), child: SourceStateAwareAppBarTitle( title: Text(title), @@ -93,7 +94,7 @@ class FilterNavigationPage extends StatelessWidget { items: chipActionsBuilder(filter) .map((action) => PopupMenuItem( value: action, - child: MenuRow(text: action.getText(), icon: action.getIcon()), + child: MenuRow(text: action.getText(context), icon: action.getIcon()), )) .toList(), ); @@ -113,21 +114,16 @@ class FilterNavigationPage extends StatelessWidget { PopupMenuItem( key: Key('menu-sort'), value: ChipSetAction.sort, - child: MenuRow(text: 'Sort…', icon: AIcons.sort), + child: MenuRow(text: context.l10n.menuActionSort, icon: AIcons.sort), ), if (groupable) PopupMenuItem( value: ChipSetAction.group, - child: MenuRow(text: 'Group…', icon: AIcons.group), - ), - if (kDebugMode) - PopupMenuItem( - value: ChipSetAction.refresh, - child: MenuRow(text: 'Refresh', icon: AIcons.refresh), + child: MenuRow(text: context.l10n.menuActionGroup, icon: AIcons.group), ), PopupMenuItem( value: ChipSetAction.stats, - child: MenuRow(text: 'Stats', icon: AIcons.stats), + child: MenuRow(text: context.l10n.menuActionStats, icon: AIcons.stats), ), ]; }, diff --git a/lib/widgets/filter_grids/common/section_keys.dart b/lib/widgets/filter_grids/common/section_keys.dart index 1f672758f..2ba69df8a 100644 --- a/lib/widgets/filter_grids/common/section_keys.dart +++ b/lib/widgets/filter_grids/common/section_keys.dart @@ -1,6 +1,7 @@ import 'package:aves/model/source/section_keys.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -29,12 +30,15 @@ class ChipSectionKey extends SectionKey { class AlbumImportanceSectionKey extends ChipSectionKey { final AlbumImportance importance; - AlbumImportanceSectionKey._private(this.importance) : super(title: importance.getText()); + AlbumImportanceSectionKey._private(BuildContext context, this.importance) : super(title: importance.getText(context)); - static AlbumImportanceSectionKey pinned = AlbumImportanceSectionKey._private(AlbumImportance.pinned); - static AlbumImportanceSectionKey special = AlbumImportanceSectionKey._private(AlbumImportance.special); - static AlbumImportanceSectionKey apps = AlbumImportanceSectionKey._private(AlbumImportance.apps); - static AlbumImportanceSectionKey regular = AlbumImportanceSectionKey._private(AlbumImportance.regular); + factory AlbumImportanceSectionKey.pinned(BuildContext context) => AlbumImportanceSectionKey._private(context, AlbumImportance.pinned); + + factory AlbumImportanceSectionKey.special(BuildContext context) => AlbumImportanceSectionKey._private(context, AlbumImportance.special); + + factory AlbumImportanceSectionKey.apps(BuildContext context) => AlbumImportanceSectionKey._private(context, AlbumImportance.apps); + + factory AlbumImportanceSectionKey.regular(BuildContext context) => AlbumImportanceSectionKey._private(context, AlbumImportance.regular); @override Widget get leading => Icon(importance.getIcon()); @@ -43,16 +47,16 @@ class AlbumImportanceSectionKey extends ChipSectionKey { enum AlbumImportance { pinned, special, apps, regular } extension ExtraAlbumImportance on AlbumImportance { - String getText() { + String getText(BuildContext context) { switch (this) { case AlbumImportance.pinned: - return 'Pinned'; + return context.l10n.albumTierPinned; case AlbumImportance.special: - return 'Common'; + return context.l10n.albumTierSpecial; case AlbumImportance.apps: - return 'Apps'; + return context.l10n.albumTierApps; case AlbumImportance.regular: - return 'Others'; + return context.l10n.albumTierRegular; } return null; } @@ -75,7 +79,7 @@ extension ExtraAlbumImportance on AlbumImportance { class StorageVolumeSectionKey extends ChipSectionKey { final StorageVolume volume; - StorageVolumeSectionKey(this.volume) : super(title: volume?.description ?? 'Unknown'); + StorageVolumeSectionKey(BuildContext context, this.volume) : super(title: volume?.getDescription(context) ?? context.l10n.sectionUnknown); @override Widget get leading => (volume?.isRemovable ?? false) ? Icon(AIcons.removableStorage) : null; diff --git a/lib/widgets/filter_grids/countries_page.dart b/lib/widgets/filter_grids/countries_page.dart index e2d7a5b5a..6cf097d8f 100644 --- a/lib/widgets/filter_grids/countries_page.dart +++ b/lib/widgets/filter_grids/countries_page.dart @@ -6,7 +6,8 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/location.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/collection/empty.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; @@ -29,7 +30,7 @@ class CountryListPage extends StatelessWidget { stream: source.eventBus.on(), builder: (context, snapshot) => FilterNavigationPage( source: source, - title: 'Countries', + title: context.l10n.countryPageTitle, chipSetActionDelegate: CountryChipSetActionDelegate(), chipActionDelegate: ChipActionDelegate(), chipActionsBuilder: (filter) => [ @@ -39,7 +40,7 @@ class CountryListPage extends StatelessWidget { filterSections: _getCountryEntries(source), emptyBuilder: () => EmptyContent( icon: AIcons.location, - text: 'No countries', + text: context.l10n.countryEmpty, ), ), ); diff --git a/lib/widgets/filter_grids/tags_page.dart b/lib/widgets/filter_grids/tags_page.dart index ee860fd6f..9ed99e93a 100644 --- a/lib/widgets/filter_grids/tags_page.dart +++ b/lib/widgets/filter_grids/tags_page.dart @@ -6,7 +6,8 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/collection/empty.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; @@ -29,7 +30,7 @@ class TagListPage extends StatelessWidget { stream: source.eventBus.on(), builder: (context, snapshot) => FilterNavigationPage( source: source, - title: 'Tags', + title: context.l10n.tagPageTitle, chipSetActionDelegate: TagChipSetActionDelegate(), chipActionDelegate: ChipActionDelegate(), chipActionsBuilder: (filter) => [ @@ -39,7 +40,7 @@ class TagListPage extends StatelessWidget { filterSections: _getTagEntries(source), emptyBuilder: () => EmptyContent( icon: AIcons.tag, - text: 'No tags', + text: context.l10n.tagEmpty, ), ), ); diff --git a/lib/widgets/search/expandable_filter_row.dart b/lib/widgets/search/expandable_filter_row.dart index c5501f33f..c88582b71 100644 --- a/lib/widgets/search/expandable_filter_row.dart +++ b/lib/widgets/search/expandable_filter_row.dart @@ -45,7 +45,7 @@ class ExpandableFilterRow extends StatelessWidget { IconButton( icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand), onPressed: () => expandedNotifier.value = isExpanded ? null : title, - tooltip: isExpanded ? 'Collapse' : 'Expand', + tooltip: isExpanded ? MaterialLocalizations.of(context).expandedIconTapHint : MaterialLocalizations.of(context).collapsedIconTapHint, ), ], ), diff --git a/lib/widgets/search/search_button.dart b/lib/widgets/search/search_button.dart index 4e99cc457..3e377a670 100644 --- a/lib/widgets/search/search_button.dart +++ b/lib/widgets/search/search_button.dart @@ -16,7 +16,7 @@ class CollectionSearchButton extends StatelessWidget { key: Key('search-button'), icon: Icon(AIcons.search), onPressed: () => _goToSearch(context), - tooltip: 'Search', + tooltip: MaterialLocalizations.of(context).searchFieldLabel, ); } diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index dfcbc4a21..1d8caeefe 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -15,11 +15,13 @@ import 'package:aves/model/source/tag.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/search/expandable_filter_row.dart'; import 'package:aves/widgets/search/search_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; class CollectionSearchDelegate { final CollectionSource source; @@ -27,6 +29,16 @@ class CollectionSearchDelegate { final ValueNotifier expandedSectionNotifier = ValueNotifier(null); static const searchHistoryCount = 10; + static final typeFilters = [ + FavouriteFilter(), + MimeFilter(MimeTypes.anyImage), + MimeFilter(MimeTypes.anyVideo), + MimeFilter(MimeTypes.svg), + TypeFilter(TypeFilter.animated), + TypeFilter(TypeFilter.panorama), + TypeFilter(TypeFilter.sphericalVideo), + TypeFilter(TypeFilter.geotiff), + ]; CollectionSearchDelegate({@required this.source, this.parentCollection}); @@ -58,7 +70,7 @@ class CollectionSearchDelegate { query = ''; showSuggestions(context); }, - tooltip: 'Clear', + tooltip: context.l10n.clearTooltip, ), ]; } @@ -71,84 +83,82 @@ class CollectionSearchDelegate { valueListenable: expandedSectionNotifier, builder: (context, expandedSection, child) { final queryFilter = _buildQueryFilter(false); - final history = settings.searchHistory; - return ListView( - padding: EdgeInsets.only(top: 8), - children: [ - _buildFilterRow( - context: context, - filters: [ - queryFilter, - FavouriteFilter(), - MimeFilter(MimeTypes.anyImage), - MimeFilter(MimeTypes.anyVideo), - MimeFilter(MimeTypes.svg), - TypeFilter(TypeFilter.animated), - TypeFilter(TypeFilter.panorama), - TypeFilter(TypeFilter.sphericalVideo), - TypeFilter(TypeFilter.geotiff), - ].where((f) => f != null && containQuery(f.label)).toList(), - // usually perform hero animation only on tapped chips, - // but we also need to animate the query chip when it is selected by submitting the search query - heroTypeBuilder: (filter) => filter == queryFilter ? HeroType.always : HeroType.onTap, - ), - if (upQuery.isEmpty && history.isNotEmpty) - _buildFilterRow( - context: context, - title: 'Recent', - filters: history, - ), - StreamBuilder( - stream: source.eventBus.on(), - builder: (context, snapshot) { - // filter twice: full path, and then unique name - final filters = source.rawAlbums.where(containQuery).map((s) => AlbumFilter(s, source.getUniqueAlbumName(s))).where((f) => containQuery(f.uniqueName)).toList()..sort(); - return _buildFilterRow( + return Selector>( + selector: (context, s) => s.hiddenFilters, + builder: (context, hiddenFilters, child) { + bool notHidden(CollectionFilter filter) => !hiddenFilters.contains(filter); + final history = settings.searchHistory.where(notHidden).toList(); + return ListView( + padding: EdgeInsets.only(top: 8), + children: [ + _buildFilterRow( context: context, - title: 'Albums', - filters: filters, - ); - }), - StreamBuilder( - stream: source.eventBus.on(), - builder: (context, snapshot) { - final filters = source.sortedCountries.where(containQuery).map((s) => LocationFilter(LocationLevel.country, s)).toList(); - return _buildFilterRow( - context: context, - title: 'Countries', - filters: filters, - ); - }), - StreamBuilder( - stream: source.eventBus.on(), - builder: (context, snapshot) { - final filters = source.sortedPlaces.where(containQuery).map((s) => LocationFilter(LocationLevel.place, s)); - final noFilter = LocationFilter(LocationLevel.place, ''); - return _buildFilterRow( - context: context, - title: 'Places', filters: [ - if (containQuery(LocationFilter.emptyLabel)) noFilter, - ...filters, - ], - ); - }), - StreamBuilder( - stream: source.eventBus.on(), - builder: (context, snapshot) { - final filters = source.sortedTags.where(containQuery).map((s) => TagFilter(s)); - final noFilter = TagFilter(''); - return _buildFilterRow( - context: context, - title: 'Tags', - filters: [ - if (containQuery(TagFilter.emptyLabel)) noFilter, - ...filters, - ], - ); - }), - ], - ); + queryFilter, + ...typeFilters.where(notHidden), + ].where((f) => f != null && containQuery(f.getLabel(context))).toList(), + // usually perform hero animation only on tapped chips, + // but we also need to animate the query chip when it is selected by submitting the search query + heroTypeBuilder: (filter) => filter == queryFilter ? HeroType.always : HeroType.onTap, + ), + if (upQuery.isEmpty && history.isNotEmpty) + _buildFilterRow( + context: context, + title: context.l10n.searchSectionRecent, + filters: history, + ), + StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) { + // filter twice: full path, and then unique name + final filters = source.rawAlbums.where(containQuery).map((s) => AlbumFilter(s, source.getUniqueAlbumName(context, s))).where((f) => containQuery(f.uniqueName)).toList()..sort(); + return _buildFilterRow( + context: context, + title: context.l10n.searchSectionAlbums, + filters: filters, + ); + }), + StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) { + final filters = source.sortedCountries.where(containQuery).map((s) => LocationFilter(LocationLevel.country, s)).toList(); + return _buildFilterRow( + context: context, + title: context.l10n.searchSectionCountries, + filters: filters, + ); + }), + StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) { + final filters = source.sortedPlaces.where(containQuery).map((s) => LocationFilter(LocationLevel.place, s)); + final noFilter = LocationFilter(LocationLevel.place, ''); + return _buildFilterRow( + context: context, + title: context.l10n.searchSectionPlaces, + filters: [ + if (containQuery(noFilter.getLabel(context))) noFilter, + ...filters, + ], + ); + }), + StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) { + final filters = source.sortedTags.where(containQuery).map((s) => TagFilter(s)); + final noFilter = TagFilter(''); + return _buildFilterRow( + context: context, + title: context.l10n.searchSectionTags, + filters: [ + if (containQuery(noFilter.getLabel(context))) noFilter, + ...filters, + ], + ); + }), + ], + ); + }); }), ); } diff --git a/lib/widgets/search/search_page.dart b/lib/widgets/search/search_page.dart index e85ced83d..89e68656a 100644 --- a/lib/widgets/search/search_page.dart +++ b/lib/widgets/search/search_page.dart @@ -1,5 +1,6 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/utils/debouncer.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -11,8 +12,8 @@ class SearchPage extends StatefulWidget { final Animation animation; const SearchPage({ - this.delegate, - this.animation, + @required this.delegate, + @required this.animation, }); @override @@ -118,7 +119,7 @@ class _SearchPageState extends State { onSubmitted: (_) => widget.delegate.showResults(context), decoration: InputDecoration( border: InputBorder.none, - hintText: 'Search collection', + hintText: context.l10n.searchCollectionFieldHint, hintStyle: theme.inputDecorationTheme.hintStyle, ), ), diff --git a/lib/widgets/settings/access_grants.dart b/lib/widgets/settings/access_grants.dart index ef9bb7808..d69ad1b1a 100644 --- a/lib/widgets/settings/access_grants.dart +++ b/lib/widgets/settings/access_grants.dart @@ -1,13 +1,14 @@ import 'package:aves/services/android_file_service.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/collection/empty.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; import 'package:flutter/material.dart'; class StorageAccessTile extends StatelessWidget { @override Widget build(BuildContext context) { return ListTile( - title: Text('Storage Access'), + title: Text(context.l10n.settingsStorageAccessTile), onTap: () { Navigator.push( context, @@ -44,7 +45,7 @@ class _StorageAccessPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('Storage Access'), + title: Text(context.l10n.settingsStorageAccessTitle), ), body: SafeArea( child: Column( @@ -56,7 +57,7 @@ class _StorageAccessPageState extends State { children: [ Icon(AIcons.info), SizedBox(width: 16), - Expanded(child: Text('Some directories require an explicit access grant to modify files in them. You can review here directories to which you previously gave access.')), + Expanded(child: Text(context.l10n.settingsStorageAccessBanner)), ], ), ), @@ -74,7 +75,7 @@ class _StorageAccessPageState extends State { _lastPaths = snapshot.data..sort(); if (_lastPaths.isEmpty) { return EmptyContent( - text: 'No access grants', + text: context.l10n.settingsStorageAccessEmpty, ); } return Column( @@ -90,7 +91,7 @@ class _StorageAccessPageState extends State { _load(); setState(() {}); }, - tooltip: 'Revoke', + tooltip: context.l10n.settingsStorageAccessRevokeTooltip, ), )) .toList(), diff --git a/lib/widgets/settings/entry_background.dart b/lib/widgets/settings/entry_background.dart index 51b0a9326..b0f1995f0 100644 --- a/lib/widgets/settings/entry_background.dart +++ b/lib/widgets/settings/entry_background.dart @@ -1,4 +1,5 @@ import 'package:aves/model/settings/entry_background.dart'; +import 'package:aves/model/settings/enums.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/settings/hidden_filters.dart b/lib/widgets/settings/hidden_filters.dart index 09d57a2a6..526b353f8 100644 --- a/lib/widgets/settings/hidden_filters.dart +++ b/lib/widgets/settings/hidden_filters.dart @@ -1,8 +1,9 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/collection/empty.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -10,7 +11,7 @@ class HiddenFilterTile extends StatelessWidget { @override Widget build(BuildContext context) { return ListTile( - title: Text('Hidden filters'), + title: Text(context.l10n.settingsHiddenFiltersTile), onTap: () { Navigator.push( context, @@ -31,7 +32,7 @@ class HiddenFilterPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('Hidden Filters'), + title: Text(context.l10n.settingsHiddenFiltersTitle), ), body: SafeArea( child: Column( @@ -43,7 +44,7 @@ class HiddenFilterPage extends StatelessWidget { children: [ Icon(AIcons.info), SizedBox(width: 16), - Expanded(child: Text('Photos and videos matching hidden filters will not appear in your collection.')), + Expanded(child: Text(context.l10n.settingsHiddenFiltersBanner)), ], ), ), @@ -58,7 +59,7 @@ class HiddenFilterPage extends StatelessWidget { if (hiddenFilters.isEmpty) { return EmptyContent( icon: AIcons.hide, - text: 'No hidden filters', + text: context.l10n.settingsHiddenFiltersEmpty, ); } return Wrap( diff --git a/lib/widgets/settings/language.dart b/lib/widgets/settings/language.dart new file mode 100644 index 000000000..ca4e1599e --- /dev/null +++ b/lib/widgets/settings/language.dart @@ -0,0 +1,52 @@ +import 'dart:collection'; + +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; + +class LanguageTile extends StatelessWidget { + final Locale _systemLocale = WidgetsBinding.instance.window.locale; + + static const _systemLocaleOption = Locale('system'); + + @override + Widget build(BuildContext context) { + final current = settings.locale; + return ListTile( + title: Text(context.l10n.settingsLanguage), + subtitle: Text('${current == null ? '${context.l10n.settingsSystemDefault} • ${_getLocaleName(_systemLocale)}' : _getLocaleName(current)}'), + onTap: () async { + final value = await showDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: settings.locale ?? _systemLocaleOption, + options: _getLocaleOptions(context), + optionSubtitleBuilder: (locale) => locale == _systemLocaleOption ? _getLocaleName(_systemLocale) : null, + title: context.l10n.settingsLanguage, + ), + ); + if (value != null) { + settings.locale = value == _systemLocaleOption ? null : value; + } + }, + ); + } + + String _getLocaleName(Locale locale) => LocaleNamesLocalizationsDelegate.nativeLocaleNames[locale.toString()]; + + LinkedHashMap _getLocaleOptions(BuildContext context) { + final supportedLocales = List.from(AppLocalizations.supportedLocales); + supportedLocales.removeWhere((locale) => locale == _systemLocale); + final displayLocales = supportedLocales.map((locale) => MapEntry(locale, _getLocaleName(locale))).toList()..sort((a, b) => compareAsciiUpperCase(a.value, b.value)); + + return LinkedHashMap.of({ + _systemLocaleOption: context.l10n.settingsSystemDefault, + ...LinkedHashMap.fromEntries(displayLocales), + }); + } +} diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index 695854bc6..77a4d516b 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -1,15 +1,18 @@ import 'package:aves/model/settings/coordinate_format.dart'; +import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/home_page.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/settings/access_grants.dart'; import 'package:aves/widgets/settings/entry_background.dart'; import 'package:aves/widgets/settings/hidden_filters.dart'; +import 'package:aves/widgets/settings/language.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:provider/provider.dart'; @@ -30,7 +33,7 @@ class _SettingsPageState extends State { return MediaQueryDataProvider( child: Scaffold( appBar: AppBar( - title: Text('Settings'), + title: Text(context.l10n.settingsPageTitle), ), body: Theme( data: theme.copyWith( @@ -73,19 +76,19 @@ class _SettingsPageState extends State { Widget _buildNavigationSection(BuildContext context) { return AvesExpansionTile( - title: 'Navigation', + title: context.l10n.settingsSectionNavigation, expandedNotifier: _expandedNotifier, children: [ ListTile( - title: Text('Home'), - subtitle: Text(settings.homePage.name), + title: Text(context.l10n.settingsHome), + subtitle: Text(settings.homePage.getName(context)), onTap: () async { final value = await showDialog( context: context, builder: (context) => AvesSelectionDialog( initialValue: settings.homePage, - options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.name))), - title: 'Home', + options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.settingsHome, ), ); if (value != null) { @@ -96,7 +99,7 @@ class _SettingsPageState extends State { SwitchListTile( value: settings.mustBackTwiceToExit, onChanged: (v) => settings.mustBackTwiceToExit = v, - title: Text('Tap “back” twice to exit'), + title: Text(context.l10n.settingsDoubleBackExit), ), ], ); @@ -104,19 +107,20 @@ class _SettingsPageState extends State { Widget _buildDisplaySection(BuildContext context) { return AvesExpansionTile( - title: 'Display', + title: context.l10n.settingsSectionDisplay, expandedNotifier: _expandedNotifier, children: [ + LanguageTile(), ListTile( - title: Text('Keep screen on'), - subtitle: Text(settings.keepScreenOn.name), + title: Text(context.l10n.settingsKeepScreenOnTile), + subtitle: Text(settings.keepScreenOn.getName(context)), onTap: () async { final value = await showDialog( context: context, builder: (context) => AvesSelectionDialog( initialValue: settings.keepScreenOn, - options: Map.fromEntries(KeepScreenOn.values.map((v) => MapEntry(v, v.name))), - title: 'Keep Screen On', + options: Map.fromEntries(KeepScreenOn.values.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.settingsKeepScreenOnTitle, ), ); if (value != null) { @@ -125,34 +129,30 @@ class _SettingsPageState extends State { }, ), ListTile( - title: Text('Raster image background'), + title: Text(context.l10n.settingsRasterImageBackground), trailing: EntryBackgroundSelector( getter: () => settings.rasterBackground, setter: (value) => settings.rasterBackground = value, ), ), ListTile( - title: Text('Vector image background'), + title: Text(context.l10n.settingsVectorImageBackground), trailing: EntryBackgroundSelector( getter: () => settings.vectorBackground, setter: (value) => settings.vectorBackground = value, ), ), ListTile( - title: Text('Coordinate format'), - subtitle: Text(settings.coordinateFormat.name), + title: Text(context.l10n.settingsCoordinateFormatTile), + subtitle: Text(settings.coordinateFormat.getName(context)), onTap: () async { final value = await showDialog( context: context, builder: (context) => AvesSelectionDialog( initialValue: settings.coordinateFormat, - options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.name))), - optionSubtitleBuilder: (dynamic value) { - // dynamic declaration followed by cast, as workaround for generics limitation - final formatter = (value as CoordinateFormat); - return formatter.format(Constants.pointNemo); - }, - title: 'Coordinate Format', + options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.getName(context)))), + optionSubtitleBuilder: (value) => value.format(Constants.pointNemo), + title: context.l10n.settingsCoordinateFormatTitle, ), ); if (value != null) { @@ -166,23 +166,23 @@ class _SettingsPageState extends State { Widget _buildThumbnailsSection(BuildContext context) { return AvesExpansionTile( - title: 'Thumbnails', + title: context.l10n.settingsSectionThumbnails, expandedNotifier: _expandedNotifier, children: [ SwitchListTile( value: settings.showThumbnailLocation, onChanged: (v) => settings.showThumbnailLocation = v, - title: Text('Show location icon'), + title: Text(context.l10n.settingsThumbnailShowLocationIcon), ), SwitchListTile( value: settings.showThumbnailRaw, onChanged: (v) => settings.showThumbnailRaw = v, - title: Text('Show raw icon'), + title: Text(context.l10n.settingsThumbnailShowRawIcon), ), SwitchListTile( value: settings.showThumbnailVideoDuration, onChanged: (v) => settings.showThumbnailVideoDuration = v, - title: Text('Show video duration'), + title: Text(context.l10n.settingsThumbnailShowVideoDuration), ), ], ); @@ -190,24 +190,24 @@ class _SettingsPageState extends State { Widget _buildViewerSection(BuildContext context) { return AvesExpansionTile( - title: 'Viewer', + title: context.l10n.settingsSectionViewer, expandedNotifier: _expandedNotifier, children: [ SwitchListTile( value: settings.showOverlayMinimap, onChanged: (v) => settings.showOverlayMinimap = v, - title: Text('Show minimap'), + title: Text(context.l10n.settingsViewerShowMinimap), ), SwitchListTile( value: settings.showOverlayInfo, onChanged: (v) => settings.showOverlayInfo = v, - title: Text('Show information'), - subtitle: Text('Show title, date, location, etc.'), + title: Text(context.l10n.settingsViewerShowInformation), + subtitle: Text(context.l10n.settingsViewerShowInformationSubtitle), ), SwitchListTile( value: settings.showOverlayShootingDetails, onChanged: settings.showOverlayInfo ? (v) => settings.showOverlayShootingDetails = v : null, - title: Text('Show shooting details'), + title: Text(context.l10n.settingsViewerShowShootingDetails), ), ], ); @@ -215,7 +215,7 @@ class _SettingsPageState extends State { Widget _buildSearchSection(BuildContext context) { return AvesExpansionTile( - title: 'Search', + title: context.l10n.settingsSectionSearch, expandedNotifier: _expandedNotifier, children: [ SwitchListTile( @@ -226,7 +226,7 @@ class _SettingsPageState extends State { settings.searchHistory = []; } }, - title: Text('Save search history'), + title: Text(context.l10n.settingsSaveSearchHistory), ), ], ); @@ -234,13 +234,13 @@ class _SettingsPageState extends State { Widget _buildPrivacySection(BuildContext context) { return AvesExpansionTile( - title: 'Privacy', + title: context.l10n.settingsSectionPrivacy, expandedNotifier: _expandedNotifier, children: [ SwitchListTile( value: settings.isCrashlyticsEnabled, onChanged: (v) => settings.isCrashlyticsEnabled = v, - title: Text('Allow anonymous analytics and crash reporting'), + title: Text(context.l10n.settingsEnableAnalytics), ), HiddenFilterTile(), StorageAccessTile(), diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart index 97964940e..9cdfaac47 100644 --- a/lib/widgets/stats/filter_table.dart +++ b/lib/widgets/stats/filter_table.dart @@ -43,7 +43,7 @@ class FilterTable extends StatelessWidget { return Table( children: sortedEntries.take(5).map((kv) { final filter = filterBuilder(kv.key); - final label = filter.label; + final label = filter.getLabel(context); final count = kv.value; final percent = count / totalEntryCount; return TableRow( diff --git a/lib/widgets/stats/stats.dart b/lib/widgets/stats/stats.dart index 1005b08b7..a411a22f1 100644 --- a/lib/widgets/stats/stats.dart +++ b/lib/widgets/stats/stats.dart @@ -12,7 +12,8 @@ import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/mime_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; -import 'package:aves/widgets/collection/empty.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/stats/filter_table.dart'; import 'package:charts_flutter/flutter.dart' as charts; @@ -62,24 +63,25 @@ class StatsPage extends StatelessWidget { if (entries.isEmpty) { child = EmptyContent( icon: AIcons.image, - text: 'No images', + text: context.l10n.collectionEmptyImages, ); } else { final byMimeTypes = groupBy(entries, (entry) => entry.mimeType).map((k, v) => MapEntry(k, v.length)); - final imagesByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('image/'))); - final videoByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('video/'))); + final imagesByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('image'))); + final videoByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('video'))); final mimeDonuts = Wrap( alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ - _buildMimeDonut(context, (sum) => Intl.plural(sum, one: 'image', other: 'images'), imagesByMimeTypes), - _buildMimeDonut(context, (sum) => Intl.plural(sum, one: 'video', other: 'videos'), videoByMimeTypes), + _buildMimeDonut(context, (sum) => context.l10n.statsImage(sum), imagesByMimeTypes), + _buildMimeDonut(context, (sum) => context.l10n.statsVideo(sum), videoByMimeTypes), ], ); final catalogued = entries.where((entry) => entry.isCatalogued); final withGps = catalogued.where((entry) => entry.hasGps); - final withGpsPercent = withGps.length / entries.length; + final withGpsCount = withGps.length; + final withGpsPercent = withGpsCount / entries.length; final textScaleFactor = MediaQuery.textScaleFactorOf(context); final lineHeight = 16 * textScaleFactor; final locationIndicator = Padding( @@ -101,7 +103,7 @@ class StatsPage extends StatelessWidget { ), ), SizedBox(height: 8), - Text('${withGps.length} ${Intl.plural(withGps.length, one: 'item', other: 'items')} with location'), + Text(context.l10n.statsWithGps(withGpsCount)), ], ), ); @@ -109,16 +111,16 @@ class StatsPage extends StatelessWidget { children: [ mimeDonuts, locationIndicator, - ..._buildTopFilters(context, 'Top Countries', entryCountPerCountry, (s) => LocationFilter(LocationLevel.country, s)), - ..._buildTopFilters(context, 'Top Places', entryCountPerPlace, (s) => LocationFilter(LocationLevel.place, s)), - ..._buildTopFilters(context, 'Top Tags', entryCountPerTag, (s) => TagFilter(s)), + ..._buildTopFilters(context, context.l10n.statsTopCountries, entryCountPerCountry, (s) => LocationFilter(LocationLevel.country, s)), + ..._buildTopFilters(context, context.l10n.statsTopPlaces, entryCountPerPlace, (s) => LocationFilter(LocationLevel.place, s)), + ..._buildTopFilters(context, context.l10n.statsTopTags, entryCountPerTag, (s) => TagFilter(s)), ], ); } return MediaQueryDataProvider( child: Scaffold( appBar: AppBar( - title: Text('Stats'), + title: Text(context.l10n.statsPageTitle), ), body: SafeArea( child: child, diff --git a/lib/widgets/viewer/entry_action_delegate.dart b/lib/widgets/viewer/entry_action_delegate.dart index c85c8cc86..eec0fdecb 100644 --- a/lib/widgets/viewer/entry_action_delegate.dart +++ b/lib/widgets/viewer/entry_action_delegate.dart @@ -12,6 +12,7 @@ import 'package:aves/services/metadata_service.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/rename_entry_dialog.dart'; import 'package:aves/widgets/filter_grids/album_pick.dart'; @@ -21,7 +22,6 @@ import 'package:aves/widgets/viewer/printer.dart'; import 'package:aves/widgets/viewer/source_viewer_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:intl/intl.dart'; import 'package:pedantic/pedantic.dart'; import 'package:provider/provider.dart'; @@ -103,14 +103,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (!await checkStoragePermission(context, {entry})) return; final success = await entry.flip(); - if (!success) showFeedback(context, 'Failed'); + if (!success) showFeedback(context, context.l10n.genericFailureFeedback); } Future _rotate(BuildContext context, AvesEntry entry, {@required bool clockwise}) async { if (!await checkStoragePermission(context, {entry})) return; final success = await entry.rotate(clockwise: clockwise); - if (!success) showFeedback(context, 'Failed'); + if (!success) showFeedback(context, context.l10n.genericFailureFeedback); } Future _showDeleteDialog(BuildContext context, AvesEntry entry) async { @@ -119,15 +119,15 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix builder: (context) { return AvesDialog( context: context, - content: Text('Are you sure?'), + content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(1)), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('Cancel'.toUpperCase()), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), TextButton( onPressed: () => Navigator.pop(context, true), - child: Text('Delete'.toUpperCase()), + child: Text(context.l10n.deleteButtonLabel), ), ], ); @@ -138,7 +138,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (!await checkStoragePermission(context, {entry})) return; if (!await entry.delete()) { - showFeedback(context, 'Failed'); + showFeedback(context, context.l10n.genericFailureFeedback); } else { if (hasCollection) { collection.source.removeEntries({entry.uri}); @@ -191,9 +191,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix final movedCount = movedOps.length; if (movedCount < selectionCount) { final count = selectionCount - movedCount; - showFeedback(context, 'Failed to export ${Intl.plural(count, one: '$count page', other: '$count pages')}'); + showFeedback(context, context.l10n.collectionExportFailureFeedback(count)); } else { - showFeedback(context, 'Done!'); + showFeedback(context, context.l10n.genericSuccessFeedback); } }, ); @@ -208,7 +208,11 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix if (!await checkStoragePermission(context, {entry})) return; - showFeedback(context, await entry.rename(newName) ? 'Done!' : 'Failed'); + if (await entry.rename(newName)) { + showFeedback(context, context.l10n.genericSuccessFeedback); + } else { + showFeedback(context, context.l10n.genericFailureFeedback); + } } void _goToSourceViewer(BuildContext context, AvesEntry entry) { diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 9f0cff0b6..6d66cd66b 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:aves/model/availability.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/settings/screen_on.dart'; +import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/window_service.dart'; diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index 8c7c39e86..ef494f5d2 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -10,8 +10,8 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/utils/file_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:collection/collection.dart'; @@ -40,37 +40,40 @@ class BasicSection extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = context.l10n; + final infoUnknown = l10n.viewerInfoUnknown; final date = entry.bestDate; - final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : Constants.infoUnknown; + final locale = l10n.localeName; + final dateText = date != null ? '${DateFormat.yMMMd(locale).format(date)} • ${DateFormat.Hm(locale).format(date)}' : infoUnknown; // TODO TLAD line break on all characters for the following fields when this is fixed: https://github.com/flutter/flutter/issues/61081 // inserting ZWSP (\u200B) between characters does help, but it messes with width and height computation (another Flutter issue) - final title = entry.bestTitle ?? Constants.infoUnknown; - final uri = entry.uri ?? Constants.infoUnknown; + final title = entry.bestTitle ?? infoUnknown; + final uri = entry.uri ?? infoUnknown; final path = entry.path; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ InfoRowGroup({ - 'Title': title, - 'Date': dateText, - if (entry.isVideo) ..._buildVideoRows(), - if (!entry.isSvg && entry.isSized) 'Resolution': rasterResolutionText, - 'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : Constants.infoUnknown, - 'URI': uri, - if (path != null) 'Path': path, + l10n.viewerInfoLabelTitle: title, + l10n.viewerInfoLabelDate: dateText, + if (entry.isVideo) ..._buildVideoRows(context), + if (!entry.isSvg && entry.isSized) l10n.viewerInfoLabelResolution: rasterResolutionText, + l10n.viewerInfoLabelSize: entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : infoUnknown, + l10n.viewerInfoLabelUri: uri, + if (path != null) l10n.viewerInfoLabelPath: path, }), OwnerProp( entry: entry, visibleNotifier: visibleNotifier, ), - _buildChips(), + _buildChips(context), ], ); } - Widget _buildChips() { + Widget _buildChips(BuildContext context) { final tags = entry.xmpSubjects..sort(compareAsciiUpperCase); final album = entry.directory; final filters = { @@ -80,7 +83,7 @@ class BasicSection extends StatelessWidget { if (entry.isImage && entry.is360) TypeFilter(TypeFilter.panorama), if (entry.isVideo && entry.is360) TypeFilter(TypeFilter.sphericalVideo), if (entry.isVideo && !entry.is360) MimeFilter(MimeTypes.anyVideo), - if (album != null) AlbumFilter(album, collection?.source?.getUniqueAlbumName(album)), + if (album != null) AlbumFilter(album, collection?.source?.getUniqueAlbumName(context, album)), ...tags.map((tag) => TagFilter(tag)), }; return AnimatedBuilder( @@ -108,9 +111,9 @@ class BasicSection extends StatelessWidget { ); } - Map _buildVideoRows() { + Map _buildVideoRows(BuildContext context) { return { - 'Duration': entry.durationText, + context.l10n.viewerInfoLabelDuration: entry.durationText, }; } } @@ -180,7 +183,7 @@ class _OwnerPropState extends State { TextSpan( children: [ TextSpan( - text: 'Owned by', + text: context.l10n.viewerInfoLabelOwner, style: InfoRowGroup.keyStyle, ), WidgetSpan( diff --git a/lib/widgets/viewer/info/common.dart b/lib/widgets/viewer/info/common.dart index d5310481f..becd341bc 100644 --- a/lib/widgets/viewer/info/common.dart +++ b/lib/widgets/viewer/info/common.dart @@ -98,7 +98,7 @@ class _InfoRowGroupState extends State { if (linkHandlers?.containsKey(key) == true) { final handler = linkHandlers[key]; - value = handler.linkText; + value = handler.linkText(context); // open link on tap recognizer = TapGestureRecognizer()..onTap = () => handler.onTap(context); style = InfoRowGroup.linkStyle; @@ -149,7 +149,7 @@ class _InfoRowGroupState extends State { } class InfoLinkHandler { - final String linkText; + final String Function(BuildContext context) linkText; final void Function(BuildContext context) onTap; const InfoLinkHandler({ diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart index 34e2aebe2..7ee11eb9d 100644 --- a/lib/widgets/viewer/info/info_app_bar.dart +++ b/lib/widgets/viewer/info/info_app_bar.dart @@ -1,6 +1,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/app_bar_title.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/info/info_search.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; import 'package:flutter/material.dart'; @@ -23,17 +24,17 @@ class InfoAppBar extends StatelessWidget { key: Key('back-button'), icon: Icon(AIcons.goUp), onPressed: onBackPressed, - tooltip: 'Back to viewer', + tooltip: context.l10n.viewerInfoBackToViewerTooltip, ), - title: TappableAppBarTitle( + title: InteractiveAppBarTitle( onTap: () => _goToSearch(context), - child: Text('Info'), + child: Text(context.l10n.viewerInfoPageTitle), ), actions: [ IconButton( icon: Icon(AIcons.search), onPressed: () => _goToSearch(context), - tooltip: 'Search', + tooltip: MaterialLocalizations.of(context).searchFieldLabel, ), ], titleSpacing: 0, @@ -45,6 +46,7 @@ class InfoAppBar extends StatelessWidget { showSearch( context: context, delegate: InfoSearchDelegate( + searchFieldLabel: context.l10n.viewerInfoSearchFieldLabel, entry: entry, metadataNotifier: metadataNotifier, ), diff --git a/lib/widgets/viewer/info/info_search.dart b/lib/widgets/viewer/info/info_search.dart index f8c94fe1b..a594ad2be 100644 --- a/lib/widgets/viewer/info/info_search.dart +++ b/lib/widgets/viewer/info/info_search.dart @@ -1,7 +1,8 @@ import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; @@ -15,19 +16,12 @@ class InfoSearchDelegate extends SearchDelegate { Map get metadata => metadataNotifier.value; - static const suggestions = { - 'Date & time': 'date or time or when -timer -uptime -exposure -timeline', - 'Description': 'abstract or description or comment or textual', - 'Dimensions': 'width or height or dimension or framesize or imagelength', - 'Resolution': 'resolution', - 'Rights': 'rights or copyright or artist or creator or by-line or credit -tool', - }; - InfoSearchDelegate({ + @required String searchFieldLabel, @required this.entry, @required this.metadataNotifier, }) : super( - searchFieldLabel: 'Search metadata', + searchFieldLabel: searchFieldLabel, ); @override @@ -57,23 +51,33 @@ class InfoSearchDelegate extends SearchDelegate { query = ''; showSuggestions(context); }, - tooltip: 'Clear', + tooltip: context.l10n.clearTooltip, ), ]; } @override - Widget buildSuggestions(BuildContext context) => ListView( - children: suggestions.entries - .map((kv) => ListTile( - title: Text(kv.key), - onTap: () { - query = kv.value; - showResults(context); - }, - )) - .toList(), - ); + Widget buildSuggestions(BuildContext context) { + final l10n = context.l10n; + final suggestions = { + l10n.viewerInfoSearchSuggestionDate: 'date or time or when -timer -uptime -exposure -timeline', + l10n.viewerInfoSearchSuggestionDescription: 'abstract or description or comment or textual', + l10n.viewerInfoSearchSuggestionDimensions: 'width or height or dimension or framesize or imagelength', + l10n.viewerInfoSearchSuggestionResolution: 'resolution', + l10n.viewerInfoSearchSuggestionRights: 'rights or copyright or artist or creator or by-line or credit -tool', + }; + return ListView( + children: suggestions.entries + .map((kv) => ListTile( + title: Text(kv.key), + onTap: () { + query = kv.value; + showResults(context); + }, + )) + .toList(), + ); + } @override Widget buildResults(BuildContext context) { @@ -107,22 +111,24 @@ class InfoSearchDelegate extends SearchDelegate { showPrefixChildren: false, )) .toList(); - return tiles.isEmpty - ? EmptyContent( - icon: AIcons.info, - text: 'No matching keys', - ) - : NotificationListener( - onNotification: (notification) { - _openTempEntry(context, notification.entry); - return true; - }, - child: ListView.builder( - padding: EdgeInsets.all(8), - itemBuilder: (context, index) => tiles[index], - itemCount: tiles.length, + return SafeArea( + child: tiles.isEmpty + ? EmptyContent( + icon: AIcons.info, + text: context.l10n.viewerInfoSearchEmpty, + ) + : NotificationListener( + onNotification: (notification) { + _openTempEntry(context, notification.entry); + return true; + }, + child: ListView.builder( + padding: EdgeInsets.all(8), + itemBuilder: (context, index) => tiles[index], + itemCount: tiles.length, + ), ), - ); + ); } void _openTempEntry(BuildContext context, AvesEntry tempEntry) { diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index 63752c3da..a700a6471 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -7,6 +7,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/maps/common.dart'; @@ -195,9 +196,10 @@ class _AddressInfoGroupState extends State<_AddressInfoGroup> { future: _addressLineLoader, builder: (context, snapshot) { final address = !snapshot.hasError && snapshot.connectionState == ConnectionState.done ? snapshot.data : null; + final l10n = context.l10n; return InfoRowGroup({ - 'Coordinates': settings.coordinateFormat.format(entry.latLng), - if (address?.isNotEmpty == true) 'Address': address, + l10n.viewerInfoLabelCoordinates: settings.coordinateFormat.format(entry.latLng), + if (address?.isNotEmpty == true) l10n.viewerInfoLabelAddress: address, }); }, ); diff --git a/lib/widgets/viewer/info/maps/common.dart b/lib/widgets/viewer/info/maps/common.dart index 6cf73e190..9bb6035b3 100644 --- a/lib/widgets/viewer/info/maps/common.dart +++ b/lib/widgets/viewer/info/maps/common.dart @@ -1,9 +1,11 @@ import 'package:aves/model/availability.dart'; +import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; @@ -68,7 +70,7 @@ class MapButtonPanel extends StatelessWidget { onPressed: () => AndroidAppService.openMap(geoUri).then((success) { if (!success) showNoMatchingAppDialog(context); }), - tooltip: 'Show on map…', + tooltip: context.l10n.entryActionOpenMap, ), SizedBox(height: padding), MapOverlayButton( @@ -83,8 +85,8 @@ class MapButtonPanel extends StatelessWidget { builder: (context) { return AvesSelectionDialog( initialValue: initialStyle, - options: Map.fromEntries(availableStyles.map((v) => MapEntry(v, v.name))), - title: 'Map Style', + options: Map.fromEntries(availableStyles.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.viewerInfoMapStyleTitle, ); }, ); @@ -95,19 +97,19 @@ class MapButtonPanel extends StatelessWidget { MapStyleChangedNotification().dispatch(context); } }, - tooltip: 'Style map…', + tooltip: context.l10n.viewerInfoMapStyleTooltip, ), Spacer(), MapOverlayButton( icon: AIcons.zoomIn, onPressed: () => zoomBy(1), - tooltip: 'Zoom in', + tooltip: context.l10n.viewerInfoMapZoomInTooltip, ), SizedBox(height: padding), MapOverlayButton( icon: AIcons.zoomOut, onPressed: () => zoomBy(-1), - tooltip: 'Zoom out', + tooltip: context.l10n.viewerInfoMapZoomOutTooltip, ), ], ), diff --git a/lib/widgets/viewer/info/maps/google_map.dart b/lib/widgets/viewer/info/maps/google_map.dart index 25781b1d1..c4ebfc55a 100644 --- a/lib/widgets/viewer/info/maps/google_map.dart +++ b/lib/widgets/viewer/info/maps/google_map.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:aves/model/settings/map_style.dart'; +import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/viewer/info/maps/common.dart'; import 'package:aves/widgets/viewer/info/maps/marker.dart'; diff --git a/lib/widgets/viewer/info/maps/leaflet_map.dart b/lib/widgets/viewer/info/maps/leaflet_map.dart index dfbc8c00b..12dca078d 100644 --- a/lib/widgets/viewer/info/maps/leaflet_map.dart +++ b/lib/widgets/viewer/info/maps/leaflet_map.dart @@ -1,5 +1,6 @@ -import 'package:aves/model/settings/map_style.dart'; +import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/maps/common.dart'; import 'package:aves/widgets/viewer/info/maps/scale_layer.dart'; @@ -110,10 +111,10 @@ class _EntryLeafletMapState extends State with AutomaticKeepAli Widget _buildAttribution() { switch (widget.style) { case EntryMapStyle.osmHot: - return _buildAttributionMarkdown('Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors, tiles by [HOT](https://www.hotosm.org/) hosted by [OSM France](https://openstreetmap.fr/)'); + return _buildAttributionMarkdown(context.l10n.mapAttributionOsmHot); case EntryMapStyle.stamenToner: case EntryMapStyle.stamenWatercolor: - return _buildAttributionMarkdown('Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors, tiles by [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0)'); + return _buildAttributionMarkdown(context.l10n.mapAttributionStamen); default: return SizedBox.shrink(); } diff --git a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart index 3bc2f74b6..ad1319723 100644 --- a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart +++ b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart @@ -7,6 +7,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_thumbnail.dart'; @@ -89,7 +90,7 @@ class MetadataDirTile extends StatelessWidget { static Map getSvgLinkHandlers(SplayTreeMap tags) { return { 'Metadata': InfoLinkHandler( - linkText: 'View XML', + linkText: (context) => context.l10n.viewerInfoViewXmlLinkText, onTap: (context) { Navigator.push( context, diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart index cccb66342..718ce3f1a 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart @@ -1,3 +1,4 @@ +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; import 'package:tuple/tuple.dart'; @@ -18,7 +19,7 @@ abstract class XmpGoogleNamespace extends XmpNamespace { ? MapEntry( dataProp.displayKey, InfoLinkHandler( - linkText: 'Open', + linkText: (context) => context.l10n.viewerInfoOpenLinkText, onTap: (context) => OpenEmbeddedDataNotification( propPath: dataProp.path, mimeType: mimeProp.value, diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/mwg.dart b/lib/widgets/viewer/info/metadata/xmp_ns/mwg.dart index 63286f574..321452173 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/mwg.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/mwg.dart @@ -26,15 +26,15 @@ class XmpMgwRegionsNamespace extends XmpNamespace { @override List buildFromExtractedData() => [ - if (dimensions.isNotEmpty) - XmpStructCard( - title: 'Applied To Dimensions', - struct: dimensions, - ), - if (regionList.isNotEmpty) - XmpStructArrayCard( - title: 'Region', - structByIndex: regionList, - ), - ]; + if (dimensions.isNotEmpty) + XmpStructCard( + title: 'Applied To Dimensions', + struct: dimensions, + ), + if (regionList.isNotEmpty) + XmpStructArrayCard( + title: 'Region', + structByIndex: regionList, + ), + ]; } diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart index 70a57de2c..9ddd5bb91 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/xmp.dart @@ -1,4 +1,5 @@ import 'package:aves/ref/mime_types.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart'; @@ -31,7 +32,7 @@ class XmpBasicNamespace extends XmpNamespace { return { if (struct.containsKey(thumbnailDataDisplayKey)) thumbnailDataDisplayKey: InfoLinkHandler( - linkText: 'Open', + linkText: (context) => context.l10n.viewerInfoOpenLinkText, onTap: (context) => OpenEmbeddedDataNotification( propPath: 'xmp:Thumbnails[$index]/xmpGImg:image', mimeType: MimeTypes.jpeg, diff --git a/lib/widgets/viewer/info/metadata/xmp_structs.dart b/lib/widgets/viewer/info/metadata/xmp_structs.dart index 8bd946184..6060f46bf 100644 --- a/lib/widgets/viewer/info/metadata/xmp_structs.dart +++ b/lib/widgets/viewer/info/metadata/xmp_structs.dart @@ -4,6 +4,7 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/basic/multi_cross_fader.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/highlight_title.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; @@ -68,13 +69,13 @@ class _XmpStructArrayCardState extends State { visualDensity: VisualDensity.compact, icon: Icon(AIcons.previous), onPressed: _index > 0 ? () => setIndex(_index - 1) : null, - tooltip: 'Previous', + tooltip: context.l10n.previousTooltip, ), IconButton( visualDensity: VisualDensity.compact, icon: Icon(AIcons.next), onPressed: _index < structs.length - 1 ? () => setIndex(_index + 1) : null, - tooltip: 'Next', + tooltip: context.l10n.nextTooltip, ), ], ), diff --git a/lib/widgets/viewer/info/metadata/xmp_tile.dart b/lib/widgets/viewer/info/metadata/xmp_tile.dart index 94ccc00f7..336fa0c79 100644 --- a/lib/widgets/viewer/info/metadata/xmp_tile.dart +++ b/lib/widgets/viewer/info/metadata/xmp_tile.dart @@ -6,6 +6,7 @@ import 'package:aves/ref/xmp.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/metadata_service.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_expansion_tile.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; @@ -106,7 +107,7 @@ class _XmpDirTileState extends State with FeedbackMixin { Future _openEmbeddedData(String propPath, String propMimeType) async { final fields = await MetadataService.extractXmpDataProp(entry, propPath, propMimeType); if (fields == null || !fields.containsKey('mimeType') || !fields.containsKey('uri')) { - showFeedback(context, 'Failed'); + showFeedback(context, context.l10n.viewerInfoOpenEmbeddedFailureFeedback); return; } diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index 03d1f0567..15a7a4923 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -9,6 +9,7 @@ import 'package:aves/services/metadata_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/viewer/multipage.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; @@ -384,9 +385,14 @@ class _DateRow extends StatelessWidget { @override Widget build(BuildContext context) { + final locale = context.l10n.localeName; final date = entry.bestDate; - final dateText = date != null ? '${DateFormat.yMMMd().format(date)} • ${DateFormat.Hm().format(date)}' : Constants.overlayUnknown; - final resolutionText = entry.isSvg ? entry.aspectRatioText : entry.isSized ? entry.resolutionText : ''; + final dateText = date != null ? '${DateFormat.yMMMd(locale).format(date)} • ${DateFormat.Hm(locale).format(date)}' : Constants.overlayUnknown; + final resolutionText = entry.isSvg + ? entry.aspectRatioText + : entry.isSized + ? entry.resolutionText + : ''; return Row( children: [ diff --git a/lib/widgets/viewer/overlay/common.dart b/lib/widgets/viewer/overlay/common.dart index 23ad79666..0ef41c77a 100644 --- a/lib/widgets/viewer/overlay/common.dart +++ b/lib/widgets/viewer/overlay/common.dart @@ -38,13 +38,13 @@ class OverlayButton extends StatelessWidget { class OverlayTextButton extends StatelessWidget { final Animation scale; - final String text; + final String buttonLabel; final VoidCallback onPressed; const OverlayTextButton({ Key key, @required this.scale, - @required this.text, + @required this.buttonLabel, this.onPressed, }) : assert(scale != null), super(key: key); @@ -71,7 +71,7 @@ class OverlayTextButton extends StatelessWidget { )), // shape: MaterialStateProperty.all(CircleBorder()), ), - child: Text(text.toUpperCase()), + child: Text(buttonLabel), ), ), ); diff --git a/lib/widgets/viewer/overlay/panorama.dart b/lib/widgets/viewer/overlay/panorama.dart index fe6a2dbb0..688e7dcba 100644 --- a/lib/widgets/viewer/overlay/panorama.dart +++ b/lib/widgets/viewer/overlay/panorama.dart @@ -1,5 +1,6 @@ import 'package:aves/model/entry.dart'; import 'package:aves/services/metadata_service.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/panorama_page.dart'; import 'package:flutter/material.dart'; @@ -22,7 +23,7 @@ class PanoramaOverlay extends StatelessWidget { Spacer(), OverlayTextButton( scale: scale, - text: 'Open Panorama', + buttonLabel: context.l10n.viewerOpenPanoramaButtonLabel, onPressed: () async { final info = await MetadataService.getPanoramaInfo(entry); if (info != null) { diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index 44fc7a3f1..a7c79e95d 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -7,6 +7,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/basic/menu_row.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/sweeper.dart'; import 'package:aves/widgets/viewer/multipage.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; @@ -160,19 +161,19 @@ class _TopOverlayRow extends StatelessWidget { child: Navigator.canPop(context) ? BackButton() : CloseButton(), ), Spacer(), - ...quickActions.map(_buildOverlayButton), + ...quickActions.map((action) => _buildOverlayButton(context, action)), OverlayButton( scale: scale, child: PopupMenuButton( key: Key('entry-menu-button'), itemBuilder: (context) => [ - ...inAppActions.map(_buildPopupMenuItem), - if (entry.canRotateAndFlip) _buildRotateAndFlipMenuItems(), + ...inAppActions.map((action) => _buildPopupMenuItem(context, action)), + if (entry.canRotateAndFlip) _buildRotateAndFlipMenuItems(context), PopupMenuDivider(), - ...externalAppActions.map(_buildPopupMenuItem), + ...externalAppActions.map((action) => _buildPopupMenuItem(context, action)), if (kDebugMode) ...[ PopupMenuDivider(), - _buildPopupMenuItem(EntryAction.debug), + _buildPopupMenuItem(context, EntryAction.debug), ] ], onSelected: (action) { @@ -185,7 +186,7 @@ class _TopOverlayRow extends StatelessWidget { ); } - Widget _buildOverlayButton(EntryAction action) { + Widget _buildOverlayButton(BuildContext context, EntryAction action) { Widget child; void onPressed() => onActionSelected(action); switch (action) { @@ -208,7 +209,7 @@ class _TopOverlayRow extends StatelessWidget { child = IconButton( icon: Icon(action.getIcon()), onPressed: onPressed, - tooltip: action.getText(), + tooltip: action.getText(context), ); break; case EntryAction.openMap: @@ -229,7 +230,7 @@ class _TopOverlayRow extends StatelessWidget { : SizedBox.shrink(); } - PopupMenuEntry _buildPopupMenuItem(EntryAction action) { + PopupMenuEntry _buildPopupMenuItem(BuildContext context, EntryAction action) { Widget child; switch (action) { // in app actions @@ -250,14 +251,14 @@ class _TopOverlayRow extends StatelessWidget { case EntryAction.share: case EntryAction.viewSource: case EntryAction.debug: - child = MenuRow(text: action.getText(), icon: action.getIcon()); + child = MenuRow(text: action.getText(context), icon: action.getIcon()); break; // external app actions case EntryAction.edit: case EntryAction.open: case EntryAction.setAs: case EntryAction.openMap: - child = Text(action.getText()); + child = Text(action.getText(context)); break; } return PopupMenuItem( @@ -266,7 +267,7 @@ class _TopOverlayRow extends StatelessWidget { ); } - PopupMenuItem _buildRotateAndFlipMenuItems() { + PopupMenuItem _buildRotateAndFlipMenuItems(BuildContext context) { Widget buildDivider() => SizedBox( height: 16, child: VerticalDivider( @@ -279,7 +280,7 @@ class _TopOverlayRow extends StatelessWidget { child: PopupMenuItem( value: action, child: Tooltip( - message: action.getText(), + message: action.getText(context), child: Center(child: Icon(action.getIcon())), ), ), @@ -346,11 +347,11 @@ class _FavouriteTogglerState extends State<_FavouriteToggler> { if (widget.isMenuItem) { return isFavourite ? MenuRow( - text: 'Remove from favourites', + text: context.l10n.entryActionRemoveFavourite, icon: AIcons.favouriteActive, ) : MenuRow( - text: 'Add to favourites', + text: context.l10n.entryActionAddFavourite, icon: AIcons.favourite, ); } @@ -360,7 +361,7 @@ class _FavouriteTogglerState extends State<_FavouriteToggler> { IconButton( icon: Icon(isFavourite ? AIcons.favouriteActive : AIcons.favourite), onPressed: widget.onPressed, - tooltip: isFavourite ? 'Remove from favourites' : 'Add to favourites', + tooltip: isFavourite ? context.l10n.entryActionRemoveFavourite : context.l10n.entryActionAddFavourite, ), Sweeper( key: ValueKey(widget.entry), diff --git a/lib/widgets/viewer/overlay/video.dart b/lib/widgets/viewer/overlay/video.dart index 59c1cd9d5..78bed2dfb 100644 --- a/lib/widgets/viewer/overlay/video.dart +++ b/lib/widgets/viewer/overlay/video.dart @@ -5,6 +5,7 @@ import 'package:aves/services/android_app_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/time_utils.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; @@ -112,7 +113,7 @@ class _VideoControlOverlayState extends State with SingleTi child: IconButton( icon: Icon(AIcons.openOutside), onPressed: () => AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype), - tooltip: 'Open', + tooltip: context.l10n.viewerOpenTooltip, ), ), ] @@ -129,7 +130,7 @@ class _VideoControlOverlayState extends State with SingleTi progress: _playPauseAnimation, ), onPressed: _playPause, - tooltip: isPlaying ? 'Pause' : 'Play', + tooltip: isPlaying ? context.l10n.viewerPauseTooltip : context.l10n.viewerPlayTooltip, ), ), ], diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index 979971d58..0c4a51653 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -1,8 +1,9 @@ -import 'package:aves/model/entry_images.dart'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/panorama.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:flutter/foundation.dart'; @@ -100,7 +101,7 @@ class _PanoramaPageState extends State { return IconButton( icon: Icon(sensorControl == SensorControl.None ? AIcons.sensorControl : AIcons.sensorControlOff), onPressed: _toggleSensor, - tooltip: sensorControl == SensorControl.None ? 'Enable sensor control' : 'Disable sensor control', + tooltip: sensorControl == SensorControl.None ? context.l10n.panoramaEnableSensorControl : context.l10n.panoramaDisableSensorControl, ); }), ), diff --git a/lib/widgets/viewer/printer.dart b/lib/widgets/viewer/printer.dart index dee300e6f..a13accf63 100644 --- a/lib/widgets/viewer/printer.dart +++ b/lib/widgets/viewer/printer.dart @@ -6,6 +6,7 @@ import 'package:aves/model/entry_images.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart'; import 'package:pdf/widgets.dart' as pdf; import 'package:pedantic/pedantic.dart'; @@ -17,12 +18,12 @@ class EntryPrinter with FeedbackMixin { EntryPrinter(this.entry); Future print(BuildContext context) async { - final documentName = entry.bestTitle ?? 'Aves'; + final documentName = entry.bestTitle ?? context.l10n.appName; final doc = pdf.Document(title: documentName); final pages = await _buildPages(context); if (pages.isNotEmpty) { - pages.forEach(doc.addPage); // Page + pages.forEach(doc.addPage); unawaited(Printing.layoutPdf( onLayout: (format) => doc.save(), name: documentName, diff --git a/lib/widgets/viewer/source_viewer_page.dart b/lib/widgets/viewer/source_viewer_page.dart index 75bd97973..62b40b044 100644 --- a/lib/widgets/viewer/source_viewer_page.dart +++ b/lib/widgets/viewer/source_viewer_page.dart @@ -1,4 +1,5 @@ import 'package:aves/widgets/common/aves_highlight.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; import 'package:flutter_highlight/themes/darcula.dart'; @@ -28,7 +29,7 @@ class _SourceViewerPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('Source'), + title: Text(context.l10n.sourceViewerPageTitle), ), body: SafeArea( child: FutureBuilder( diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index cf212cfa1..784956cb2 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -4,6 +4,7 @@ import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/model/settings/entry_background.dart'; +import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/magnifier/controller/controller.dart'; import 'package:aves/widgets/common/magnifier/controller/state.dart'; diff --git a/lib/widgets/viewer/visual/error.dart b/lib/widgets/viewer/visual/error.dart index f16192aed..34d3cf97b 100644 --- a/lib/widgets/viewer/visual/error.dart +++ b/lib/widgets/viewer/visual/error.dart @@ -2,7 +2,8 @@ import 'dart:io'; import 'package:aves/model/entry.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/collection/empty.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/empty.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -45,7 +46,7 @@ class _ErrorViewState extends State { final exists = snapshot.data; return EmptyContent( icon: AIcons.error, - text: exists ? 'Oops!' : 'The file no longer exists.', + text: exists ? context.l10n.viewerErrorUnknown : context.l10n.viewerErrorDoesNotExist, alignment: Alignment.center, ); }), diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index 72098fb99..77bf923b5 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -4,6 +4,7 @@ import 'package:aves/image_providers/region_provider.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; import 'package:aves/model/settings/entry_background.dart'; +import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index 9567d0f6b..f2fdef7e5 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -1,6 +1,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/basic/labeled_checkbox.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/home_page.dart'; import 'package:flutter/foundation.dart'; @@ -66,7 +67,7 @@ class _WelcomePageState extends State { List _buildTop(BuildContext context) { final message = Text( - 'Welcome to Aves', + context.l10n.welcomeMessage, style: Theme.of(context).textTheme.headline5, ); return [ @@ -97,20 +98,20 @@ class _WelcomePageState extends State { LabeledCheckbox( value: settings.isCrashlyticsEnabled, onChanged: (v) => setState(() => settings.isCrashlyticsEnabled = v), - text: 'Allow anonymous analytics and crash reporting', + text: context.l10n.welcomeAnalyticsToggle, ), LabeledCheckbox( key: Key('agree-checkbox'), value: _hasAcceptedTerms, onChanged: (v) => setState(() => _hasAcceptedTerms = v), - text: 'I agree to the terms and conditions', + text: context.l10n.welcomeTermsToggle, ), ], ); final button = ElevatedButton( key: Key('continue-button'), - child: Text('Continue'), + child: Text(context.l10n.continueButtonLabel), onPressed: _hasAcceptedTerms ? () { settings.hasAcceptedTerms = true; diff --git a/pubspec.lock b/pubspec.lock index 49d5dc7b8..80d22c3c8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -323,6 +323,18 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_localized_locales: + dependency: "direct main" + description: + name: flutter_localized_locales + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" flutter_map: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index eb7bdb767..b7c3725a2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,108 @@ name: aves -description: Aves is a gallery and metadata explorer app, built for Android. - -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - +description: A visual media gallery and metadata explorer app. +repository: https://github.com/deckerst/aves version: 1.3.5+41 +environment: + sdk: ">=2.7.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + charts_flutter: # not null safe, as of 2021/03/09 - https://github.com/google/charts/issues/579 + collection: + connectivity: + country_code: # not null safe, as of 2021/03/09 - unmaintained? + decorated_icon: # not null safe, as of 2021/03/09 - https://github.com/benPesso/flutter_decorated_icon/issues/2 + event_bus: + # TODO TLAD merge null safe `expansion_tile_card` to fork + expansion_tile_card: +# path: ../expansion_tile_card + git: + url: git://github.com/deckerst/expansion_tile_card.git + firebase_core: + firebase_analytics: + firebase_crashlytics: + # TODO TLAD migrate to basic SnackBar or `another_flushbar` + flushbar: # not null safe, as of 2021/03/09 - discontinued + flutter_highlight: + flutter_ijkplayer: # not null safe, as of 2021/03/09 - unmaintained? +# path: ../flutter_ijkplayer + git: + url: git://github.com/deckerst/flutter_ijkplayer.git + flutter_localized_locales: + flutter_map: # not null safe, as of 2021/03/09 - https://github.com/fleaflet/flutter_map/issues/829 + flutter_markdown: + flutter_staggered_animations: + flutter_svg: + # TODO TLAD migrate to `geocoding` (or reimplement) - https://github.com/Baseflow/flutter-geocoding/issues/37 + geocoder: # not null safe, as of 2021/03/09 - unmaintained? - https://github.com/aloisdeniel/flutter_geocoder/issues/61 + github: + google_api_availability: + google_maps_flutter: + intl: + latlong: # not null safe, as of 2021/03/09 - archived - migrate to maps_toolkit? cf https://github.com/fleaflet/flutter_map/pull/750 + material_design_icons_flutter: + overlay_support: + package_info: + palette_generator: # not null safe, as of 2021/03/09 - https://github.com/flutter/packages/pull/287 + panorama: # not null safe, as of 2021/03/09 - no issue/PR + pdf: + pedantic: + percent_indicator: + permission_handler: + printing: + provider: + shared_preferences: + sqflite: + streams_channel: # not null safe, as of 2021/03/09 - unmaintained? - no issue/PR + tuple: + url_launcher: + version: + xml: + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + test: + +flutter: + assets: + - assets/ + generate: true + uses-material-design: true + +################################################################################ +# Localization + +# language files: +# - /lib/l10n/app_{language}.arb +# - /android/app/src/main/res/values-{language}/strings.xml +# - /android/app/src/debug/res/values-{language}/strings.xml (optional) +# - /android/app/src/profile/res/values-{language}/strings.xml (optional) + +# generate `AppLocalizations` +# % flutter gen-l10n + +# list untranslated messages +# % flutter gen-l10n --untranslated-messages-file untranslated.json + +################################################################################ +# Test driver + +# run (any device): +# % flutter drive -t test_driver/app.dart + +# capture shaders in profile mode (real device only): +# % flutter drive -t test_driver/app.dart --profile --cache-sksl --write-sksl-on-exit shaders.sksl.json + +################################################################################ +# Package study + # brendan-duncan/image (as of v2.1.19): # - does not support TIFF with JPEG compression (issue #184) # - TIFF tile decoding is not public (issue #258) @@ -24,76 +122,3 @@ version: 1.3.5+41 # - support content URIs (`DataSource.photoManagerUrl` from v0.3.6, but need fork to support content URIs on Android =2.7.0 <3.0.0" - -dependencies: - flutter: - sdk: flutter - charts_flutter: - collection: - connectivity: - country_code: - decorated_icon: - event_bus: - expansion_tile_card: -# path: ../expansion_tile_card - git: - url: git://github.com/deckerst/expansion_tile_card.git - firebase_core: - firebase_analytics: - firebase_crashlytics: - flushbar: - flutter_highlight: - flutter_ijkplayer: -# path: ../flutter_ijkplayer - git: - url: git://github.com/deckerst/flutter_ijkplayer.git - flutter_map: - flutter_markdown: - flutter_staggered_animations: - flutter_svg: - geocoder: - github: - google_api_availability: - google_maps_flutter: - intl: - latlong: # for flutter_map - material_design_icons_flutter: - overlay_support: - package_info: - palette_generator: - panorama: - pdf: - pedantic: - percent_indicator: - permission_handler: - printing: - provider: - shared_preferences: - sqflite: - streams_channel: - tuple: - url_launcher: - version: - xml: - -dev_dependencies: - flutter_test: - sdk: flutter - - # run on any device: - # % flutter drive -t test_driver/app.dart - # capture shaders in profile mode (real device only): - # % flutter drive -t test_driver/app.dart --profile --cache-sksl --write-sksl-on-exit shaders.sksl.json - flutter_driver: - sdk: flutter - - test: any - -flutter: - uses-material-design: true - - assets: - - assets/ diff --git a/test_driver/app.dart b/test_driver/app.dart index b139f282a..e67e4cfd9 100644 --- a/test_driver/app.dart +++ b/test_driver/app.dart @@ -1,5 +1,5 @@ import 'package:aves/main.dart' as app; -import 'package:aves/model/settings/screen_on.dart'; +import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/android_file_service.dart'; import 'package:flutter_driver/driver_extension.dart';