diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2091c9c95..5440ce590 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file.
## [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
### Added
- Info: edit Exif dates (setting, shifting, deleting)
diff --git a/README.md b/README.md
index c306ede1a..d17cf244c 100644
--- a/README.md
+++ b/README.md
@@ -4,25 +4,34 @@
-[
](https://play.google.com/store/apps/details?id=deckers.thibault.aves&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1)
-
+[
](https://play.google.com/store/apps/details?id=deckers.thibault.aves&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1)
+[
](https://github.com/deckerst/aves/releases/latest)
+
Aves is a gallery and metadata explorer app. It is built for Android, with Flutter.


## Features
-- support raster images: JPEG, GIF, PNG, HEIC/HEIF (including multi-track, from Android Pie), WEBP, TIFF (including multi-page), BMP, WBMP, ICO
-- support animated images: GIF, WEBP
-- support raw images: ARW, CR2, DNG, NEF, NRW, ORF, PEF, RAF, RW2, SRW
-- support vector images: SVG
-- support videos: MP4, AVI, MKV, AVCHD & probably others
-- identify panoramas (aka photo spheres), 360° videos, GeoTIFF files
-- search and filter by country, place, XMP tag, type (animated, raster, vector…)
-- favorites
-- statistics
-- support Android API 20 ~ 31 (Lollipop ~ S)
-- Android integration (app shortcuts, handle view/pick intents)
+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**!
+
+It scans your media collection to identify **motion photos**, **panoramas** (aka photo spheres), **360° videos**, as well as **GeoTIFF** files.
+
+**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.
+
+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**.
+
+## Permissions
+
+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
diff --git a/android/app/build.gradle b/android/app/build.gradle
index f82f67e68..aa7b88db9 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -123,7 +123,7 @@ dependencies {
// https://jitpack.io/com/github/deckerst/Android-TiffBitmapFactory/**********/build.log
implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a' // forked, built by JitPack
// 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'
kapt 'androidx.annotation:annotation:1.2.0'
diff --git a/android/app/libs/fijkplayer-full-release.aar b/android/app/libs/fijkplayer-full-release.aar
index a3588ec7b..4a5f64390 100644
Binary files a/android/app/libs/fijkplayer-full-release.aar and b/android/app/libs/fijkplayer-full-release.aar differ
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
index 1c4d47dce..1377c88a3 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
@@ -31,6 +31,10 @@ class MainActivity : FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Log.i(LOG_TAG, "onCreate intent=$intent")
+ intent.extras?.takeUnless { it.isEmpty }?.let {
+ Log.i(LOG_TAG, "onCreate intent extras=$it")
+ }
+
// StrictMode.setThreadPolicy(
// StrictMode.ThreadPolicy.Builder()
// .detectAll()
@@ -194,6 +198,9 @@ class MainActivity : FlutterActivity() {
"query" to intent.getStringExtra(SearchManager.QUERY),
)
}
+ Intent.ACTION_RUN -> {
+ // flutter run
+ }
else -> {
Log.w(LOG_TAG, "unhandled intent action=${intent?.action}")
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
index 5681e56be..b1cb53d20 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
@@ -68,15 +68,19 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
return
}
+ if (!isSupportedByPixyMeta(mimeType)) {
+ result.error("getPixyMetadata-unsupported", "PixyMeta does not support mimeType=$mimeType", null)
+ return
+ }
+
val metadataMap = HashMap()
- if (isSupportedByPixyMeta(mimeType)) {
- try {
- StorageUtils.openInputStream(context, uri)?.use { input ->
- metadataMap.putAll(PixyMetaHelper.describe(input))
- }
- } catch (e: Exception) {
- result.error("getPixyMetadata-exception", e.message, e.stackTraceToString())
+ try {
+ StorageUtils.openInputStream(context, uri)?.use { input ->
+ metadataMap.putAll(PixyMetaHelper.describe(input))
}
+ } catch (e: Exception) {
+ result.error("getPixyMetadata-exception", e.message, e.stackTraceToString())
+ return
}
result.success(metadataMap)
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/SvgGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/SvgGlideModule.kt
index 4a90afec2..c0bd10e44 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/SvgGlideModule.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/SvgGlideModule.kt
@@ -50,7 +50,7 @@ internal class SvgFetcher(val model: SvgThumbnail, val width: Int, val height: I
val context = model.context
val uri = model.uri
- StorageUtils.openInputStream(context, uri)?.use { input ->
+ val bitmap: Bitmap? = StorageUtils.openInputStream(context, uri)?.use { input ->
try {
SVG.getFromInputStream(input)?.let { svg ->
svg.normalizeSize()
@@ -71,12 +71,19 @@ internal class SvgFetcher(val model: SvgThumbnail, val width: Int, val height: I
val canvas = Canvas(bitmap)
svg.renderToCanvas(canvas)
- callback.onDataReady(bitmap)
+ bitmap
}
} catch (ex: SVGParseException) {
callback.onLoadFailed(ex)
+ return
}
}
+
+ if (bitmap == null) {
+ callback.onLoadFailed(Exception("failed to load SVG for uri=$uri"))
+ } else {
+ callback.onDataReady(bitmap)
+ }
}
override fun cleanup() {}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt
index 37bc13071..d0f2138ff 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt
@@ -52,7 +52,9 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
override fun loadData(priority: Priority, callback: DataCallback) {
GlobalScope.launch(Dispatchers.IO) {
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 {
var bytes = retriever.embeddedPicture
if (bytes == null) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
index c4c3ba8db..a5f2f3f29 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
@@ -467,6 +467,7 @@ object StorageUtils {
}
} catch (e: Exception) {
// unsupported format
+ Log.w(LOG_TAG, "failed to initialize MediaMetadataRetriever for uri=$uri")
null
}
}
diff --git a/assets/terms.md b/assets/terms.md
index 752eb1e13..101150057 100644
--- a/assets/terms.md
+++ b/assets/terms.md
@@ -14,4 +14,4 @@ __We collect anonymous data to improve the app.__ We use Google Firebase for Cra
## Links
[Sources](https://github.com/deckerst/aves)
-[License](https://github.com/deckerst/aves/blob/master/LICENSE)
+[License](https://github.com/deckerst/aves/blob/main/LICENSE)
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 7bd7cf0cd..49cd2dc3e 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -363,8 +363,11 @@
"aboutPageTitle": "About",
"@aboutPageTitle": {},
- "aboutFlutter": "Flutter",
- "@aboutFlutter": {},
+ "aboutLinkSources": "Sources",
+ "@aboutLinkSources": {},
+ "aboutLinkLicense": "License",
+ "@aboutLinkLicense": {},
+
"aboutUpdate": "New Version Available",
"@aboutUpdate": {},
"aboutUpdateLinks1": "A new version of Aves is available on",
@@ -377,12 +380,29 @@
"@aboutUpdateGitHub": {},
"aboutUpdateGooglePlay": "Google Play",
"@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": {},
"aboutCreditsWorldAtlas1": "This app uses a TopoJSON file from",
"@aboutCreditsWorldAtlas1": {},
"aboutCreditsWorldAtlas2": "under ISC License.",
"@aboutCreditsWorldAtlas2": {},
+
"aboutLicenses": "Open-Source Licenses",
"@aboutLicenses": {},
"aboutLicensesBanner": "This app uses the following open-source packages and libraries.",
@@ -714,7 +734,7 @@
"settingsSectionPrivacy": "Privacy",
"@settingsSectionPrivacy": {},
- "settingsEnableCrashReport": "Allow anonymous crash reporting",
+ "settingsEnableCrashReport": "Allow anonymous error reporting",
"@settingsEnableCrashReport": {},
"settingsSaveSearchHistory": "Save search history",
"@settingsSaveSearchHistory": {},
diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb
index 5f4e891a4..9e5c7453b 100644
--- a/lib/l10n/app_ko.arb
+++ b/lib/l10n/app_ko.arb
@@ -142,8 +142,8 @@
"renameEntryDialogLabel": "이름",
"editEntryDateDialogTitle": "날짜 및 시간",
- "editEntryDateDialogSet": "설정",
- "editEntryDateDialogShift": "앞뒤로",
+ "editEntryDateDialogSet": "편집",
+ "editEntryDateDialogShift": "시간 이동",
"editEntryDateDialogClear": "삭제",
"editEntryDateDialogFieldSelection": "필드 선택",
"editEntryDateDialogHours": "시간",
@@ -170,16 +170,28 @@
"menuActionStats": "통계",
"aboutPageTitle": "앱 정보",
- "aboutFlutter": "플러터",
+ "aboutLinkSources": "소스 코드",
+ "aboutLinkLicense": "라이선스",
+
"aboutUpdate": "업데이트 사용 가능",
"aboutUpdateLinks1": "앱의 최신 버전을",
"aboutUpdateLinks2": "와",
"aboutUpdateLinks3": "에서 다운로드 사용 가능합니다.",
"aboutUpdateGitHub": "깃허브",
"aboutUpdateGooglePlay": "구글 플레이",
+
+ "aboutBug": "버그 보고",
+ "aboutBugSaveLogInstruction": "앱 로그를 파일에 저장하기",
+ "aboutBugSaveLogButton": "저장",
+ "aboutBugCopyInfoInstruction": "시스템 정보를 복사하기",
+ "aboutBugCopyInfoButton": "복사",
+ "aboutBugReportInstruction": "로그와 시스템 정보를 첨부하여 깃허브에서 이슈를 제출하기",
+ "aboutBugReportButton": "제출",
+
"aboutCredits": "크레딧",
"aboutCreditsWorldAtlas1": "이 앱은",
"aboutCreditsWorldAtlas2": "의 TopoJSON 파일(ISC 라이선스)을 이용합니다.",
+
"aboutLicenses": "오픈 소스 라이선스",
"aboutLicensesBanner": "이 앱은 다음의 오픈 소스 패키지와 라이브러리를 이용합니다.",
"aboutLicensesAndroidLibraries": "안드로이드 라이브러리",
diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart
index e7c44407e..7636c330d 100644
--- a/lib/model/settings/defaults.dart
+++ b/lib/model/settings/defaults.dart
@@ -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/filters/favourite.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/countries_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
+import 'package:flutter/material.dart';
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
static final drawerTypeBookmarks = [
null,
MimeFilter.video,
FavouriteFilter.instance,
];
- static final drawerPageBookmarks = [
+ static const drawerPageBookmarks = [
AlbumListPage.routeName,
CountryListPage.routeName,
TagListPage.routeName,
];
// collection
+ static const collectionSectionFactor = EntryGroupFactor.month;
+ static const collectionSortFactor = EntrySortFactor.date;
static const collectionSelectionQuickActions = [
EntrySetAction.share,
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
static const viewerQuickActions = [
@@ -32,10 +53,37 @@ class SettingsDefaults {
EntryAction.share,
EntryAction.rotateScreen,
];
+ static const showOverlayMinimap = false;
+ static const showOverlayInfo = true;
+ static const showOverlayShootingDetails = false;
+ static const enableOverlayBlurEffect = true;
+ static const viewerUseCutout = true;
// video
static const videoQuickActions = [
VideoAction.replay10,
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;
}
diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart
index 76ec17bee..b06cc923f 100644
--- a/lib/model/settings/settings.dart
+++ b/lib/model/settings/settings.dart
@@ -143,11 +143,11 @@ class Settings extends ChangeNotifier {
// app
- bool get hasAcceptedTerms => getBoolOrDefault(hasAcceptedTermsKey, false);
+ bool get hasAcceptedTerms => getBoolOrDefault(hasAcceptedTermsKey, SettingsDefaults.hasAcceptedTerms);
set hasAcceptedTerms(bool newValue) => setAndNotify(hasAcceptedTermsKey, newValue);
- bool get isCrashlyticsEnabled => getBoolOrDefault(isCrashlyticsEnabledKey, false);
+ bool get isCrashlyticsEnabled => getBoolOrDefault(isCrashlyticsEnabledKey, SettingsDefaults.isCrashlyticsEnabled);
set isCrashlyticsEnabled(bool newValue) {
setAndNotify(isCrashlyticsEnabledKey, newValue);
@@ -182,18 +182,18 @@ class Settings extends ChangeNotifier {
setAndNotify(localeKey, tag);
}
- bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, true);
+ bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, SettingsDefaults.mustBackTwiceToExit);
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) {
setAndNotify(keepScreenOnKey, newValue.toString());
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());
@@ -226,11 +226,11 @@ class Settings extends ChangeNotifier {
// 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());
- 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());
@@ -238,33 +238,33 @@ class Settings extends ChangeNotifier {
set collectionSelectionQuickActions(List 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);
- bool get showThumbnailRaw => getBoolOrDefault(showThumbnailRawKey, true);
+ bool get showThumbnailRaw => getBoolOrDefault(showThumbnailRawKey, SettingsDefaults.showThumbnailRaw);
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);
// 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());
- 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());
- 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());
- 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());
@@ -282,23 +282,23 @@ class Settings extends ChangeNotifier {
set viewerQuickActions(List 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);
- bool get showOverlayInfo => getBoolOrDefault(showOverlayInfoKey, true);
+ bool get showOverlayInfo => getBoolOrDefault(showOverlayInfoKey, SettingsDefaults.showOverlayInfo);
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);
- bool get enableOverlayBlurEffect => getBoolOrDefault(enableOverlayBlurEffectKey, true);
+ bool get enableOverlayBlurEffect => getBoolOrDefault(enableOverlayBlurEffectKey, SettingsDefaults.enableOverlayBlurEffect);
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);
@@ -308,67 +308,67 @@ class Settings extends ChangeNotifier {
set videoQuickActions(List 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);
- bool get enableVideoAutoPlay => getBoolOrDefault(enableVideoAutoPlayKey, false);
+ bool get enableVideoAutoPlay => getBoolOrDefault(enableVideoAutoPlayKey, SettingsDefaults.enableVideoAutoPlay);
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());
- bool get videoShowRawTimedText => getBoolOrDefault(videoShowRawTimedTextKey, false);
+ bool get videoShowRawTimedText => getBoolOrDefault(videoShowRawTimedTextKey, SettingsDefaults.videoShowRawTimedText);
set videoShowRawTimedText(bool newValue) => setAndNotify(videoShowRawTimedTextKey, newValue);
// subtitles
- double get subtitleFontSize => _prefs!.getDouble(subtitleFontSizeKey) ?? 20;
+ double get subtitleFontSize => _prefs!.getDouble(subtitleFontSizeKey) ?? SettingsDefaults.subtitleFontSize;
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());
- bool get subtitleShowOutline => getBoolOrDefault(subtitleShowOutlineKey, true);
+ bool get subtitleShowOutline => getBoolOrDefault(subtitleShowOutlineKey, SettingsDefaults.subtitleShowOutline);
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);
- 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);
// 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());
- double get infoMapZoom => _prefs!.getDouble(infoMapZoomKey) ?? 12;
+ double get infoMapZoom => _prefs!.getDouble(infoMapZoomKey) ?? SettingsDefaults.infoMapZoom;
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());
// 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());
// search
- bool get saveSearchHistory => getBoolOrDefault(saveSearchHistoryKey, true);
+ bool get saveSearchHistory => getBoolOrDefault(saveSearchHistoryKey, SettingsDefaults.saveSearchHistory);
set saveSearchHistory(bool newValue) => setAndNotify(saveSearchHistoryKey, newValue);
diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart
index 2a792dc7e..c20c3a805 100644
--- a/lib/model/video/metadata.dart
+++ b/lib/model/video/metadata.dart
@@ -82,8 +82,13 @@ class VideoMetadataFormatter {
int? dateMillis;
- final dateString = mediaInfo[Keys.date];
- if (dateString is String && dateString != '0') {
+ bool isDefined(dynamic value) => value is String && value != '0';
+
+ var dateString = mediaInfo[Keys.date];
+ if (!isDefined(dateString)) {
+ dateString = mediaInfo[Keys.creationTime];
+ }
+ if (isDefined(dateString)) {
final date = DateTime.tryParse(dateString);
if (date != null) {
dateMillis = date.millisecondsSinceEpoch;
diff --git a/lib/ref/mime_types.dart b/lib/ref/mime_types.dart
index 62d576670..9868a5110 100644
--- a/lib/ref/mime_types.dart
+++ b/lib/ref/mime_types.dart
@@ -47,8 +47,10 @@ class MimeTypes {
static const mp2t = 'video/mp2t'; // .m2ts
static const mp4 = 'video/mp4';
static const ogg = 'video/ogg';
+ static const webm = 'video/webm';
static const json = 'application/json';
+ static const plainText = 'text/plain';
// groups
@@ -62,7 +64,7 @@ class MimeTypes {
static const Set _knownOpaqueImages = {heic, heif, jpeg};
- static const Set _knownVideos = {avi, aviVnd, mkv, mov, mp2t, mp4, ogg};
+ static const Set _knownVideos = {avi, aviVnd, mkv, mov, mp2t, mp4, ogg, webm};
static final Set knownMediaTypes = {..._knownOpaqueImages, ...alphaImages, ...rawImages, ...undecodableImages, ..._knownVideos};
diff --git a/lib/services/report_service.dart b/lib/services/report_service.dart
index 00930ee28..fb61799d2 100644
--- a/lib/services/report_service.dart
+++ b/lib/services/report_service.dart
@@ -1,5 +1,8 @@
+import 'package:collection/collection.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:stack_trace/stack_trace.dart';
abstract class ReportService {
bool get isCollectionEnabled;
@@ -40,6 +43,19 @@ class CrashlyticsReportService extends ReportService {
@override
Future 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);
}
diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart
index a23273655..ffe0201b0 100644
--- a/lib/theme/icons.dart
+++ b/lib/theme/icons.dart
@@ -102,4 +102,7 @@ class AIcons {
static const IconData threeSixty = Icons.threesixty_outlined;
static const IconData selected = Icons.check_circle_outline;
static const IconData unselected = Icons.radio_button_unchecked;
+
+ static const IconData github = MdiIcons.github;
+ static const IconData legal = MdiIcons.scaleBalance;
}
diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart
index ab0ab82c2..4ef6ce2d9 100644
--- a/lib/utils/constants.dart
+++ b/lib/utils/constants.dart
@@ -38,6 +38,8 @@ class Constants {
static const int infoGroupMaxValueLength = 140;
+ static const String avesGithub = 'https://github.com/deckerst/aves';
+
static const List androidDependencies = [
Dependency(
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',
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(
name: 'FlutterFire (Core, Crashlytics)',
license: 'BSD 3-Clause',
@@ -289,15 +297,20 @@ class Constants {
license: 'Apache 2.0',
sourceUrl: 'https://github.com/DavBfr/dart_pdf',
),
+ Dependency(
+ name: 'Stack Trace',
+ license: 'BSD 3-Clause',
+ sourceUrl: 'https://github.com/dart-lang/stack_trace',
+ ),
Dependency(
name: 'Transparent Image',
license: 'MIT',
- sourceUrl: 'https://pub.dev/packages/transparent_image',
+ sourceUrl: 'https://github.com/brianegan/transparent_image',
),
Dependency(
name: 'Tuple',
license: 'BSD 2-Clause',
- sourceUrl: 'https://github.com/dart-lang/tuple',
+ sourceUrl: 'https://github.com/google/tuple.dart',
),
Dependency(
name: 'Version',
diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart
index 564d36509..7e124c114 100644
--- a/lib/widgets/about/about_page.dart
+++ b/lib/widgets/about/about_page.dart
@@ -1,4 +1,5 @@
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/licenses.dart';
import 'package:aves/widgets/about/update.dart';
@@ -27,6 +28,8 @@ class AboutPage extends StatelessWidget {
AppReference(),
Divider(),
AboutUpdate(),
+ BugReport(),
+ Divider(),
AboutCredits(),
Divider(),
],
diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart
index f995f0a7a..77f8d0b5f 100644
--- a/lib/widgets/about/app_ref.dart
+++ b/lib/widgets/about/app_ref.dart
@@ -1,6 +1,7 @@
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/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_logo.dart';
@@ -29,8 +30,8 @@ class _AppReferenceState extends State {
child: Column(
children: [
_buildAvesLine(),
- _buildFlutterLine(),
const SizedBox(height: 16),
+ _buildLinks(),
],
),
);
@@ -47,37 +48,45 @@ class _AppReferenceState extends State {
return FutureBuilder(
future: _packageInfoLoader,
builder: (context, snapshot) {
- return LinkChip(
- leading: AvesLogo(
- size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.25,
- ),
- text: '${context.l10n.appName} ${snapshot.data?.version}',
- url: 'https://github.com/deckerst/aves',
- textStyle: style,
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ AvesLogo(
+ size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.25,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ '${context.l10n.appName} ${snapshot.data?.version}',
+ style: style,
+ ),
+ ],
);
},
);
}
- Widget _buildFlutterLine() {
- final style = DefaultTextStyle.of(context).style;
- final subColor = style.color!.withOpacity(.6);
-
- return Text.rich(
- TextSpan(
- children: [
- WidgetSpan(
- child: Padding(
- padding: const EdgeInsetsDirectional.only(end: 4),
- child: FlutterLogo(
- size: style.fontSize! * 1.25,
- ),
- ),
+ Widget _buildLinks() {
+ return Wrap(
+ crossAxisAlignment: WrapCrossAlignment.center,
+ spacing: 16,
+ children: [
+ LinkChip(
+ leading: const Icon(
+ AIcons.github,
+ size: 24,
),
- TextSpan(text: '${context.l10n.aboutFlutter} ${version['frameworkVersion']}'),
- ],
- ),
- style: TextStyle(color: subColor),
+ 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',
+ ),
+ ],
);
}
}
diff --git a/lib/widgets/about/bug_report.dart b/lib/widgets/about/bug_report.dart
new file mode 100644
index 000000000..1d39146ca
--- /dev/null
+++ b/lib/widgets/about/bug_report.dart
@@ -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 with FeedbackMixin {
+ late Future _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(
+ 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(color: Theme.of(context).accentColor)),
+ foregroundColor: MaterialStateProperty.all(Colors.white),
+ ),
+ child: Text(buttonText),
+ )
+ ],
+ ),
+ );
+ }
+
+ Future _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 _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 _copySystemInfo() async {
+ await Clipboard.setData(ClipboardData(text: await _infoLoader));
+ showFeedback(context, context.l10n.genericSuccessFeedback);
+ }
+
+ Future _goToGithub() async {
+ await launch('${Constants.avesGithub}/issues/new');
+ }
+}
diff --git a/lib/widgets/about/update.dart b/lib/widgets/about/update.dart
index 3c904f5b4..dbd9e2d4d 100644
--- a/lib/widgets/about/update.dart
+++ b/lib/widgets/about/update.dart
@@ -62,7 +62,7 @@ class _AboutUpdateState extends State {
WidgetSpan(
child: LinkChip(
text: context.l10n.aboutUpdateGitHub,
- url: 'https://github.com/deckerst/aves/releases',
+ url: '${Constants.avesGithub}/releases',
textStyle: const TextStyle(fontWeight: FontWeight.bold),
),
alignment: PlaceholderAlignment.middle,
diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart
index af2f44867..cca2f4edc 100644
--- a/lib/widgets/common/basic/insets.dart
+++ b/lib/widgets/common/basic/insets.dart
@@ -1,3 +1,5 @@
+import 'dart:math';
+
import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -52,7 +54,7 @@ class BottomPaddingSliver extends StatelessWidget {
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Selector(
- selector: (context, mq) => mq.effectiveBottomPadding,
+ selector: (context, mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom),
builder: (context, mqPaddingBottom, child) {
return SizedBox(height: mqPaddingBottom);
},
diff --git a/lib/widgets/common/basic/popup_menu_button.dart b/lib/widgets/common/basic/popup_menu_button.dart
new file mode 100644
index 000000000..ede8487ee
--- /dev/null
+++ b/lib/widgets/common/basic/popup_menu_button.dart
@@ -0,0 +1,53 @@
+import 'package:flutter/material.dart';
+
+class AvesPopupMenuButton extends PopupMenuButton {
+ final VoidCallback? onMenuOpened;
+
+ const AvesPopupMenuButton({
+ Key? key,
+ required PopupMenuItemBuilder itemBuilder,
+ T? initialValue,
+ PopupMenuItemSelected? 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 createState() => _AvesPopupMenuButtonState();
+}
+
+class _AvesPopupMenuButtonState extends PopupMenuButtonState {
+ @override
+ void showButtonMenu() {
+ (widget as AvesPopupMenuButton).onMenuOpened?.call();
+ super.showButtonMenu();
+ }
+}
diff --git a/lib/widgets/settings/common/quick_actions/available_actions.dart b/lib/widgets/settings/common/quick_actions/available_actions.dart
index 2f1a9ed7d..bfa5bc6a1 100644
--- a/lib/widgets/settings/common/quick_actions/available_actions.dart
+++ b/lib/widgets/settings/common/quick_actions/available_actions.dart
@@ -1,6 +1,8 @@
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/placeholder.dart';
+import 'package:aves/widgets/viewer/overlay/common.dart';
+import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class AvailableActionPanel extends StatelessWidget {
@@ -75,10 +77,6 @@ class AvailableActionPanel extends StatelessWidget {
Widget child,
) =>
LongPressDraggable(
- data: action,
- maxSimultaneousDrags: 1,
- onDragStarted: () => _setDraggedAvailableAction(action),
- onDragEnd: (details) => _setDraggedAvailableAction(null),
feedback: MediaQueryDataProvider(
child: _buildActionButton(
context,
@@ -86,6 +84,13 @@ class AvailableActionPanel extends StatelessWidget {
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,
child: child,
);
diff --git a/lib/widgets/settings/common/quick_actions/quick_actions.dart b/lib/widgets/settings/common/quick_actions/quick_actions.dart
index e45bdae6a..5e351048b 100644
--- a/lib/widgets/settings/common/quick_actions/quick_actions.dart
+++ b/lib/widgets/settings/common/quick_actions/quick_actions.dart
@@ -1,4 +1,5 @@
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
+import 'package:aves/widgets/viewer/overlay/common.dart';
import 'package:flutter/widgets.dart';
enum QuickActionPlacement { header, action, footer }
@@ -58,16 +59,19 @@ class QuickActionButton extends StatelessWidget {
}
Widget _buildDraggable(Widget child, T action) => LongPressDraggable(
+ feedback: MediaQueryDataProvider(
+ child: draggableFeedbackBuilder!(action),
+ ),
data: action,
+ dragAnchorStrategy: (draggable, context, position) {
+ return childDragAnchorStrategy(draggable, context, position) + Offset(0, OverlayButton.getSize(context));
+ },
maxSimultaneousDrags: 1,
onDragStarted: () => _setDraggedQuickAction(action),
// `onDragEnd` is only called when the widget is mounted,
// so we rely on `onDraggableCanceled` and `onDragCompleted` instead
onDraggableCanceled: (velocity, offset) => _setDraggedQuickAction(null),
onDragCompleted: () => _setDraggedQuickAction(null),
- feedback: MediaQueryDataProvider(
- child: draggableFeedbackBuilder!(action),
- ),
childWhenDragging: child,
child: child,
);
diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart
index ece1e739d..e00045c73 100644
--- a/lib/widgets/viewer/entry_viewer_stack.dart
+++ b/lib/widgets/viewer/entry_viewer_stack.dart
@@ -304,6 +304,13 @@ class _EntryViewerStackState extends State with FeedbackMixin,
_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) {
diff --git a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart
index 81fbde239..9beea2310 100644
--- a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart
+++ b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart
@@ -4,6 +4,7 @@ import 'package:aves/utils/constants.dart';
import 'package:aves/utils/string_utils.dart';
import 'package:aves/widgets/common/identity/highlight_title.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/google.dart';
import 'package:aves/widgets/viewer/info/metadata/xmp_ns/iptc.dart';
@@ -30,6 +31,8 @@ class XmpNamespace extends Equatable {
switch (namespace) {
case XmpBasicNamespace.ns:
return XmpBasicNamespace(rawProps);
+ case XmpDarktableNamespace.ns:
+ return XmpDarktableNamespace(rawProps);
case XmpExifNamespace.ns:
return XmpExifNamespace(rawProps);
case XmpGAudioNamespace.ns:
@@ -136,8 +139,10 @@ class XmpProp {
return propPath.splitMapJoin(XMP.structFieldSeparator,
onMatch: (match) => ' ${match.group(0)} ',
onNonMatch: (s) {
- // strip namespace & format
- return s.split(XMP.propNamespaceSeparator).last.toSentenceCase();
+ // strip namespace
+ final key = s.split(XMP.propNamespaceSeparator).last;
+ // format
+ return key.replaceAll('_', ' ').toSentenceCase();
});
}
diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/darktable.dart b/lib/widgets/viewer/info/metadata/xmp_ns/darktable.dart
new file mode 100644
index 000000000..1959630e2
--- /dev/null
+++ b/lib/widgets/viewer/info/metadata/xmp_ns/darktable.dart
@@ -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 = >{};
+
+ XmpDarktableNamespace(Map rawProps) : super(ns, rawProps);
+
+ @override
+ bool extractData(XmpProp prop) => extractIndexedStruct(prop, historyPattern, history);
+
+ @override
+ List buildFromExtractedData() => [
+ if (history.isNotEmpty)
+ XmpStructArrayCard(
+ title: 'History',
+ structByIndex: history,
+ ),
+ ];
+}
diff --git a/lib/widgets/viewer/overlay/bottom/video.dart b/lib/widgets/viewer/overlay/bottom/video.dart
index addcdf028..a0ba9ee3b 100644
--- a/lib/widgets/viewer/overlay/bottom/video.dart
+++ b/lib/widgets/viewer/overlay/bottom/video.dart
@@ -9,6 +9,7 @@ import 'package:aves/theme/format.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.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/fx/blurred.dart';
import 'package:aves/widgets/common/fx/borders.dart';
@@ -24,6 +25,7 @@ class VideoControlOverlay extends StatefulWidget {
final AvesVideoController? controller;
final Animation scale;
final Function(VideoAction value) onActionSelected;
+ final VoidCallback onActionMenuOpened;
const VideoControlOverlay({
Key? key,
@@ -31,6 +33,7 @@ class VideoControlOverlay extends StatefulWidget {
required this.controller,
required this.scale,
required this.onActionSelected,
+ required this.onActionMenuOpened,
}) : super(key: key);
@override
@@ -94,6 +97,7 @@ class _VideoControlOverlayState extends State with SingleTi
scale: scale,
controller: controller,
onActionSelected: widget.onActionSelected,
+ onActionMenuOpened: widget.onActionMenuOpened,
),
const SizedBox(height: 8),
_buildProgressBar(),
@@ -199,6 +203,7 @@ class _ButtonRow extends StatelessWidget {
final Animation scale;
final AvesVideoController? controller;
final Function(VideoAction value) onActionSelected;
+ final VoidCallback onActionMenuOpened;
const _ButtonRow({
Key? key,
@@ -207,6 +212,7 @@ class _ButtonRow extends StatelessWidget {
required this.scale,
required this.controller,
required this.onActionSelected,
+ required this.onActionMenuOpened,
}) : super(key: key);
static const double padding = 8;
@@ -225,12 +231,13 @@ class _ButtonRow extends StatelessWidget {
child: OverlayButton(
scale: scale,
child: MenuIconTheme(
- child: PopupMenuButton(
+ child: AvesPopupMenuButton(
itemBuilder: (context) => menuActions.map((action) => _buildPopupMenuItem(context, action)).toList(),
onSelected: (action) {
// wait for the popup menu to hide before proceeding with the action
Future.delayed(Durations.popupMenuAnimation * timeDilation, () => onActionSelected(action));
},
+ onMenuOpened: onActionMenuOpened,
),
),
),
diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart
index 8244c8d2c..db300afb7 100644
--- a/lib/widgets/viewer/overlay/top.dart
+++ b/lib/widgets/viewer/overlay/top.dart
@@ -5,12 +5,14 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/basic/menu.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/fx/sweeper.dart';
import 'package:aves/widgets/viewer/entry_action_delegate.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/overlay/common.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/visual/conductor.dart';
import 'package:flutter/foundation.dart';
@@ -167,7 +169,7 @@ class _TopOverlayRow extends StatelessWidget {
OverlayButton(
scale: scale,
child: MenuIconTheme(
- child: PopupMenuButton(
+ child: AvesPopupMenuButton(
key: const Key('entry-menu-button'),
itemBuilder: (context) => [
...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
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);
+ },
),
),
),
diff --git a/lib/widgets/viewer/video_action_delegate.dart b/lib/widgets/viewer/video_action_delegate.dart
index 9e2178c4f..0b5c21bd6 100644
--- a/lib/widgets/viewer/video_action_delegate.dart
+++ b/lib/widgets/viewer/video_action_delegate.dart
@@ -31,12 +31,12 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
});
void dispose() {
- _stopOverlayHidingTimer();
+ stopOverlayHidingTimer();
}
void onActionSelected(BuildContext context, AvesVideoController controller, VideoAction action) {
// make sure overlay is not disappearing when selecting an action
- _stopOverlayHidingTimer();
+ stopOverlayHidingTimer();
const ToggleOverlayNotification(visible: true).dispatch(context);
switch (action) {
@@ -187,5 +187,5 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
}
}
- void _stopOverlayHidingTimer() => _overlayHidingTimer?.cancel();
+ void stopOverlayHidingTimer() => _overlayHidingTimer?.cancel();
}
diff --git a/pubspec.lock b/pubspec.lock
index 27b31a9e7..f58765f0c 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -105,7 +105,7 @@ packages:
name: connectivity_plus
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.7"
+ version: "1.1.0"
connectivity_plus_linux:
dependency: transitive
description:
@@ -119,7 +119,7 @@ packages:
name: connectivity_plus_macos
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.2"
+ version: "1.1.0"
connectivity_plus_platform_interface:
dependency: transitive
description:
@@ -133,14 +133,14 @@ packages:
name: connectivity_plus_web
url: "https://pub.dartlang.org"
source: hosted
- version: "1.1.0"
+ version: "1.1.0+1"
connectivity_plus_windows:
dependency: transitive
description:
name: connectivity_plus_windows
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.2"
+ version: "1.1.0"
convert:
dependency: transitive
description:
@@ -190,6 +190,48 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: "direct main"
description:
@@ -232,7 +274,7 @@ packages:
description:
path: "."
ref: aves
- resolved-ref: "9542ec208248bfa4d459e3967087a4b236da1368"
+ resolved-ref: "2aefcebb9f4bc08107e7de16927d91e577e10d7d"
url: "git://github.com/deckerst/fijkplayer.git"
source: git
version: "0.10.0"
@@ -249,7 +291,7 @@ packages:
name: firebase_core
url: "https://pub.dartlang.org"
source: hosted
- version: "1.5.0"
+ version: "1.6.0"
firebase_core_platform_interface:
dependency: transitive
description:
@@ -270,14 +312,14 @@ packages:
name: firebase_crashlytics
url: "https://pub.dartlang.org"
source: hosted
- version: "2.2.0"
+ version: "2.2.1"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
url: "https://pub.dartlang.org"
source: hosted
- version: "3.1.1"
+ version: "3.1.2"
flex_color_picker:
dependency: "direct main"
description:
@@ -341,14 +383,14 @@ packages:
name: flutter_markdown
url: "https://pub.dartlang.org"
source: hosted
- version: "0.6.4"
+ version: "0.6.6"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.2"
+ version: "2.0.3"
flutter_staggered_animations:
dependency: "direct main"
description:
@@ -405,7 +447,7 @@ packages:
name: google_maps_flutter
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.6"
+ version: "2.0.8"
google_maps_flutter_platform_interface:
dependency: transitive
description:
@@ -594,7 +636,7 @@ packages:
name: package_info_plus
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.5"
+ version: "1.0.6"
package_info_plus_linux:
dependency: transitive
description:
@@ -622,7 +664,7 @@ packages:
name: package_info_plus_web
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.3"
+ version: "1.0.4"
package_info_plus_windows:
dependency: transitive
description:
@@ -636,7 +678,7 @@ packages:
name: palette_generator
url: "https://pub.dartlang.org"
source: hosted
- version: "0.3.0"
+ version: "0.3.1"
panorama:
dependency: "direct main"
description:
@@ -685,7 +727,7 @@ packages:
name: pdf
url: "https://pub.dartlang.org"
source: hosted
- version: "3.4.2"
+ version: "3.5.0"
pedantic:
dependency: transitive
description:
@@ -804,7 +846,7 @@ packages:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.6"
+ version: "2.0.7"
shared_preferences_linux:
dependency: transitive
description:
@@ -832,7 +874,7 @@ packages:
name: shared_preferences_web
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.1"
+ version: "2.0.2"
shared_preferences_windows:
dependency: transitive
description:
@@ -900,16 +942,16 @@ packages:
name: sqflite
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.0+3"
+ version: "2.0.0+4"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.0+2"
+ version: "2.0.1"
stack_trace:
- dependency: transitive
+ dependency: "direct main"
description:
name: stack_trace
url: "https://pub.dartlang.org"
@@ -1021,21 +1063,21 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
- version: "6.0.9"
+ version: "6.0.10"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.1"
+ version: "2.0.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.1"
+ version: "2.0.2"
url_launcher_platform_interface:
dependency: transitive
description:
@@ -1049,7 +1091,7 @@ packages:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.2"
+ version: "2.0.4"
url_launcher_windows:
dependency: transitive
description:
@@ -1112,7 +1154,7 @@ packages:
name: win32
url: "https://pub.dartlang.org"
source: hosted
- version: "2.2.5"
+ version: "2.2.8"
wkt_parser:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index f0da08e0f..b73edacc5 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,7 +1,7 @@
name: aves
description: A visual media gallery and metadata explorer app.
repository: https://github.com/deckerst/aves
-version: 1.5.0+54
+version: 1.5.1+55
publish_to: none
environment:
@@ -19,6 +19,7 @@ dependencies:
# TODO TLAD as of 2021/08/04, null safe version is pre-release
custom_rounded_rectangle_border: '>=0.2.0-nullsafety.0'
decorated_icon:
+ device_info_plus:
equatable:
event_bus:
expansion_tile_card:
@@ -56,6 +57,7 @@ dependencies:
provider:
shared_preferences:
sqflite:
+ stack_trace:
streams_channel:
git:
url: git://github.com/deckerst/aves_streams_channel.git
diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US
index f399a334a..bca2efbf0 100644
--- a/whatsnew/whatsnew-en-US
+++ b/whatsnew/whatsnew-en-US
@@ -1,4 +1,7 @@
Thanks for using Aves!
+v1.5.1:
+- fixed hanging app for collections with specific video formats
+- added bug reporting instructions
v1.5.0:
- faster launch
- edit Exif dates