Merge branch 'develop'
This commit is contained in:
commit
f3ec152ce5
36 changed files with 636 additions and 139 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -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)
|
||||||
|
|
33
README.md
33
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
Binary file not shown.
|
@ -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}")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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": {},
|
||||||
|
|
|
@ -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": "안드로이드 라이브러리",
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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(),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
163
lib/widgets/about/bug_report.dart
Normal file
163
lib/widgets/about/bug_report.dart
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
53
lib/widgets/common/basic/popup_menu_button.dart
Normal file
53
lib/widgets/common/basic/popup_menu_button.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
25
lib/widgets/viewer/info/metadata/xmp_ns/darktable.dart
Normal file
25
lib/widgets/viewer/info/metadata/xmp_ns/darktable.dart
Normal 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,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
92
pubspec.lock
92
pubspec.lock
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue