mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 06:46:22 +01:00
Show file tags in detail pane
This commit is contained in:
parent
64b1d92927
commit
693e7d6a58
9 changed files with 411 additions and 1 deletions
|
@ -33,6 +33,7 @@ class Api {
|
|||
|
||||
_Files files() => _Files(this);
|
||||
_Ocs ocs() => _Ocs(this);
|
||||
_SystemtagsRelations systemtagsRelations() => _SystemtagsRelations(this);
|
||||
|
||||
static String getAuthorizationHeaderValue(Account account) {
|
||||
final auth =
|
||||
|
@ -716,3 +717,82 @@ class _OcsFilesSharingSharees {
|
|||
|
||||
static final _log = Logger("api.api._OcsFilesSharingSharees");
|
||||
}
|
||||
|
||||
class _SystemtagsRelations {
|
||||
const _SystemtagsRelations(this.api);
|
||||
|
||||
_SystemtagsRelationsFiles files(int fileId) =>
|
||||
_SystemtagsRelationsFiles(this, fileId);
|
||||
|
||||
final Api api;
|
||||
}
|
||||
|
||||
class _SystemtagsRelationsFiles {
|
||||
const _SystemtagsRelationsFiles(this.systemtagsRelations, this.fileId);
|
||||
|
||||
/// List systemtags associated with a file
|
||||
///
|
||||
/// Warning: this Nextcloud API is undocumented
|
||||
Future<Response> propfind({
|
||||
id,
|
||||
displayName,
|
||||
userVisible,
|
||||
userAssignable,
|
||||
canAssign,
|
||||
}) async {
|
||||
final endpoint = "remote.php/dav/systemtags-relations/files/$fileId";
|
||||
try {
|
||||
if (id == null &&
|
||||
displayName == null &&
|
||||
userVisible == null &&
|
||||
userAssignable == null &&
|
||||
canAssign == null) {
|
||||
// no body
|
||||
return await systemtagsRelations.api.request("PROPFIND", endpoint);
|
||||
}
|
||||
|
||||
final namespaces = <String, String>{
|
||||
"DAV:": "d",
|
||||
"http://owncloud.org/ns": "oc",
|
||||
};
|
||||
final builder = XmlBuilder();
|
||||
builder
|
||||
..processing("xml", "version=\"1.0\"")
|
||||
..element("d:propfind", namespaces: namespaces, nest: () {
|
||||
builder.element("d:prop", nest: () {
|
||||
if (id != null) {
|
||||
builder.element("oc:id");
|
||||
}
|
||||
if (displayName != null) {
|
||||
builder.element("oc:display-name");
|
||||
}
|
||||
if (userVisible != null) {
|
||||
builder.element("oc:user-visible");
|
||||
}
|
||||
if (userAssignable != null) {
|
||||
builder.element("oc:user-assignable");
|
||||
}
|
||||
if (canAssign != null) {
|
||||
builder.element("oc:can-assign");
|
||||
}
|
||||
});
|
||||
});
|
||||
return await systemtagsRelations.api.request(
|
||||
"PROPFIND",
|
||||
endpoint,
|
||||
header: {
|
||||
"Content-Type": "application/xml",
|
||||
},
|
||||
body: builder.buildDocument().toXmlString(),
|
||||
);
|
||||
} catch (e) {
|
||||
_log.severe("[propfind] Failed while propfind", e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
final _SystemtagsRelations systemtagsRelations;
|
||||
final int fileId;
|
||||
|
||||
static final _log = Logger("api.api._SystemtagsRelationsFiles");
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:nc_photos/entity/file.dart';
|
|||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/pref.dart';
|
||||
|
||||
|
@ -17,6 +18,7 @@ enum DiType {
|
|||
shareRepo,
|
||||
shareeRepo,
|
||||
favoriteRepo,
|
||||
tagRepo,
|
||||
appDb,
|
||||
pref,
|
||||
}
|
||||
|
@ -30,6 +32,7 @@ class DiContainer {
|
|||
ShareRepo? shareRepo,
|
||||
ShareeRepo? shareeRepo,
|
||||
FavoriteRepo? favoriteRepo,
|
||||
TagRepo? tagRepo,
|
||||
AppDb? appDb,
|
||||
Pref? pref,
|
||||
}) : _albumRepo = albumRepo,
|
||||
|
@ -39,6 +42,7 @@ class DiContainer {
|
|||
_shareRepo = shareRepo,
|
||||
_shareeRepo = shareeRepo,
|
||||
_favoriteRepo = favoriteRepo,
|
||||
_tagRepo = tagRepo,
|
||||
_appDb = appDb,
|
||||
_pref = pref;
|
||||
|
||||
|
@ -58,6 +62,8 @@ class DiContainer {
|
|||
return contianer._shareeRepo != null;
|
||||
case DiType.favoriteRepo:
|
||||
return contianer._favoriteRepo != null;
|
||||
case DiType.tagRepo:
|
||||
return contianer._tagRepo != null;
|
||||
case DiType.appDb:
|
||||
return contianer._appDb != null;
|
||||
case DiType.pref:
|
||||
|
@ -73,6 +79,7 @@ class DiContainer {
|
|||
OrNull<ShareRepo>? shareRepo,
|
||||
OrNull<ShareeRepo>? shareeRepo,
|
||||
OrNull<FavoriteRepo>? favoriteRepo,
|
||||
OrNull<TagRepo>? tagRepo,
|
||||
OrNull<AppDb>? appDb,
|
||||
OrNull<Pref>? pref,
|
||||
}) {
|
||||
|
@ -84,6 +91,7 @@ class DiContainer {
|
|||
shareRepo: shareRepo == null ? _shareRepo : shareRepo.obj,
|
||||
shareeRepo: shareeRepo == null ? _shareeRepo : shareeRepo.obj,
|
||||
favoriteRepo: favoriteRepo == null ? _favoriteRepo : favoriteRepo.obj,
|
||||
tagRepo: tagRepo == null ? _tagRepo : tagRepo.obj,
|
||||
appDb: appDb == null ? _appDb : appDb.obj,
|
||||
pref: pref == null ? _pref : pref.obj,
|
||||
);
|
||||
|
@ -96,6 +104,7 @@ class DiContainer {
|
|||
ShareRepo get shareRepo => _shareRepo!;
|
||||
ShareeRepo get shareeRepo => _shareeRepo!;
|
||||
FavoriteRepo get favoriteRepo => _favoriteRepo!;
|
||||
TagRepo get tagRepo => _tagRepo!;
|
||||
|
||||
AppDb get appDb => _appDb!;
|
||||
Pref get pref => _pref!;
|
||||
|
@ -107,6 +116,7 @@ class DiContainer {
|
|||
final ShareRepo? _shareRepo;
|
||||
final ShareeRepo? _shareeRepo;
|
||||
final FavoriteRepo? _favoriteRepo;
|
||||
final TagRepo? _tagRepo;
|
||||
|
||||
final AppDb? _appDb;
|
||||
final Pref? _pref;
|
||||
|
|
107
lib/entity/tag.dart
Normal file
107
lib/entity/tag.dart
Normal file
|
@ -0,0 +1,107 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/json_util.dart' as json_util;
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/type.dart';
|
||||
|
||||
class Tag with EquatableMixin {
|
||||
const Tag({
|
||||
required this.id,
|
||||
required this.displayName,
|
||||
this.userVisible,
|
||||
this.userAssignable,
|
||||
this.canAssign,
|
||||
});
|
||||
|
||||
factory Tag.fromJson(JsonObj json) => Tag(
|
||||
id: json["id"],
|
||||
displayName: json["displayName"],
|
||||
userVisible: json_util.boolFromJson(json["userVisible"]),
|
||||
userAssignable: json_util.boolFromJson(json["userAssignable"]),
|
||||
canAssign: json_util.boolFromJson(json["canAssign"]),
|
||||
);
|
||||
|
||||
JsonObj toJson() => {
|
||||
"id": id,
|
||||
"displayName": displayName,
|
||||
if (userVisible != null) "userVisible": userVisible,
|
||||
if (userAssignable != null) "userAssignable": userAssignable,
|
||||
if (canAssign != null) "canAssign": canAssign,
|
||||
};
|
||||
|
||||
@override
|
||||
toString() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.write("$runtimeType {"
|
||||
"id: $id, "
|
||||
"displayName: '$displayName', ");
|
||||
if (userVisible != null) {
|
||||
buffer.write("userVisible: $userVisible, ");
|
||||
}
|
||||
if (userAssignable != null) {
|
||||
buffer.write("userAssignable: $userAssignable, ");
|
||||
}
|
||||
if (canAssign != null) {
|
||||
buffer.write("canAssign: $canAssign, ");
|
||||
}
|
||||
buffer.write("}");
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
Tag copyWith({
|
||||
int? id,
|
||||
String? displayName,
|
||||
OrNull<bool>? userVisible,
|
||||
OrNull<bool>? userAssignable,
|
||||
OrNull<bool>? canAssign,
|
||||
}) =>
|
||||
Tag(
|
||||
id: id ?? this.id,
|
||||
displayName: displayName ?? this.displayName,
|
||||
userVisible: userVisible == null ? this.userVisible : userVisible.obj,
|
||||
userAssignable:
|
||||
userAssignable == null ? this.userAssignable : userAssignable.obj,
|
||||
canAssign: canAssign == null ? this.canAssign : canAssign.obj,
|
||||
);
|
||||
|
||||
@override
|
||||
get props => [
|
||||
id,
|
||||
displayName,
|
||||
userVisible,
|
||||
userAssignable,
|
||||
canAssign,
|
||||
];
|
||||
|
||||
final int id;
|
||||
final String displayName;
|
||||
final bool? userVisible;
|
||||
final bool? userAssignable;
|
||||
final bool? canAssign;
|
||||
}
|
||||
|
||||
extension TagExtension on Tag {
|
||||
/// Compare the server identity of two Tags
|
||||
///
|
||||
/// Return true if two Tags point to the same tag on server. Be careful that
|
||||
/// this does NOT mean that the two Tags are identical
|
||||
bool compareServerIdentity(Tag other) {
|
||||
return id == other.id && displayName == other.displayName;
|
||||
}
|
||||
}
|
||||
|
||||
class TagRepo {
|
||||
const TagRepo(this.dataSrc);
|
||||
|
||||
/// See [TagDataSource.listByFile]
|
||||
Future<List<Tag>> listByFile(Account account, File file) =>
|
||||
dataSrc.listByFile(account, file);
|
||||
|
||||
final TagDataSource dataSrc;
|
||||
}
|
||||
|
||||
abstract class TagDataSource {
|
||||
/// List all tags associated with [file]
|
||||
Future<List<Tag>> listByFile(Account account, File file);
|
||||
}
|
36
lib/entity/tag/data_source.dart
Normal file
36
lib/entity/tag/data_source.dart
Normal file
|
@ -0,0 +1,36 @@
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/entity/webdav_response_parser.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
class TagRemoteDataSource implements TagDataSource {
|
||||
const TagRemoteDataSource();
|
||||
|
||||
@override
|
||||
listByFile(Account account, File file) async {
|
||||
_log.info("[listByFile] ${file.path}");
|
||||
final response =
|
||||
await Api(account).systemtagsRelations().files(file.fileId!).propfind(
|
||||
id: 1,
|
||||
displayName: 1,
|
||||
userVisible: 1,
|
||||
userAssignable: 1,
|
||||
canAssign: 1,
|
||||
);
|
||||
if (!response.isGood) {
|
||||
_log.severe("[listByFile] Failed requesting server: $response");
|
||||
throw ApiException(
|
||||
response: response,
|
||||
message: "Failed communicating with server: ${response.statusCode}");
|
||||
}
|
||||
|
||||
final xml = XmlDocument.parse(response.body);
|
||||
return WebdavResponseParser().parseTags(xml);
|
||||
}
|
||||
|
||||
static final _log = Logger("entity.tag.data_source.TagRemoteDataSource");
|
||||
}
|
|
@ -5,6 +5,7 @@ import 'package:logging/logging.dart';
|
|||
import 'package:nc_photos/ci_string.dart';
|
||||
import 'package:nc_photos/entity/favorite.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/string_extension.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
|
@ -14,9 +15,11 @@ class WebdavResponseParser {
|
|||
List<Favorite> parseFavorites(XmlDocument xml) =>
|
||||
_parse<Favorite>(xml, _toFavorite);
|
||||
|
||||
List<Tag> parseTags(XmlDocument xml) => _parse<Tag>(xml, _toTag);
|
||||
|
||||
Map<String, String> get namespaces => _namespaces;
|
||||
|
||||
List<T> _parse<T>(XmlDocument xml, T Function(XmlElement) mapper) {
|
||||
List<T> _parse<T>(XmlDocument xml, T? Function(XmlElement) mapper) {
|
||||
_namespaces = _parseNamespaces(xml);
|
||||
final body = () {
|
||||
try {
|
||||
|
@ -174,6 +177,56 @@ class WebdavResponseParser {
|
|||
);
|
||||
}
|
||||
|
||||
/// Map <DAV:response> contents to Tag
|
||||
Tag? _toTag(XmlElement element) {
|
||||
String? path;
|
||||
int? id;
|
||||
String? displayName;
|
||||
bool? userVisible;
|
||||
bool? userAssignable;
|
||||
bool? canAssign;
|
||||
|
||||
for (final child in element.children.whereType<XmlElement>()) {
|
||||
if (child.matchQualifiedName("href",
|
||||
prefix: "DAV:", namespaces: _namespaces)) {
|
||||
path = _hrefToPath(child);
|
||||
} else if (child.matchQualifiedName("propstat",
|
||||
prefix: "DAV:", namespaces: _namespaces)) {
|
||||
final status = child.children
|
||||
.whereType<XmlElement>()
|
||||
.firstWhere((element) => element.matchQualifiedName("status",
|
||||
prefix: "DAV:", namespaces: _namespaces))
|
||||
.innerText;
|
||||
if (!status.contains(" 200 ")) {
|
||||
continue;
|
||||
}
|
||||
final prop = child.children.whereType<XmlElement>().firstWhere(
|
||||
(element) => element.matchQualifiedName("prop",
|
||||
prefix: "DAV:", namespaces: _namespaces));
|
||||
final propParser =
|
||||
_TagPropParser(namespaces: _namespaces, logFilePath: path);
|
||||
propParser.parse(prop);
|
||||
id = propParser.id;
|
||||
displayName = propParser.displayName;
|
||||
userVisible = propParser.userVisible;
|
||||
userAssignable = propParser.userAssignable;
|
||||
canAssign = propParser.canAssign;
|
||||
}
|
||||
}
|
||||
if (id == null) {
|
||||
// the first returned item is not a valid tag
|
||||
return null;
|
||||
}
|
||||
|
||||
return Tag(
|
||||
id: id,
|
||||
displayName: displayName!,
|
||||
userVisible: userVisible!,
|
||||
userAssignable: userAssignable!,
|
||||
canAssign: canAssign!,
|
||||
);
|
||||
}
|
||||
|
||||
String _hrefToPath(XmlElement href) {
|
||||
final rawPath = Uri.decodeComponent(href.innerText).trimLeftAny("/");
|
||||
final pos = rawPath.indexOf("remote.php");
|
||||
|
@ -336,6 +389,52 @@ class _FileIdPropParser {
|
|||
int? _fileId;
|
||||
}
|
||||
|
||||
class _TagPropParser {
|
||||
_TagPropParser({
|
||||
this.namespaces = const {},
|
||||
this.logFilePath,
|
||||
});
|
||||
|
||||
/// Parse <DAV:prop> element contents
|
||||
void parse(XmlElement element) {
|
||||
for (final child in element.children.whereType<XmlElement>()) {
|
||||
if (child.matchQualifiedName("id",
|
||||
prefix: "http://owncloud.org/ns", namespaces: namespaces)) {
|
||||
_id = int.parse(child.innerText);
|
||||
} else if (child.matchQualifiedName("display-name",
|
||||
prefix: "http://owncloud.org/ns", namespaces: namespaces)) {
|
||||
_displayName = child.innerText;
|
||||
} else if (child.matchQualifiedName("user-visible",
|
||||
prefix: "http://owncloud.org/ns", namespaces: namespaces)) {
|
||||
_userVisible = child.innerText == "true";
|
||||
} else if (child.matchQualifiedName("user-assignable",
|
||||
prefix: "http://owncloud.org/ns", namespaces: namespaces)) {
|
||||
_userAssignable = child.innerText == "true";
|
||||
} else if (child.matchQualifiedName("can-assign",
|
||||
prefix: "http://owncloud.org/ns", namespaces: namespaces)) {
|
||||
_canAssign = child.innerText == "true";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int? get id => _id;
|
||||
String? get displayName => _displayName;
|
||||
bool? get userVisible => _userVisible;
|
||||
bool? get userAssignable => _userAssignable;
|
||||
bool? get canAssign => _canAssign;
|
||||
|
||||
final Map<String, String> namespaces;
|
||||
|
||||
/// File path for logging only
|
||||
final String? logFilePath;
|
||||
|
||||
int? _id;
|
||||
String? _displayName;
|
||||
bool? _userVisible;
|
||||
bool? _userAssignable;
|
||||
bool? _canAssign;
|
||||
}
|
||||
|
||||
extension on XmlElement {
|
||||
bool matchQualifiedName(
|
||||
String local, {
|
||||
|
|
|
@ -23,6 +23,8 @@ import 'package:nc_photos/entity/share.dart';
|
|||
import 'package:nc_photos/entity/share/data_source.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
import 'package:nc_photos/entity/sharee/data_source.dart';
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/entity/tag/data_source.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/mobile/android/android_info.dart';
|
||||
import 'package:nc_photos/mobile/self_signed_cert_manager.dart';
|
||||
|
@ -157,6 +159,7 @@ void _initDiContainer() {
|
|||
shareRepo: ShareRepo(ShareRemoteDataSource()),
|
||||
shareeRepo: ShareeRepo(ShareeRemoteDataSource()),
|
||||
favoriteRepo: const FavoriteRepo(FavoriteRemoteDataSource()),
|
||||
tagRepo: const TagRepo(TagRemoteDataSource()),
|
||||
appDb: AppDb(),
|
||||
pref: Pref(),
|
||||
));
|
||||
|
|
|
@ -65,6 +65,11 @@ class AppTheme extends StatelessWidget {
|
|||
: Colors.white60;
|
||||
}
|
||||
|
||||
static Color getPrimaryTextColorInverse(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? primaryTextColorDark
|
||||
: primaryTextColorLight;
|
||||
|
||||
static Color getAppBarDarkModeSwitchColor(BuildContext context) {
|
||||
return Colors.black87;
|
||||
}
|
||||
|
|
16
lib/use_case/list_file_tag.dart
Normal file
16
lib/use_case/list_file_tag.dart
Normal file
|
@ -0,0 +1,16 @@
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
|
||||
class ListFileTag {
|
||||
ListFileTag(this._c) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.tagRepo);
|
||||
|
||||
/// Return list of tags associated with [file]
|
||||
Future<List<Tag>> call(Account account, File file) =>
|
||||
_c.tagRepo.listByFile(account, file);
|
||||
|
||||
final DiContainer _c;
|
||||
}
|
|
@ -24,6 +24,7 @@ import 'package:nc_photos/platform/features.dart' as features;
|
|||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/use_case/list_file_tag.dart';
|
||||
import 'package:nc_photos/use_case/remove_from_album.dart';
|
||||
import 'package:nc_photos/use_case/update_album.dart';
|
||||
import 'package:nc_photos/use_case/update_property.dart';
|
||||
|
@ -71,6 +72,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
_initMetadata();
|
||||
}
|
||||
}
|
||||
_initTags();
|
||||
|
||||
// postpone loading map to improve responsiveness
|
||||
Future.delayed(const Duration(milliseconds: 750)).then((_) {
|
||||
|
@ -196,6 +198,46 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
title: Text(widget.file.ownerId!.toString()),
|
||||
subtitle: Text(L10n.global().fileSharedByDescription),
|
||||
),
|
||||
if (_tags.isNotEmpty)
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.local_offer_outlined,
|
||||
color: AppTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
title: SizedBox(
|
||||
height: 40,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _tags.length,
|
||||
itemBuilder: (context, index) => Center(
|
||||
child: Wrap(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.getUnfocusedIconColor(context),
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
_tags[index],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
AppTheme.getPrimaryTextColorInverse(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(width: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.calendar_today_outlined,
|
||||
|
@ -296,6 +338,16 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _initTags() async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
try {
|
||||
final tags = await ListFileTag(c)(widget.account, widget.file);
|
||||
_tags.addAll(tags.map((t) => t.displayName));
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_initTags] Failed while ListFileTag", e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onRemoveFromAlbumPressed(BuildContext context) async {
|
||||
assert(widget.album!.provider is AlbumStaticProvider);
|
||||
try {
|
||||
|
@ -477,6 +529,8 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
int? _isoSpeedRatings;
|
||||
Tuple2<double, double>? _gps;
|
||||
|
||||
final _tags = <String>[];
|
||||
|
||||
late final bool _canRemoveFromAlbum = _checkCanRemoveFromAlbum();
|
||||
|
||||
var _shouldBlockGpsMap = true;
|
||||
|
|
Loading…
Reference in a new issue