This commit is contained in:
Thibault Deckers 2021-03-09 12:36:49 +09:00
parent a5c3971303
commit a47d82ebfc
122 changed files with 1955 additions and 905 deletions

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<resources>
<string name="app_name">아베스 [Debug]</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<resources>
<string name="app_name">아베스</string>
<string name="search_shortcut_short_label">검색</string>
<string name="videos_shortcut_short_label">동영상</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<resources>
<string name="app_name">아베스 [Profile]</string>
</resources>

8
l10n.yaml Normal file
View file

@ -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

659
lib/l10n/app_en.arb Normal file
View file

@ -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": {}
}

8
lib/l10n/app_ko.arb Normal file
View file

@ -0,0 +1,8 @@
{
"appName": "아베스",
"collectionSelectionPageTitle": "{count, plural, =0{항목 선택} other{{count}개}}",
"settingsLanguage": "언어",
"settingsSystemDefault": "시스템"
}

View file

@ -9,6 +9,7 @@ import 'package:aves/theme/icons.dart';
import 'package:aves/utils/debouncer.dart'; import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/common/behaviour/route_tracker.dart'; import 'package:aves/widgets/common/behaviour/route_tracker.dart';
import 'package:aves/widgets/common/behaviour/routes.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/common/providers/highlight_info_provider.dart';
import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/welcome_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/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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:overlay_support/overlay_support.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -120,19 +123,30 @@ class _AvesAppState extends State<AvesApp> {
child: FutureBuilder<void>( child: FutureBuilder<void>(
future: _appSetup, future: _appSetup,
builder: (context, snapshot) { builder: (context, snapshot) {
final home = (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done;
final home = initialized
? getFirstPage() ? getFirstPage()
: Scaffold( : Scaffold(
body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(), body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox(),
); );
return MaterialApp( return Selector<Settings, Locale>(
navigatorKey: _navigatorKey, selector: (context, s) => s.locale,
home: home, builder: (context, settingsLocale, child) {
navigatorObservers: _navigatorObservers, return MaterialApp(
title: 'Aves', navigatorKey: _navigatorKey,
darkTheme: darkTheme, home: home,
themeMode: ThemeMode.dark, navigatorObservers: _navigatorObservers,
); onGenerateTitle: (context) => context.l10n.appName,
darkTheme: darkTheme,
themeMode: ThemeMode.dark,
locale: settingsLocale,
localizationsDelegates: [
...AppLocalizations.localizationsDelegates,
LocaleNamesLocalizationsDelegate(),
],
supportedLocales: AppLocalizations.supportedLocales,
);
});
}, },
), ),
), ),

View file

@ -1,10 +1,10 @@
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
enum ChipSetAction { enum ChipSetAction {
group, group,
sort, sort,
refresh,
stats, stats,
} }
@ -20,24 +20,24 @@ enum ChipAction {
} }
extension ExtraChipAction on ChipAction { extension ExtraChipAction on ChipAction {
String getText() { String getText(BuildContext context) {
switch (this) { switch (this) {
case ChipAction.delete: case ChipAction.delete:
return 'Delete'; return context.l10n.chipActionDelete;
case ChipAction.goToAlbumPage: case ChipAction.goToAlbumPage:
return 'Show in Albums'; return context.l10n.chipActionGoToAlbumPage;
case ChipAction.goToCountryPage: case ChipAction.goToCountryPage:
return 'Show in Countries'; return context.l10n.chipActionGoToCountryPage;
case ChipAction.goToTagPage: case ChipAction.goToTagPage:
return 'Show in Tags'; return context.l10n.chipActionGoToTagPage;
case ChipAction.hide: case ChipAction.hide:
return 'Hide'; return context.l10n.chipActionHide;
case ChipAction.pin: case ChipAction.pin:
return 'Pin to top'; return context.l10n.chipActionPin;
case ChipAction.unpin: case ChipAction.unpin:
return 'Unpin from top'; return context.l10n.chipActionUnpin;
case ChipAction.rename: case ChipAction.rename:
return 'Rename'; return context.l10n.chipActionRename;
} }
return null; return null;
} }

View file

@ -2,7 +2,6 @@ enum CollectionAction {
addShortcut, addShortcut,
sort, sort,
group, group,
refresh,
select, select,
selectAll, selectAll,
selectNone, selectNone,

View file

@ -1,4 +1,5 @@
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
enum EntryAction { enum EntryAction {
@ -46,41 +47,41 @@ class EntryActions {
} }
extension ExtraEntryAction on EntryAction { extension ExtraEntryAction on EntryAction {
String getText() { String getText(BuildContext context) {
switch (this) { switch (this) {
// in app actions // in app actions
case EntryAction.toggleFavourite: case EntryAction.toggleFavourite:
// different data depending on toggle state // different data depending on toggle state
return null; return null;
case EntryAction.delete: case EntryAction.delete:
return 'Delete'; return context.l10n.entryActionDelete;
case EntryAction.export: case EntryAction.export:
return 'Export'; return context.l10n.entryActionExport;
case EntryAction.info: case EntryAction.info:
return 'Info'; return context.l10n.entryActionInfo;
case EntryAction.rename: case EntryAction.rename:
return 'Rename'; return context.l10n.entryActionRename;
case EntryAction.rotateCCW: case EntryAction.rotateCCW:
return 'Rotate counterclockwise'; return context.l10n.entryActionRotateCCW;
case EntryAction.rotateCW: case EntryAction.rotateCW:
return 'Rotate clockwise'; return context.l10n.entryActionRotateCW;
case EntryAction.flip: case EntryAction.flip:
return 'Flip horizontally'; return context.l10n.entryActionFlip;
case EntryAction.print: case EntryAction.print:
return 'Print'; return context.l10n.entryActionPrint;
case EntryAction.share: case EntryAction.share:
return 'Share'; return context.l10n.entryActionShare;
case EntryAction.viewSource: case EntryAction.viewSource:
return 'View source'; return context.l10n.entryActionViewSource;
// external app actions // external app actions
case EntryAction.edit: case EntryAction.edit:
return 'Edit with…'; return context.l10n.entryActionEdit;
case EntryAction.open: case EntryAction.open:
return 'Open with…'; return context.l10n.entryActionOpen;
case EntryAction.setAs: case EntryAction.setAs:
return 'Set as…'; return context.l10n.entryActionSetAs;
case EntryAction.openMap: case EntryAction.openMap:
return 'Show on map…'; return context.l10n.entryActionOpenMap;
case EntryAction.debug: case EntryAction.debug:
return 'Debug'; return 'Debug';
} }

View file

@ -35,10 +35,10 @@ class AlbumFilter extends CollectionFilter {
EntryFilter get test => (entry) => entry.directory == album; EntryFilter get test => (entry) => entry.directory == album;
@override @override
String get label => uniqueName ?? album.split(separator).last; String get universalLabel => uniqueName ?? album.split(separator).last;
@override @override
String get tooltip => album; String getTooltip(BuildContext context) => album;
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) { Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) {
@ -74,7 +74,10 @@ class AlbumFilter extends CollectionFilter {
} }
@override @override
String get typeKey => type; String get category => type;
@override
String get key => '$type-$album';
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {

View file

@ -1,11 +1,15 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/icons.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/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class FavouriteFilter extends CollectionFilter { class FavouriteFilter extends CollectionFilter {
static const type = 'favourite'; static const type = 'favourite';
const FavouriteFilter();
@override @override
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
'type': type, 'type': type,
@ -15,13 +19,22 @@ class FavouriteFilter extends CollectionFilter {
EntryFilter get test => (entry) => entry.isFavourite; EntryFilter get test => (entry) => entry.isFavourite;
@override @override
String get label => 'Favourite'; String get universalLabel => type;
@override
String getLabel(BuildContext context) => context.l10n.filterFavouriteLabel;
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.favourite, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.favourite, size: size);
@override @override
String get typeKey => type; Future<Color> color(BuildContext context) => SynchronousFuture(Colors.red);
@override
String get category => type;
@override
String get key => type;
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {

View file

@ -14,7 +14,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
abstract class CollectionFilter implements Comparable<CollectionFilter> { abstract class CollectionFilter implements Comparable<CollectionFilter> {
static const List<String> collectionFilterOrder = [ static const List<String> categoryOrder = [
QueryFilter.type, QueryFilter.type,
FavouriteFilter.type, FavouriteFilter.type,
MimeFilter.type, MimeFilter.type,
@ -57,25 +57,28 @@ abstract class CollectionFilter implements Comparable<CollectionFilter> {
bool get isUnique => true; 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}); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false});
Future<Color> color(BuildContext context) => SynchronousFuture(stringToColor(label)); Future<Color> color(BuildContext context) => SynchronousFuture(stringToColor(getLabel(context)));
String get typeKey; String get category;
int get displayPriority => collectionFilterOrder.indexOf(typeKey);
// to be used as widget key // to be used as widget key
String get key => '$typeKey-$label'; String get key;
int get displayPriority => categoryOrder.indexOf(category);
@override @override
int compareTo(CollectionFilter other) { int compareTo(CollectionFilter other) {
final c = displayPriority.compareTo(other.displayPriority); 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);
} }
} }

View file

@ -1,11 +1,11 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class LocationFilter extends CollectionFilter { class LocationFilter extends CollectionFilter {
static const type = 'location'; static const type = 'location';
static const emptyLabel = 'unlocated';
static const locationSeparator = ';'; static const locationSeparator = ';';
final LocationLevel level; final LocationLevel level;
@ -48,7 +48,10 @@ class LocationFilter extends CollectionFilter {
EntryFilter get test => _test; EntryFilter get test => _test;
@override @override
String get label => _location.isEmpty ? emptyLabel : _location; String get universalLabel => _location;
@override
String getLabel(BuildContext context) => _location.isEmpty ? context.l10n.filterLocationEmptyLabel : _location;
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) { Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) {
@ -66,7 +69,10 @@ class LocationFilter extends CollectionFilter {
} }
@override @override
String get typeKey => type; String get category => type;
@override
String get key => '$type-$level-$_location';
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {

View file

@ -1,6 +1,8 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/mime_utils.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/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -17,14 +19,12 @@ class MimeFilter extends CollectionFilter {
if (lowMime.endsWith('/*')) { if (lowMime.endsWith('/*')) {
lowMime = lowMime.substring(0, lowMime.length - 2); lowMime = lowMime.substring(0, lowMime.length - 2);
_test = (entry) => entry.mimeType.startsWith(lowMime); _test = (entry) => entry.mimeType.startsWith(lowMime);
if (lowMime == 'video') { _label = lowMime.toUpperCase();
_label = 'Video'; if (mime == MimeTypes.anyImage) {
_icon = AIcons.video;
} else if (lowMime == 'image') {
_label = 'Image';
_icon = AIcons.image; _icon = AIcons.image;
} else if (mime == MimeTypes.anyVideo) {
_icon = AIcons.video;
} }
_label ??= lowMime.split('/')[0].toUpperCase();
} else { } else {
_test = (entry) => entry.mimeType == lowMime; _test = (entry) => entry.mimeType == lowMime;
_label = MimeUtils.displayType(lowMime); _label = MimeUtils.displayType(lowMime);
@ -47,13 +47,28 @@ class MimeFilter extends CollectionFilter {
EntryFilter get test => _test; EntryFilter get test => _test;
@override @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 @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(_icon, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(_icon, size: size);
@override @override
String get typeKey => type; String get category => type;
@override
String get key => '$type-$mime';
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {

View file

@ -50,7 +50,7 @@ class QueryFilter extends CollectionFilter {
bool get isUnique => false; bool get isUnique => false;
@override @override
String get label => '$query'; String get universalLabel => query;
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.text, size: size); 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> color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(AvesFilterChip.defaultOutlineColor); Future<Color> color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(AvesFilterChip.defaultOutlineColor);
@override @override
String get typeKey => type; String get category => type;
@override
String get key => '$type-$query';
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {

View file

@ -1,11 +1,11 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class TagFilter extends CollectionFilter { class TagFilter extends CollectionFilter {
static const type = 'tag'; static const type = 'tag';
static const emptyLabel = 'untagged';
final String tag; final String tag;
EntryFilter _test; EntryFilter _test;
@ -36,13 +36,19 @@ class TagFilter extends CollectionFilter {
bool get isUnique => false; bool get isUnique => false;
@override @override
String get label => tag.isEmpty ? emptyLabel : tag; String get universalLabel => tag;
@override
String getLabel(BuildContext context) => tag.isEmpty ? context.l10n.filterTagEmptyLabel : tag;
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagOff : AIcons.tag, size: size) : null; Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagOff : AIcons.tag, size: size) : null;
@override @override
String get typeKey => type; String get category => type;
@override
String get key => '$type-$tag';
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {

View file

@ -1,5 +1,6 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -13,26 +14,26 @@ class TypeFilter extends CollectionFilter {
final String itemType; final String itemType;
EntryFilter _test; EntryFilter _test;
String _label;
IconData _icon; IconData _icon;
TypeFilter(this.itemType) { TypeFilter(this.itemType) {
if (itemType == animated) { switch (itemType) {
_test = (entry) => entry.isAnimated; case animated:
_label = 'Animated'; _test = (entry) => entry.isAnimated;
_icon = AIcons.animated; _icon = AIcons.animated;
} else if (itemType == panorama) { break;
_test = (entry) => entry.isImage && entry.is360; case panorama:
_label = 'Panorama'; _test = (entry) => entry.isImage && entry.is360;
_icon = AIcons.threesixty; _icon = AIcons.threesixty;
} else if (itemType == sphericalVideo) { break;
_test = (entry) => entry.isVideo && entry.is360; case sphericalVideo:
_label = '360° Video'; _test = (entry) => entry.isVideo && entry.is360;
_icon = AIcons.threesixty; _icon = AIcons.threesixty;
} else if (itemType == geotiff) { break;
_test = (entry) => entry.isGeotiff; case geotiff:
_label = 'GeoTIFF'; _test = (entry) => entry.isGeotiff;
_icon = AIcons.geo; _icon = AIcons.geo;
break;
} }
} }
@ -51,13 +52,32 @@ class TypeFilter extends CollectionFilter {
EntryFilter get test => _test; EntryFilter get test => _test;
@override @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 @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(_icon, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(_icon, size: size);
@override @override
String get typeKey => type; String get category => type;
@override
String get key => '$type-$itemType';
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {

View file

@ -1,15 +1,17 @@
import 'package:aves/geo/format.dart'; 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'; import 'package:latlong/latlong.dart';
enum CoordinateFormat { dms, decimal } import 'enums.dart';
extension ExtraCoordinateFormat on CoordinateFormat { extension ExtraCoordinateFormat on CoordinateFormat {
String get name { String getName(BuildContext context) {
switch (this) { switch (this) {
case CoordinateFormat.dms: case CoordinateFormat.dms:
return 'DMS'; return context.l10n.coordinateFormatDms;
case CoordinateFormat.decimal: case CoordinateFormat.decimal:
return 'Decimal degrees'; return context.l10n.coordinateFormatDecimal;
default: default:
return toString(); return toString();
} }

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
enum EntryBackground { black, white, transparent, checkered } import 'enums.dart';
extension ExtraEntryBackground on EntryBackground { extension ExtraEntryBackground on EntryBackground {
bool get isColor { bool get isColor {

View file

@ -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 }

View file

@ -1,15 +1,17 @@
import 'package:aves/widgets/collection/collection_page.dart'; 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:aves/widgets/filter_grids/albums_page.dart';
import 'package:flutter/widgets.dart';
enum HomePageSetting { collection, albums } import 'enums.dart';
extension ExtraHomePageSetting on HomePageSetting { extension ExtraHomePageSetting on HomePageSetting {
String get name { String getName(BuildContext context) {
switch (this) { switch (this) {
case HomePageSetting.collection: case HomePageSetting.collection:
return 'Collection'; return context.l10n.collectionPageTitle;
case HomePageSetting.albums: case HomePageSetting.albums:
return 'Albums'; return context.l10n.albumPageTitle;
default: default:
return toString(); return toString();
} }

View file

@ -1,21 +1,23 @@
// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ import 'package:aves/widgets/common/extensions/build_context.dart';
enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor } import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraEntryMapStyle on EntryMapStyle { extension ExtraEntryMapStyle on EntryMapStyle {
String get name { String getName(BuildContext context) {
switch (this) { switch (this) {
case EntryMapStyle.googleNormal: case EntryMapStyle.googleNormal:
return 'Google Maps'; return context.l10n.mapStyleGoogleNormal;
case EntryMapStyle.googleHybrid: case EntryMapStyle.googleHybrid:
return 'Google Maps (Hybrid)'; return context.l10n.mapStyleGoogleHybrid;
case EntryMapStyle.googleTerrain: case EntryMapStyle.googleTerrain:
return 'Google Maps (Terrain)'; return context.l10n.mapStyleGoogleTerrain;
case EntryMapStyle.osmHot: case EntryMapStyle.osmHot:
return 'Humanitarian OSM'; return context.l10n.mapStyleOsmHot;
case EntryMapStyle.stamenToner: case EntryMapStyle.stamenToner:
return 'Stamen Toner'; return context.l10n.mapStyleStamenToner;
case EntryMapStyle.stamenWatercolor: case EntryMapStyle.stamenWatercolor:
return 'Stamen Watercolor'; return context.l10n.mapStyleStamenWatercolor;
default: default:
return toString(); return toString();
} }

View file

@ -1,16 +1,18 @@
import 'package:aves/services/window_service.dart'; 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 { extension ExtraKeepScreenOn on KeepScreenOn {
String get name { String getName(BuildContext context) {
switch (this) { switch (this) {
case KeepScreenOn.never: case KeepScreenOn.never:
return 'Never'; return context.l10n.keepScreenOnNever;
case KeepScreenOn.viewerOnly: case KeepScreenOn.viewerOnly:
return 'Viewer page only'; return context.l10n.keepScreenOnViewerOnly;
case KeepScreenOn.always: case KeepScreenOn.always:
return 'Always'; return context.l10n.keepScreenOnAlways;
default: default:
return toString(); return toString();
} }

View file

@ -1,18 +1,14 @@
import 'package:aves/model/filters/filters.dart'; 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:aves/model/settings/screen_on.dart';
import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../source/enums.dart'; import '../source/enums.dart';
import 'enums.dart';
final Settings settings = Settings._private(); final Settings settings = Settings._private();
@ -24,6 +20,7 @@ class Settings extends ChangeNotifier {
// app // app
static const hasAcceptedTermsKey = 'has_accepted_terms'; static const hasAcceptedTermsKey = 'has_accepted_terms';
static const isCrashlyticsEnabledKey = 'is_crashlytics_enabled'; static const isCrashlyticsEnabledKey = 'is_crashlytics_enabled';
static const localeKey = 'locale';
static const mustBackTwiceToExitKey = 'must_back_twice_to_exit'; static const mustBackTwiceToExitKey = 'must_back_twice_to_exit';
static const keepScreenOnKey = 'keep_screen_on'; static const keepScreenOnKey = 'keep_screen_on';
static const homePageKey = 'home_page'; static const homePageKey = 'home_page';
@ -99,6 +96,34 @@ class Settings extends ChangeNotifier {
unawaited(initFirebase()); 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); bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, true);
set mustBackTwiceToExit(bool newValue) => setAndNotify(mustBackTwiceToExitKey, newValue); set mustBackTwiceToExit(bool newValue) => setAndNotify(mustBackTwiceToExitKey, newValue);
@ -182,7 +207,7 @@ class Settings extends ChangeNotifier {
set showOverlayInfo(bool newValue) => setAndNotify(showOverlayInfoKey, newValue); 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); set showOverlayShootingDetails(bool newValue) => setAndNotify(showOverlayShootingDetailsKey, newValue);

View file

@ -4,6 +4,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
mixin AlbumMixin on SourceBase { mixin AlbumMixin on SourceBase {
@ -12,8 +13,8 @@ mixin AlbumMixin on SourceBase {
List<String> get rawAlbums => List.unmodifiable(_directories); List<String> get rawAlbums => List.unmodifiable(_directories);
int compareAlbumsByName(String a, String b) { int compareAlbumsByName(String a, String b) {
final ua = getUniqueAlbumName(a); final ua = getUniqueAlbumName(null, a);
final ub = getUniqueAlbumName(b); final ub = getUniqueAlbumName(null, b);
final c = compareAsciiUpperCase(ua, ub); final c = compareAsciiUpperCase(ua, ub);
if (c != 0) return c; if (c != 0) return c;
final va = androidFileUtils.getStorageVolume(a)?.path ?? ''; final va = androidFileUtils.getStorageVolume(a)?.path ?? '';
@ -23,7 +24,7 @@ mixin AlbumMixin on SourceBase {
void _notifyAlbumChange() => eventBus.fire(AlbumsChangedEvent()); void _notifyAlbumChange() => eventBus.fire(AlbumsChangedEvent());
String getUniqueAlbumName(String dirPath) { String getUniqueAlbumName(BuildContext context, String dirPath) {
String unique(String dirPath, [bool Function(String) test]) { String unique(String dirPath, [bool Function(String) test]) {
final otherAlbums = _directories.where(test ?? (_) => true).where((item) => item != dirPath); final otherAlbums = _directories.where(test ?? (_) => true).where((item) => item != dirPath);
final parts = dirPath.split(separator); final parts = dirPath.split(separator);
@ -51,7 +52,7 @@ mixin AlbumMixin on SourceBase {
if (volume.isPrimary) { if (volume.isPrimary) {
return uniqueNameInVolume; return uniqueNameInVolume;
} else { } else {
return '$uniqueNameInVolume (${volume.description})'; return '$uniqueNameInVolume (${volume.getDescription(context)})';
} }
} }
} }
@ -99,7 +100,7 @@ mixin AlbumMixin on SourceBase {
invalidateAlbumFilterSummary(directories: emptyAlbums); invalidateAlbumFilterSummary(directories: emptyAlbums);
final pinnedFilters = settings.pinnedFilters; 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; settings.pinnedFilters = pinnedFilters;
} }
} }

View file

@ -92,7 +92,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin {
void addFilter(CollectionFilter filter) { void addFilter(CollectionFilter filter) {
if (filter == null || filters.contains(filter)) return; if (filter == null || filters.contains(filter)) return;
if (filter.isUnique) { if (filter.isUnique) {
filters.removeWhere((old) => old.typeKey == filter.typeKey); filters.removeWhere((old) => old.category == filter.category);
} }
filters.add(filter); filters.add(filter);
onFilterChanged(); onFilterChanged();

View file

@ -10,6 +10,7 @@ import 'package:aves/model/metadata.dart';
import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/metadata_db.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/album.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/location.dart';
import 'package:aves/model/source/tag.dart'; import 'package:aves/model/source/tag.dart';
import 'package:aves/services/image_op_events.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 { class EntryAddedEvent {
final Set<AvesEntry> entries; final Set<AvesEntry> entries;

View file

@ -1,5 +1,7 @@
enum Activity { browse, select } enum Activity { browse, select }
enum SourceState { loading, cataloguing, locating, ready }
enum ChipSortFactor { date, name, count } enum ChipSortFactor { date, name, count }
enum AlbumChipGroupFactor { none, importance, volume } enum AlbumChipGroupFactor { none, importance, volume }

View file

@ -7,6 +7,7 @@ import 'package:aves/model/filters/location.dart';
import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata.dart';
import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/metadata_db.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:tuple/tuple.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 // 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 // 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 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); final updatedCountries = countriesByCode.entries.map((kv) => '${kv.value}${LocationFilter.locationSeparator}${kv.key}').toList()..sort(compareAsciiUpperCase);

View file

@ -6,6 +6,7 @@ import 'package:aves/model/favourite_repo.dart';
import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/metadata_db.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.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/image_file_service.dart';
import 'package:aves/services/media_store_service.dart'; import 'package:aves/services/media_store_service.dart';
import 'package:aves/services/time_service.dart'; import 'package:aves/services/time_service.dart';

View file

@ -3,6 +3,7 @@ import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/metadata.dart'; import 'package:aves/model/metadata.dart';
import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/metadata_db.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';

View file

@ -41,7 +41,6 @@ class AndroidAppService {
static Future<bool> edit(String uri, String mimeType) async { static Future<bool> edit(String uri, String mimeType) async {
try { try {
return await platform.invokeMethod('edit', <String, dynamic>{ return await platform.invokeMethod('edit', <String, dynamic>{
'title': 'Edit with:',
'uri': uri, 'uri': uri,
'mimeType': mimeType, 'mimeType': mimeType,
}); });
@ -54,7 +53,6 @@ class AndroidAppService {
static Future<bool> open(String uri, String mimeType) async { static Future<bool> open(String uri, String mimeType) async {
try { try {
return await platform.invokeMethod('open', <String, dynamic>{ return await platform.invokeMethod('open', <String, dynamic>{
'title': 'Open with:',
'uri': uri, 'uri': uri,
'mimeType': mimeType, 'mimeType': mimeType,
}); });
@ -78,7 +76,6 @@ class AndroidAppService {
static Future<bool> setAs(String uri, String mimeType) async { static Future<bool> setAs(String uri, String mimeType) async {
try { try {
return await platform.invokeMethod('setAs', <String, dynamic>{ return await platform.invokeMethod('setAs', <String, dynamic>{
'title': 'Set as:',
'uri': uri, 'uri': uri,
'mimeType': mimeType, 'mimeType': mimeType,
}); });
@ -94,7 +91,6 @@ class AndroidAppService {
final urisByMimeType = groupBy<AvesEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())); final urisByMimeType = groupBy<AvesEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
try { try {
return await platform.invokeMethod('share', <String, dynamic>{ return await platform.invokeMethod('share', <String, dynamic>{
'title': 'Share via:',
'urisByMimeType': urisByMimeType, 'urisByMimeType': urisByMimeType,
}); });
} on PlatformException catch (e) { } on PlatformException catch (e) {
@ -106,7 +102,6 @@ class AndroidAppService {
static Future<bool> shareSingle(String uri, String mimeType) async { static Future<bool> shareSingle(String uri, String mimeType) async {
try { try {
return await platform.invokeMethod('share', <String, dynamic>{ return await platform.invokeMethod('share', <String, dynamic>{
'title': 'Share via:',
'urisByMimeType': { 'urisByMimeType': {
mimeType: [uri] mimeType: [uri]
}, },

View file

@ -43,7 +43,6 @@ class AIcons {
static const IconData openOutside = Icons.open_in_new_outlined; static const IconData openOutside = Icons.open_in_new_outlined;
static const IconData pin = Icons.push_pin_outlined; static const IconData pin = Icons.push_pin_outlined;
static const IconData print = Icons.print_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 rename = Icons.title_outlined;
static const IconData rotateLeft = Icons.rotate_left_outlined; static const IconData rotateLeft = Icons.rotate_left_outlined;
static const IconData rotateRight = Icons.rotate_right_outlined; static const IconData rotateRight = Icons.rotate_right_outlined;

View file

@ -1,6 +1,7 @@
import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/android_file_service.dart'; import 'package:aves/services/android_file_service.dart';
import 'package:aves/utils/change_notifier.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/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
@ -115,21 +116,30 @@ class Package {
@immutable @immutable
class StorageVolume { class StorageVolume {
final String description, path, state; final String _description, path, state;
final bool isPrimary, isRemovable; final bool isPrimary, isRemovable;
const StorageVolume({ const StorageVolume({
this.description, String description,
this.isPrimary, this.isPrimary,
this.isRemovable, this.isRemovable,
this.path, this.path,
this.state, 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) { factory StorageVolume.fromMap(Map map) {
final isPrimary = map['isPrimary'] ?? false; final isPrimary = map['isPrimary'] ?? false;
return StorageVolume( return StorageVolume(
description: map['description'] ?? (isPrimary ? 'Internal storage' : 'SD card'), description: map['description'],
isPrimary: isPrimary, isPrimary: isPrimary,
isRemovable: map['isRemovable'] ?? false, isRemovable: map['isRemovable'] ?? false,
path: map['path'] ?? '', path: map['path'] ?? '',
@ -167,11 +177,9 @@ class VolumeRelativeDirectory {
); );
} }
String get directoryDescription => relativeDir.isEmpty ? 'root' : '$relativeDir'; String getVolumeDescription(BuildContext context) {
String get volumeDescription {
final volume = androidFileUtils.storageVolumes.firstWhere((volume) => volume.path == volumePath, orElse: () => null); final volume = androidFileUtils.storageVolumes.firstWhere((volume) => volume.path == volumePath, orElse: () => null);
return volume?.description ?? volumePath; return volume?.getDescription(context) ?? volumePath;
} }
@override @override

View file

@ -21,7 +21,6 @@ class Constants {
); );
static const overlayUnknown = ''; // em dash static const overlayUnknown = ''; // em dash
static const infoUnknown = 'unknown';
static final pointNemo = LatLng(-48.876667, -123.393333); static final pointNemo = LatLng(-48.876667, -123.393333);
@ -66,61 +65,175 @@ class Constants {
), ),
]; ];
static const List<Dependency> flutterPackages = [ static const List<Dependency> flutterPlugins = [
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',
),
Dependency( Dependency(
name: 'Connectivity', name: 'Connectivity',
license: 'BSD 3-Clause', license: 'BSD 3-Clause',
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/connectivity/connectivity/LICENSE', licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/connectivity/connectivity/LICENSE',
sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/connectivity/connectivity', 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<Dependency> 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( Dependency(
name: 'Country Code', name: 'Country Code',
license: 'MIT', license: 'MIT',
licenseUrl: 'https://github.com/denixport/dart.country/blob/master/LICENSE', licenseUrl: 'https://github.com/denixport/dart.country/blob/master/LICENSE',
sourceUrl: 'https://github.com/denixport/dart.country', 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( Dependency(
name: 'Event Bus', name: 'Event Bus',
license: 'MIT', license: 'MIT',
licenseUrl: 'https://github.com/marcojakob/dart-event-bus/blob/master/LICENSE', licenseUrl: 'https://github.com/marcojakob/dart-event-bus/blob/master/LICENSE',
sourceUrl: 'https://github.com/marcojakob/dart-event-bus', 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<Dependency> 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( Dependency(
name: 'Expansion Tile Card', name: 'Expansion Tile Card',
license: 'BSD 3-Clause', license: 'BSD 3-Clause',
licenseUrl: 'https://github.com/Skylled/expansion_tile_card/blob/master/LICENSE', licenseUrl: 'https://github.com/Skylled/expansion_tile_card/blob/master/LICENSE',
sourceUrl: 'https://github.com/Skylled/expansion_tile_card', 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( Dependency(
name: 'Flushbar', name: 'Flushbar',
license: 'Apache 2.0', license: 'Apache 2.0',
@ -134,10 +247,10 @@ class Constants {
sourceUrl: 'https://github.com/git-touch/highlight', sourceUrl: 'https://github.com/git-touch/highlight',
), ),
Dependency( Dependency(
name: 'Flutter ijkplayer', name: 'Flutter Localized Locales',
license: 'MIT', license: 'MIT',
licenseUrl: 'https://github.com/CaiJingLong/flutter_ijkplayer/blob/master/LICENSE', licenseUrl: 'https://github.com/guidezpl/flutter-localized-locales/blob/master/LICENSE',
sourceUrl: 'https://github.com/CaiJingLong/flutter_ijkplayer', sourceUrl: 'https://github.com/guidezpl/flutter-localized-locales',
), ),
Dependency( Dependency(
name: 'Flutter Map', name: 'Flutter Map',
@ -163,36 +276,6 @@ class Constants {
licenseUrl: 'https://github.com/dnfield/flutter_svg/blob/master/LICENSE', licenseUrl: 'https://github.com/dnfield/flutter_svg/blob/master/LICENSE',
sourceUrl: 'https://github.com/dnfield/flutter_svg', 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( Dependency(
name: 'Material Design Icons Flutter', name: 'Material Design Icons Flutter',
license: 'MIT', license: 'MIT',
@ -205,12 +288,6 @@ class Constants {
licenseUrl: 'https://github.com/boyan01/overlay_support/blob/master/LICENSE', licenseUrl: 'https://github.com/boyan01/overlay_support/blob/master/LICENSE',
sourceUrl: 'https://github.com/boyan01/overlay_support', 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( Dependency(
name: 'Palette Generator', name: 'Palette Generator',
license: 'BSD 3-Clause', license: 'BSD 3-Clause',
@ -223,84 +300,18 @@ class Constants {
licenseUrl: 'https://github.com/zesage/panorama/blob/master/LICENSE', licenseUrl: 'https://github.com/zesage/panorama/blob/master/LICENSE',
sourceUrl: 'https://github.com/zesage/panorama', 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( Dependency(
name: 'Percent Indicator', name: 'Percent Indicator',
license: 'BSD 2-Clause', license: 'BSD 2-Clause',
licenseUrl: 'https://github.com/diegoveloper/flutter_percent_indicator/blob/master/LICENSE', licenseUrl: 'https://github.com/diegoveloper/flutter_percent_indicator/blob/master/LICENSE',
sourceUrl: 'https://github.com/diegoveloper/flutter_percent_indicator/', 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( Dependency(
name: 'Provider', name: 'Provider',
license: 'MIT', license: 'MIT',
licenseUrl: 'https://github.com/rrousselGit/provider/blob/master/LICENSE', licenseUrl: 'https://github.com/rrousselGit/provider/blob/master/LICENSE',
sourceUrl: 'https://github.com/rrousselGit/provider', 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',
),
]; ];
} }

View file

@ -1,7 +1,8 @@
import 'package:aves/widgets/about/app_ref.dart'; import 'package:aves/widgets/about/app_ref.dart';
import 'package:aves/widgets/about/credits.dart'; import 'package:aves/widgets/about/credits.dart';
import 'package:aves/widgets/about/licenses.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'; import 'package:flutter/material.dart';
class AboutPage extends StatelessWidget { class AboutPage extends StatelessWidget {
@ -11,7 +12,7 @@ class AboutPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('About'), title: Text(context.l10n.aboutPageTitle),
), ),
body: SafeArea( body: SafeArea(
child: CustomScrollView( child: CustomScrollView(
@ -23,7 +24,7 @@ class AboutPage extends StatelessWidget {
[ [
AppReference(), AppReference(),
Divider(), Divider(),
AboutNewVersion(), AboutUpdate(),
AboutCredits(), AboutCredits(),
Divider(), Divider(),
], ],

View file

@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:aves/flutter_version.dart'; import 'package:aves/flutter_version.dart';
import 'package:aves/widgets/common/basic/link_chip.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:aves/widgets/common/identity/aves_logo.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:package_info/package_info.dart'; import 'package:package_info/package_info.dart';
@ -48,7 +49,7 @@ class _AppReferenceState extends State<AppReference> {
leading: AvesLogo( leading: AvesLogo(
size: style.fontSize * MediaQuery.textScaleFactorOf(context) * 1.25, 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', url: 'https://github.com/deckerst/aves',
textStyle: style, textStyle: style,
); );
@ -71,7 +72,7 @@ class _AppReferenceState extends State<AppReference> {
), ),
), ),
), ),
TextSpan(text: 'Flutter ${version['frameworkVersion']}'), TextSpan(text: '${context.l10n.aboutFlutter} ${version['frameworkVersion']}'),
], ],
), ),
style: TextStyle(color: subColor), style: TextStyle(color: subColor),

View file

@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/basic/link_chip.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AboutCredits extends StatelessWidget { class AboutCredits extends StatelessWidget {
@ -16,13 +17,13 @@ class AboutCredits extends StatelessWidget {
constraints: BoxConstraints(minHeight: 48), constraints: BoxConstraints(minHeight: 48),
child: Align( child: Align(
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
child: Text('Credits', style: Constants.titleTextStyle), child: Text(context.l10n.aboutCredits, style: Constants.titleTextStyle),
), ),
), ),
Text.rich( Text.rich(
TextSpan( TextSpan(
children: [ children: [
TextSpan(text: 'This app uses a TopoJSON file from'), TextSpan(text: context.l10n.aboutCreditsWorldAtlas1),
WidgetSpan( WidgetSpan(
child: LinkChip( child: LinkChip(
text: 'World Atlas', text: 'World Atlas',
@ -31,7 +32,7 @@ class AboutCredits extends StatelessWidget {
), ),
alignment: PlaceholderAlignment.middle, alignment: PlaceholderAlignment.middle,
), ),
TextSpan(text: 'under ISC License.'), TextSpan(text: context.l10n.aboutCreditsWorldAtlas2),
], ],
), ),
), ),

View file

@ -3,6 +3,7 @@ import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/basic/link_chip.dart';
import 'package:aves/widgets/common/basic/menu_row.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:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -15,13 +16,15 @@ class Licenses extends StatefulWidget {
class _LicensesState extends State<Licenses> { class _LicensesState extends State<Licenses> {
final ValueNotifier<String> _expandedNotifier = ValueNotifier(null); final ValueNotifier<String> _expandedNotifier = ValueNotifier(null);
LicenseSort _sort = LicenseSort.name; LicenseSort _sort = LicenseSort.name;
List<Dependency> _platform, _flutter; List<Dependency> _platform, _flutterPlugins, _flutterPackages, _dartPackages;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_platform = List.from(Constants.androidDependencies); _platform = List<Dependency>.from(Constants.androidDependencies);
_flutter = List.from(Constants.flutterPackages); _flutterPlugins = List<Dependency>.from(Constants.flutterPlugins);
_flutterPackages = List<Dependency>.from(Constants.flutterPackages);
_dartPackages = List<Dependency>.from(Constants.dartPackages);
_sortPackages(); _sortPackages();
} }
@ -38,7 +41,9 @@ class _LicensesState extends State<Licenses> {
} }
_platform.sort(compare); _platform.sort(compare);
_flutter.sort(compare); _flutterPlugins.sort(compare);
_flutterPackages.sort(compare);
_dartPackages.sort(compare);
} }
@override @override
@ -51,16 +56,28 @@ class _LicensesState extends State<Licenses> {
_buildHeader(), _buildHeader(),
SizedBox(height: 16), SizedBox(height: 16),
AvesExpansionTile( AvesExpansionTile(
title: 'Android Libraries', title: context.l10n.aboutLicensesAndroidLibraries,
color: BrandColors.android, color: BrandColors.android,
expandedNotifier: _expandedNotifier, expandedNotifier: _expandedNotifier,
children: _platform.map((package) => LicenseRow(package)).toList(), children: _platform.map((package) => LicenseRow(package)).toList(),
), ),
AvesExpansionTile( AvesExpansionTile(
title: 'Flutter Packages', title: context.l10n.aboutLicensesFlutterPlugins,
color: BrandColors.flutter, color: BrandColors.flutter,
expandedNotifier: _expandedNotifier, 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( Center(
child: TextButton( child: TextButton(
@ -76,7 +93,7 @@ class _LicensesState extends State<Licenses> {
), ),
), ),
), ),
child: Text('Show All Licenses'.toUpperCase()), child: Text(context.l10n.aboutLicensesShowAllButtonLabel),
), ),
), ),
], ],
@ -94,17 +111,17 @@ class _LicensesState extends State<Licenses> {
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: Text('Open-Source Licenses', style: Constants.titleTextStyle), child: Text(context.l10n.aboutLicenses, style: Constants.titleTextStyle),
), ),
PopupMenuButton<LicenseSort>( PopupMenuButton<LicenseSort>(
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
value: LicenseSort.name, value: LicenseSort.name,
child: MenuRow(text: 'Sort by name', checked: _sort == LicenseSort.name), child: MenuRow(text: context.l10n.aboutLicensesSortByName, checked: _sort == LicenseSort.name),
), ),
PopupMenuItem( PopupMenuItem(
value: LicenseSort.license, 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) { onSelected: (newSort) {
@ -112,7 +129,7 @@ class _LicensesState extends State<Licenses> {
_sortPackages(); _sortPackages();
setState(() {}); setState(() {});
}, },
tooltip: 'Sort', tooltip: context.l10n.aboutLicensesSortTooltip,
icon: Icon(AIcons.sort), icon: Icon(AIcons.sort),
), ),
], ],
@ -121,7 +138,7 @@ class _LicensesState extends State<Licenses> {
SizedBox(height: 8), SizedBox(height: 8),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 8), 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),
), ),
], ],
); );

View file

@ -2,29 +2,30 @@ import 'package:aves/model/availability.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/about/news_badge.dart'; import 'package:aves/widgets/about/news_badge.dart';
import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/basic/link_chip.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AboutNewVersion extends StatefulWidget { class AboutUpdate extends StatefulWidget {
@override @override
_AboutNewVersionState createState() => _AboutNewVersionState(); _AboutUpdateState createState() => _AboutUpdateState();
} }
class _AboutNewVersionState extends State<AboutNewVersion> { class _AboutUpdateState extends State<AboutUpdate> {
Future<bool> _newVersionLoader; Future<bool> _updateChecker;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_newVersionLoader = availability.isNewVersionAvailable; _updateChecker = availability.isNewVersionAvailable;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<bool>( return FutureBuilder<bool>(
future: _newVersionLoader, future: _updateChecker,
builder: (context, snapshot) { builder: (context, snapshot) {
final newVersion = snapshot.data == true; final newVersionAvailable = snapshot.data == true;
if (!newVersion) return SizedBox(); if (!newVersionAvailable) return SizedBox();
return Column( return Column(
children: [ children: [
Padding( Padding(
@ -46,7 +47,7 @@ class _AboutNewVersionState extends State<AboutNewVersion> {
), ),
alignment: PlaceholderAlignment.middle, 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<AboutNewVersion> {
Text.rich( Text.rich(
TextSpan( TextSpan(
children: [ children: [
TextSpan(text: 'A new version of Aves is available on '), TextSpan(text: context.l10n.aboutUpdateLinks1),
WidgetSpan( WidgetSpan(
child: LinkChip( child: LinkChip(
text: 'Github', text: context.l10n.aboutUpdateGithub,
url: 'https://github.com/deckerst/aves/releases', url: 'https://github.com/deckerst/aves/releases',
textStyle: TextStyle(fontWeight: FontWeight.bold), textStyle: TextStyle(fontWeight: FontWeight.bold),
), ),
alignment: PlaceholderAlignment.middle, alignment: PlaceholderAlignment.middle,
), ),
TextSpan(text: ' and '), TextSpan(text: context.l10n.aboutUpdateLinks2),
WidgetSpan( WidgetSpan(
child: LinkChip( child: LinkChip(
text: 'Google Play', text: context.l10n.aboutUpdateGooglePlay,
url: 'https://play.google.com/store/apps/details?id=deckers.thibault.aves', url: 'https://play.google.com/store/apps/details?id=deckers.thibault.aves',
textStyle: TextStyle(fontWeight: FontWeight.bold), textStyle: TextStyle(fontWeight: FontWeight.bold),
), ),
alignment: PlaceholderAlignment.middle, alignment: PlaceholderAlignment.middle,
), ),
TextSpan(text: '.'), TextSpan(text: context.l10n.aboutUpdateLinks3),
], ],
), ),
), ),

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:aves/main.dart'; import 'package:aves/main.dart';
import 'package:aves/model/actions/collection_actions.dart'; import 'package:aves/model/actions/collection_actions.dart';
import 'package:aves/model/actions/entry_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/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.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_subtitle.dart';
import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/app_bar_title.dart';
import 'package:aves/widgets/common/basic/menu_row.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/add_shortcut_dialog.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
import 'package:aves/widgets/search/search_button.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/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:intl/intl.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
class CollectionAppBar extends StatefulWidget { class CollectionAppBar extends StatefulWidget {
@ -141,7 +142,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
Widget _buildAppBarTitle() { Widget _buildAppBarTitle() {
if (collection.isBrowsing) { if (collection.isBrowsing) {
Widget title = Text( Widget title = Text(
AvesApp.mode == AppMode.pick ? 'Pick' : 'Collection', AvesApp.mode == AppMode.pick ? context.l10n.collectionPickPageTitle : context.l10n.collectionPageTitle,
key: Key('appbar-title'), key: Key('appbar-title'),
); );
if (AvesApp.mode == AppMode.main) { if (AvesApp.mode == AppMode.main) {
@ -150,7 +151,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
source: source, source: source,
); );
} }
return TappableAppBarTitle( return InteractiveAppBarTitle(
onTap: _goToSearch, onTap: _goToSearch,
child: title, child: title,
); );
@ -159,7 +160,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
animation: collection.selectionChangeNotifier, animation: collection.selectionChangeNotifier,
builder: (context, child) { builder: (context, child) {
final count = collection.selection.length; 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<CollectionAppBar> with SingleTickerPr
return IconButton( return IconButton(
icon: Icon(action.getIcon()), icon: Icon(action.getIcon()),
onPressed: collection.selection.isEmpty ? null : () => _actionDelegate.onEntryActionSelected(context, action), onPressed: collection.selection.isEmpty ? null : () => _actionDelegate.onEntryActionSelected(context, action),
tooltip: action.getText(), tooltip: action.getText(context),
); );
}, },
)), )),
@ -197,35 +198,30 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
PopupMenuItem( PopupMenuItem(
key: Key('menu-sort'), key: Key('menu-sort'),
value: CollectionAction.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) if (collection.sortFactor == EntrySortFactor.date)
PopupMenuItem( PopupMenuItem(
key: Key('menu-group'), key: Key('menu-group'),
value: CollectionAction.group, value: CollectionAction.group,
child: MenuRow(text: 'Group…', icon: AIcons.group), child: MenuRow(text: context.l10n.menuActionGroup, icon: AIcons.group),
), ),
if (collection.isBrowsing) ...[ if (collection.isBrowsing) ...[
if (kDebugMode)
PopupMenuItem(
value: CollectionAction.refresh,
child: MenuRow(text: 'Refresh', icon: AIcons.refresh),
),
if (AvesApp.mode == AppMode.main) if (AvesApp.mode == AppMode.main)
PopupMenuItem( PopupMenuItem(
value: CollectionAction.select, value: CollectionAction.select,
enabled: isNotEmpty, enabled: isNotEmpty,
child: MenuRow(text: 'Select', icon: AIcons.select), child: MenuRow(text: context.l10n.collectionActionSelect, icon: AIcons.select),
), ),
PopupMenuItem( PopupMenuItem(
value: CollectionAction.stats, value: CollectionAction.stats,
enabled: isNotEmpty, enabled: isNotEmpty,
child: MenuRow(text: 'Stats', icon: AIcons.stats), child: MenuRow(text: context.l10n.menuActionStats, icon: AIcons.stats),
), ),
if (AvesApp.mode == AppMode.main && canAddShortcuts) if (AvesApp.mode == AppMode.main && canAddShortcuts)
PopupMenuItem( PopupMenuItem(
value: CollectionAction.addShortcut, value: CollectionAction.addShortcut,
child: MenuRow(text: 'Add shortcut…', icon: AIcons.addShortcut), child: MenuRow(text: context.l10n.collectionActionAddShortcut, icon: AIcons.addShortcut),
), ),
], ],
if (collection.isSelecting) ...[ if (collection.isSelecting) ...[
@ -233,28 +229,28 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
PopupMenuItem( PopupMenuItem(
value: CollectionAction.copy, value: CollectionAction.copy,
enabled: hasSelection, enabled: hasSelection,
child: MenuRow(text: 'Copy to album'), child: MenuRow(text: context.l10n.collectionActionCopy),
), ),
PopupMenuItem( PopupMenuItem(
value: CollectionAction.move, value: CollectionAction.move,
enabled: hasSelection, enabled: hasSelection,
child: MenuRow(text: 'Move to album'), child: MenuRow(text: context.l10n.collectionActionMove),
), ),
PopupMenuItem( PopupMenuItem(
value: CollectionAction.refreshMetadata, value: CollectionAction.refreshMetadata,
enabled: hasSelection, enabled: hasSelection,
child: MenuRow(text: 'Refresh metadata'), child: MenuRow(text: context.l10n.collectionActionRefreshMetadata),
), ),
PopupMenuDivider(), PopupMenuDivider(),
PopupMenuItem( PopupMenuItem(
value: CollectionAction.selectAll, value: CollectionAction.selectAll,
enabled: collection.selection.length < collection.entryCount, enabled: collection.selection.length < collection.entryCount,
child: MenuRow(text: 'Select all'), child: MenuRow(text: context.l10n.collectionActionSelectAll),
), ),
PopupMenuItem( PopupMenuItem(
value: CollectionAction.selectNone, value: CollectionAction.selectNone,
enabled: hasSelection, enabled: hasSelection,
child: MenuRow(text: 'Select none'), child: MenuRow(text: context.l10n.collectionActionSelectNone),
), ),
] ]
]; ];
@ -289,9 +285,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
case CollectionAction.refreshMetadata: case CollectionAction.refreshMetadata:
_actionDelegate.onCollectionActionSelected(context, action); _actionDelegate.onCollectionActionSelected(context, action);
break; break;
case CollectionAction.refresh:
unawaited(source.refresh());
break;
case CollectionAction.select: case CollectionAction.select:
collection.select(); collection.select();
break; break;
@ -313,12 +306,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
builder: (context) => AvesSelectionDialog<EntryGroupFactor>( builder: (context) => AvesSelectionDialog<EntryGroupFactor>(
initialValue: settings.collectionGroupFactor, initialValue: settings.collectionGroupFactor,
options: { options: {
EntryGroupFactor.album: 'By album', EntryGroupFactor.album: context.l10n.collectionGroupAlbum,
EntryGroupFactor.month: 'By month', EntryGroupFactor.month: context.l10n.collectionGroupMonth,
EntryGroupFactor.day: 'By day', EntryGroupFactor.day: context.l10n.collectionGroupDay,
EntryGroupFactor.none: 'Do not group', EntryGroupFactor.none: context.l10n.collectionGroupNone,
}, },
title: 'Group', title: context.l10n.collectionGroupTitle,
), ),
); );
if (value != null) { if (value != null) {
@ -332,11 +325,11 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
builder: (context) => AvesSelectionDialog<EntrySortFactor>( builder: (context) => AvesSelectionDialog<EntrySortFactor>(
initialValue: settings.collectionSortFactor, initialValue: settings.collectionSortFactor,
options: { options: {
EntrySortFactor.date: 'By date', EntrySortFactor.date: context.l10n.collectionSortDate,
EntrySortFactor.size: 'By size', EntrySortFactor.size: context.l10n.collectionSortSize,
EntrySortFactor.name: 'By album & file name', EntrySortFactor.name: context.l10n.collectionSortName,
}, },
title: 'Sort', title: context.l10n.collectionSortTitle,
), ),
); );
if (value != null) { if (value != null) {
@ -348,14 +341,24 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
} }
Future<void> _showShortcutDialog(BuildContext context) async { Future<void> _showShortcutDialog(BuildContext context) async {
final filters = collection.filters;
var defaultName;
if (filters.isEmpty) {
defaultName = context.l10n.collectionPageTitle;
} else {
final sortedFilters = List<CollectionFilter>.from(filters)..sort();
defaultName = sortedFilters.first.getLabel(context);
}
final name = await showDialog<String>( final name = await showDialog<String>(
context: context, context: context,
builder: (context) => AddShortcutDialog(collection.filters), builder: (context) {
return AddShortcutDialog(defaultName: defaultName);
},
); );
if (name == null || name.isEmpty) return; if (name == null || name.isEmpty) return;
final iconEntry = collection.sortedEntries.isNotEmpty ? collection.sortedEntries.first : null; final iconEntry = collection.sortedEntries.isNotEmpty ? collection.sortedEntries.first : null;
unawaited(AppShortcutService.pin(name, iconEntry, collection.filters)); unawaited(AppShortcutService.pin(name, iconEntry, filters));
} }
void _goToSearch() { void _goToSearch() {

View file

@ -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/feedback.dart';
import 'package:aves/widgets/common/action_mixins/permission_aware.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/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/aves_dialog.dart';
import 'package:aves/widgets/filter_grids/album_pick.dart'; import 'package:aves/widgets/filter_grids/album_pick.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
final CollectionLens collection; final CollectionLens collection;
@ -112,10 +112,10 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
final movedCount = movedOps.length; final movedCount = movedOps.length;
if (movedCount < todoCount) { if (movedCount < todoCount) {
final count = todoCount - movedCount; 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 { } else {
final count = movedCount; 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( await source.updateAfterMove(
todoEntries: todoEntries, todoEntries: todoEntries,
@ -138,15 +138,15 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
builder: (context) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context, 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: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()), child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, true), 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; final deletedCount = deletedUris.length;
if (deletedCount < selectionCount) { if (deletedCount < selectionCount) {
final count = selectionCount - deletedCount; 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.removeEntries(deletedUris);
collection.browse(); collection.browse();

View file

@ -14,7 +14,7 @@ class FilterBar extends StatefulWidget implements PreferredSizeWidget {
Key key, Key key,
@required Set<CollectionFilter> filters, @required Set<CollectionFilter> filters,
@required this.onPressed, @required this.onPressed,
}) : filters = List.from(filters)..sort(), }) : filters = List<CollectionFilter>.from(filters)..sort(),
super(key: key); super(key: key);
@override @override

View file

@ -9,12 +9,11 @@ import 'package:flutter/material.dart';
class AlbumSectionHeader extends StatelessWidget { class AlbumSectionHeader extends StatelessWidget {
final String directory, albumName; final String directory, albumName;
AlbumSectionHeader({ const AlbumSectionHeader({
Key key, Key key,
@required CollectionSource source,
@required this.directory, @required this.directory,
}) : albumName = source.getUniqueAlbumName(directory), @required this.albumName,
super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -47,7 +46,7 @@ class AlbumSectionHeader extends StatelessWidget {
return SectionHeader.getPreferredHeight( return SectionHeader.getPreferredHeight(
context: context, context: context,
maxWidth: maxWidth, maxWidth: maxWidth,
title: source.getUniqueAlbumName(directory), title: source.getUniqueAlbumName(context, directory),
hasLeading: androidFileUtils.getAlbumType(directory) != AlbumType.regular, hasLeading: androidFileUtils.getAlbumType(directory) != AlbumType.regular,
hasTrailing: androidFileUtils.isOnRemovableStorage(directory), hasTrailing: androidFileUtils.isOnRemovableStorage(directory),
); );

View file

@ -23,7 +23,7 @@ class CollectionSectionHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final header = _buildHeader(); final header = _buildHeader(context);
return header != null return header != null
? SizedBox( ? SizedBox(
height: height, height: height,
@ -32,18 +32,12 @@ class CollectionSectionHeader extends StatelessWidget {
: SizedBox.shrink(); : SizedBox.shrink();
} }
Widget _buildHeader() { Widget _buildHeader(BuildContext context) {
Widget _buildAlbumHeader() => AlbumSectionHeader(
key: ValueKey(sectionKey),
source: collection.source,
directory: (sectionKey as EntryAlbumSectionKey).directory,
);
switch (collection.sortFactor) { switch (collection.sortFactor) {
case EntrySortFactor.date: case EntrySortFactor.date:
switch (collection.groupFactor) { switch (collection.groupFactor) {
case EntryGroupFactor.album: case EntryGroupFactor.album:
return _buildAlbumHeader(); return _buildAlbumHeader(context);
case EntryGroupFactor.month: case EntryGroupFactor.month:
return MonthSectionHeader(key: ValueKey(sectionKey), date: (sectionKey as EntryDateSectionKey).date); return MonthSectionHeader(key: ValueKey(sectionKey), date: (sectionKey as EntryDateSectionKey).date);
case EntryGroupFactor.day: case EntryGroupFactor.day:
@ -53,13 +47,23 @@ class CollectionSectionHeader extends StatelessWidget {
} }
break; break;
case EntrySortFactor.name: case EntrySortFactor.name:
return _buildAlbumHeader(); return _buildAlbumHeader(context);
case EntrySortFactor.size: case EntrySortFactor.size:
break; break;
} }
return null; 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) { static double getPreferredHeight(BuildContext context, double maxWidth, CollectionSource source, SectionKey sectionKey) {
var headerExtent = 0.0; var headerExtent = 0.0;
if (sectionKey is EntryAlbumSectionKey) { if (sectionKey is EntryAlbumSectionKey) {

View file

@ -1,73 +1,79 @@
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/utils/time_utils.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:aves/widgets/common/grid/header.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class DaySectionHeader extends StatelessWidget { class DaySectionHeader extends StatelessWidget {
final DateTime date; final DateTime date;
final String text;
DaySectionHeader({ const DaySectionHeader({
Key key, Key key,
@required this.date, @required this.date,
}) : text = _formatDate(date), }) : super(key: key);
super(key: key);
// Examples (en_US): // Examples (en_US):
// `MMMMd`: `April 15` // `MMMMd`: `April 15`
// `yMMMMd`: `April 15, 2020` // `yMMMMd`: `April 15, 2020`
// `MMMEd`: `Wed, Apr 15` // `MMMEd`: `Wed, Apr 15`
// `yMMMEd`: `Wed, Apr 15, 2020` // `yMMMEd`: `Wed, Apr 15, 2020`
// `MMMMEEEEd`: `Wednesday, April 15` // `MMMMEEEEd`: `Wednesday, April 15`
// `yMMMMEEEEd`: `Wednesday, April 15, 2020` // `yMMMMEEEEd`: `Wednesday, April 15, 2020`
// `MEd`: `Wed, 4/15` // `MEd`: `Wed, 4/15`
// `yMEd`: `Wed, 4/15/2020` // `yMEd`: `Wed, 4/15/2020`
static DateFormat md = DateFormat.MMMMd();
static DateFormat ymd = DateFormat.yMMMMd();
static DateFormat day = DateFormat.E();
static String _formatDate(DateTime date) { // Examples (ko):
if (date.isToday) return 'Today'; // `MMMMd`: `1 26`
if (date.isYesterday) return 'Yesterday'; // `yMMMMd`: `2021 1 26`
if (date.isThisYear) return '${md.format(date)} (${day.format(date)})'; // `MMMEd`: `1 26 ()`
return '${ymd.format(date)} (${day.format(date)})'; // `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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SectionHeader( return SectionHeader(
sectionKey: EntryDateSectionKey(date), sectionKey: EntryDateSectionKey(date),
title: text, title: _formatDate(context, date),
); );
} }
} }
class MonthSectionHeader extends StatelessWidget { class MonthSectionHeader extends StatelessWidget {
final DateTime date; final DateTime date;
final String text;
MonthSectionHeader({ const MonthSectionHeader({
Key key, Key key,
@required this.date, @required this.date,
}) : text = _formatDate(date), }) : super(key: key);
super(key: key);
static DateFormat m = DateFormat.MMMM(); static String _formatDate(BuildContext context, DateTime date) {
static DateFormat ym = DateFormat.yMMMM(); final l10n = context.l10n;
if (date == null) return l10n.sectionUnknown;
static String _formatDate(DateTime date) { if (date.isThisMonth) return l10n.dateThisMonth;
if (date == null) return 'Unknown'; final locale = l10n.localeName;
if (date.isThisMonth) return 'This month'; if (date.isThisYear) return DateFormat.MMMM(locale).format(date);
if (date.isThisYear) return m.format(date); return DateFormat.yMMMM(locale).format(date);
return ym.format(date);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SectionHeader( return SectionHeader(
sectionKey: EntryDateSectionKey(date), sectionKey: EntryDateSectionKey(date),
title: text, title: _formatDate(context, date),
); );
} }
} }

View file

@ -1,7 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:aves/model/highlight.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/highlight.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';

View file

@ -3,6 +3,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/entry_images.dart'; import 'package:aves/model/entry_images.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/collection/thumbnail/error.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:aves/widgets/common/fx/transition_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -79,7 +80,7 @@ class _RasterImageThumbnailState extends State<RasterImageThumbnail> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!entry.canDecode) { if (!entry.canDecode) {
return _buildError(context, '${entry.mimeType} not supported', null); return _buildError(context, context.l10n.errorUnsupportedMimeType(entry.mimeType), null);
} }
final fastImage = Image( final fastImage = Image(

View file

@ -1,6 +1,7 @@
import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:aves/image_providers/uri_picture_provider.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/entry_background.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/model/settings/settings.dart';
import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View file

@ -6,12 +6,11 @@ import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:aves/model/source/collection_lens.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/ref/mime_types.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/app_bar.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/section_layout.dart';
import 'package:aves/widgets/collection/grid/selector.dart'; import 'package:aves/widgets/collection/grid/selector.dart';
import 'package:aves/widgets/collection/grid/thumbnail.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/extensions/media_query.dart';
import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/section_layout.dart';
import 'package:aves/widgets/common/grid/sliver.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/identity/scroll_thumb.dart';
import 'package:aves/widgets/common/scaling.dart'; import 'package:aves/widgets/common/scaling.dart';
import 'package:aves/widgets/common/tile_extent_manager.dart'; import 'package:aves/widgets/common/tile_extent_manager.dart';
@ -265,18 +265,18 @@ class _CollectionScrollViewState extends State<CollectionScrollView> {
if (collection.filters.any((filter) => filter is FavouriteFilter)) { if (collection.filters.any((filter) => filter is FavouriteFilter)) {
return EmptyContent( return EmptyContent(
icon: AIcons.favourite, icon: AIcons.favourite,
text: 'No favourites', text: context.l10n.collectionEmptyFavourites,
); );
} }
if (collection.filters.any((filter) => filter is MimeFilter && filter.mime == MimeTypes.anyVideo)) { if (collection.filters.any((filter) => filter is MimeFilter && filter.mime == MimeTypes.anyVideo)) {
return EmptyContent( return EmptyContent(
icon: AIcons.video, icon: AIcons.video,
text: 'No videos', text: context.l10n.collectionEmptyVideos,
); );
} }
return EmptyContent( return EmptyContent(
icon: AIcons.image, icon: AIcons.image,
text: 'No images', text: context.l10n.collectionEmptyImages,
); );
}, },
); );

View file

@ -1,6 +1,7 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/services/android_file_service.dart'; import 'package:aves/services/android_file_service.dart';
import 'package:aves/utils/android_file_utils.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:aves/widgets/dialogs/aves_dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -26,18 +27,20 @@ mixin PermissionAwareMixin {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) {
final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir);
final volume = dir.getVolumeDescription(context);
return AvesDialog( return AvesDialog(
context: context, context: context,
title: 'Storage Volume Access', title: context.l10n.storageAccessDialogTitle,
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.'), content: Text(context.l10n.storageVolumeAccessDialogMessage(directory, volume)),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()), child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => Navigator.pop(context, true),
child: Text('OK'.toUpperCase()), child: Text(MaterialLocalizations.of(context).okButtonLabel),
), ),
], ],
); );
@ -58,14 +61,16 @@ mixin PermissionAwareMixin {
return showDialog<bool>( return showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) {
final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir);
final volume = dir.getVolumeDescription(context);
return AvesDialog( return AvesDialog(
context: context, context: context,
title: 'Restricted Access', title: context.l10n.restrictedAccessDialogTitle,
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.'), content: Text(context.l10n.restrictedAccessDialogMessage(directory, volume)),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('OK'.toUpperCase()), child: Text(MaterialLocalizations.of(context).okButtonLabel),
), ),
], ],
); );

View file

@ -6,6 +6,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/services/android_file_service.dart'; import 'package:aves/services/android_file_service.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/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:aves/widgets/dialogs/aves_dialog.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -43,14 +44,17 @@ mixin SizeAwareMixin {
await showDialog( await showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
final neededSize = formatFilesize(needed);
final freeSize = formatFilesize(free);
final volume = destinationVolume.getDescription(context);
return AvesDialog( return AvesDialog(
context: context, context: context,
title: 'Not Enough Space', title: context.l10n.notEnoughSpaceDialogTitle,
content: Text('This operation needs ${formatFilesize(needed)} of free space on “${destinationVolume.description}” to complete, but there is only ${formatFilesize(free)} left.'), content: Text(context.l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('OK'.toUpperCase()), child: Text(MaterialLocalizations.of(context).okButtonLabel),
), ),
], ],
); );

View file

@ -1,5 +1,7 @@
import 'package:aves/model/source/collection_source.dart'; 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/durations.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class SourceStateAwareAppBarTitle extends StatelessWidget { class SourceStateAwareAppBarTitle extends StatelessWidget {
@ -54,13 +56,13 @@ class SourceStateSubtitle extends StatelessWidget {
String subtitle; String subtitle;
switch (source.stateNotifier.value) { switch (source.stateNotifier.value) {
case SourceState.loading: case SourceState.loading:
subtitle = 'Loading'; subtitle = context.l10n.sourceStateLoading;
break; break;
case SourceState.cataloguing: case SourceState.cataloguing:
subtitle = 'Cataloguing'; subtitle = context.l10n.sourceStateCataloguing;
break; break;
case SourceState.locating: case SourceState.locating:
subtitle = 'Locating'; subtitle = context.l10n.sourceStateLocating;
break; break;
case SourceState.ready: case SourceState.ready:
default: default:

View file

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class TappableAppBarTitle extends StatelessWidget { class InteractiveAppBarTitle extends StatelessWidget {
final GestureTapCallback onTap; final GestureTapCallback onTap;
final Widget child; final Widget child;
const TappableAppBarTitle({ const InteractiveAppBarTitle({
this.onTap, this.onTap,
@required this.child, @required this.child,
}); });

View file

@ -1,6 +1,7 @@
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/debouncer.dart'; import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -34,7 +35,7 @@ class _QueryBarState extends State<QueryBar> {
_controller.clear(); _controller.clear();
filterNotifier.value = ''; filterNotifier.value = '';
}, },
tooltip: 'Clear', tooltip: context.l10n.clearTooltip,
); );
return Row( return Row(

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/action_mixins/feedback.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/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:overlay_support/overlay_support.dart'; import 'package:overlay_support/overlay_support.dart';
@ -37,7 +38,7 @@ class _DoubleBackPopScopeState extends State<DoubleBackPopScope> with FeedbackMi
_stopBackTimer(); _stopBackTimer();
_backTimer = Timer(Durations.doubleBackTimerDelay, () => _backOnce = false); _backTimer = Timer(Durations.doubleBackTimerDelay, () => _backOnce = false);
toast( toast(
'Tap “back” again to exit.', context.l10n.doubleBackExitMessage,
duration: Durations.doubleBackTimerDelay, duration: Durations.doubleBackTimerDelay,
); );
return SynchronousFuture(false); return SynchronousFuture(false);

View file

@ -1,5 +1,8 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension ExtraContext on BuildContext { extension ExtraContext on BuildContext {
String get currentRouteName => ModalRoute.of(this)?.settings?.name; String get currentRouteName => ModalRoute.of(this)?.settings?.name;
AppLocalizations get l10n => AppLocalizations.of(this);
} }

View file

@ -4,6 +4,7 @@ import 'package:aves/model/source/section_keys.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -161,7 +162,7 @@ class _SectionSelectableLeading extends StatelessWidget {
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,
icon: Icon(selected ? AIcons.selected : AIcons.unselected), icon: Icon(selected ? AIcons.selected : AIcons.unselected),
onPressed: onPressed, onPressed: onPressed,
tooltip: selected ? 'Deselect section' : 'Select section', tooltip: selected ? context.l10n.collectionDeselectSectionTooltip : context.l10n.collectionSelectSectionTooltip,
constraints: BoxConstraints( constraints: BoxConstraints(
minHeight: leadingDimension, minHeight: leadingDimension,
minWidth: leadingDimension, minWidth: leadingDimension,

View file

@ -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. // 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 // 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. // 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<T> extends StatelessWidget { class SectionedListSliver<T> extends StatelessWidget {
const SectionedListSliver(); const SectionedListSliver();

View file

@ -73,7 +73,7 @@ class AvesFilterChip extends StatefulWidget {
items: actions items: actions
.map((action) => PopupMenuItem( .map((action) => PopupMenuItem(
value: action, value: action,
child: MenuRow(text: action.getText(), icon: action.getIcon()), child: MenuRow(text: action.getText(context), icon: action.getIcon()),
)) ))
.toList(), .toList(),
); );
@ -103,10 +103,15 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initColorLoader();
_tapped = false; _tapped = false;
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
_initColorLoader();
}
@override @override
void didUpdateWidget(covariant AvesFilterChip oldWidget) { void didUpdateWidget(covariant AvesFilterChip oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
@ -146,7 +151,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
], ],
Flexible( Flexible(
child: Text( child: Text(
filter.label, filter.getLabel(context),
softWrap: false, softWrap: false,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
maxLines: 1, maxLines: 1,
@ -203,7 +208,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
child: widget.background, child: widget.background,
), ),
Tooltip( Tooltip(
message: filter.tooltip, message: filter.getTooltip(context),
preferBelow: false, preferBelow: false,
child: Material( child: Material(
color: hasBackground ? Colors.transparent : Theme.of(context).scaffoldBackgroundColor, color: hasBackground ? Colors.transparent : Theme.of(context).scaffoldBackgroundColor,

View file

@ -40,6 +40,8 @@ class DebugSettingsSection extends StatelessWidget {
'pinnedFilters': toMultiline(settings.pinnedFilters), 'pinnedFilters': toMultiline(settings.pinnedFilters),
'searchHistory': toMultiline(settings.searchHistory), 'searchHistory': toMultiline(settings.searchHistory),
'lastVersionCheckDate': '${settings.lastVersionCheckDate}', 'lastVersionCheckDate': '${settings.lastVersionCheckDate}',
'locale': '${settings.locale}',
'systemLocale': '${WidgetsBinding.instance.window.locale}',
}), }),
), ),
], ],

View file

@ -39,7 +39,7 @@ class _DebugStorageSectionState extends State<DebugStorageSection> with Automati
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 8), padding: EdgeInsets.symmetric(horizontal: 8),
child: InfoRowGroup({ child: InfoRowGroup({
'description': '${v.description}', 'description': '${v.getDescription(context)}',
'isPrimary': '${v.isPrimary}', 'isPrimary': '${v.isPrimary}',
'isRemovable': '${v.isRemovable}', 'isRemovable': '${v.isRemovable}',
'state': '${v.state}', 'state': '${v.state}',

View file

@ -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 'package:flutter/material.dart';
import 'aves_dialog.dart'; import 'aves_dialog.dart';
class AddShortcutDialog extends StatefulWidget { class AddShortcutDialog extends StatefulWidget {
final Set<CollectionFilter> filters; final String defaultName;
const AddShortcutDialog(this.filters); const AddShortcutDialog({
@required this.defaultName,
});
@override @override
_AddShortcutDialogState createState() => _AddShortcutDialogState(); _AddShortcutDialogState createState() => _AddShortcutDialogState();
@ -19,12 +21,7 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final filters = List.from(widget.filters)..sort(); _nameController.text = widget.defaultName;
if (filters.isEmpty) {
_nameController.text = 'Collection';
} else {
_nameController.text = filters.first.label;
}
_validate(); _validate();
} }
@ -41,7 +38,7 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
content: TextField( content: TextField(
controller: _nameController, controller: _nameController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Shortcut label', labelText: context.l10n.addShortcutDialogLabel,
), ),
autofocus: true, autofocus: true,
maxLength: 25, maxLength: 25,
@ -51,14 +48,14 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()), child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
), ),
ValueListenableBuilder<bool>( ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier, valueListenable: _isValidNotifier,
builder: (context, isValid, child) { builder: (context, isValid, child) {
return TextButton( return TextButton(
onPressed: isValid ? () => _submit(context) : null, onPressed: isValid ? () => _submit(context) : null,
child: Text('Add'.toUpperCase()), child: Text(context.l10n.addShortcutButtonLabel),
); );
}, },
) )

View file

@ -1,5 +1,6 @@
import 'dart:ui'; import 'dart:ui';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -92,12 +93,12 @@ void showNoMatchingAppDialog(BuildContext context) {
builder: (context) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context, context: context,
title: 'No Matching App', title: context.l10n.noMatchingAppDialogTitle,
content: Text('There are no apps that can handle this.'), content: Text(context.l10n.noMatchingAppDialogMessage),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('OK'.toUpperCase()), child: Text(MaterialLocalizations.of(context).okButtonLabel),
), ),
], ],
); );

View file

@ -23,7 +23,7 @@ class AvesSelectionDialog<T> extends StatefulWidget {
_AvesSelectionDialogState<T> createState() => _AvesSelectionDialogState<T>(); _AvesSelectionDialogState<T> createState() => _AvesSelectionDialogState<T>();
} }
class _AvesSelectionDialogState<T> extends State<AvesSelectionDialog> { class _AvesSelectionDialogState<T> extends State<AvesSelectionDialog<T>> {
T _selectedValue; T _selectedValue;
@override @override
@ -41,13 +41,14 @@ class _AvesSelectionDialogState<T> extends State<AvesSelectionDialog> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()), child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
), ),
], ],
); );
} }
Widget _buildRadioListTile(T value, String title) { Widget _buildRadioListTile(T value, String title) {
final subtitle = widget.optionSubtitleBuilder?.call(value);
return ReselectableRadioListTile<T>( return ReselectableRadioListTile<T>(
key: Key(value.toString()), key: Key(value.toString()),
value: value, value: value,
@ -64,9 +65,9 @@ class _AvesSelectionDialogState<T> extends State<AvesSelectionDialog> {
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
maxLines: 1, maxLines: 1,
), ),
subtitle: widget.optionSubtitleBuilder != null subtitle: subtitle != null
? Text( ? Text(
widget.optionSubtitleBuilder(value), subtitle,
softWrap: false, softWrap: false,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
maxLines: 1, maxLines: 1,

View file

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/utils/android_file_utils.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:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -50,17 +51,17 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
volumeTiles.addAll([ volumeTiles.addAll([
Padding( Padding(
padding: AvesDialog.contentHorizontalPadding + EdgeInsets.only(top: 20), padding: AvesDialog.contentHorizontalPadding + EdgeInsets.only(top: 20),
child: Text('Storage:'), child: Text(context.l10n.newAlbumDialogStorageLabel),
), ),
...primaryVolumes.map(_buildVolumeTile), ...primaryVolumes.map((volume) => _buildVolumeTile(context, volume)),
...otherVolumes.map(_buildVolumeTile), ...otherVolumes.map((volume) => _buildVolumeTile(context, volume)),
SizedBox(height: 8), SizedBox(height: 8),
]); ]);
} }
return AvesDialog( return AvesDialog(
context: context, context: context,
title: 'New Album', title: context.l10n.newAlbumDialogTitle,
scrollController: _scrollController, scrollController: _scrollController,
scrollableContent: [ scrollableContent: [
...volumeTiles, ...volumeTiles,
@ -73,8 +74,8 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
controller: _nameController, controller: _nameController,
focusNode: _nameFieldFocusNode, focusNode: _nameFieldFocusNode,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Album name', labelText: context.l10n.newAlbumDialogNameLabel,
helperText: exists ? 'Directory already exists' : '', helperText: exists ? context.l10n.newAlbumDialogNameLabelAlreadyExistsHelper : '',
), ),
autofocus: _allVolumes.length == 1, autofocus: _allVolumes.length == 1,
onChanged: (_) => _validate(), onChanged: (_) => _validate(),
@ -86,14 +87,14 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()), child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
), ),
ValueListenableBuilder<bool>( ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier, valueListenable: _isValidNotifier,
builder: (context, isValid, child) { builder: (context, isValid, child) {
return TextButton( return TextButton(
onPressed: isValid ? () => _submit(context) : null, onPressed: isValid ? () => _submit(context) : null,
child: Text('Create'.toUpperCase()), child: Text(context.l10n.createAlbumButtonLabel),
); );
}, },
), ),
@ -101,7 +102,7 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
); );
} }
Widget _buildVolumeTile(StorageVolume volume) => RadioListTile<StorageVolume>( Widget _buildVolumeTile(BuildContext context, StorageVolume volume) => RadioListTile<StorageVolume>(
value: volume, value: volume,
groupValue: _selectedVolume, groupValue: _selectedVolume,
onChanged: (volume) { onChanged: (volume) {
@ -110,7 +111,7 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
setState(() {}); setState(() {});
}, },
title: Text( title: Text(
volume.description, volume.getDescription(context),
softWrap: false, softWrap: false,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
maxLines: 1, maxLines: 1,

View file

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@ -46,8 +47,8 @@ class _RenameAlbumDialogState extends State<RenameAlbumDialog> {
return TextField( return TextField(
controller: _nameController, controller: _nameController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'New name', labelText: context.l10n.renameAlbumDialogLabel,
helperText: exists ? 'Directory already exists' : '', helperText: exists ? context.l10n.renameAlbumDialogLabelAlreadyExistsHelper : '',
), ),
autofocus: true, autofocus: true,
onChanged: (_) => _validate(), onChanged: (_) => _validate(),
@ -57,14 +58,14 @@ class _RenameAlbumDialogState extends State<RenameAlbumDialog> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()), child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
), ),
ValueListenableBuilder<bool>( ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier, valueListenable: _isValidNotifier,
builder: (context, isValid, child) { builder: (context, isValid, child) {
return TextButton( return TextButton(
onPressed: isValid ? () => _submit(context) : null, onPressed: isValid ? () => _submit(context) : null,
child: Text('Apply'.toUpperCase()), child: Text(context.l10n.applyButtonLabel),
); );
}, },
) )

View file

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@ -41,7 +42,7 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
content: TextField( content: TextField(
controller: _nameController, controller: _nameController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'New name', labelText: context.l10n.renameEntryDialogLabel,
suffixText: entry.extension, suffixText: entry.extension,
), ),
autofocus: true, autofocus: true,
@ -51,14 +52,14 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()), child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
), ),
ValueListenableBuilder<bool>( ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier, valueListenable: _isValidNotifier,
builder: (context, isValid, child) { builder: (context, isValid, child) {
return TextButton( return TextButton(
onPressed: isValid ? () => _submit(context) : null, onPressed: isValid ? () => _submit(context) : null,
child: Text('Apply'.toUpperCase()), child: Text(context.l10n.applyButtonLabel),
); );
}, },
) )

View file

@ -15,7 +15,7 @@ class AlbumTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
final uniqueName = source.getUniqueAlbumName(album); final uniqueName = source.getUniqueAlbumName(context, album);
return CollectionNavTile( return CollectionNavTile(
leading: IconUtils.getAlbumIcon( leading: IconUtils.getAlbumIcon(
context: context, context: context,

View file

@ -12,6 +12,7 @@ import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/about/about_page.dart'; import 'package:aves/widgets/about/about_page.dart';
import 'package:aves/widgets/about/news_badge.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/extensions/media_query.dart';
import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart';
import 'package:aves/widgets/debug/app_debug_page.dart'; import 'package:aves/widgets/debug/app_debug_page.dart';
@ -105,7 +106,7 @@ class _AppDrawerState extends State<AppDrawer> {
children: [ children: [
AvesLogo(size: 64), AvesLogo(size: 64),
Text( Text(
'Aves', context.l10n.appName,
style: TextStyle( style: TextStyle(
fontSize: 44, fontSize: 44,
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
@ -146,25 +147,25 @@ class _AppDrawerState extends State<AppDrawer> {
Widget get allCollectionTile => CollectionNavTile( Widget get allCollectionTile => CollectionNavTile(
leading: Icon(AIcons.allCollection), leading: Icon(AIcons.allCollection),
title: 'All collection', title: context.l10n.drawerCollectionAll,
filter: null, filter: null,
); );
Widget get videoTile => CollectionNavTile( Widget get videoTile => CollectionNavTile(
leading: Icon(AIcons.video), leading: Icon(AIcons.video),
title: 'Videos', title: context.l10n.drawerCollectionVideos,
filter: MimeFilter(MimeTypes.anyVideo), filter: MimeFilter(MimeTypes.anyVideo),
); );
Widget get favouriteTile => CollectionNavTile( Widget get favouriteTile => CollectionNavTile(
leading: Icon(AIcons.favourite), leading: Icon(AIcons.favourite),
title: 'Favourites', title: context.l10n.drawerCollectionFavourites,
filter: FavouriteFilter(), filter: FavouriteFilter(),
); );
Widget get albumListTile => NavTile( Widget get albumListTile => NavTile(
icon: AIcons.album, icon: AIcons.album,
title: 'Albums', title: context.l10n.albumPageTitle,
trailing: StreamBuilder( trailing: StreamBuilder(
stream: source.eventBus.on<AlbumsChangedEvent>(), stream: source.eventBus.on<AlbumsChangedEvent>(),
builder: (context, _) => Text('${source.rawAlbums.length}'), builder: (context, _) => Text('${source.rawAlbums.length}'),
@ -175,7 +176,7 @@ class _AppDrawerState extends State<AppDrawer> {
Widget get countryListTile => NavTile( Widget get countryListTile => NavTile(
icon: AIcons.location, icon: AIcons.location,
title: 'Countries', title: context.l10n.countryPageTitle,
trailing: StreamBuilder( trailing: StreamBuilder(
stream: source.eventBus.on<CountriesChangedEvent>(), stream: source.eventBus.on<CountriesChangedEvent>(),
builder: (context, _) => Text('${source.sortedCountries.length}'), builder: (context, _) => Text('${source.sortedCountries.length}'),
@ -186,7 +187,7 @@ class _AppDrawerState extends State<AppDrawer> {
Widget get tagListTile => NavTile( Widget get tagListTile => NavTile(
icon: AIcons.tag, icon: AIcons.tag,
title: 'Tags', title: context.l10n.tagPageTitle,
trailing: StreamBuilder( trailing: StreamBuilder(
stream: source.eventBus.on<TagsChangedEvent>(), stream: source.eventBus.on<TagsChangedEvent>(),
builder: (context, _) => Text('${source.sortedTags.length}'), builder: (context, _) => Text('${source.sortedTags.length}'),
@ -197,7 +198,7 @@ class _AppDrawerState extends State<AppDrawer> {
Widget get settingsTile => NavTile( Widget get settingsTile => NavTile(
icon: AIcons.settings, icon: AIcons.settings,
title: 'Settings', title: context.l10n.settingsPageTitle,
topLevel: false, topLevel: false,
routeName: SettingsPage.routeName, routeName: SettingsPage.routeName,
pageBuilder: (_) => SettingsPage(), pageBuilder: (_) => SettingsPage(),
@ -209,7 +210,7 @@ class _AppDrawerState extends State<AppDrawer> {
final newVersion = snapshot.data == true; final newVersion = snapshot.data == true;
return NavTile( return NavTile(
icon: AIcons.info, icon: AIcons.info,
title: 'About', title: context.l10n.aboutPageTitle,
trailing: newVersion ? AboutNewsBadge() : null, trailing: newVersion ? AboutNewsBadge() : null,
topLevel: false, topLevel: false,
routeName: AboutPage.routeName, routeName: AboutPage.routeName,

View file

@ -7,10 +7,11 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.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/app_bar_subtitle.dart';
import 'package:aves/widgets/common/basic/menu_row.dart'; import 'package:aves/widgets/common/basic/menu_row.dart';
import 'package:aves/widgets/common/basic/query_bar.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/dialogs/create_album_dialog.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart';
@ -58,7 +59,7 @@ class _AlbumPickPageState extends State<AlbumPickPage> {
stream: source.eventBus.on<AlbumsChangedEvent>(), stream: source.eventBus.on<AlbumsChangedEvent>(),
builder: (context, snapshot) => FilterGridPage<AlbumFilter>( builder: (context, snapshot) => FilterGridPage<AlbumFilter>(
appBar: appBar, appBar: appBar,
filterSections: AlbumListPage.getAlbumEntries(source), filterSections: AlbumListPage.getAlbumEntries(context, source),
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
applyQuery: (filters, query) { applyQuery: (filters, query) {
if (query == null || query.isEmpty) return filters; if (query == null || query.isEmpty) return filters;
@ -68,7 +69,7 @@ class _AlbumPickPageState extends State<AlbumPickPage> {
queryNotifier: _queryNotifier, queryNotifier: _queryNotifier,
emptyBuilder: () => EmptyContent( emptyBuilder: () => EmptyContent(
icon: AIcons.album, icon: AIcons.album,
text: 'No albums', text: context.l10n.albumEmpty,
), ),
settingsRouteKey: AlbumListPage.routeName, settingsRouteKey: AlbumListPage.routeName,
appBarHeight: AlbumPickAppBar.preferredHeight, appBarHeight: AlbumPickAppBar.preferredHeight,
@ -100,11 +101,11 @@ class AlbumPickAppBar extends StatelessWidget {
String title() { String title() {
switch (moveType) { switch (moveType) {
case MoveType.copy: case MoveType.copy:
return 'Copy to Album'; return context.l10n.albumPickPageTitleCopy;
case MoveType.export: case MoveType.export:
return 'Export to Album'; return context.l10n.albumPickPageTitleExport;
case MoveType.move: case MoveType.move:
return 'Move to Album'; return context.l10n.albumPickPageTitleMove;
default: default:
return null; return null;
} }
@ -131,22 +132,26 @@ class AlbumPickAppBar extends StatelessWidget {
Navigator.pop<String>(context, newAlbum); Navigator.pop<String>(context, newAlbum);
} }
}, },
tooltip: 'Create album', tooltip: context.l10n.createAlbumTooltip,
), ),
PopupMenuButton<ChipSetAction>( PopupMenuButton<ChipSetAction>(
itemBuilder: (context) { itemBuilder: (context) {
return [ return [
PopupMenuItem( PopupMenuItem(
value: ChipSetAction.sort, value: ChipSetAction.sort,
child: MenuRow(text: 'Sort…', icon: AIcons.sort), child: MenuRow(text: context.l10n.menuActionSort, icon: AIcons.sort),
), ),
PopupMenuItem( PopupMenuItem(
value: ChipSetAction.group, value: ChipSetAction.group,
child: MenuRow(text: 'Group…', icon: AIcons.group), child: MenuRow(text: context.l10n.menuActionGroup, icon: AIcons.group),
), ),
]; ];
}, },
onSelected: (action) { 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 // wait for the popup menu to hide before proceeding with the action
Future.delayed(Durations.popupMenuAnimation * timeDilation, () => actionDelegate.onActionSelected(context, action)); Future.delayed(Durations.popupMenuAnimation * timeDilation, () => actionDelegate.onActionSelected(context, action));
}, },

View file

@ -7,7 +7,8 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.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_action_delegate.dart';
import 'package:aves/widgets/filter_grids/common/chip_set_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'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart';
@ -32,7 +33,7 @@ class AlbumListPage extends StatelessWidget {
stream: source.eventBus.on<AlbumsChangedEvent>(), stream: source.eventBus.on<AlbumsChangedEvent>(),
builder: (context, snapshot) => FilterNavigationPage<AlbumFilter>( builder: (context, snapshot) => FilterNavigationPage<AlbumFilter>(
source: source, source: source,
title: 'Albums', title: context.l10n.albumPageTitle,
groupable: true, groupable: true,
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
chipSetActionDelegate: AlbumChipSetActionDelegate(), chipSetActionDelegate: AlbumChipSetActionDelegate(),
@ -43,10 +44,10 @@ class AlbumListPage extends StatelessWidget {
ChipAction.delete, ChipAction.delete,
ChipAction.hide, ChipAction.hide,
], ],
filterSections: getAlbumEntries(source), filterSections: getAlbumEntries(context, source),
emptyBuilder: () => EmptyContent( emptyBuilder: () => EmptyContent(
icon: AIcons.album, 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 // common with album selection page to move/copy entries
static Map<ChipSectionKey, List<FilterGridItem<AlbumFilter>>> getAlbumEntries(CollectionSource source) { static Map<ChipSectionKey, List<FilterGridItem<AlbumFilter>>> getAlbumEntries(BuildContext context, CollectionSource source) {
final filters = source.rawAlbums.map((album) => AlbumFilter(album, source.getUniqueAlbumName(album))).toSet(); final filters = source.rawAlbums.map((album) => AlbumFilter(album, source.getUniqueAlbumName(context, album))).toSet();
final sorted = FilterNavigationPage.sort(settings.albumSortFactor, source, filters); final sorted = FilterNavigationPage.sort(settings.albumSortFactor, source, filters);
return _group(sorted); return _group(context, sorted);
} }
static Map<ChipSectionKey, List<FilterGridItem<AlbumFilter>>> _group(Iterable<FilterGridItem<AlbumFilter>> sortedMapEntries) { static Map<ChipSectionKey, List<FilterGridItem<AlbumFilter>>> _group(BuildContext context, Iterable<FilterGridItem<AlbumFilter>> sortedMapEntries) {
final pinned = settings.pinnedFilters.whereType<AlbumFilter>(); final pinned = settings.pinnedFilters.whereType<AlbumFilter>();
final byPin = groupBy<FilterGridItem<AlbumFilter>, bool>(sortedMapEntries, (e) => pinned.contains(e.filter)); final byPin = groupBy<FilterGridItem<AlbumFilter>, bool>(sortedMapEntries, (e) => pinned.contains(e.filter));
final pinnedMapEntries = (byPin[true] ?? []); final pinnedMapEntries = (byPin[true] ?? []);
@ -73,25 +74,28 @@ class AlbumListPage extends StatelessWidget {
var sections = <ChipSectionKey, List<FilterGridItem<AlbumFilter>>>{}; var sections = <ChipSectionKey, List<FilterGridItem<AlbumFilter>>>{};
switch (settings.albumGroupFactor) { switch (settings.albumGroupFactor) {
case AlbumChipGroupFactor.importance: case AlbumChipGroupFactor.importance:
final specialKey = AlbumImportanceSectionKey.special(context);
final appsKey = AlbumImportanceSectionKey.apps(context);
final regularKey = AlbumImportanceSectionKey.regular(context);
sections = groupBy<FilterGridItem<AlbumFilter>, ChipSectionKey>(unpinnedMapEntries, (kv) { sections = groupBy<FilterGridItem<AlbumFilter>, ChipSectionKey>(unpinnedMapEntries, (kv) {
switch (androidFileUtils.getAlbumType(kv.filter.album)) { switch (androidFileUtils.getAlbumType(kv.filter.album)) {
case AlbumType.regular: case AlbumType.regular:
return AlbumImportanceSectionKey.regular; return regularKey;
case AlbumType.app: case AlbumType.app:
return AlbumImportanceSectionKey.apps; return appsKey;
default: default:
return AlbumImportanceSectionKey.special; return specialKey;
} }
}); });
sections = { sections = {
AlbumImportanceSectionKey.special: sections[AlbumImportanceSectionKey.special], specialKey: sections[specialKey],
AlbumImportanceSectionKey.apps: sections[AlbumImportanceSectionKey.apps], appsKey: sections[appsKey],
AlbumImportanceSectionKey.regular: sections[AlbumImportanceSectionKey.regular], regularKey: sections[regularKey],
}..removeWhere((key, value) => value == null); }..removeWhere((key, value) => value == null);
break; break;
case AlbumChipGroupFactor.volume: case AlbumChipGroupFactor.volume:
sections = groupBy<FilterGridItem<AlbumFilter>, ChipSectionKey>(unpinnedMapEntries, (kv) { sections = groupBy<FilterGridItem<AlbumFilter>, ChipSectionKey>(unpinnedMapEntries, (kv) {
return StorageVolumeSectionKey(androidFileUtils.getStorageVolume(kv.filter.album)); return StorageVolumeSectionKey(context, androidFileUtils.getStorageVolume(kv.filter.album));
}); });
break; break;
case AlbumChipGroupFactor.none: case AlbumChipGroupFactor.none:
@ -106,7 +110,7 @@ class AlbumListPage extends StatelessWidget {
if (pinnedMapEntries.isNotEmpty) { if (pinnedMapEntries.isNotEmpty) {
sections = Map.fromEntries([ sections = Map.fromEntries([
MapEntry(AlbumImportanceSectionKey.pinned, pinnedMapEntries), MapEntry(AlbumImportanceSectionKey.pinned(context), pinnedMapEntries),
...sections.entries, ...sections.entries,
]); ]);
} }

View file

@ -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/feedback.dart';
import 'package:aves/widgets/common/action_mixins/permission_aware.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/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/aves_dialog.dart';
import 'package:aves/widgets/dialogs/rename_album_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/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -52,15 +52,15 @@ class ChipActionDelegate {
builder: (context) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context, 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: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()), child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, true), 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) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context, 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: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()), child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, true), 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; final deletedCount = deletedUris.length;
if (deletedCount < selectionCount) { if (deletedCount < selectionCount) {
final count = selectionCount - deletedCount; 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.removeEntries(deletedUris);
source.resumeMonitoring(); source.resumeMonitoring();
@ -183,9 +183,9 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
final movedCount = movedOps.length; final movedCount = movedOps.length;
if (movedCount < todoCount) { if (movedCount < todoCount) {
final count = todoCount - movedCount; 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 { } else {
showFeedback(context, 'Done!'); showFeedback(context, context.l10n.genericSuccessFeedback);
} }
final pinned = settings.pinnedFilters.contains(filter); final pinned = settings.pinnedFilters.contains(filter);
await source.updateAfterMove( await source.updateAfterMove(
@ -197,7 +197,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
); );
// repin new album after obsolete album got removed and unpinned // repin new album after obsolete album got removed and unpinned
if (pinned) { if (pinned) {
final newFilter = AlbumFilter(destinationAlbum, source.getUniqueAlbumName(destinationAlbum)); final newFilter = AlbumFilter(destinationAlbum, source.getUniqueAlbumName(context, destinationAlbum));
settings.pinnedFilters = settings.pinnedFilters..add(newFilter); settings.pinnedFilters = settings.pinnedFilters..add(newFilter);
} }
source.resumeMonitoring(); source.resumeMonitoring();

View file

@ -2,6 +2,7 @@ import 'package:aves/model/actions/chip_actions.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.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/dialogs/aves_selection_dialog.dart';
import 'package:aves/widgets/stats/stats.dart'; import 'package:aves/widgets/stats/stats.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -13,14 +14,10 @@ abstract class ChipSetActionDelegate {
set sortFactor(ChipSortFactor factor); set sortFactor(ChipSortFactor factor);
void onActionSelected(BuildContext context, ChipSetAction action) { void onActionSelected(BuildContext context, ChipSetAction action) {
final source = context.read<CollectionSource>();
switch (action) { switch (action) {
case ChipSetAction.sort: case ChipSetAction.sort:
_showSortDialog(context); _showSortDialog(context);
break; break;
case ChipSetAction.refresh:
source.refresh();
break;
case ChipSetAction.stats: case ChipSetAction.stats:
_goToStats(context); _goToStats(context);
break; break;
@ -35,11 +32,11 @@ abstract class ChipSetActionDelegate {
builder: (context) => AvesSelectionDialog<ChipSortFactor>( builder: (context) => AvesSelectionDialog<ChipSortFactor>(
initialValue: sortFactor, initialValue: sortFactor,
options: { options: {
ChipSortFactor.date: 'By date', ChipSortFactor.date: context.l10n.chipSortDate,
ChipSortFactor.name: 'By name', ChipSortFactor.name: context.l10n.chipSortName,
ChipSortFactor.count: 'By item count', ChipSortFactor.count: context.l10n.chipSortCount,
}, },
title: 'Sort', title: context.l10n.chipSortTitle,
), ),
); );
if (factor != null) { if (factor != null) {
@ -86,11 +83,11 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate {
builder: (context) => AvesSelectionDialog<AlbumChipGroupFactor>( builder: (context) => AvesSelectionDialog<AlbumChipGroupFactor>(
initialValue: settings.albumGroupFactor, initialValue: settings.albumGroupFactor,
options: { options: {
AlbumChipGroupFactor.importance: 'By tier', AlbumChipGroupFactor.importance: context.l10n.albumGroupTier,
AlbumChipGroupFactor.volume: 'By storage volume', AlbumChipGroupFactor.volume: context.l10n.albumGroupVolume,
AlbumChipGroupFactor.none: 'Do not group', AlbumChipGroupFactor.none: context.l10n.albumGroupNone,
}, },
title: 'Group', title: context.l10n.albumGroupTitle,
), ),
); );
if (factor != null) { if (factor != null) {

View file

@ -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_subtitle.dart';
import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/app_bar_title.dart';
import 'package:aves/widgets/common/basic/menu_row.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_action_delegate.dart';
import 'package:aves/widgets/filter_grids/common/chip_set_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'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart';
@ -49,7 +50,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
return FilterGridPage<T>( return FilterGridPage<T>(
key: ValueKey('filter-grid-page'), key: ValueKey('filter-grid-page'),
appBar: SliverAppBar( appBar: SliverAppBar(
title: TappableAppBarTitle( title: InteractiveAppBarTitle(
onTap: () => _goToSearch(context), onTap: () => _goToSearch(context),
child: SourceStateAwareAppBarTitle( child: SourceStateAwareAppBarTitle(
title: Text(title), title: Text(title),
@ -93,7 +94,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
items: chipActionsBuilder(filter) items: chipActionsBuilder(filter)
.map((action) => PopupMenuItem( .map((action) => PopupMenuItem(
value: action, value: action,
child: MenuRow(text: action.getText(), icon: action.getIcon()), child: MenuRow(text: action.getText(context), icon: action.getIcon()),
)) ))
.toList(), .toList(),
); );
@ -113,21 +114,16 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
PopupMenuItem( PopupMenuItem(
key: Key('menu-sort'), key: Key('menu-sort'),
value: ChipSetAction.sort, value: ChipSetAction.sort,
child: MenuRow(text: 'Sort…', icon: AIcons.sort), child: MenuRow(text: context.l10n.menuActionSort, icon: AIcons.sort),
), ),
if (groupable) if (groupable)
PopupMenuItem( PopupMenuItem(
value: ChipSetAction.group, value: ChipSetAction.group,
child: MenuRow(text: 'Group…', icon: AIcons.group), child: MenuRow(text: context.l10n.menuActionGroup, icon: AIcons.group),
),
if (kDebugMode)
PopupMenuItem(
value: ChipSetAction.refresh,
child: MenuRow(text: 'Refresh', icon: AIcons.refresh),
), ),
PopupMenuItem( PopupMenuItem(
value: ChipSetAction.stats, value: ChipSetAction.stats,
child: MenuRow(text: 'Stats', icon: AIcons.stats), child: MenuRow(text: context.l10n.menuActionStats, icon: AIcons.stats),
), ),
]; ];
}, },

View file

@ -1,6 +1,7 @@
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.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/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -29,12 +30,15 @@ class ChipSectionKey extends SectionKey {
class AlbumImportanceSectionKey extends ChipSectionKey { class AlbumImportanceSectionKey extends ChipSectionKey {
final AlbumImportance importance; 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); factory AlbumImportanceSectionKey.pinned(BuildContext context) => AlbumImportanceSectionKey._private(context, AlbumImportance.pinned);
static AlbumImportanceSectionKey special = AlbumImportanceSectionKey._private(AlbumImportance.special);
static AlbumImportanceSectionKey apps = AlbumImportanceSectionKey._private(AlbumImportance.apps); factory AlbumImportanceSectionKey.special(BuildContext context) => AlbumImportanceSectionKey._private(context, AlbumImportance.special);
static AlbumImportanceSectionKey regular = AlbumImportanceSectionKey._private(AlbumImportance.regular);
factory AlbumImportanceSectionKey.apps(BuildContext context) => AlbumImportanceSectionKey._private(context, AlbumImportance.apps);
factory AlbumImportanceSectionKey.regular(BuildContext context) => AlbumImportanceSectionKey._private(context, AlbumImportance.regular);
@override @override
Widget get leading => Icon(importance.getIcon()); Widget get leading => Icon(importance.getIcon());
@ -43,16 +47,16 @@ class AlbumImportanceSectionKey extends ChipSectionKey {
enum AlbumImportance { pinned, special, apps, regular } enum AlbumImportance { pinned, special, apps, regular }
extension ExtraAlbumImportance on AlbumImportance { extension ExtraAlbumImportance on AlbumImportance {
String getText() { String getText(BuildContext context) {
switch (this) { switch (this) {
case AlbumImportance.pinned: case AlbumImportance.pinned:
return 'Pinned'; return context.l10n.albumTierPinned;
case AlbumImportance.special: case AlbumImportance.special:
return 'Common'; return context.l10n.albumTierSpecial;
case AlbumImportance.apps: case AlbumImportance.apps:
return 'Apps'; return context.l10n.albumTierApps;
case AlbumImportance.regular: case AlbumImportance.regular:
return 'Others'; return context.l10n.albumTierRegular;
} }
return null; return null;
} }
@ -75,7 +79,7 @@ extension ExtraAlbumImportance on AlbumImportance {
class StorageVolumeSectionKey extends ChipSectionKey { class StorageVolumeSectionKey extends ChipSectionKey {
final StorageVolume volume; 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 @override
Widget get leading => (volume?.isRemovable ?? false) ? Icon(AIcons.removableStorage) : null; Widget get leading => (volume?.isRemovable ?? false) ? Icon(AIcons.removableStorage) : null;

View file

@ -6,7 +6,8 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/location.dart';
import 'package:aves/theme/icons.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_action_delegate.dart';
import 'package:aves/widgets/filter_grids/common/chip_set_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'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart';
@ -29,7 +30,7 @@ class CountryListPage extends StatelessWidget {
stream: source.eventBus.on<CountriesChangedEvent>(), stream: source.eventBus.on<CountriesChangedEvent>(),
builder: (context, snapshot) => FilterNavigationPage<LocationFilter>( builder: (context, snapshot) => FilterNavigationPage<LocationFilter>(
source: source, source: source,
title: 'Countries', title: context.l10n.countryPageTitle,
chipSetActionDelegate: CountryChipSetActionDelegate(), chipSetActionDelegate: CountryChipSetActionDelegate(),
chipActionDelegate: ChipActionDelegate(), chipActionDelegate: ChipActionDelegate(),
chipActionsBuilder: (filter) => [ chipActionsBuilder: (filter) => [
@ -39,7 +40,7 @@ class CountryListPage extends StatelessWidget {
filterSections: _getCountryEntries(source), filterSections: _getCountryEntries(source),
emptyBuilder: () => EmptyContent( emptyBuilder: () => EmptyContent(
icon: AIcons.location, icon: AIcons.location,
text: 'No countries', text: context.l10n.countryEmpty,
), ),
), ),
); );

View file

@ -6,7 +6,8 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/model/source/tag.dart'; import 'package:aves/model/source/tag.dart';
import 'package:aves/theme/icons.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_action_delegate.dart';
import 'package:aves/widgets/filter_grids/common/chip_set_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'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart';
@ -29,7 +30,7 @@ class TagListPage extends StatelessWidget {
stream: source.eventBus.on<TagsChangedEvent>(), stream: source.eventBus.on<TagsChangedEvent>(),
builder: (context, snapshot) => FilterNavigationPage<TagFilter>( builder: (context, snapshot) => FilterNavigationPage<TagFilter>(
source: source, source: source,
title: 'Tags', title: context.l10n.tagPageTitle,
chipSetActionDelegate: TagChipSetActionDelegate(), chipSetActionDelegate: TagChipSetActionDelegate(),
chipActionDelegate: ChipActionDelegate(), chipActionDelegate: ChipActionDelegate(),
chipActionsBuilder: (filter) => [ chipActionsBuilder: (filter) => [
@ -39,7 +40,7 @@ class TagListPage extends StatelessWidget {
filterSections: _getTagEntries(source), filterSections: _getTagEntries(source),
emptyBuilder: () => EmptyContent( emptyBuilder: () => EmptyContent(
icon: AIcons.tag, icon: AIcons.tag,
text: 'No tags', text: context.l10n.tagEmpty,
), ),
), ),
); );

View file

@ -45,7 +45,7 @@ class ExpandableFilterRow extends StatelessWidget {
IconButton( IconButton(
icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand), icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand),
onPressed: () => expandedNotifier.value = isExpanded ? null : title, onPressed: () => expandedNotifier.value = isExpanded ? null : title,
tooltip: isExpanded ? 'Collapse' : 'Expand', tooltip: isExpanded ? MaterialLocalizations.of(context).expandedIconTapHint : MaterialLocalizations.of(context).collapsedIconTapHint,
), ),
], ],
), ),

View file

@ -16,7 +16,7 @@ class CollectionSearchButton extends StatelessWidget {
key: Key('search-button'), key: Key('search-button'),
icon: Icon(AIcons.search), icon: Icon(AIcons.search),
onPressed: () => _goToSearch(context), onPressed: () => _goToSearch(context),
tooltip: 'Search', tooltip: MaterialLocalizations.of(context).searchFieldLabel,
); );
} }

View file

@ -15,11 +15,13 @@ import 'package:aves/model/source/tag.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/collection_page.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/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/search/expandable_filter_row.dart'; import 'package:aves/widgets/search/expandable_filter_row.dart';
import 'package:aves/widgets/search/search_page.dart'; import 'package:aves/widgets/search/search_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
class CollectionSearchDelegate { class CollectionSearchDelegate {
final CollectionSource source; final CollectionSource source;
@ -27,6 +29,16 @@ class CollectionSearchDelegate {
final ValueNotifier<String> expandedSectionNotifier = ValueNotifier(null); final ValueNotifier<String> expandedSectionNotifier = ValueNotifier(null);
static const searchHistoryCount = 10; 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}); CollectionSearchDelegate({@required this.source, this.parentCollection});
@ -58,7 +70,7 @@ class CollectionSearchDelegate {
query = ''; query = '';
showSuggestions(context); showSuggestions(context);
}, },
tooltip: 'Clear', tooltip: context.l10n.clearTooltip,
), ),
]; ];
} }
@ -71,84 +83,82 @@ class CollectionSearchDelegate {
valueListenable: expandedSectionNotifier, valueListenable: expandedSectionNotifier,
builder: (context, expandedSection, child) { builder: (context, expandedSection, child) {
final queryFilter = _buildQueryFilter(false); final queryFilter = _buildQueryFilter(false);
final history = settings.searchHistory; return Selector<Settings, Set<CollectionFilter>>(
return ListView( selector: (context, s) => s.hiddenFilters,
padding: EdgeInsets.only(top: 8), builder: (context, hiddenFilters, child) {
children: [ bool notHidden(CollectionFilter filter) => !hiddenFilters.contains(filter);
_buildFilterRow( final history = settings.searchHistory.where(notHidden).toList();
context: context, return ListView(
filters: [ padding: EdgeInsets.only(top: 8),
queryFilter, children: [
FavouriteFilter(), _buildFilterRow(
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<AlbumsChangedEvent>(),
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(
context: context, context: context,
title: 'Albums',
filters: filters,
);
}),
StreamBuilder(
stream: source.eventBus.on<CountriesChangedEvent>(),
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<PlacesChangedEvent>(),
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: [ filters: [
if (containQuery(LocationFilter.emptyLabel)) noFilter, queryFilter,
...filters, ...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
StreamBuilder( heroTypeBuilder: (filter) => filter == queryFilter ? HeroType.always : HeroType.onTap,
stream: source.eventBus.on<TagsChangedEvent>(), ),
builder: (context, snapshot) { if (upQuery.isEmpty && history.isNotEmpty)
final filters = source.sortedTags.where(containQuery).map((s) => TagFilter(s)); _buildFilterRow(
final noFilter = TagFilter(''); context: context,
return _buildFilterRow( title: context.l10n.searchSectionRecent,
context: context, filters: history,
title: 'Tags', ),
filters: [ StreamBuilder(
if (containQuery(TagFilter.emptyLabel)) noFilter, stream: source.eventBus.on<AlbumsChangedEvent>(),
...filters, 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<CountriesChangedEvent>(),
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<PlacesChangedEvent>(),
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<TagsChangedEvent>(),
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,
],
);
}),
],
);
});
}), }),
); );
} }

View file

@ -1,5 +1,6 @@
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/utils/debouncer.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:aves/widgets/search/search_delegate.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -11,8 +12,8 @@ class SearchPage extends StatefulWidget {
final Animation<double> animation; final Animation<double> animation;
const SearchPage({ const SearchPage({
this.delegate, @required this.delegate,
this.animation, @required this.animation,
}); });
@override @override
@ -118,7 +119,7 @@ class _SearchPageState extends State<SearchPage> {
onSubmitted: (_) => widget.delegate.showResults(context), onSubmitted: (_) => widget.delegate.showResults(context),
decoration: InputDecoration( decoration: InputDecoration(
border: InputBorder.none, border: InputBorder.none,
hintText: 'Search collection', hintText: context.l10n.searchCollectionFieldHint,
hintStyle: theme.inputDecorationTheme.hintStyle, hintStyle: theme.inputDecorationTheme.hintStyle,
), ),
), ),

View file

@ -1,13 +1,14 @@
import 'package:aves/services/android_file_service.dart'; import 'package:aves/services/android_file_service.dart';
import 'package:aves/theme/icons.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'; import 'package:flutter/material.dart';
class StorageAccessTile extends StatelessWidget { class StorageAccessTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
title: Text('Storage Access'), title: Text(context.l10n.settingsStorageAccessTile),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@ -44,7 +45,7 @@ class _StorageAccessPageState extends State<StorageAccessPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Storage Access'), title: Text(context.l10n.settingsStorageAccessTitle),
), ),
body: SafeArea( body: SafeArea(
child: Column( child: Column(
@ -56,7 +57,7 @@ class _StorageAccessPageState extends State<StorageAccessPage> {
children: [ children: [
Icon(AIcons.info), Icon(AIcons.info),
SizedBox(width: 16), 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<StorageAccessPage> {
_lastPaths = snapshot.data..sort(); _lastPaths = snapshot.data..sort();
if (_lastPaths.isEmpty) { if (_lastPaths.isEmpty) {
return EmptyContent( return EmptyContent(
text: 'No access grants', text: context.l10n.settingsStorageAccessEmpty,
); );
} }
return Column( return Column(
@ -90,7 +91,7 @@ class _StorageAccessPageState extends State<StorageAccessPage> {
_load(); _load();
setState(() {}); setState(() {});
}, },
tooltip: 'Revoke', tooltip: context.l10n.settingsStorageAccessRevokeTooltip,
), ),
)) ))
.toList(), .toList(),

View file

@ -1,4 +1,5 @@
import 'package:aves/model/settings/entry_background.dart'; 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/borders.dart';
import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View file

@ -1,8 +1,9 @@
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/icons.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/aves_filter_chip.dart';
import 'package:aves/widgets/common/identity/empty.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -10,7 +11,7 @@ class HiddenFilterTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
title: Text('Hidden filters'), title: Text(context.l10n.settingsHiddenFiltersTile),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@ -31,7 +32,7 @@ class HiddenFilterPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Hidden Filters'), title: Text(context.l10n.settingsHiddenFiltersTitle),
), ),
body: SafeArea( body: SafeArea(
child: Column( child: Column(
@ -43,7 +44,7 @@ class HiddenFilterPage extends StatelessWidget {
children: [ children: [
Icon(AIcons.info), Icon(AIcons.info),
SizedBox(width: 16), 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) { if (hiddenFilters.isEmpty) {
return EmptyContent( return EmptyContent(
icon: AIcons.hide, icon: AIcons.hide,
text: 'No hidden filters', text: context.l10n.settingsHiddenFiltersEmpty,
); );
} }
return Wrap( return Wrap(

View file

@ -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<Locale>(
context: context,
builder: (context) => AvesSelectionDialog<Locale>(
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<Locale, String> _getLocaleOptions(BuildContext context) {
final supportedLocales = List<Locale>.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),
});
}
}

View file

@ -1,15 +1,18 @@
import 'package:aves/model/settings/coordinate_format.dart'; 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/home_page.dart';
import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/screen_on.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/utils/constants.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/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.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/dialogs/aves_selection_dialog.dart';
import 'package:aves/widgets/settings/access_grants.dart'; import 'package:aves/widgets/settings/access_grants.dart';
import 'package:aves/widgets/settings/entry_background.dart'; import 'package:aves/widgets/settings/entry_background.dart';
import 'package:aves/widgets/settings/hidden_filters.dart'; import 'package:aves/widgets/settings/hidden_filters.dart';
import 'package:aves/widgets/settings/language.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -30,7 +33,7 @@ class _SettingsPageState extends State<SettingsPage> {
return MediaQueryDataProvider( return MediaQueryDataProvider(
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Settings'), title: Text(context.l10n.settingsPageTitle),
), ),
body: Theme( body: Theme(
data: theme.copyWith( data: theme.copyWith(
@ -73,19 +76,19 @@ class _SettingsPageState extends State<SettingsPage> {
Widget _buildNavigationSection(BuildContext context) { Widget _buildNavigationSection(BuildContext context) {
return AvesExpansionTile( return AvesExpansionTile(
title: 'Navigation', title: context.l10n.settingsSectionNavigation,
expandedNotifier: _expandedNotifier, expandedNotifier: _expandedNotifier,
children: [ children: [
ListTile( ListTile(
title: Text('Home'), title: Text(context.l10n.settingsHome),
subtitle: Text(settings.homePage.name), subtitle: Text(settings.homePage.getName(context)),
onTap: () async { onTap: () async {
final value = await showDialog<HomePageSetting>( final value = await showDialog<HomePageSetting>(
context: context, context: context,
builder: (context) => AvesSelectionDialog<HomePageSetting>( builder: (context) => AvesSelectionDialog<HomePageSetting>(
initialValue: settings.homePage, initialValue: settings.homePage,
options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.name))), options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.getName(context)))),
title: 'Home', title: context.l10n.settingsHome,
), ),
); );
if (value != null) { if (value != null) {
@ -96,7 +99,7 @@ class _SettingsPageState extends State<SettingsPage> {
SwitchListTile( SwitchListTile(
value: settings.mustBackTwiceToExit, value: settings.mustBackTwiceToExit,
onChanged: (v) => settings.mustBackTwiceToExit = v, 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<SettingsPage> {
Widget _buildDisplaySection(BuildContext context) { Widget _buildDisplaySection(BuildContext context) {
return AvesExpansionTile( return AvesExpansionTile(
title: 'Display', title: context.l10n.settingsSectionDisplay,
expandedNotifier: _expandedNotifier, expandedNotifier: _expandedNotifier,
children: [ children: [
LanguageTile(),
ListTile( ListTile(
title: Text('Keep screen on'), title: Text(context.l10n.settingsKeepScreenOnTile),
subtitle: Text(settings.keepScreenOn.name), subtitle: Text(settings.keepScreenOn.getName(context)),
onTap: () async { onTap: () async {
final value = await showDialog<KeepScreenOn>( final value = await showDialog<KeepScreenOn>(
context: context, context: context,
builder: (context) => AvesSelectionDialog<KeepScreenOn>( builder: (context) => AvesSelectionDialog<KeepScreenOn>(
initialValue: settings.keepScreenOn, initialValue: settings.keepScreenOn,
options: Map.fromEntries(KeepScreenOn.values.map((v) => MapEntry(v, v.name))), options: Map.fromEntries(KeepScreenOn.values.map((v) => MapEntry(v, v.getName(context)))),
title: 'Keep Screen On', title: context.l10n.settingsKeepScreenOnTitle,
), ),
); );
if (value != null) { if (value != null) {
@ -125,34 +129,30 @@ class _SettingsPageState extends State<SettingsPage> {
}, },
), ),
ListTile( ListTile(
title: Text('Raster image background'), title: Text(context.l10n.settingsRasterImageBackground),
trailing: EntryBackgroundSelector( trailing: EntryBackgroundSelector(
getter: () => settings.rasterBackground, getter: () => settings.rasterBackground,
setter: (value) => settings.rasterBackground = value, setter: (value) => settings.rasterBackground = value,
), ),
), ),
ListTile( ListTile(
title: Text('Vector image background'), title: Text(context.l10n.settingsVectorImageBackground),
trailing: EntryBackgroundSelector( trailing: EntryBackgroundSelector(
getter: () => settings.vectorBackground, getter: () => settings.vectorBackground,
setter: (value) => settings.vectorBackground = value, setter: (value) => settings.vectorBackground = value,
), ),
), ),
ListTile( ListTile(
title: Text('Coordinate format'), title: Text(context.l10n.settingsCoordinateFormatTile),
subtitle: Text(settings.coordinateFormat.name), subtitle: Text(settings.coordinateFormat.getName(context)),
onTap: () async { onTap: () async {
final value = await showDialog<CoordinateFormat>( final value = await showDialog<CoordinateFormat>(
context: context, context: context,
builder: (context) => AvesSelectionDialog<CoordinateFormat>( builder: (context) => AvesSelectionDialog<CoordinateFormat>(
initialValue: settings.coordinateFormat, initialValue: settings.coordinateFormat,
options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.name))), options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.getName(context)))),
optionSubtitleBuilder: (dynamic value) { optionSubtitleBuilder: (value) => value.format(Constants.pointNemo),
// dynamic declaration followed by cast, as workaround for generics limitation title: context.l10n.settingsCoordinateFormatTitle,
final formatter = (value as CoordinateFormat);
return formatter.format(Constants.pointNemo);
},
title: 'Coordinate Format',
), ),
); );
if (value != null) { if (value != null) {
@ -166,23 +166,23 @@ class _SettingsPageState extends State<SettingsPage> {
Widget _buildThumbnailsSection(BuildContext context) { Widget _buildThumbnailsSection(BuildContext context) {
return AvesExpansionTile( return AvesExpansionTile(
title: 'Thumbnails', title: context.l10n.settingsSectionThumbnails,
expandedNotifier: _expandedNotifier, expandedNotifier: _expandedNotifier,
children: [ children: [
SwitchListTile( SwitchListTile(
value: settings.showThumbnailLocation, value: settings.showThumbnailLocation,
onChanged: (v) => settings.showThumbnailLocation = v, onChanged: (v) => settings.showThumbnailLocation = v,
title: Text('Show location icon'), title: Text(context.l10n.settingsThumbnailShowLocationIcon),
), ),
SwitchListTile( SwitchListTile(
value: settings.showThumbnailRaw, value: settings.showThumbnailRaw,
onChanged: (v) => settings.showThumbnailRaw = v, onChanged: (v) => settings.showThumbnailRaw = v,
title: Text('Show raw icon'), title: Text(context.l10n.settingsThumbnailShowRawIcon),
), ),
SwitchListTile( SwitchListTile(
value: settings.showThumbnailVideoDuration, value: settings.showThumbnailVideoDuration,
onChanged: (v) => settings.showThumbnailVideoDuration = v, onChanged: (v) => settings.showThumbnailVideoDuration = v,
title: Text('Show video duration'), title: Text(context.l10n.settingsThumbnailShowVideoDuration),
), ),
], ],
); );
@ -190,24 +190,24 @@ class _SettingsPageState extends State<SettingsPage> {
Widget _buildViewerSection(BuildContext context) { Widget _buildViewerSection(BuildContext context) {
return AvesExpansionTile( return AvesExpansionTile(
title: 'Viewer', title: context.l10n.settingsSectionViewer,
expandedNotifier: _expandedNotifier, expandedNotifier: _expandedNotifier,
children: [ children: [
SwitchListTile( SwitchListTile(
value: settings.showOverlayMinimap, value: settings.showOverlayMinimap,
onChanged: (v) => settings.showOverlayMinimap = v, onChanged: (v) => settings.showOverlayMinimap = v,
title: Text('Show minimap'), title: Text(context.l10n.settingsViewerShowMinimap),
), ),
SwitchListTile( SwitchListTile(
value: settings.showOverlayInfo, value: settings.showOverlayInfo,
onChanged: (v) => settings.showOverlayInfo = v, onChanged: (v) => settings.showOverlayInfo = v,
title: Text('Show information'), title: Text(context.l10n.settingsViewerShowInformation),
subtitle: Text('Show title, date, location, etc.'), subtitle: Text(context.l10n.settingsViewerShowInformationSubtitle),
), ),
SwitchListTile( SwitchListTile(
value: settings.showOverlayShootingDetails, value: settings.showOverlayShootingDetails,
onChanged: settings.showOverlayInfo ? (v) => settings.showOverlayShootingDetails = v : null, 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<SettingsPage> {
Widget _buildSearchSection(BuildContext context) { Widget _buildSearchSection(BuildContext context) {
return AvesExpansionTile( return AvesExpansionTile(
title: 'Search', title: context.l10n.settingsSectionSearch,
expandedNotifier: _expandedNotifier, expandedNotifier: _expandedNotifier,
children: [ children: [
SwitchListTile( SwitchListTile(
@ -226,7 +226,7 @@ class _SettingsPageState extends State<SettingsPage> {
settings.searchHistory = []; settings.searchHistory = [];
} }
}, },
title: Text('Save search history'), title: Text(context.l10n.settingsSaveSearchHistory),
), ),
], ],
); );
@ -234,13 +234,13 @@ class _SettingsPageState extends State<SettingsPage> {
Widget _buildPrivacySection(BuildContext context) { Widget _buildPrivacySection(BuildContext context) {
return AvesExpansionTile( return AvesExpansionTile(
title: 'Privacy', title: context.l10n.settingsSectionPrivacy,
expandedNotifier: _expandedNotifier, expandedNotifier: _expandedNotifier,
children: [ children: [
SwitchListTile( SwitchListTile(
value: settings.isCrashlyticsEnabled, value: settings.isCrashlyticsEnabled,
onChanged: (v) => settings.isCrashlyticsEnabled = v, onChanged: (v) => settings.isCrashlyticsEnabled = v,
title: Text('Allow anonymous analytics and crash reporting'), title: Text(context.l10n.settingsEnableAnalytics),
), ),
HiddenFilterTile(), HiddenFilterTile(),
StorageAccessTile(), StorageAccessTile(),

View file

@ -43,7 +43,7 @@ class FilterTable extends StatelessWidget {
return Table( return Table(
children: sortedEntries.take(5).map((kv) { children: sortedEntries.take(5).map((kv) {
final filter = filterBuilder(kv.key); final filter = filterBuilder(kv.key);
final label = filter.label; final label = filter.getLabel(context);
final count = kv.value; final count = kv.value;
final percent = count / totalEntryCount; final percent = count / totalEntryCount;
return TableRow( return TableRow(

View file

@ -12,7 +12,8 @@ import 'package:aves/utils/color_utils.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/utils/mime_utils.dart'; import 'package:aves/utils/mime_utils.dart';
import 'package:aves/widgets/collection/collection_page.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/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/stats/filter_table.dart'; import 'package:aves/widgets/stats/filter_table.dart';
import 'package:charts_flutter/flutter.dart' as charts; import 'package:charts_flutter/flutter.dart' as charts;
@ -62,24 +63,25 @@ class StatsPage extends StatelessWidget {
if (entries.isEmpty) { if (entries.isEmpty) {
child = EmptyContent( child = EmptyContent(
icon: AIcons.image, icon: AIcons.image,
text: 'No images', text: context.l10n.collectionEmptyImages,
); );
} else { } else {
final byMimeTypes = groupBy<AvesEntry, String>(entries, (entry) => entry.mimeType).map<String, int>((k, v) => MapEntry(k, v.length)); final byMimeTypes = groupBy<AvesEntry, String>(entries, (entry) => entry.mimeType).map<String, int>((k, v) => MapEntry(k, v.length));
final imagesByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('image/'))); 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 videoByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('video')));
final mimeDonuts = Wrap( final mimeDonuts = Wrap(
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ children: [
_buildMimeDonut(context, (sum) => Intl.plural(sum, one: 'image', other: 'images'), imagesByMimeTypes), _buildMimeDonut(context, (sum) => context.l10n.statsImage(sum), imagesByMimeTypes),
_buildMimeDonut(context, (sum) => Intl.plural(sum, one: 'video', other: 'videos'), videoByMimeTypes), _buildMimeDonut(context, (sum) => context.l10n.statsVideo(sum), videoByMimeTypes),
], ],
); );
final catalogued = entries.where((entry) => entry.isCatalogued); final catalogued = entries.where((entry) => entry.isCatalogued);
final withGps = catalogued.where((entry) => entry.hasGps); 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 textScaleFactor = MediaQuery.textScaleFactorOf(context);
final lineHeight = 16 * textScaleFactor; final lineHeight = 16 * textScaleFactor;
final locationIndicator = Padding( final locationIndicator = Padding(
@ -101,7 +103,7 @@ class StatsPage extends StatelessWidget {
), ),
), ),
SizedBox(height: 8), 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: [ children: [
mimeDonuts, mimeDonuts,
locationIndicator, locationIndicator,
..._buildTopFilters(context, 'Top Countries', entryCountPerCountry, (s) => LocationFilter(LocationLevel.country, s)), ..._buildTopFilters(context, context.l10n.statsTopCountries, entryCountPerCountry, (s) => LocationFilter(LocationLevel.country, s)),
..._buildTopFilters(context, 'Top Places', entryCountPerPlace, (s) => LocationFilter(LocationLevel.place, s)), ..._buildTopFilters(context, context.l10n.statsTopPlaces, entryCountPerPlace, (s) => LocationFilter(LocationLevel.place, s)),
..._buildTopFilters(context, 'Top Tags', entryCountPerTag, (s) => TagFilter(s)), ..._buildTopFilters(context, context.l10n.statsTopTags, entryCountPerTag, (s) => TagFilter(s)),
], ],
); );
} }
return MediaQueryDataProvider( return MediaQueryDataProvider(
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Stats'), title: Text(context.l10n.statsPageTitle),
), ),
body: SafeArea( body: SafeArea(
child: child, child: child,

View file

@ -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/feedback.dart';
import 'package:aves/widgets/common/action_mixins/permission_aware.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/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/aves_dialog.dart';
import 'package:aves/widgets/dialogs/rename_entry_dialog.dart'; import 'package:aves/widgets/dialogs/rename_entry_dialog.dart';
import 'package:aves/widgets/filter_grids/album_pick.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:aves/widgets/viewer/source_viewer_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:intl/intl.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -103,14 +103,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
if (!await checkStoragePermission(context, {entry})) return; if (!await checkStoragePermission(context, {entry})) return;
final success = await entry.flip(); final success = await entry.flip();
if (!success) showFeedback(context, 'Failed'); if (!success) showFeedback(context, context.l10n.genericFailureFeedback);
} }
Future<void> _rotate(BuildContext context, AvesEntry entry, {@required bool clockwise}) async { Future<void> _rotate(BuildContext context, AvesEntry entry, {@required bool clockwise}) async {
if (!await checkStoragePermission(context, {entry})) return; if (!await checkStoragePermission(context, {entry})) return;
final success = await entry.rotate(clockwise: clockwise); final success = await entry.rotate(clockwise: clockwise);
if (!success) showFeedback(context, 'Failed'); if (!success) showFeedback(context, context.l10n.genericFailureFeedback);
} }
Future<void> _showDeleteDialog(BuildContext context, AvesEntry entry) async { Future<void> _showDeleteDialog(BuildContext context, AvesEntry entry) async {
@ -119,15 +119,15 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
builder: (context) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context, context: context,
content: Text('Are you sure?'), content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(1)),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()), child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, true), 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 checkStoragePermission(context, {entry})) return;
if (!await entry.delete()) { if (!await entry.delete()) {
showFeedback(context, 'Failed'); showFeedback(context, context.l10n.genericFailureFeedback);
} else { } else {
if (hasCollection) { if (hasCollection) {
collection.source.removeEntries({entry.uri}); collection.source.removeEntries({entry.uri});
@ -191,9 +191,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
final movedCount = movedOps.length; final movedCount = movedOps.length;
if (movedCount < selectionCount) { if (movedCount < selectionCount) {
final count = selectionCount - movedCount; 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 { } 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; 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) { void _goToSourceViewer(BuildContext context, AvesEntry entry) {

View file

@ -3,7 +3,7 @@ import 'dart:math';
import 'package:aves/model/availability.dart'; import 'package:aves/model/availability.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.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/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/services/window_service.dart'; import 'package:aves/services/window_service.dart';

View file

@ -10,8 +10,8 @@ import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/metadata_service.dart'; import 'package:aves/services/metadata_service.dart';
import 'package:aves/utils/android_file_utils.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/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/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/common.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -40,37 +40,40 @@ class BasicSection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n;
final infoUnknown = l10n.viewerInfoUnknown;
final date = entry.bestDate; 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 // 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) // 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 title = entry.bestTitle ?? infoUnknown;
final uri = entry.uri ?? Constants.infoUnknown; final uri = entry.uri ?? infoUnknown;
final path = entry.path; final path = entry.path;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
InfoRowGroup({ InfoRowGroup({
'Title': title, l10n.viewerInfoLabelTitle: title,
'Date': dateText, l10n.viewerInfoLabelDate: dateText,
if (entry.isVideo) ..._buildVideoRows(), if (entry.isVideo) ..._buildVideoRows(context),
if (!entry.isSvg && entry.isSized) 'Resolution': rasterResolutionText, if (!entry.isSvg && entry.isSized) l10n.viewerInfoLabelResolution: rasterResolutionText,
'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : Constants.infoUnknown, l10n.viewerInfoLabelSize: entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : infoUnknown,
'URI': uri, l10n.viewerInfoLabelUri: uri,
if (path != null) 'Path': path, if (path != null) l10n.viewerInfoLabelPath: path,
}), }),
OwnerProp( OwnerProp(
entry: entry, entry: entry,
visibleNotifier: visibleNotifier, visibleNotifier: visibleNotifier,
), ),
_buildChips(), _buildChips(context),
], ],
); );
} }
Widget _buildChips() { Widget _buildChips(BuildContext context) {
final tags = entry.xmpSubjects..sort(compareAsciiUpperCase); final tags = entry.xmpSubjects..sort(compareAsciiUpperCase);
final album = entry.directory; final album = entry.directory;
final filters = { final filters = {
@ -80,7 +83,7 @@ class BasicSection extends StatelessWidget {
if (entry.isImage && entry.is360) TypeFilter(TypeFilter.panorama), if (entry.isImage && entry.is360) TypeFilter(TypeFilter.panorama),
if (entry.isVideo && entry.is360) TypeFilter(TypeFilter.sphericalVideo), if (entry.isVideo && entry.is360) TypeFilter(TypeFilter.sphericalVideo),
if (entry.isVideo && !entry.is360) MimeFilter(MimeTypes.anyVideo), 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)), ...tags.map((tag) => TagFilter(tag)),
}; };
return AnimatedBuilder( return AnimatedBuilder(
@ -108,9 +111,9 @@ class BasicSection extends StatelessWidget {
); );
} }
Map<String, String> _buildVideoRows() { Map<String, String> _buildVideoRows(BuildContext context) {
return { return {
'Duration': entry.durationText, context.l10n.viewerInfoLabelDuration: entry.durationText,
}; };
} }
} }
@ -180,7 +183,7 @@ class _OwnerPropState extends State<OwnerProp> {
TextSpan( TextSpan(
children: [ children: [
TextSpan( TextSpan(
text: 'Owned by', text: context.l10n.viewerInfoLabelOwner,
style: InfoRowGroup.keyStyle, style: InfoRowGroup.keyStyle,
), ),
WidgetSpan( WidgetSpan(

View file

@ -98,7 +98,7 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
if (linkHandlers?.containsKey(key) == true) { if (linkHandlers?.containsKey(key) == true) {
final handler = linkHandlers[key]; final handler = linkHandlers[key];
value = handler.linkText; value = handler.linkText(context);
// open link on tap // open link on tap
recognizer = TapGestureRecognizer()..onTap = () => handler.onTap(context); recognizer = TapGestureRecognizer()..onTap = () => handler.onTap(context);
style = InfoRowGroup.linkStyle; style = InfoRowGroup.linkStyle;
@ -149,7 +149,7 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
} }
class InfoLinkHandler { class InfoLinkHandler {
final String linkText; final String Function(BuildContext context) linkText;
final void Function(BuildContext context) onTap; final void Function(BuildContext context) onTap;
const InfoLinkHandler({ const InfoLinkHandler({

View file

@ -1,6 +1,7 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/app_bar_title.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/info_search.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -23,17 +24,17 @@ class InfoAppBar extends StatelessWidget {
key: Key('back-button'), key: Key('back-button'),
icon: Icon(AIcons.goUp), icon: Icon(AIcons.goUp),
onPressed: onBackPressed, onPressed: onBackPressed,
tooltip: 'Back to viewer', tooltip: context.l10n.viewerInfoBackToViewerTooltip,
), ),
title: TappableAppBarTitle( title: InteractiveAppBarTitle(
onTap: () => _goToSearch(context), onTap: () => _goToSearch(context),
child: Text('Info'), child: Text(context.l10n.viewerInfoPageTitle),
), ),
actions: [ actions: [
IconButton( IconButton(
icon: Icon(AIcons.search), icon: Icon(AIcons.search),
onPressed: () => _goToSearch(context), onPressed: () => _goToSearch(context),
tooltip: 'Search', tooltip: MaterialLocalizations.of(context).searchFieldLabel,
), ),
], ],
titleSpacing: 0, titleSpacing: 0,
@ -45,6 +46,7 @@ class InfoAppBar extends StatelessWidget {
showSearch( showSearch(
context: context, context: context,
delegate: InfoSearchDelegate( delegate: InfoSearchDelegate(
searchFieldLabel: context.l10n.viewerInfoSearchFieldLabel,
entry: entry, entry: entry,
metadataNotifier: metadataNotifier, metadataNotifier: metadataNotifier,
), ),

View file

@ -1,7 +1,8 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/theme/icons.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/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/entry_viewer_page.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart';
@ -15,19 +16,12 @@ class InfoSearchDelegate extends SearchDelegate {
Map<String, MetadataDirectory> get metadata => metadataNotifier.value; Map<String, MetadataDirectory> 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({ InfoSearchDelegate({
@required String searchFieldLabel,
@required this.entry, @required this.entry,
@required this.metadataNotifier, @required this.metadataNotifier,
}) : super( }) : super(
searchFieldLabel: 'Search metadata', searchFieldLabel: searchFieldLabel,
); );
@override @override
@ -57,23 +51,33 @@ class InfoSearchDelegate extends SearchDelegate {
query = ''; query = '';
showSuggestions(context); showSuggestions(context);
}, },
tooltip: 'Clear', tooltip: context.l10n.clearTooltip,
), ),
]; ];
} }
@override @override
Widget buildSuggestions(BuildContext context) => ListView( Widget buildSuggestions(BuildContext context) {
children: suggestions.entries final l10n = context.l10n;
.map((kv) => ListTile( final suggestions = {
title: Text(kv.key), l10n.viewerInfoSearchSuggestionDate: 'date or time or when -timer -uptime -exposure -timeline',
onTap: () { l10n.viewerInfoSearchSuggestionDescription: 'abstract or description or comment or textual',
query = kv.value; l10n.viewerInfoSearchSuggestionDimensions: 'width or height or dimension or framesize or imagelength',
showResults(context); l10n.viewerInfoSearchSuggestionResolution: 'resolution',
}, l10n.viewerInfoSearchSuggestionRights: 'rights or copyright or artist or creator or by-line or credit -tool',
)) };
.toList(), return ListView(
); children: suggestions.entries
.map((kv) => ListTile(
title: Text(kv.key),
onTap: () {
query = kv.value;
showResults(context);
},
))
.toList(),
);
}
@override @override
Widget buildResults(BuildContext context) { Widget buildResults(BuildContext context) {
@ -107,22 +111,24 @@ class InfoSearchDelegate extends SearchDelegate {
showPrefixChildren: false, showPrefixChildren: false,
)) ))
.toList(); .toList();
return tiles.isEmpty return SafeArea(
? EmptyContent( child: tiles.isEmpty
icon: AIcons.info, ? EmptyContent(
text: 'No matching keys', icon: AIcons.info,
) text: context.l10n.viewerInfoSearchEmpty,
: NotificationListener<OpenTempEntryNotification>( )
onNotification: (notification) { : NotificationListener<OpenTempEntryNotification>(
_openTempEntry(context, notification.entry); onNotification: (notification) {
return true; _openTempEntry(context, notification.entry);
}, return true;
child: ListView.builder( },
padding: EdgeInsets.all(8), child: ListView.builder(
itemBuilder: (context, index) => tiles[index], padding: EdgeInsets.all(8),
itemCount: tiles.length, itemBuilder: (context, index) => tiles[index],
itemCount: tiles.length,
),
), ),
); );
} }
void _openTempEntry(BuildContext context, AvesEntry tempEntry) { void _openTempEntry(BuildContext context, AvesEntry tempEntry) {

View file

@ -7,6 +7,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.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/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/viewer/info/common.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/common.dart';
@ -195,9 +196,10 @@ class _AddressInfoGroupState extends State<_AddressInfoGroup> {
future: _addressLineLoader, future: _addressLineLoader,
builder: (context, snapshot) { builder: (context, snapshot) {
final address = !snapshot.hasError && snapshot.connectionState == ConnectionState.done ? snapshot.data : null; final address = !snapshot.hasError && snapshot.connectionState == ConnectionState.done ? snapshot.data : null;
final l10n = context.l10n;
return InfoRowGroup({ return InfoRowGroup({
'Coordinates': settings.coordinateFormat.format(entry.latLng), l10n.viewerInfoLabelCoordinates: settings.coordinateFormat.format(entry.latLng),
if (address?.isNotEmpty == true) 'Address': address, if (address?.isNotEmpty == true) l10n.viewerInfoLabelAddress: address,
}); });
}, },
); );

View file

@ -1,9 +1,11 @@
import 'package:aves/model/availability.dart'; 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/map_style.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/android_app_service.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.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/blurred.dart';
import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/borders.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart';
@ -68,7 +70,7 @@ class MapButtonPanel extends StatelessWidget {
onPressed: () => AndroidAppService.openMap(geoUri).then((success) { onPressed: () => AndroidAppService.openMap(geoUri).then((success) {
if (!success) showNoMatchingAppDialog(context); if (!success) showNoMatchingAppDialog(context);
}), }),
tooltip: 'Show on map…', tooltip: context.l10n.entryActionOpenMap,
), ),
SizedBox(height: padding), SizedBox(height: padding),
MapOverlayButton( MapOverlayButton(
@ -83,8 +85,8 @@ class MapButtonPanel extends StatelessWidget {
builder: (context) { builder: (context) {
return AvesSelectionDialog<EntryMapStyle>( return AvesSelectionDialog<EntryMapStyle>(
initialValue: initialStyle, initialValue: initialStyle,
options: Map.fromEntries(availableStyles.map((v) => MapEntry(v, v.name))), options: Map.fromEntries(availableStyles.map((v) => MapEntry(v, v.getName(context)))),
title: 'Map Style', title: context.l10n.viewerInfoMapStyleTitle,
); );
}, },
); );
@ -95,19 +97,19 @@ class MapButtonPanel extends StatelessWidget {
MapStyleChangedNotification().dispatch(context); MapStyleChangedNotification().dispatch(context);
} }
}, },
tooltip: 'Style map…', tooltip: context.l10n.viewerInfoMapStyleTooltip,
), ),
Spacer(), Spacer(),
MapOverlayButton( MapOverlayButton(
icon: AIcons.zoomIn, icon: AIcons.zoomIn,
onPressed: () => zoomBy(1), onPressed: () => zoomBy(1),
tooltip: 'Zoom in', tooltip: context.l10n.viewerInfoMapZoomInTooltip,
), ),
SizedBox(height: padding), SizedBox(height: padding),
MapOverlayButton( MapOverlayButton(
icon: AIcons.zoomOut, icon: AIcons.zoomOut,
onPressed: () => zoomBy(-1), onPressed: () => zoomBy(-1),
tooltip: 'Zoom out', tooltip: context.l10n.viewerInfoMapZoomOutTooltip,
), ),
], ],
), ),

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; 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/model/settings/settings.dart';
import 'package:aves/widgets/viewer/info/maps/common.dart'; import 'package:aves/widgets/viewer/info/maps/common.dart';
import 'package:aves/widgets/viewer/info/maps/marker.dart'; import 'package:aves/widgets/viewer/info/maps/marker.dart';

Some files were not shown because too many files have changed in this diff Show more