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]
|
||||
|
||||
## [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)
|
||||
|
|
35
README.md
35
README.md
|
@ -4,25 +4,34 @@
|
|||
<br />
|
||||
<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.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/1-S10-collection.png" alt='Collection screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/2-S10-viewer.png" alt='Image screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/5-S10-stats.png" alt='Stats screenshot' height="400" />
|
||||
|
||||
## 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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
Binary file not shown.
|
@ -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}")
|
||||
}
|
||||
|
|
|
@ -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<String, String>()
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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() {}
|
||||
|
|
|
@ -52,7 +52,9 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
|
|||
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) {
|
||||
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) {
|
||||
|
|
|
@ -467,6 +467,7 @@ object StorageUtils {
|
|||
}
|
||||
} catch (e: Exception) {
|
||||
// unsupported format
|
||||
Log.w(LOG_TAG, "failed to initialize MediaMetadataRetriever for uri=$uri")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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": {},
|
||||
|
|
|
@ -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": "안드로이드 라이브러리",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<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);
|
||||
|
||||
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<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);
|
||||
|
||||
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<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);
|
||||
|
||||
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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<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};
|
||||
|
||||
|
|
|
@ -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<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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ class Constants {
|
|||
|
||||
static const int infoGroupMaxValueLength = 140;
|
||||
|
||||
static const String avesGithub = 'https://github.com/deckerst/aves';
|
||||
|
||||
static const List<Dependency> 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',
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
|
|
|
@ -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<AppReference> {
|
|||
child: Column(
|
||||
children: [
|
||||
_buildAvesLine(),
|
||||
_buildFlutterLine(),
|
||||
const SizedBox(height: 16),
|
||||
_buildLinks(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -47,37 +48,45 @@ class _AppReferenceState extends State<AppReference> {
|
|||
return FutureBuilder<PackageInfo>(
|
||||
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',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
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(
|
||||
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,
|
||||
|
|
|
@ -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<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.effectiveBottomPadding,
|
||||
selector: (context, mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom),
|
||||
builder: (context, mqPaddingBottom, child) {
|
||||
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/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<T extends Object> extends StatelessWidget {
|
||||
|
@ -75,10 +77,6 @@ class AvailableActionPanel<T extends Object> extends StatelessWidget {
|
|||
Widget child,
|
||||
) =>
|
||||
LongPressDraggable<T>(
|
||||
data: action,
|
||||
maxSimultaneousDrags: 1,
|
||||
onDragStarted: () => _setDraggedAvailableAction(action),
|
||||
onDragEnd: (details) => _setDraggedAvailableAction(null),
|
||||
feedback: MediaQueryDataProvider(
|
||||
child: _buildActionButton(
|
||||
context,
|
||||
|
@ -86,6 +84,13 @@ class AvailableActionPanel<T extends Object> 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,
|
||||
);
|
||||
|
|
|
@ -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<T extends Object> 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,
|
||||
);
|
||||
|
|
|
@ -304,6 +304,13 @@ class _EntryViewerStackState extends State<EntryViewerStack> 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) {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
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/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<double> 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<VideoControlOverlay> 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<double> 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<VideoAction>(
|
||||
child: AvesPopupMenuButton<VideoAction>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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<EntryAction>(
|
||||
child: AvesPopupMenuButton<EntryAction>(
|
||||
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);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
92
pubspec.lock
92
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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue