Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2021-09-08 10:02:12 +09:00
commit f3ec152ce5
36 changed files with 636 additions and 139 deletions

View file

@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file.
## [Unreleased] ## [Unreleased]
## [v1.5.1] - 2021-09-08
### Added
- About: bug reporting instructions
### Changed
- Collection: improved video date detection
### Fixed
- fixed hanging app when loading thumbnails for some video formats on some devices
## [v1.5.0] - 2021-09-02 ## [v1.5.0] - 2021-09-02
### Added ### Added
- Info: edit Exif dates (setting, shifting, deleting) - Info: edit Exif dates (setting, shifting, deleting)

View file

@ -4,7 +4,12 @@
<br /> <br />
<img src="https://raw.githubusercontent.com/deckerst/aves/develop/aves_logo.svg" alt='Aves logo' width="200" /> <img src="https://raw.githubusercontent.com/deckerst/aves/develop/aves_logo.svg" alt='Aves logo' width="200" />
[<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" alt='Get it on Google Play' width="200">](https://play.google.com/store/apps/details?id=deckers.thibault.aves&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1) [<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"
alt='Get it on Google Play'
height="80">](https://play.google.com/store/apps/details?id=deckers.thibault.aves&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1)
[<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/get-it-on-github.png"
alt='Get it on GitHub'
height="80">](https://github.com/deckerst/aves/releases/latest)
Aves is a gallery and metadata explorer app. It is built for Android, with Flutter. Aves is a gallery and metadata explorer app. It is built for Android, with Flutter.
@ -12,17 +17,21 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt
## Features ## Features
- support raster images: JPEG, GIF, PNG, HEIC/HEIF (including multi-track, from Android Pie), WEBP, TIFF (including multi-page), BMP, WBMP, ICO Aves can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like **multi-page TIFFs, SVGs, old AVIs and more**!
- support animated images: GIF, WEBP
- support raw images: ARW, CR2, DNG, NEF, NRW, ORF, PEF, RAF, RW2, SRW It scans your media collection to identify **motion photos**, **panoramas** (aka photo spheres), **360° videos**, as well as **GeoTIFF** files.
- support vector images: SVG
- support videos: MP4, AVI, MKV, AVCHD & probably others **Navigation and search** is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc.
- identify panoramas (aka photo spheres), 360° videos, GeoTIFF files
- search and filter by country, place, XMP tag, type (animated, raster, vector…) Aves integrates with Android (from **API 20 to 31**, i.e. from Lollipop to S) with features such as **app shortcuts** and **global search** handling. It also works as a **media viewer and picker**.
- favorites
- statistics ## Permissions
- support Android API 20 ~ 31 (Lollipop ~ S)
- Android integration (app shortcuts, handle view/pick intents) Aves requires a few permissions to do its job:
- **read contents of shared storage**: the app only accesses media files, and modifying them requires explicit access grants from the user,
- **read locations from media collection**: necessary to display the media coordinates, and to group them by country (via reverse geocoding),
- **have network access**: necessary for the map view, and most likely for precise reverse geocoding too,
- **view network connections**: checking for connection states allows Aves to gracefully degrade features that depend on internet.
## Project Setup ## Project Setup

View file

@ -123,7 +123,7 @@ dependencies {
// https://jitpack.io/com/github/deckerst/Android-TiffBitmapFactory/**********/build.log // https://jitpack.io/com/github/deckerst/Android-TiffBitmapFactory/**********/build.log
implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a' // forked, built by JitPack implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a' // forked, built by JitPack
// https://jitpack.io/com/github/deckerst/pixymeta-android/**********/build.log // https://jitpack.io/com/github/deckerst/pixymeta-android/**********/build.log
implementation 'com.github.deckerst:pixymeta-android:f90140ed2b' // forked, built by JitPack implementation 'com.github.deckerst:pixymeta-android:e4e50da939' // forked, built by JitPack
implementation 'com.github.bumptech.glide:glide:4.12.0' implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'androidx.annotation:annotation:1.2.0' kapt 'androidx.annotation:annotation:1.2.0'

View file

@ -31,6 +31,10 @@ class MainActivity : FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Log.i(LOG_TAG, "onCreate intent=$intent") Log.i(LOG_TAG, "onCreate intent=$intent")
intent.extras?.takeUnless { it.isEmpty }?.let {
Log.i(LOG_TAG, "onCreate intent extras=$it")
}
// StrictMode.setThreadPolicy( // StrictMode.setThreadPolicy(
// StrictMode.ThreadPolicy.Builder() // StrictMode.ThreadPolicy.Builder()
// .detectAll() // .detectAll()
@ -194,6 +198,9 @@ class MainActivity : FlutterActivity() {
"query" to intent.getStringExtra(SearchManager.QUERY), "query" to intent.getStringExtra(SearchManager.QUERY),
) )
} }
Intent.ACTION_RUN -> {
// flutter run
}
else -> { else -> {
Log.w(LOG_TAG, "unhandled intent action=${intent?.action}") Log.w(LOG_TAG, "unhandled intent action=${intent?.action}")
} }

View file

@ -68,15 +68,19 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
return return
} }
if (!isSupportedByPixyMeta(mimeType)) {
result.error("getPixyMetadata-unsupported", "PixyMeta does not support mimeType=$mimeType", null)
return
}
val metadataMap = HashMap<String, String>() val metadataMap = HashMap<String, String>()
if (isSupportedByPixyMeta(mimeType)) {
try { try {
StorageUtils.openInputStream(context, uri)?.use { input -> StorageUtils.openInputStream(context, uri)?.use { input ->
metadataMap.putAll(PixyMetaHelper.describe(input)) metadataMap.putAll(PixyMetaHelper.describe(input))
} }
} catch (e: Exception) { } catch (e: Exception) {
result.error("getPixyMetadata-exception", e.message, e.stackTraceToString()) result.error("getPixyMetadata-exception", e.message, e.stackTraceToString())
} return
} }
result.success(metadataMap) result.success(metadataMap)
} }

View file

@ -50,7 +50,7 @@ internal class SvgFetcher(val model: SvgThumbnail, val width: Int, val height: I
val context = model.context val context = model.context
val uri = model.uri val uri = model.uri
StorageUtils.openInputStream(context, uri)?.use { input -> val bitmap: Bitmap? = StorageUtils.openInputStream(context, uri)?.use { input ->
try { try {
SVG.getFromInputStream(input)?.let { svg -> SVG.getFromInputStream(input)?.let { svg ->
svg.normalizeSize() svg.normalizeSize()
@ -71,12 +71,19 @@ internal class SvgFetcher(val model: SvgThumbnail, val width: Int, val height: I
val canvas = Canvas(bitmap) val canvas = Canvas(bitmap)
svg.renderToCanvas(canvas) svg.renderToCanvas(canvas)
callback.onDataReady(bitmap) bitmap
} }
} catch (ex: SVGParseException) { } catch (ex: SVGParseException) {
callback.onLoadFailed(ex) callback.onLoadFailed(ex)
return
} }
} }
if (bitmap == null) {
callback.onLoadFailed(Exception("failed to load SVG for uri=$uri"))
} else {
callback.onDataReady(bitmap)
}
} }
override fun cleanup() {} override fun cleanup() {}

View file

@ -52,7 +52,9 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) { override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
val retriever = openMetadataRetriever(model.context, model.uri) val retriever = openMetadataRetriever(model.context, model.uri)
if (retriever != null) { if (retriever == null) {
callback.onLoadFailed(Exception("failed to initialize MediaMetadataRetriever for uri=${model.uri}"))
} else {
try { try {
var bytes = retriever.embeddedPicture var bytes = retriever.embeddedPicture
if (bytes == null) { if (bytes == null) {

View file

@ -467,6 +467,7 @@ object StorageUtils {
} }
} catch (e: Exception) { } catch (e: Exception) {
// unsupported format // unsupported format
Log.w(LOG_TAG, "failed to initialize MediaMetadataRetriever for uri=$uri")
null null
} }
} }

View file

@ -14,4 +14,4 @@ __We collect anonymous data to improve the app.__ We use Google Firebase for Cra
## Links ## Links
[Sources](https://github.com/deckerst/aves) [Sources](https://github.com/deckerst/aves)
[License](https://github.com/deckerst/aves/blob/master/LICENSE) [License](https://github.com/deckerst/aves/blob/main/LICENSE)

View file

@ -363,8 +363,11 @@
"aboutPageTitle": "About", "aboutPageTitle": "About",
"@aboutPageTitle": {}, "@aboutPageTitle": {},
"aboutFlutter": "Flutter", "aboutLinkSources": "Sources",
"@aboutFlutter": {}, "@aboutLinkSources": {},
"aboutLinkLicense": "License",
"@aboutLinkLicense": {},
"aboutUpdate": "New Version Available", "aboutUpdate": "New Version Available",
"@aboutUpdate": {}, "@aboutUpdate": {},
"aboutUpdateLinks1": "A new version of Aves is available on", "aboutUpdateLinks1": "A new version of Aves is available on",
@ -377,12 +380,29 @@
"@aboutUpdateGitHub": {}, "@aboutUpdateGitHub": {},
"aboutUpdateGooglePlay": "Google Play", "aboutUpdateGooglePlay": "Google Play",
"@aboutUpdateGooglePlay": {}, "@aboutUpdateGooglePlay": {},
"aboutBug": "Bug Report",
"@aboutBug": {},
"aboutBugSaveLogInstruction": "Save app logs to a file",
"@aboutBugSaveLogInstruction": {},
"aboutBugSaveLogButton": "Save",
"@aboutBugSaveLogButton": {},
"aboutBugCopyInfoInstruction": "Copy system information",
"@aboutBugCopyInfoInstruction": {},
"aboutBugCopyInfoButton": "Copy",
"@aboutBugCopyInfoButton": {},
"aboutBugReportInstruction": "Report on GitHub with the logs and system information",
"@aboutBugReportInstruction": {},
"aboutBugReportButton": "Report",
"@aboutBugReportButton": {},
"aboutCredits": "Credits", "aboutCredits": "Credits",
"@aboutCredits": {}, "@aboutCredits": {},
"aboutCreditsWorldAtlas1": "This app uses a TopoJSON file from", "aboutCreditsWorldAtlas1": "This app uses a TopoJSON file from",
"@aboutCreditsWorldAtlas1": {}, "@aboutCreditsWorldAtlas1": {},
"aboutCreditsWorldAtlas2": "under ISC License.", "aboutCreditsWorldAtlas2": "under ISC License.",
"@aboutCreditsWorldAtlas2": {}, "@aboutCreditsWorldAtlas2": {},
"aboutLicenses": "Open-Source Licenses", "aboutLicenses": "Open-Source Licenses",
"@aboutLicenses": {}, "@aboutLicenses": {},
"aboutLicensesBanner": "This app uses the following open-source packages and libraries.", "aboutLicensesBanner": "This app uses the following open-source packages and libraries.",
@ -714,7 +734,7 @@
"settingsSectionPrivacy": "Privacy", "settingsSectionPrivacy": "Privacy",
"@settingsSectionPrivacy": {}, "@settingsSectionPrivacy": {},
"settingsEnableCrashReport": "Allow anonymous crash reporting", "settingsEnableCrashReport": "Allow anonymous error reporting",
"@settingsEnableCrashReport": {}, "@settingsEnableCrashReport": {},
"settingsSaveSearchHistory": "Save search history", "settingsSaveSearchHistory": "Save search history",
"@settingsSaveSearchHistory": {}, "@settingsSaveSearchHistory": {},

View file

@ -142,8 +142,8 @@
"renameEntryDialogLabel": "이름", "renameEntryDialogLabel": "이름",
"editEntryDateDialogTitle": "날짜 및 시간", "editEntryDateDialogTitle": "날짜 및 시간",
"editEntryDateDialogSet": "설정", "editEntryDateDialogSet": "편집",
"editEntryDateDialogShift": "앞뒤로", "editEntryDateDialogShift": "시간 이동",
"editEntryDateDialogClear": "삭제", "editEntryDateDialogClear": "삭제",
"editEntryDateDialogFieldSelection": "필드 선택", "editEntryDateDialogFieldSelection": "필드 선택",
"editEntryDateDialogHours": "시간", "editEntryDateDialogHours": "시간",
@ -170,16 +170,28 @@
"menuActionStats": "통계", "menuActionStats": "통계",
"aboutPageTitle": "앱 정보", "aboutPageTitle": "앱 정보",
"aboutFlutter": "플러터", "aboutLinkSources": "소스 코드",
"aboutLinkLicense": "라이선스",
"aboutUpdate": "업데이트 사용 가능", "aboutUpdate": "업데이트 사용 가능",
"aboutUpdateLinks1": "앱의 최신 버전을", "aboutUpdateLinks1": "앱의 최신 버전을",
"aboutUpdateLinks2": "와", "aboutUpdateLinks2": "와",
"aboutUpdateLinks3": "에서 다운로드 사용 가능합니다.", "aboutUpdateLinks3": "에서 다운로드 사용 가능합니다.",
"aboutUpdateGitHub": "깃허브", "aboutUpdateGitHub": "깃허브",
"aboutUpdateGooglePlay": "구글 플레이", "aboutUpdateGooglePlay": "구글 플레이",
"aboutBug": "버그 보고",
"aboutBugSaveLogInstruction": "앱 로그를 파일에 저장하기",
"aboutBugSaveLogButton": "저장",
"aboutBugCopyInfoInstruction": "시스템 정보를 복사하기",
"aboutBugCopyInfoButton": "복사",
"aboutBugReportInstruction": "로그와 시스템 정보를 첨부하여 깃허브에서 이슈를 제출하기",
"aboutBugReportButton": "제출",
"aboutCredits": "크레딧", "aboutCredits": "크레딧",
"aboutCreditsWorldAtlas1": "이 앱은", "aboutCreditsWorldAtlas1": "이 앱은",
"aboutCreditsWorldAtlas2": "의 TopoJSON 파일(ISC 라이선스)을 이용합니다.", "aboutCreditsWorldAtlas2": "의 TopoJSON 파일(ISC 라이선스)을 이용합니다.",
"aboutLicenses": "오픈 소스 라이선스", "aboutLicenses": "오픈 소스 라이선스",
"aboutLicensesBanner": "이 앱은 다음의 오픈 소스 패키지와 라이브러리를 이용합니다.", "aboutLicensesBanner": "이 앱은 다음의 오픈 소스 패키지와 라이브러리를 이용합니다.",
"aboutLicensesAndroidLibraries": "안드로이드 라이브러리", "aboutLicensesAndroidLibraries": "안드로이드 라이브러리",

View file

@ -3,28 +3,49 @@ import 'package:aves/model/actions/entry_set_actions.dart';
import 'package:aves/model/actions/video_actions.dart'; import 'package:aves/model/actions/video_actions.dart';
import 'package:aves/model/filters/favourite.dart'; 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/settings/enums.dart';
import 'package:aves/model/source/enums.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';
class SettingsDefaults { class SettingsDefaults {
// app
static const hasAcceptedTerms = false;
static const isCrashlyticsEnabled = false;
static const mustBackTwiceToExit = true;
static const keepScreenOn = KeepScreenOn.viewerOnly;
static const homePage = HomePageSetting.collection;
// drawer // drawer
static final drawerTypeBookmarks = [ static final drawerTypeBookmarks = [
null, null,
MimeFilter.video, MimeFilter.video,
FavouriteFilter.instance, FavouriteFilter.instance,
]; ];
static final drawerPageBookmarks = [ static const drawerPageBookmarks = [
AlbumListPage.routeName, AlbumListPage.routeName,
CountryListPage.routeName, CountryListPage.routeName,
TagListPage.routeName, TagListPage.routeName,
]; ];
// collection // collection
static const collectionSectionFactor = EntryGroupFactor.month;
static const collectionSortFactor = EntrySortFactor.date;
static const collectionSelectionQuickActions = [ static const collectionSelectionQuickActions = [
EntrySetAction.share, EntrySetAction.share,
EntrySetAction.delete, EntrySetAction.delete,
]; ];
static const showThumbnailLocation = true;
static const showThumbnailRaw = true;
static const showThumbnailVideoDuration = true;
// filter grids
static const albumGroupFactor = AlbumChipGroupFactor.importance;
static const albumSortFactor = ChipSortFactor.name;
static const countrySortFactor = ChipSortFactor.name;
static const tagSortFactor = ChipSortFactor.name;
// viewer // viewer
static const viewerQuickActions = [ static const viewerQuickActions = [
@ -32,10 +53,37 @@ class SettingsDefaults {
EntryAction.share, EntryAction.share,
EntryAction.rotateScreen, EntryAction.rotateScreen,
]; ];
static const showOverlayMinimap = false;
static const showOverlayInfo = true;
static const showOverlayShootingDetails = false;
static const enableOverlayBlurEffect = true;
static const viewerUseCutout = true;
// video // video
static const videoQuickActions = [ static const videoQuickActions = [
VideoAction.replay10, VideoAction.replay10,
VideoAction.togglePlay, VideoAction.togglePlay,
]; ];
static const enableVideoHardwareAcceleration = true;
static const enableVideoAutoPlay = false;
static const videoLoopMode = VideoLoopMode.shortOnly;
static const videoShowRawTimedText = false;
// subtitles
static const subtitleFontSize = 20.0;
static const subtitleTextAlignment = TextAlign.center;
static const subtitleShowOutline = true;
static const subtitleTextColor = Colors.white;
static const subtitleBackgroundColor = Colors.transparent;
// info
static const infoMapStyle = EntryMapStyle.stamenWatercolor;
static const infoMapZoom = 12.0;
static const coordinateFormat = CoordinateFormat.dms;
// rendering
static const imageBackground = EntryBackground.white;
// search
static const saveSearchHistory = true;
} }

View file

@ -143,11 +143,11 @@ class Settings extends ChangeNotifier {
// app // app
bool get hasAcceptedTerms => getBoolOrDefault(hasAcceptedTermsKey, false); bool get hasAcceptedTerms => getBoolOrDefault(hasAcceptedTermsKey, SettingsDefaults.hasAcceptedTerms);
set hasAcceptedTerms(bool newValue) => setAndNotify(hasAcceptedTermsKey, newValue); set hasAcceptedTerms(bool newValue) => setAndNotify(hasAcceptedTermsKey, newValue);
bool get isCrashlyticsEnabled => getBoolOrDefault(isCrashlyticsEnabledKey, false); bool get isCrashlyticsEnabled => getBoolOrDefault(isCrashlyticsEnabledKey, SettingsDefaults.isCrashlyticsEnabled);
set isCrashlyticsEnabled(bool newValue) { set isCrashlyticsEnabled(bool newValue) {
setAndNotify(isCrashlyticsEnabledKey, newValue); setAndNotify(isCrashlyticsEnabledKey, newValue);
@ -182,18 +182,18 @@ class Settings extends ChangeNotifier {
setAndNotify(localeKey, tag); setAndNotify(localeKey, tag);
} }
bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, true); bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, SettingsDefaults.mustBackTwiceToExit);
set mustBackTwiceToExit(bool newValue) => setAndNotify(mustBackTwiceToExitKey, newValue); set mustBackTwiceToExit(bool newValue) => setAndNotify(mustBackTwiceToExitKey, newValue);
KeepScreenOn get keepScreenOn => getEnumOrDefault(keepScreenOnKey, KeepScreenOn.viewerOnly, KeepScreenOn.values); KeepScreenOn get keepScreenOn => getEnumOrDefault(keepScreenOnKey, SettingsDefaults.keepScreenOn, KeepScreenOn.values);
set keepScreenOn(KeepScreenOn newValue) { set keepScreenOn(KeepScreenOn newValue) {
setAndNotify(keepScreenOnKey, newValue.toString()); setAndNotify(keepScreenOnKey, newValue.toString());
newValue.apply(); newValue.apply();
} }
HomePageSetting get homePage => getEnumOrDefault(homePageKey, HomePageSetting.collection, HomePageSetting.values); HomePageSetting get homePage => getEnumOrDefault(homePageKey, SettingsDefaults.homePage, HomePageSetting.values);
set homePage(HomePageSetting newValue) => setAndNotify(homePageKey, newValue.toString()); set homePage(HomePageSetting newValue) => setAndNotify(homePageKey, newValue.toString());
@ -226,11 +226,11 @@ class Settings extends ChangeNotifier {
// collection // collection
EntryGroupFactor get collectionSectionFactor => getEnumOrDefault(collectionGroupFactorKey, EntryGroupFactor.month, EntryGroupFactor.values); EntryGroupFactor get collectionSectionFactor => getEnumOrDefault(collectionGroupFactorKey, SettingsDefaults.collectionSectionFactor, EntryGroupFactor.values);
set collectionSectionFactor(EntryGroupFactor newValue) => setAndNotify(collectionGroupFactorKey, newValue.toString()); set collectionSectionFactor(EntryGroupFactor newValue) => setAndNotify(collectionGroupFactorKey, newValue.toString());
EntrySortFactor get collectionSortFactor => getEnumOrDefault(collectionSortFactorKey, EntrySortFactor.date, EntrySortFactor.values); EntrySortFactor get collectionSortFactor => getEnumOrDefault(collectionSortFactorKey, SettingsDefaults.collectionSortFactor, EntrySortFactor.values);
set collectionSortFactor(EntrySortFactor newValue) => setAndNotify(collectionSortFactorKey, newValue.toString()); set collectionSortFactor(EntrySortFactor newValue) => setAndNotify(collectionSortFactorKey, newValue.toString());
@ -238,33 +238,33 @@ class Settings extends ChangeNotifier {
set collectionSelectionQuickActions(List<EntrySetAction> newValue) => setAndNotify(collectionSelectionQuickActionsKey, newValue.map((v) => v.toString()).toList()); set collectionSelectionQuickActions(List<EntrySetAction> newValue) => setAndNotify(collectionSelectionQuickActionsKey, newValue.map((v) => v.toString()).toList());
bool get showThumbnailLocation => getBoolOrDefault(showThumbnailLocationKey, true); bool get showThumbnailLocation => getBoolOrDefault(showThumbnailLocationKey, SettingsDefaults.showThumbnailLocation);
set showThumbnailLocation(bool newValue) => setAndNotify(showThumbnailLocationKey, newValue); set showThumbnailLocation(bool newValue) => setAndNotify(showThumbnailLocationKey, newValue);
bool get showThumbnailRaw => getBoolOrDefault(showThumbnailRawKey, true); bool get showThumbnailRaw => getBoolOrDefault(showThumbnailRawKey, SettingsDefaults.showThumbnailRaw);
set showThumbnailRaw(bool newValue) => setAndNotify(showThumbnailRawKey, newValue); set showThumbnailRaw(bool newValue) => setAndNotify(showThumbnailRawKey, newValue);
bool get showThumbnailVideoDuration => getBoolOrDefault(showThumbnailVideoDurationKey, true); bool get showThumbnailVideoDuration => getBoolOrDefault(showThumbnailVideoDurationKey, SettingsDefaults.showThumbnailVideoDuration);
set showThumbnailVideoDuration(bool newValue) => setAndNotify(showThumbnailVideoDurationKey, newValue); set showThumbnailVideoDuration(bool newValue) => setAndNotify(showThumbnailVideoDurationKey, newValue);
// filter grids // filter grids
AlbumChipGroupFactor get albumGroupFactor => getEnumOrDefault(albumGroupFactorKey, AlbumChipGroupFactor.importance, AlbumChipGroupFactor.values); AlbumChipGroupFactor get albumGroupFactor => getEnumOrDefault(albumGroupFactorKey, SettingsDefaults.albumGroupFactor, AlbumChipGroupFactor.values);
set albumGroupFactor(AlbumChipGroupFactor newValue) => setAndNotify(albumGroupFactorKey, newValue.toString()); set albumGroupFactor(AlbumChipGroupFactor newValue) => setAndNotify(albumGroupFactorKey, newValue.toString());
ChipSortFactor get albumSortFactor => getEnumOrDefault(albumSortFactorKey, ChipSortFactor.name, ChipSortFactor.values); ChipSortFactor get albumSortFactor => getEnumOrDefault(albumSortFactorKey, SettingsDefaults.albumSortFactor, ChipSortFactor.values);
set albumSortFactor(ChipSortFactor newValue) => setAndNotify(albumSortFactorKey, newValue.toString()); set albumSortFactor(ChipSortFactor newValue) => setAndNotify(albumSortFactorKey, newValue.toString());
ChipSortFactor get countrySortFactor => getEnumOrDefault(countrySortFactorKey, ChipSortFactor.name, ChipSortFactor.values); ChipSortFactor get countrySortFactor => getEnumOrDefault(countrySortFactorKey, SettingsDefaults.countrySortFactor, ChipSortFactor.values);
set countrySortFactor(ChipSortFactor newValue) => setAndNotify(countrySortFactorKey, newValue.toString()); set countrySortFactor(ChipSortFactor newValue) => setAndNotify(countrySortFactorKey, newValue.toString());
ChipSortFactor get tagSortFactor => getEnumOrDefault(tagSortFactorKey, ChipSortFactor.name, ChipSortFactor.values); ChipSortFactor get tagSortFactor => getEnumOrDefault(tagSortFactorKey, SettingsDefaults.tagSortFactor, ChipSortFactor.values);
set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString()); set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString());
@ -282,23 +282,23 @@ class Settings extends ChangeNotifier {
set viewerQuickActions(List<EntryAction> newValue) => setAndNotify(viewerQuickActionsKey, newValue.map((v) => v.toString()).toList()); set viewerQuickActions(List<EntryAction> newValue) => setAndNotify(viewerQuickActionsKey, newValue.map((v) => v.toString()).toList());
bool get showOverlayMinimap => getBoolOrDefault(showOverlayMinimapKey, false); bool get showOverlayMinimap => getBoolOrDefault(showOverlayMinimapKey, SettingsDefaults.showOverlayMinimap);
set showOverlayMinimap(bool newValue) => setAndNotify(showOverlayMinimapKey, newValue); set showOverlayMinimap(bool newValue) => setAndNotify(showOverlayMinimapKey, newValue);
bool get showOverlayInfo => getBoolOrDefault(showOverlayInfoKey, true); bool get showOverlayInfo => getBoolOrDefault(showOverlayInfoKey, SettingsDefaults.showOverlayInfo);
set showOverlayInfo(bool newValue) => setAndNotify(showOverlayInfoKey, newValue); set showOverlayInfo(bool newValue) => setAndNotify(showOverlayInfoKey, newValue);
bool get showOverlayShootingDetails => getBoolOrDefault(showOverlayShootingDetailsKey, false); bool get showOverlayShootingDetails => getBoolOrDefault(showOverlayShootingDetailsKey, SettingsDefaults.showOverlayShootingDetails);
set showOverlayShootingDetails(bool newValue) => setAndNotify(showOverlayShootingDetailsKey, newValue); set showOverlayShootingDetails(bool newValue) => setAndNotify(showOverlayShootingDetailsKey, newValue);
bool get enableOverlayBlurEffect => getBoolOrDefault(enableOverlayBlurEffectKey, true); bool get enableOverlayBlurEffect => getBoolOrDefault(enableOverlayBlurEffectKey, SettingsDefaults.enableOverlayBlurEffect);
set enableOverlayBlurEffect(bool newValue) => setAndNotify(enableOverlayBlurEffectKey, newValue); set enableOverlayBlurEffect(bool newValue) => setAndNotify(enableOverlayBlurEffectKey, newValue);
bool get viewerUseCutout => getBoolOrDefault(viewerUseCutoutKey, true); bool get viewerUseCutout => getBoolOrDefault(viewerUseCutoutKey, SettingsDefaults.viewerUseCutout);
set viewerUseCutout(bool newValue) => setAndNotify(viewerUseCutoutKey, newValue); set viewerUseCutout(bool newValue) => setAndNotify(viewerUseCutoutKey, newValue);
@ -308,67 +308,67 @@ class Settings extends ChangeNotifier {
set videoQuickActions(List<VideoAction> newValue) => setAndNotify(videoQuickActionsKey, newValue.map((v) => v.toString()).toList()); set videoQuickActions(List<VideoAction> newValue) => setAndNotify(videoQuickActionsKey, newValue.map((v) => v.toString()).toList());
bool get enableVideoHardwareAcceleration => getBoolOrDefault(enableVideoHardwareAccelerationKey, true); bool get enableVideoHardwareAcceleration => getBoolOrDefault(enableVideoHardwareAccelerationKey, SettingsDefaults.enableVideoHardwareAcceleration);
set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue); set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue);
bool get enableVideoAutoPlay => getBoolOrDefault(enableVideoAutoPlayKey, false); bool get enableVideoAutoPlay => getBoolOrDefault(enableVideoAutoPlayKey, SettingsDefaults.enableVideoAutoPlay);
set enableVideoAutoPlay(bool newValue) => setAndNotify(enableVideoAutoPlayKey, newValue); set enableVideoAutoPlay(bool newValue) => setAndNotify(enableVideoAutoPlayKey, newValue);
VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, VideoLoopMode.shortOnly, VideoLoopMode.values); VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, SettingsDefaults.videoLoopMode, VideoLoopMode.values);
set videoLoopMode(VideoLoopMode newValue) => setAndNotify(videoLoopModeKey, newValue.toString()); set videoLoopMode(VideoLoopMode newValue) => setAndNotify(videoLoopModeKey, newValue.toString());
bool get videoShowRawTimedText => getBoolOrDefault(videoShowRawTimedTextKey, false); bool get videoShowRawTimedText => getBoolOrDefault(videoShowRawTimedTextKey, SettingsDefaults.videoShowRawTimedText);
set videoShowRawTimedText(bool newValue) => setAndNotify(videoShowRawTimedTextKey, newValue); set videoShowRawTimedText(bool newValue) => setAndNotify(videoShowRawTimedTextKey, newValue);
// subtitles // subtitles
double get subtitleFontSize => _prefs!.getDouble(subtitleFontSizeKey) ?? 20; double get subtitleFontSize => _prefs!.getDouble(subtitleFontSizeKey) ?? SettingsDefaults.subtitleFontSize;
set subtitleFontSize(double newValue) => setAndNotify(subtitleFontSizeKey, newValue); set subtitleFontSize(double newValue) => setAndNotify(subtitleFontSizeKey, newValue);
TextAlign get subtitleTextAlignment => getEnumOrDefault(subtitleTextAlignmentKey, TextAlign.center, TextAlign.values); TextAlign get subtitleTextAlignment => getEnumOrDefault(subtitleTextAlignmentKey, SettingsDefaults.subtitleTextAlignment, TextAlign.values);
set subtitleTextAlignment(TextAlign newValue) => setAndNotify(subtitleTextAlignmentKey, newValue.toString()); set subtitleTextAlignment(TextAlign newValue) => setAndNotify(subtitleTextAlignmentKey, newValue.toString());
bool get subtitleShowOutline => getBoolOrDefault(subtitleShowOutlineKey, true); bool get subtitleShowOutline => getBoolOrDefault(subtitleShowOutlineKey, SettingsDefaults.subtitleShowOutline);
set subtitleShowOutline(bool newValue) => setAndNotify(subtitleShowOutlineKey, newValue); set subtitleShowOutline(bool newValue) => setAndNotify(subtitleShowOutlineKey, newValue);
Color get subtitleTextColor => Color(_prefs!.getInt(subtitleTextColorKey) ?? Colors.white.value); Color get subtitleTextColor => Color(_prefs!.getInt(subtitleTextColorKey) ?? SettingsDefaults.subtitleTextColor.value);
set subtitleTextColor(Color newValue) => setAndNotify(subtitleTextColorKey, newValue.value); set subtitleTextColor(Color newValue) => setAndNotify(subtitleTextColorKey, newValue.value);
Color get subtitleBackgroundColor => Color(_prefs!.getInt(subtitleBackgroundColorKey) ?? Colors.transparent.value); Color get subtitleBackgroundColor => Color(_prefs!.getInt(subtitleBackgroundColorKey) ?? SettingsDefaults.subtitleBackgroundColor.value);
set subtitleBackgroundColor(Color newValue) => setAndNotify(subtitleBackgroundColorKey, newValue.value); set subtitleBackgroundColor(Color newValue) => setAndNotify(subtitleBackgroundColorKey, newValue.value);
// info // info
EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values); EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, SettingsDefaults.infoMapStyle, EntryMapStyle.values);
set infoMapStyle(EntryMapStyle newValue) => setAndNotify(infoMapStyleKey, newValue.toString()); set infoMapStyle(EntryMapStyle newValue) => setAndNotify(infoMapStyleKey, newValue.toString());
double get infoMapZoom => _prefs!.getDouble(infoMapZoomKey) ?? 12; double get infoMapZoom => _prefs!.getDouble(infoMapZoomKey) ?? SettingsDefaults.infoMapZoom;
set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue); set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue);
CoordinateFormat get coordinateFormat => getEnumOrDefault(coordinateFormatKey, CoordinateFormat.dms, CoordinateFormat.values); CoordinateFormat get coordinateFormat => getEnumOrDefault(coordinateFormatKey, SettingsDefaults.coordinateFormat, CoordinateFormat.values);
set coordinateFormat(CoordinateFormat newValue) => setAndNotify(coordinateFormatKey, newValue.toString()); set coordinateFormat(CoordinateFormat newValue) => setAndNotify(coordinateFormatKey, newValue.toString());
// rendering // rendering
EntryBackground get imageBackground => getEnumOrDefault(imageBackgroundKey, EntryBackground.white, EntryBackground.values); EntryBackground get imageBackground => getEnumOrDefault(imageBackgroundKey, SettingsDefaults.imageBackground, EntryBackground.values);
set imageBackground(EntryBackground newValue) => setAndNotify(imageBackgroundKey, newValue.toString()); set imageBackground(EntryBackground newValue) => setAndNotify(imageBackgroundKey, newValue.toString());
// search // search
bool get saveSearchHistory => getBoolOrDefault(saveSearchHistoryKey, true); bool get saveSearchHistory => getBoolOrDefault(saveSearchHistoryKey, SettingsDefaults.saveSearchHistory);
set saveSearchHistory(bool newValue) => setAndNotify(saveSearchHistoryKey, newValue); set saveSearchHistory(bool newValue) => setAndNotify(saveSearchHistoryKey, newValue);

View file

@ -82,8 +82,13 @@ class VideoMetadataFormatter {
int? dateMillis; int? dateMillis;
final dateString = mediaInfo[Keys.date]; bool isDefined(dynamic value) => value is String && value != '0';
if (dateString is String && dateString != '0') {
var dateString = mediaInfo[Keys.date];
if (!isDefined(dateString)) {
dateString = mediaInfo[Keys.creationTime];
}
if (isDefined(dateString)) {
final date = DateTime.tryParse(dateString); final date = DateTime.tryParse(dateString);
if (date != null) { if (date != null) {
dateMillis = date.millisecondsSinceEpoch; dateMillis = date.millisecondsSinceEpoch;

View file

@ -47,8 +47,10 @@ class MimeTypes {
static const mp2t = 'video/mp2t'; // .m2ts static const mp2t = 'video/mp2t'; // .m2ts
static const mp4 = 'video/mp4'; static const mp4 = 'video/mp4';
static const ogg = 'video/ogg'; static const ogg = 'video/ogg';
static const webm = 'video/webm';
static const json = 'application/json'; static const json = 'application/json';
static const plainText = 'text/plain';
// groups // groups
@ -62,7 +64,7 @@ class MimeTypes {
static const Set<String> _knownOpaqueImages = {heic, heif, jpeg}; static const Set<String> _knownOpaqueImages = {heic, heif, jpeg};
static const Set<String> _knownVideos = {avi, aviVnd, mkv, mov, mp2t, mp4, ogg}; static const Set<String> _knownVideos = {avi, aviVnd, mkv, mov, mp2t, mp4, ogg, webm};
static final Set<String> knownMediaTypes = {..._knownOpaqueImages, ...alphaImages, ...rawImages, ...undecodableImages, ..._knownVideos}; static final Set<String> knownMediaTypes = {..._knownOpaqueImages, ...alphaImages, ...rawImages, ...undecodableImages, ..._knownVideos};

View file

@ -1,5 +1,8 @@
import 'package:collection/collection.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:stack_trace/stack_trace.dart';
abstract class ReportService { abstract class ReportService {
bool get isCollectionEnabled; bool get isCollectionEnabled;
@ -40,6 +43,19 @@ class CrashlyticsReportService extends ReportService {
@override @override
Future<void> recordError(dynamic exception, StackTrace? stack) { Future<void> recordError(dynamic exception, StackTrace? stack) {
if (exception is PlatformException && stack != null) {
// simply creating a trace with `Trace.current(1)` or creating a `Trace` from modified frames
// does not yield a stack trace that Crashlytics can segment,
// so we reconstruct a string stack trace instead
stack = StackTrace.fromString(Trace.from(stack)
.frames
.skip(2)
.toList()
.mapIndexed(
(i, f) => '#${(i++).toString().padRight(8)}${f.member} (${f.uri}:${f.line}:${f.column})',
)
.join('\n'));
}
return instance.recordError(exception, stack); return instance.recordError(exception, stack);
} }

View file

@ -102,4 +102,7 @@ class AIcons {
static const IconData threeSixty = Icons.threesixty_outlined; static const IconData threeSixty = Icons.threesixty_outlined;
static const IconData selected = Icons.check_circle_outline; static const IconData selected = Icons.check_circle_outline;
static const IconData unselected = Icons.radio_button_unchecked; static const IconData unselected = Icons.radio_button_unchecked;
static const IconData github = MdiIcons.github;
static const IconData legal = MdiIcons.scaleBalance;
} }

View file

@ -38,6 +38,8 @@ class Constants {
static const int infoGroupMaxValueLength = 140; static const int infoGroupMaxValueLength = 140;
static const String avesGithub = 'https://github.com/deckerst/aves';
static const List<Dependency> androidDependencies = [ static const List<Dependency> androidDependencies = [
Dependency( Dependency(
name: 'AndroidX Core-KTX', name: 'AndroidX Core-KTX',
@ -91,6 +93,12 @@ class Constants {
licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/blob/main/packages/connectivity_plus/connectivity_plus/LICENSE', licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/blob/main/packages/connectivity_plus/connectivity_plus/LICENSE',
sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/connectivity_plus', sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/connectivity_plus',
), ),
Dependency(
name: 'Device Info Plus',
license: 'BSD 3-Clause',
licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/blob/main/packages/device_info_plus/device_info_plus/LICENSE',
sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/device_info_plus',
),
Dependency( Dependency(
name: 'FlutterFire (Core, Crashlytics)', name: 'FlutterFire (Core, Crashlytics)',
license: 'BSD 3-Clause', license: 'BSD 3-Clause',
@ -289,15 +297,20 @@ class Constants {
license: 'Apache 2.0', license: 'Apache 2.0',
sourceUrl: 'https://github.com/DavBfr/dart_pdf', sourceUrl: 'https://github.com/DavBfr/dart_pdf',
), ),
Dependency(
name: 'Stack Trace',
license: 'BSD 3-Clause',
sourceUrl: 'https://github.com/dart-lang/stack_trace',
),
Dependency( Dependency(
name: 'Transparent Image', name: 'Transparent Image',
license: 'MIT', license: 'MIT',
sourceUrl: 'https://pub.dev/packages/transparent_image', sourceUrl: 'https://github.com/brianegan/transparent_image',
), ),
Dependency( Dependency(
name: 'Tuple', name: 'Tuple',
license: 'BSD 2-Clause', license: 'BSD 2-Clause',
sourceUrl: 'https://github.com/dart-lang/tuple', sourceUrl: 'https://github.com/google/tuple.dart',
), ),
Dependency( Dependency(
name: 'Version', name: 'Version',

View file

@ -1,4 +1,5 @@
import 'package:aves/widgets/about/app_ref.dart'; import 'package:aves/widgets/about/app_ref.dart';
import 'package:aves/widgets/about/bug_report.dart';
import 'package:aves/widgets/about/credits.dart'; import 'package:aves/widgets/about/credits.dart';
import 'package:aves/widgets/about/licenses.dart'; import 'package:aves/widgets/about/licenses.dart';
import 'package:aves/widgets/about/update.dart'; import 'package:aves/widgets/about/update.dart';
@ -27,6 +28,8 @@ class AboutPage extends StatelessWidget {
AppReference(), AppReference(),
Divider(), Divider(),
AboutUpdate(), AboutUpdate(),
BugReport(),
Divider(),
AboutCredits(), AboutCredits(),
Divider(), Divider(),
], ],

View file

@ -1,6 +1,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:aves/flutter_version.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/basic/link_chip.dart';
import 'package:aves/widgets/common/extensions/build_context.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';
@ -29,8 +30,8 @@ class _AppReferenceState extends State<AppReference> {
child: Column( child: Column(
children: [ children: [
_buildAvesLine(), _buildAvesLine(),
_buildFlutterLine(),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildLinks(),
], ],
), ),
); );
@ -47,37 +48,45 @@ class _AppReferenceState extends State<AppReference> {
return FutureBuilder<PackageInfo>( return FutureBuilder<PackageInfo>(
future: _packageInfoLoader, future: _packageInfoLoader,
builder: (context, snapshot) { builder: (context, snapshot) {
return LinkChip( return Row(
leading: AvesLogo( mainAxisSize: MainAxisSize.min,
children: [
AvesLogo(
size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.25, size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.25,
), ),
text: '${context.l10n.appName} ${snapshot.data?.version}', const SizedBox(width: 8),
url: 'https://github.com/deckerst/aves', Text(
textStyle: style, '${context.l10n.appName} ${snapshot.data?.version}',
style: style,
),
],
); );
}, },
); );
} }
Widget _buildFlutterLine() { Widget _buildLinks() {
final style = DefaultTextStyle.of(context).style; return Wrap(
final subColor = style.color!.withOpacity(.6); crossAxisAlignment: WrapCrossAlignment.center,
spacing: 16,
return Text.rich(
TextSpan(
children: [ children: [
WidgetSpan( LinkChip(
child: Padding( leading: const Icon(
padding: const EdgeInsetsDirectional.only(end: 4), AIcons.github,
child: FlutterLogo( size: 24,
size: style.fontSize! * 1.25,
), ),
text: context.l10n.aboutLinkSources,
url: Constants.avesGithub,
), ),
LinkChip(
leading: const Icon(
AIcons.legal,
size: 22,
),
text: context.l10n.aboutLinkLicense,
url: '${Constants.avesGithub}/blob/main/LICENSE',
), ),
TextSpan(text: '${context.l10n.aboutFlutter} ${version['frameworkVersion']}'),
], ],
),
style: TextStyle(color: subColor),
); );
} }
} }

View file

@ -0,0 +1,163 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:aves/flutter_version.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/services.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class BugReport extends StatefulWidget {
const BugReport({Key? key}) : super(key: key);
@override
_BugReportState createState() => _BugReportState();
}
class _BugReportState extends State<BugReport> with FeedbackMixin {
late Future<String> _infoLoader;
bool _showInstructions = false;
@override
void initState() {
super.initState();
_infoLoader = _getInfo();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return ExpansionPanelList(
expansionCallback: (index, isExpanded) {
setState(() => _showInstructions = !isExpanded);
},
expandedHeaderPadding: EdgeInsets.zero,
elevation: 0,
children: [
ExpansionPanel(
headerBuilder: (context, isExpanded) => ConstrainedBox(
constraints: const BoxConstraints(minHeight: 48),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: AlignmentDirectional.centerStart,
child: Text(l10n.aboutBug, style: Constants.titleTextStyle),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildStep(1, l10n.aboutBugSaveLogInstruction, l10n.aboutBugSaveLogButton, _saveLogs),
_buildStep(2, l10n.aboutBugCopyInfoInstruction, l10n.aboutBugCopyInfoButton, _copySystemInfo),
FutureBuilder<String>(
future: _infoLoader,
builder: (context, snapshot) {
final info = snapshot.data;
if (info == null) return const SizedBox();
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade800,
border: Border.all(
color: Colors.white,
),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
margin: const EdgeInsets.symmetric(vertical: 8),
child: SelectableText(info));
},
),
_buildStep(3, l10n.aboutBugReportInstruction, l10n.aboutBugReportButton, _goToGithub),
const SizedBox(height: 16),
],
),
),
isExpanded: _showInstructions,
canTapOnHeader: true,
backgroundColor: Colors.transparent,
),
],
);
}
Widget _buildStep(int step, String text, String buttonText, VoidCallback onPressed) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.fromBorderSide(BorderSide(
color: Theme.of(context).accentColor,
width: AvesFilterChip.outlineWidth,
)),
shape: BoxShape.circle,
),
child: Text('$step'),
),
const SizedBox(width: 8),
Expanded(child: Text(text)),
const SizedBox(width: 8),
OutlinedButton(
onPressed: onPressed,
style: ButtonStyle(
side: MaterialStateProperty.all<BorderSide>(BorderSide(color: Theme.of(context).accentColor)),
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
),
child: Text(buttonText),
)
],
),
);
}
Future<String> _getInfo() async {
final packageInfo = await PackageInfo.fromPlatform();
final androidInfo = await DeviceInfoPlugin().androidInfo;
final hasPlayServices = await availability.hasPlayServices;
return [
'Aves version: ${packageInfo.version} (Build ${packageInfo.buildNumber})',
'Flutter version: ${version['frameworkVersion']} (Channel ${version['channel']})',
'Android version: ${androidInfo.version.release} (SDK ${androidInfo.version.sdkInt})',
'Device: ${androidInfo.manufacturer} ${androidInfo.model}',
'Google Play services: ${hasPlayServices ? 'ready' : 'not available'}',
].join('\n');
}
Future<void> _saveLogs() async {
final result = await Process.run('logcat', ['-d']);
final logs = result.stdout;
final success = await storageService.createFile(
'aves-logs-${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}.txt',
MimeTypes.plainText,
Uint8List.fromList(utf8.encode(logs)),
);
if (success != null) {
if (success) {
showFeedback(context, context.l10n.genericSuccessFeedback);
} else {
showFeedback(context, context.l10n.genericFailureFeedback);
}
}
}
Future<void> _copySystemInfo() async {
await Clipboard.setData(ClipboardData(text: await _infoLoader));
showFeedback(context, context.l10n.genericSuccessFeedback);
}
Future<void> _goToGithub() async {
await launch('${Constants.avesGithub}/issues/new');
}
}

View file

@ -62,7 +62,7 @@ class _AboutUpdateState extends State<AboutUpdate> {
WidgetSpan( WidgetSpan(
child: LinkChip( child: LinkChip(
text: context.l10n.aboutUpdateGitHub, text: context.l10n.aboutUpdateGitHub,
url: 'https://github.com/deckerst/aves/releases', url: '${Constants.avesGithub}/releases',
textStyle: const TextStyle(fontWeight: FontWeight.bold), textStyle: const TextStyle(fontWeight: FontWeight.bold),
), ),
alignment: PlaceholderAlignment.middle, alignment: PlaceholderAlignment.middle,

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -52,7 +54,7 @@ class BottomPaddingSliver extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Selector<MediaQueryData, double>( child: Selector<MediaQueryData, double>(
selector: (context, mq) => mq.effectiveBottomPadding, selector: (context, mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom),
builder: (context, mqPaddingBottom, child) { builder: (context, mqPaddingBottom, child) {
return SizedBox(height: mqPaddingBottom); return SizedBox(height: mqPaddingBottom);
}, },

View file

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
class AvesPopupMenuButton<T> extends PopupMenuButton<T> {
final VoidCallback? onMenuOpened;
const AvesPopupMenuButton({
Key? key,
required PopupMenuItemBuilder<T> itemBuilder,
T? initialValue,
PopupMenuItemSelected<T>? onSelected,
PopupMenuCanceled? onCanceled,
String? tooltip,
double? elevation,
EdgeInsetsGeometry padding = const EdgeInsets.all(8.0),
Widget? child,
Widget? icon,
Offset offset = Offset.zero,
bool enabled = true,
ShapeBorder? shape,
Color? color,
bool? enableFeedback,
double? iconSize,
this.onMenuOpened,
}) : super(
key: key,
itemBuilder: itemBuilder,
initialValue: initialValue,
onSelected: onSelected,
onCanceled: onCanceled,
tooltip: tooltip,
elevation: elevation,
padding: padding,
child: child,
icon: icon,
iconSize: iconSize,
offset: offset,
enabled: enabled,
shape: shape,
color: color,
enableFeedback: enableFeedback,
);
@override
_AvesPopupMenuButtonState<T> createState() => _AvesPopupMenuButtonState<T>();
}
class _AvesPopupMenuButtonState<T> extends PopupMenuButtonState<T> {
@override
void showButtonMenu() {
(widget as AvesPopupMenuButton).onMenuOpened?.call();
super.showButtonMenu();
}
}

View file

@ -1,6 +1,8 @@
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/settings/common/quick_actions/action_button.dart'; import 'package:aves/widgets/settings/common/quick_actions/action_button.dart';
import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart'; import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart';
import 'package:aves/widgets/viewer/overlay/common.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class AvailableActionPanel<T extends Object> extends StatelessWidget { class AvailableActionPanel<T extends Object> extends StatelessWidget {
@ -75,10 +77,6 @@ class AvailableActionPanel<T extends Object> extends StatelessWidget {
Widget child, Widget child,
) => ) =>
LongPressDraggable<T>( LongPressDraggable<T>(
data: action,
maxSimultaneousDrags: 1,
onDragStarted: () => _setDraggedAvailableAction(action),
onDragEnd: (details) => _setDraggedAvailableAction(null),
feedback: MediaQueryDataProvider( feedback: MediaQueryDataProvider(
child: _buildActionButton( child: _buildActionButton(
context, context,
@ -86,6 +84,13 @@ class AvailableActionPanel<T extends Object> extends StatelessWidget {
showCaption: false, showCaption: false,
), ),
), ),
data: action,
dragAnchorStrategy: (draggable, context, position) {
return childDragAnchorStrategy(draggable, context, position) + Offset(0, OverlayButton.getSize(context));
},
maxSimultaneousDrags: 1,
onDragStarted: () => _setDraggedAvailableAction(action),
onDragEnd: (details) => _setDraggedAvailableAction(null),
childWhenDragging: child, childWhenDragging: child,
child: child, child: child,
); );

View file

@ -1,4 +1,5 @@
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/viewer/overlay/common.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
enum QuickActionPlacement { header, action, footer } enum QuickActionPlacement { header, action, footer }
@ -58,16 +59,19 @@ class QuickActionButton<T extends Object> extends StatelessWidget {
} }
Widget _buildDraggable(Widget child, T action) => LongPressDraggable( Widget _buildDraggable(Widget child, T action) => LongPressDraggable(
feedback: MediaQueryDataProvider(
child: draggableFeedbackBuilder!(action),
),
data: action, data: action,
dragAnchorStrategy: (draggable, context, position) {
return childDragAnchorStrategy(draggable, context, position) + Offset(0, OverlayButton.getSize(context));
},
maxSimultaneousDrags: 1, maxSimultaneousDrags: 1,
onDragStarted: () => _setDraggedQuickAction(action), onDragStarted: () => _setDraggedQuickAction(action),
// `onDragEnd` is only called when the widget is mounted, // `onDragEnd` is only called when the widget is mounted,
// so we rely on `onDraggableCanceled` and `onDragCompleted` instead // so we rely on `onDraggableCanceled` and `onDragCompleted` instead
onDraggableCanceled: (velocity, offset) => _setDraggedQuickAction(null), onDraggableCanceled: (velocity, offset) => _setDraggedQuickAction(null),
onDragCompleted: () => _setDraggedQuickAction(null), onDragCompleted: () => _setDraggedQuickAction(null),
feedback: MediaQueryDataProvider(
child: draggableFeedbackBuilder!(action),
),
childWhenDragging: child, childWhenDragging: child,
child: child, child: child,
); );

View file

@ -304,6 +304,13 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
_videoActionDelegate.onActionSelected(context, videoController, action); _videoActionDelegate.onActionSelected(context, videoController, action);
} }
}, },
onActionMenuOpened: () {
// if the menu is opened while overlay is hiding,
// the popup menu button is disposed and menu items are ineffective,
// so we make sure overlay stays visible
_videoActionDelegate.stopOverlayHidingTimer();
const ToggleOverlayNotification(visible: true).dispatch(context);
},
), ),
); );
} else if (targetEntry.is360) { } else if (targetEntry.is360) {

View file

@ -4,6 +4,7 @@ import 'package:aves/utils/constants.dart';
import 'package:aves/utils/string_utils.dart'; import 'package:aves/utils/string_utils.dart';
import 'package:aves/widgets/common/identity/highlight_title.dart'; import 'package:aves/widgets/common/identity/highlight_title.dart';
import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/common.dart';
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/darktable.dart';
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/exif.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/exif.dart';
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/google.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/google.dart';
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/iptc.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_ns/iptc.dart';
@ -30,6 +31,8 @@ class XmpNamespace extends Equatable {
switch (namespace) { switch (namespace) {
case XmpBasicNamespace.ns: case XmpBasicNamespace.ns:
return XmpBasicNamespace(rawProps); return XmpBasicNamespace(rawProps);
case XmpDarktableNamespace.ns:
return XmpDarktableNamespace(rawProps);
case XmpExifNamespace.ns: case XmpExifNamespace.ns:
return XmpExifNamespace(rawProps); return XmpExifNamespace(rawProps);
case XmpGAudioNamespace.ns: case XmpGAudioNamespace.ns:
@ -136,8 +139,10 @@ class XmpProp {
return propPath.splitMapJoin(XMP.structFieldSeparator, return propPath.splitMapJoin(XMP.structFieldSeparator,
onMatch: (match) => ' ${match.group(0)} ', onMatch: (match) => ' ${match.group(0)} ',
onNonMatch: (s) { onNonMatch: (s) {
// strip namespace & format // strip namespace
return s.split(XMP.propNamespaceSeparator).last.toSentenceCase(); final key = s.split(XMP.propNamespaceSeparator).last;
// format
return key.replaceAll('_', ' ').toSentenceCase();
}); });
} }

View file

@ -0,0 +1,25 @@
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
import 'package:aves/widgets/viewer/info/metadata/xmp_structs.dart';
import 'package:flutter/material.dart';
class XmpDarktableNamespace extends XmpNamespace {
static const ns = 'darktable';
static final historyPattern = RegExp(r'darktable:history\[(\d+)\]/(.*)');
final history = <int, Map<String, String>>{};
XmpDarktableNamespace(Map<String, String> rawProps) : super(ns, rawProps);
@override
bool extractData(XmpProp prop) => extractIndexedStruct(prop, historyPattern, history);
@override
List<Widget> buildFromExtractedData() => [
if (history.isNotEmpty)
XmpStructArrayCard(
title: 'History',
structByIndex: history,
),
];
}

View file

@ -9,6 +9,7 @@ import 'package:aves/theme/format.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/basic/menu.dart'; import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/basic/popup_menu_button.dart';
import 'package:aves/widgets/common/extensions/build_context.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';
@ -24,6 +25,7 @@ class VideoControlOverlay extends StatefulWidget {
final AvesVideoController? controller; final AvesVideoController? controller;
final Animation<double> scale; final Animation<double> scale;
final Function(VideoAction value) onActionSelected; final Function(VideoAction value) onActionSelected;
final VoidCallback onActionMenuOpened;
const VideoControlOverlay({ const VideoControlOverlay({
Key? key, Key? key,
@ -31,6 +33,7 @@ class VideoControlOverlay extends StatefulWidget {
required this.controller, required this.controller,
required this.scale, required this.scale,
required this.onActionSelected, required this.onActionSelected,
required this.onActionMenuOpened,
}) : super(key: key); }) : super(key: key);
@override @override
@ -94,6 +97,7 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
scale: scale, scale: scale,
controller: controller, controller: controller,
onActionSelected: widget.onActionSelected, onActionSelected: widget.onActionSelected,
onActionMenuOpened: widget.onActionMenuOpened,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildProgressBar(), _buildProgressBar(),
@ -199,6 +203,7 @@ class _ButtonRow extends StatelessWidget {
final Animation<double> scale; final Animation<double> scale;
final AvesVideoController? controller; final AvesVideoController? controller;
final Function(VideoAction value) onActionSelected; final Function(VideoAction value) onActionSelected;
final VoidCallback onActionMenuOpened;
const _ButtonRow({ const _ButtonRow({
Key? key, Key? key,
@ -207,6 +212,7 @@ class _ButtonRow extends StatelessWidget {
required this.scale, required this.scale,
required this.controller, required this.controller,
required this.onActionSelected, required this.onActionSelected,
required this.onActionMenuOpened,
}) : super(key: key); }) : super(key: key);
static const double padding = 8; static const double padding = 8;
@ -225,12 +231,13 @@ class _ButtonRow extends StatelessWidget {
child: OverlayButton( child: OverlayButton(
scale: scale, scale: scale,
child: MenuIconTheme( child: MenuIconTheme(
child: PopupMenuButton<VideoAction>( child: AvesPopupMenuButton<VideoAction>(
itemBuilder: (context) => menuActions.map((action) => _buildPopupMenuItem(context, action)).toList(), itemBuilder: (context) => menuActions.map((action) => _buildPopupMenuItem(context, action)).toList(),
onSelected: (action) { onSelected: (action) {
// 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, () => onActionSelected(action)); Future.delayed(Durations.popupMenuAnimation * timeDilation, () => onActionSelected(action));
}, },
onMenuOpened: onActionMenuOpened,
), ),
), ),
), ),

View file

@ -5,12 +5,14 @@ import 'package:aves/model/settings/settings.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/basic/menu.dart'; import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/basic/popup_menu_button.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/sweeper.dart'; import 'package:aves/widgets/common/fx/sweeper.dart';
import 'package:aves/widgets/viewer/entry_action_delegate.dart'; import 'package:aves/widgets/viewer/entry_action_delegate.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/overlay/common.dart';
import 'package:aves/widgets/viewer/overlay/minimap.dart'; import 'package:aves/widgets/viewer/overlay/minimap.dart';
import 'package:aves/widgets/viewer/overlay/notifications.dart';
import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart';
import 'package:aves/widgets/viewer/visual/conductor.dart'; import 'package:aves/widgets/viewer/visual/conductor.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -167,7 +169,7 @@ class _TopOverlayRow extends StatelessWidget {
OverlayButton( OverlayButton(
scale: scale, scale: scale,
child: MenuIconTheme( child: MenuIconTheme(
child: PopupMenuButton<EntryAction>( child: AvesPopupMenuButton<EntryAction>(
key: const Key('entry-menu-button'), key: const Key('entry-menu-button'),
itemBuilder: (context) => [ itemBuilder: (context) => [
...inAppActions.map((action) => _buildPopupMenuItem(context, action)), ...inAppActions.map((action) => _buildPopupMenuItem(context, action)),
@ -183,6 +185,12 @@ class _TopOverlayRow extends StatelessWidget {
// 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, () => _onActionSelected(context, action)); Future.delayed(Durations.popupMenuAnimation * timeDilation, () => _onActionSelected(context, action));
}, },
onMenuOpened: () {
// if the menu is opened while overlay is hiding,
// the popup menu button is disposed and menu items are ineffective,
// so we make sure overlay stays visible
const ToggleOverlayNotification(visible: true).dispatch(context);
},
), ),
), ),
), ),

View file

@ -31,12 +31,12 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
}); });
void dispose() { void dispose() {
_stopOverlayHidingTimer(); stopOverlayHidingTimer();
} }
void onActionSelected(BuildContext context, AvesVideoController controller, VideoAction action) { void onActionSelected(BuildContext context, AvesVideoController controller, VideoAction action) {
// make sure overlay is not disappearing when selecting an action // make sure overlay is not disappearing when selecting an action
_stopOverlayHidingTimer(); stopOverlayHidingTimer();
const ToggleOverlayNotification(visible: true).dispatch(context); const ToggleOverlayNotification(visible: true).dispatch(context);
switch (action) { switch (action) {
@ -187,5 +187,5 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
} }
} }
void _stopOverlayHidingTimer() => _overlayHidingTimer?.cancel(); void stopOverlayHidingTimer() => _overlayHidingTimer?.cancel();
} }

View file

@ -105,7 +105,7 @@ packages:
name: connectivity_plus name: connectivity_plus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.7" version: "1.1.0"
connectivity_plus_linux: connectivity_plus_linux:
dependency: transitive dependency: transitive
description: description:
@ -119,7 +119,7 @@ packages:
name: connectivity_plus_macos name: connectivity_plus_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.2" version: "1.1.0"
connectivity_plus_platform_interface: connectivity_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -133,14 +133,14 @@ packages:
name: connectivity_plus_web name: connectivity_plus_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0+1"
connectivity_plus_windows: connectivity_plus_windows:
dependency: transitive dependency: transitive
description: description:
name: connectivity_plus_windows name: connectivity_plus_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.2" version: "1.1.0"
convert: convert:
dependency: transitive dependency: transitive
description: description:
@ -190,6 +190,48 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.1" version: "1.2.1"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
device_info_plus_linux:
dependency: transitive
description:
name: device_info_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
device_info_plus_macos:
dependency: transitive
description:
name: device_info_plus_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
device_info_plus_web:
dependency: transitive
description:
name: device_info_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
device_info_plus_windows:
dependency: transitive
description:
name: device_info_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
equatable: equatable:
dependency: "direct main" dependency: "direct main"
description: description:
@ -232,7 +274,7 @@ packages:
description: description:
path: "." path: "."
ref: aves ref: aves
resolved-ref: "9542ec208248bfa4d459e3967087a4b236da1368" resolved-ref: "2aefcebb9f4bc08107e7de16927d91e577e10d7d"
url: "git://github.com/deckerst/fijkplayer.git" url: "git://github.com/deckerst/fijkplayer.git"
source: git source: git
version: "0.10.0" version: "0.10.0"
@ -249,7 +291,7 @@ packages:
name: firebase_core name: firebase_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.5.0" version: "1.6.0"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -270,14 +312,14 @@ packages:
name: firebase_crashlytics name: firebase_crashlytics
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1"
firebase_crashlytics_platform_interface: firebase_crashlytics_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_crashlytics_platform_interface name: firebase_crashlytics_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.1" version: "3.1.2"
flex_color_picker: flex_color_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -341,14 +383,14 @@ packages:
name: flutter_markdown name: flutter_markdown
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.4" version: "0.6.6"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.0.3"
flutter_staggered_animations: flutter_staggered_animations:
dependency: "direct main" dependency: "direct main"
description: description:
@ -405,7 +447,7 @@ packages:
name: google_maps_flutter name: google_maps_flutter
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.6" version: "2.0.8"
google_maps_flutter_platform_interface: google_maps_flutter_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -594,7 +636,7 @@ packages:
name: package_info_plus name: package_info_plus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.5" version: "1.0.6"
package_info_plus_linux: package_info_plus_linux:
dependency: transitive dependency: transitive
description: description:
@ -622,7 +664,7 @@ packages:
name: package_info_plus_web name: package_info_plus_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "1.0.4"
package_info_plus_windows: package_info_plus_windows:
dependency: transitive dependency: transitive
description: description:
@ -636,7 +678,7 @@ packages:
name: palette_generator name: palette_generator
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.0" version: "0.3.1"
panorama: panorama:
dependency: "direct main" dependency: "direct main"
description: description:
@ -685,7 +727,7 @@ packages:
name: pdf name: pdf
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.4.2" version: "3.5.0"
pedantic: pedantic:
dependency: transitive dependency: transitive
description: description:
@ -804,7 +846,7 @@ packages:
name: shared_preferences name: shared_preferences
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.6" version: "2.0.7"
shared_preferences_linux: shared_preferences_linux:
dependency: transitive dependency: transitive
description: description:
@ -832,7 +874,7 @@ packages:
name: shared_preferences_web name: shared_preferences_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.2"
shared_preferences_windows: shared_preferences_windows:
dependency: transitive dependency: transitive
description: description:
@ -900,16 +942,16 @@ packages:
name: sqflite name: sqflite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0+3" version: "2.0.0+4"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0+2" version: "2.0.1"
stack_trace: stack_trace:
dependency: transitive dependency: "direct main"
description: description:
name: stack_trace name: stack_trace
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
@ -1021,21 +1063,21 @@ packages:
name: url_launcher name: url_launcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.9" version: "6.0.10"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_linux name: url_launcher_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.2"
url_launcher_macos: url_launcher_macos:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_macos name: url_launcher_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.2"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1049,7 +1091,7 @@ packages:
name: url_launcher_web name: url_launcher_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.0.4"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
@ -1112,7 +1154,7 @@ packages:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.5" version: "2.2.8"
wkt_parser: wkt_parser:
dependency: transitive dependency: transitive
description: description:

View file

@ -1,7 +1,7 @@
name: aves name: aves
description: A visual media gallery and metadata explorer app. description: A visual media gallery and metadata explorer app.
repository: https://github.com/deckerst/aves repository: https://github.com/deckerst/aves
version: 1.5.0+54 version: 1.5.1+55
publish_to: none publish_to: none
environment: environment:
@ -19,6 +19,7 @@ dependencies:
# TODO TLAD as of 2021/08/04, null safe version is pre-release # TODO TLAD as of 2021/08/04, null safe version is pre-release
custom_rounded_rectangle_border: '>=0.2.0-nullsafety.0' custom_rounded_rectangle_border: '>=0.2.0-nullsafety.0'
decorated_icon: decorated_icon:
device_info_plus:
equatable: equatable:
event_bus: event_bus:
expansion_tile_card: expansion_tile_card:
@ -56,6 +57,7 @@ dependencies:
provider: provider:
shared_preferences: shared_preferences:
sqflite: sqflite:
stack_trace:
streams_channel: streams_channel:
git: git:
url: git://github.com/deckerst/aves_streams_channel.git url: git://github.com/deckerst/aves_streams_channel.git

View file

@ -1,4 +1,7 @@
Thanks for using Aves! Thanks for using Aves!
v1.5.1:
- fixed hanging app for collections with specific video formats
- added bug reporting instructions
v1.5.0: v1.5.0:
- faster launch - faster launch
- edit Exif dates - edit Exif dates