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';