Customize date/time of a file

This commit is contained in:
Ming Ming 2021-06-21 18:39:17 +08:00
parent deacacdd96
commit b47cb5c63c
11 changed files with 505 additions and 66 deletions

View file

@ -219,6 +219,7 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> {
if (!ev.hasAnyProperties([
FilePropertyUpdatedEvent.propMetadata,
FilePropertyUpdatedEvent.propIsArchived,
FilePropertyUpdatedEvent.propOverrideDateTime,
])) {
// not interested
return;
@ -234,7 +235,10 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> {
if (_successivePropertyUpdatedCount % 10 == 0) {
add(_ScanDirBlocExternalEvent());
} else {
if (ev.hasAnyProperties([FilePropertyUpdatedEvent.propIsArchived])) {
if (ev.hasAnyProperties([
FilePropertyUpdatedEvent.propIsArchived,
FilePropertyUpdatedEvent.propOverrideDateTime,
])) {
_propertyUpdatedSubscription =
Future.delayed(const Duration(seconds: 2)).asStream().listen((_) {
add(_ScanDirBlocExternalEvent());

View file

@ -209,6 +209,7 @@ class File with EquatableMixin {
this.ownerId,
this.metadata,
this.isArchived,
this.overrideDateTime,
}) : this.path = path.trimAny("/");
@override
@ -253,6 +254,9 @@ class File with EquatableMixin {
),
),
isArchived: json["isArchived"],
overrideDateTime: json["overrideDateTime"] == null
? null
: DateTime.parse(json["overrideDateTime"]),
);
}
@ -293,6 +297,9 @@ class File with EquatableMixin {
if (isArchived != null) {
product += "isArchived: $isArchived, ";
}
if (overrideDateTime != null) {
product += "overrideDateTime: $overrideDateTime, ";
}
return product + "}";
}
@ -311,6 +318,8 @@ class File with EquatableMixin {
if (ownerId != null) "ownerId": ownerId,
if (metadata != null) "metadata": metadata.toJson(),
if (isArchived != null) "isArchived": isArchived,
if (overrideDateTime != null)
"overrideDateTime": overrideDateTime.toUtc().toIso8601String(),
};
}
@ -327,6 +336,7 @@ class File with EquatableMixin {
String ownerId,
OrNull<Metadata> metadata,
OrNull<bool> isArchived,
OrNull<DateTime> overrideDateTime,
}) {
return File(
path: path ?? this.path,
@ -341,6 +351,9 @@ class File with EquatableMixin {
ownerId: ownerId ?? this.ownerId,
metadata: metadata == null ? this.metadata : metadata.obj,
isArchived: isArchived == null ? this.isArchived : isArchived.obj,
overrideDateTime: overrideDateTime == null
? this.overrideDateTime
: overrideDateTime.obj,
);
}
@ -375,6 +388,7 @@ class File with EquatableMixin {
ownerId,
// metadata is handled separately, see [equals]
isArchived,
overrideDateTime,
];
final String path;
@ -391,11 +405,12 @@ class File with EquatableMixin {
// metadata
final Metadata metadata;
final bool isArchived;
final DateTime overrideDateTime;
}
extension FileExtension on File {
DateTime get bestDateTime {
return metadata?.exif?.dateTimeOriginal ?? lastModified;
return overrideDateTime ?? metadata?.exif?.dateTimeOriginal ?? lastModified;
}
}
@ -424,12 +439,14 @@ class FileRepo {
File file, {
OrNull<Metadata> metadata,
OrNull<bool> isArchived,
OrNull<DateTime> overrideDateTime,
}) =>
this.dataSrc.updateProperty(
account,
file,
metadata: metadata,
isArchived: isArchived,
overrideDateTime: overrideDateTime,
);
/// See [FileDataSource.copy]
@ -486,6 +503,7 @@ abstract class FileDataSource {
File f, {
OrNull<Metadata> metadata,
OrNull<bool> isArchived,
OrNull<DateTime> overrideDateTime,
});
/// Copy [f] to [destination]

View file

@ -46,6 +46,7 @@ class FileWebdavDataSource implements FileDataSource {
customProperties: [
"app:metadata",
"app:is-archived",
"app:override-date-time"
],
);
if (!response.isGood) {
@ -112,6 +113,7 @@ class FileWebdavDataSource implements FileDataSource {
File f, {
OrNull<Metadata> metadata,
OrNull<bool> isArchived,
OrNull<DateTime> overrideDateTime,
}) async {
_log.info("[updateProperty] ${f.path}");
if (metadata?.obj != null && metadata.obj.fileEtag != f.etag) {
@ -122,10 +124,13 @@ class FileWebdavDataSource implements FileDataSource {
if (metadata?.obj != null)
"app:metadata": jsonEncode(metadata.obj.toJson()),
if (isArchived?.obj != null) "app:is-archived": isArchived.obj,
if (overrideDateTime?.obj != null)
"app:override-date-time": overrideDateTime.obj.toUtc().toIso8601String(),
};
final removeProps = [
if (OrNull.isNull(metadata)) "app:metadata",
if (OrNull.isNull(isArchived)) "app:is-archived",
if (OrNull.isNull(overrideDateTime)) "app:override-date-time",
];
final response = await Api(account).files().proppatch(
path: f.path,
@ -251,6 +256,7 @@ class FileAppDbDataSource implements FileDataSource {
File f, {
OrNull<Metadata> metadata,
OrNull<bool> isArchived,
OrNull<DateTime> overrideDateTime,
}) {
_log.info("[updateProperty] ${f.path}");
return AppDb.use((db) async {
@ -266,6 +272,7 @@ class FileAppDbDataSource implements FileDataSource {
return e.copyWith(
metadata: metadata,
isArchived: isArchived,
overrideDateTime: overrideDateTime,
);
} else {
return e;
@ -278,6 +285,7 @@ class FileAppDbDataSource implements FileDataSource {
final newFile = f.copyWith(
metadata: metadata,
isArchived: isArchived,
overrideDateTime: overrideDateTime,
);
await fileDbStore.put(
AppDbFileDbEntry.fromFile(account, newFile).toJson(),
@ -414,6 +422,7 @@ class FileCachedDataSource implements FileDataSource {
File f, {
OrNull<Metadata> metadata,
OrNull<bool> isArchived,
OrNull<DateTime> overrideDateTime,
}) async {
await _remoteSrc
.updateProperty(
@ -421,12 +430,14 @@ class FileCachedDataSource implements FileDataSource {
f,
metadata: metadata,
isArchived: isArchived,
overrideDateTime: overrideDateTime,
)
.then((_) => _appDbSrc.updateProperty(
account,
f,
metadata: metadata,
isArchived: isArchived,
overrideDateTime: overrideDateTime,
));
// generate a new random token

View file

@ -67,6 +67,7 @@ class WebdavFileParser {
String ownerId;
Metadata metadata;
bool isArchived;
DateTime overrideDateTime;
for (final child in element.children.whereType<XmlElement>()) {
if (child.matchQualifiedName("href",
@ -99,6 +100,7 @@ class WebdavFileParser {
ownerId = propParser.ownerId;
metadata = propParser.metadata;
isArchived = propParser.isArchived;
overrideDateTime = propParser.overrideDateTime;
}
}
@ -115,6 +117,7 @@ class WebdavFileParser {
ownerId: ownerId,
metadata: metadata,
isArchived: isArchived,
overrideDateTime: overrideDateTime,
);
}
@ -174,6 +177,9 @@ class _PropParser {
} else if (child.matchQualifiedName("is-archived",
prefix: "com.nkming.nc_photos", namespaces: namespaces)) {
_isArchived = child.innerText == "true";
} else if (child.matchQualifiedName("override-date-time",
prefix: "com.nkming.nc_photos", namespaces: namespaces)) {
_overrideDateTime = DateTime.parse(child.innerText);
}
}
// 2nd pass that depends on data in 1st pass
@ -206,6 +212,7 @@ class _PropParser {
String get ownerId => _ownerId;
Metadata get metadata => _metadata;
bool get isArchived => _isArchived;
DateTime get overrideDateTime => _overrideDateTime;
final Map<String, String> namespaces;
@ -223,6 +230,7 @@ class _PropParser {
String _ownerId;
Metadata _metadata;
bool _isArchived;
DateTime _overrideDateTime;
}
extension on XmlElement {

View file

@ -58,6 +58,7 @@ class FilePropertyUpdatedEvent {
// Bit masks for properties field
static const propMetadata = 0x01;
static const propIsArchived = 0x02;
static const propOverrideDateTime = 0x04;
}
class FileRemovedEvent {

View file

@ -432,6 +432,25 @@
"@mobileSelectRangeNotification": {
"description": "Inform mobile user how to select items in range"
},
"updateDateTimeDialogTitle": "Modify date & time",
"@updateDateTimeDialogTitle": {
"description": "Dialog to modify the date & time of a file"
},
"dateSubtitle": "Date",
"timeSubtitle": "Time",
"dateYearInputHint": "Year",
"dateMonthInputHint": "Month",
"dateDayInputHint": "Day",
"timeHourInputHint": "Hour",
"timeMinuteInputHint": "Minute",
"dateTimeInputInvalid": "Invalid value",
"@dateTimeInputInvalid": {
"description": "Invalid date/time input (e.g., non-numeric characters)"
},
"updateDateTimeFailureNotification": "Failed modifying date & time",
"@updateDateTimeFailureNotification": {
"description": "Failed to set the date & time of a file"
},
"changelogTitle": "Changelog",
"@changelogTitle": {
"description": "Title of the changelog dialog"

View file

@ -14,8 +14,9 @@ class UpdateProperty {
File file, {
OrNull<Metadata> metadata,
OrNull<bool> isArchived,
OrNull<DateTime> overrideDateTime,
}) async {
if (metadata == null && isArchived == null) {
if (metadata == null && isArchived == null && overrideDateTime == null) {
// ?
_log.warning("[call] Nothing to update");
return;
@ -30,6 +31,7 @@ class UpdateProperty {
file,
metadata: metadata,
isArchived: isArchived,
overrideDateTime: overrideDateTime,
);
int properties = 0;
@ -39,6 +41,9 @@ class UpdateProperty {
if (isArchived != null) {
properties |= FilePropertyUpdatedEvent.propIsArchived;
}
if (overrideDateTime != null) {
properties |= FilePropertyUpdatedEvent.propOverrideDateTime;
}
assert(properties != 0);
KiwiContainer()
.resolve<EventBus>()
@ -62,4 +67,11 @@ extension UpdatePropertyExtension on UpdateProperty {
/// See [UpdateProperty.call]
Future<void> updateIsArchived(Account account, File file, bool isArchived) =>
call(account, file, isArchived: OrNull(isArchived));
/// Convenience function to only update overrideDateTime
///
/// See [UpdateProperty.call]
Future<void> updateOverrideDateTime(
Account account, File file, DateTime overrideDateTime) =>
call(account, file, overrideDateTime: OrNull(overrideDateTime));
}

View file

@ -0,0 +1,205 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/num_extension.dart';
class PhotoDateTimeEditDialog extends StatefulWidget {
PhotoDateTimeEditDialog({
Key key,
@required this.initialDateTime,
}) : super(key: key);
@override
createState() => _PhotoDateTimeEditDialogState();
final DateTime initialDateTime;
}
class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
@override
build(BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).updateDateTimeDialogTitle),
content: Form(
key: _formKey,
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppLocalizations.of(context).dateSubtitle,
style: Theme.of(context).textTheme.subtitle2,
),
Row(
children: [
Flexible(
child: TextFormField(
decoration: InputDecoration(
hintText:
AppLocalizations.of(context).dateYearInputHint,
),
keyboardType: TextInputType.number,
validator: (value) {
try {
int.parse(value);
return null;
} catch (_) {
return AppLocalizations.of(context)
.dateTimeInputInvalid;
}
},
onSaved: (value) {
_formValue.year = int.parse(value);
},
initialValue: "${widget.initialDateTime.year}",
),
flex: 1,
),
const SizedBox(width: 4),
Flexible(
child: TextFormField(
decoration: InputDecoration(
hintText:
AppLocalizations.of(context).dateMonthInputHint,
),
keyboardType: TextInputType.number,
validator: (value) {
if (int.tryParse(value)?.inRange(1, 12) == true) {
return null;
}
return AppLocalizations.of(context)
.dateTimeInputInvalid;
},
onSaved: (value) {
_formValue.month = int.parse(value);
},
initialValue: widget.initialDateTime.month
.toString()
.padLeft(2, "0"),
),
flex: 1,
),
const SizedBox(width: 4),
Flexible(
child: TextFormField(
decoration: InputDecoration(
hintText: AppLocalizations.of(context).dateDayInputHint,
),
keyboardType: TextInputType.number,
validator: (value) {
if (int.tryParse(value)?.inRange(1, 31) == true) {
return null;
}
return AppLocalizations.of(context)
.dateTimeInputInvalid;
},
onSaved: (value) {
_formValue.day = int.parse(value);
},
initialValue:
widget.initialDateTime.day.toString().padLeft(2, "0"),
),
flex: 1,
),
],
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context).timeSubtitle,
style: Theme.of(context).textTheme.subtitle2,
),
Row(
children: [
Flexible(
child: TextFormField(
decoration: InputDecoration(
hintText:
AppLocalizations.of(context).timeHourInputHint,
),
keyboardType: TextInputType.number,
validator: (value) {
if (int.tryParse(value)?.inRange(0, 23) == true) {
return null;
}
return AppLocalizations.of(context)
.dateTimeInputInvalid;
},
onSaved: (value) {
_formValue.hour = int.parse(value);
},
initialValue: widget.initialDateTime.hour
.toString()
.padLeft(2, "0"),
),
flex: 1,
),
const SizedBox(width: 4),
Flexible(
child: TextFormField(
decoration: InputDecoration(
hintText:
AppLocalizations.of(context).timeMinuteInputHint,
),
keyboardType: TextInputType.number,
validator: (value) {
if (int.tryParse(value)?.inRange(0, 59) == true) {
return null;
}
return AppLocalizations.of(context)
.dateTimeInputInvalid;
},
onSaved: (value) {
_formValue.minute = int.parse(value);
},
initialValue: widget.initialDateTime.minute
.toString()
.padLeft(2, "0"),
),
flex: 1,
),
const SizedBox(width: 4),
Flexible(
child: const SizedBox(),
flex: 1,
),
],
),
],
),
),
),
actions: [
TextButton(
onPressed: () => _onSavePressed(context),
child: Text(MaterialLocalizations.of(context).saveButtonLabel),
),
],
);
}
void _onSavePressed(BuildContext context) {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
final d = DateTime(_formValue.year, _formValue.month, _formValue.day,
_formValue.hour, _formValue.minute);
_log.info("[_onSavePressed] Set date time: $d");
Navigator.of(context).pop(d);
}
}
final _formKey = GlobalKey<FormState>();
final _formValue = _FormValue();
static final _log = Logger(
"widget.photo_date_time_edit_dialog._PhotoDateTimeEditDialogState");
}
class _FormValue {
int year;
int month;
int day;
int hour;
int minute;
}

View file

@ -22,7 +22,9 @@ import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/remove.dart';
import 'package:nc_photos/use_case/update_album.dart';
import 'package:nc_photos/use_case/update_property.dart';
import 'package:nc_photos/widget/album_picker_dialog.dart';
import 'package:nc_photos/widget/photo_date_time_edit_dialog.dart';
import 'package:path/path.dart';
import 'package:tuple/tuple.dart';
@ -45,6 +47,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
initState() {
super.initState();
_dateTime = widget.file.bestDateTime.toLocal();
if (widget.file.metadata == null) {
_log.info("[initState] Metadata missing in File");
} else {
@ -57,9 +60,9 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
@override
build(BuildContext context) {
final dateTime = widget.file.bestDateTime.toLocal();
final dateStr = DateFormat(DateFormat.YEAR_ABBR_MONTH_DAY).format(dateTime);
final timeStr = DateFormat(DateFormat.HOUR_MINUTE).format(dateTime);
final dateStr =
DateFormat(DateFormat.YEAR_ABBR_MONTH_DAY).format(_dateTime);
final timeStr = DateFormat(DateFormat.HOUR_MINUTE).format(_dateTime);
String sizeSubStr = "";
const space = " ";
@ -95,80 +98,88 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
}
cameraSubStr = cameraSubStr.trim();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_DetailPaneButton(
icon: Icons.playlist_add_outlined,
label: AppLocalizations.of(context).addToAlbumTooltip,
onPressed: () => _onAddToAlbumPressed(context),
),
_DetailPaneButton(
icon: Icons.delete_outline,
label: AppLocalizations.of(context).deleteTooltip,
onPressed: () => _onDeletePressed(context),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: const Divider(),
),
ListTile(
leading: Icon(
Icons.image_outlined,
color: AppTheme.getSecondaryTextColor(context),
return Material(
type: MaterialType.transparency,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_DetailPaneButton(
icon: Icons.playlist_add_outlined,
label: AppLocalizations.of(context).addToAlbumTooltip,
onPressed: () => _onAddToAlbumPressed(context),
),
_DetailPaneButton(
icon: Icons.delete_outline,
label: AppLocalizations.of(context).deleteTooltip,
onPressed: () => _onDeletePressed(context),
),
],
),
title: Text(basenameWithoutExtension(widget.file.path)),
subtitle: Text(widget.file.strippedPath),
),
ListTile(
leading: Icon(
Icons.calendar_today_outlined,
color: AppTheme.getSecondaryTextColor(context),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: const Divider(),
),
title: Text("$dateStr $timeStr"),
),
if (widget.file.metadata?.imageWidth != null &&
widget.file.metadata?.imageHeight != null)
ListTile(
leading: Icon(
Icons.aspect_ratio,
Icons.image_outlined,
color: AppTheme.getSecondaryTextColor(context),
),
title: Text(
"${widget.file.metadata.imageWidth} x ${widget.file.metadata.imageHeight}"),
subtitle: Text(sizeSubStr),
)
else
title: Text(basenameWithoutExtension(widget.file.path)),
subtitle: Text(widget.file.strippedPath),
),
ListTile(
leading: Icon(
Icons.aspect_ratio,
Icons.calendar_today_outlined,
color: AppTheme.getSecondaryTextColor(context),
),
title: Text(_byteSizeToString(widget.file.contentLength)),
),
if (_model != null)
ListTile(
leading: Icon(
Icons.camera_outlined,
title: Text("$dateStr $timeStr"),
trailing: Icon(
Icons.edit_outlined,
color: AppTheme.getSecondaryTextColor(context),
),
title: Text(_model),
subtitle: cameraSubStr.isNotEmpty ? Text(cameraSubStr) : null,
onTap: () => _onDateTimeTap(context),
),
if (features.isSupportMapView && _gps != null)
SizedBox(
height: 256,
child: platform.Map(
center: _gps,
zoom: 16,
onTap: _onMapTap,
if (widget.file.metadata?.imageWidth != null &&
widget.file.metadata?.imageHeight != null)
ListTile(
leading: Icon(
Icons.aspect_ratio,
color: AppTheme.getSecondaryTextColor(context),
),
title: Text(
"${widget.file.metadata.imageWidth} x ${widget.file.metadata.imageHeight}"),
subtitle: Text(sizeSubStr),
)
else
ListTile(
leading: Icon(
Icons.aspect_ratio,
color: AppTheme.getSecondaryTextColor(context),
),
title: Text(_byteSizeToString(widget.file.contentLength)),
),
),
],
if (_model != null)
ListTile(
leading: Icon(
Icons.camera_outlined,
color: AppTheme.getSecondaryTextColor(context),
),
title: Text(_model),
subtitle: cameraSubStr.isNotEmpty ? Text(cameraSubStr) : null,
),
if (features.isSupportMapView && _gps != null)
SizedBox(
height: 256,
child: platform.Map(
center: _gps,
zoom: 16,
onTap: _onMapTap,
),
),
],
),
);
}
@ -290,6 +301,38 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
}
}
void _onDateTimeTap(BuildContext context) {
showDialog(
context: context,
builder: (context) => PhotoDateTimeEditDialog(initialDateTime: _dateTime),
).then((value) async {
if (value == null || value is! DateTime) {
return;
}
final fileRepo = FileRepo(FileCachedDataSource());
try {
await UpdateProperty(fileRepo)
.updateOverrideDateTime(widget.account, widget.file, value);
setState(() {
_dateTime = value;
});
} catch (e, stacktrace) {
_log.shout(
"[_onDateTimeTap] Failed while updateOverrideDateTime" +
(kDebugMode ? ": ${widget.file.path}" : ""),
e,
stacktrace);
SnackBarManager().showSnackBar(SnackBar(
content: Text(
AppLocalizations.of(context).updateDateTimeFailureNotification),
duration: k.snackBarDurationNormal,
));
}
}).catchError((e, stacktrace) {
_log.shout("[_onDateTimeTap] Failed while showDialog", e, stacktrace);
});
}
static double _gpsDmsToDouble(List<Rational> dms) {
double product = dms[0].toDouble();
if (dms.length > 1) {
@ -334,6 +377,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
}
}
DateTime _dateTime;
// EXIF data
String _model;
double _fNumber;

View file

@ -461,6 +461,18 @@ void main() {
final file = File.fromJson(json);
expect(file, File(path: "", isArchived: true));
});
test("overrideDateTime", () {
final json = <String, dynamic>{
"path": "",
"overrideDateTime": "2021-01-02T03:04:05.000Z",
};
final file = File.fromJson(json);
expect(
file,
File(
path: "", overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5)));
});
});
group("toJson", () {
@ -578,6 +590,16 @@ void main() {
"isArchived": true,
});
});
test("overrideDateTime", () {
final file = File(
path: "remote.php/dav/files/admin/test.jpg",
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5));
expect(file.toJson(), <String, dynamic>{
"path": "remote.php/dav/files/admin/test.jpg",
"overrideDateTime": "2021-01-02T03:04:05.000Z",
});
});
});
group("copyWith", () {
@ -594,6 +616,7 @@ void main() {
ownerId: "admin",
metadata: null,
isArchived: true,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
);
test("path", () {
@ -612,6 +635,7 @@ void main() {
fileId: 123,
ownerId: "admin",
isArchived: true,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
));
});
@ -631,6 +655,7 @@ void main() {
fileId: 123,
ownerId: "admin",
isArchived: true,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
));
});
@ -650,6 +675,7 @@ void main() {
fileId: 123,
ownerId: "admin",
isArchived: true,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
));
});
@ -669,6 +695,7 @@ void main() {
fileId: 123,
ownerId: "admin",
isArchived: true,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
));
});
@ -689,6 +716,7 @@ void main() {
fileId: 123,
ownerId: "admin",
isArchived: true,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
));
});
@ -708,6 +736,7 @@ void main() {
fileId: 123,
ownerId: "admin",
isArchived: true,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
));
});
@ -727,6 +756,7 @@ void main() {
fileId: 123,
ownerId: "admin",
isArchived: true,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
));
});
@ -746,6 +776,7 @@ void main() {
fileId: 123,
ownerId: "admin",
isArchived: true,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
));
});
@ -765,6 +796,7 @@ void main() {
fileId: 321,
ownerId: "admin",
isArchived: true,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
));
});
@ -784,6 +816,7 @@ void main() {
fileId: 123,
ownerId: "user",
isArchived: true,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
));
});
@ -805,6 +838,7 @@ void main() {
ownerId: "admin",
metadata: metadata,
isArchived: true,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
));
});
@ -822,6 +856,7 @@ void main() {
ownerId: "admin",
metadata: Metadata(),
isArchived: true,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
);
final file = src.copyWith(metadata: OrNull(null));
expect(
@ -838,6 +873,7 @@ void main() {
fileId: 123,
ownerId: "admin",
isArchived: true,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
));
});
@ -857,6 +893,7 @@ void main() {
fileId: 123,
ownerId: "admin",
isArchived: false,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
));
});
@ -875,6 +912,47 @@ void main() {
hasPreview: true,
fileId: 123,
ownerId: "admin",
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
));
});
test("overrideDateTime", () {
final file = src.copyWith(
overrideDateTime: OrNull(DateTime.utc(2022, 3, 4, 5, 6, 7)));
expect(
file,
File(
path: "remote.php/dav/files/admin/test.jpg",
contentLength: 123,
contentType: "image/jpeg",
etag: "8a3e0799b6f0711c23cc2d93950eceb5",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
isCollection: true,
usedBytes: 123456,
hasPreview: true,
fileId: 123,
ownerId: "admin",
isArchived: true,
overrideDateTime: DateTime.utc(2022, 3, 4, 5, 6, 7),
));
});
test("clear overrideDateTime", () {
final file = src.copyWith(overrideDateTime: OrNull(null));
expect(
file,
File(
path: "remote.php/dav/files/admin/test.jpg",
contentLength: 123,
contentType: "image/jpeg",
etag: "8a3e0799b6f0711c23cc2d93950eceb5",
lastModified: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
isCollection: true,
usedBytes: 123456,
hasPreview: true,
fileId: 123,
ownerId: "admin",
isArchived: true,
));
});
});

View file

@ -175,6 +175,45 @@ void main() {
]);
});
test("file w/ override-date-time", () {
final xml = XmlDocument.parse("""
<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:"
xmlns:s="http://sabredav.org/ns"
xmlns:oc="http://owncloud.org/ns"
xmlns:nc="http://nextcloud.org/ns">
<d:response>
<d:href>/remote.php/dav/files/admin/Photos/Nextcloud%20community.jpg</d:href>
<d:propstat>
<d:prop>
<d:getlastmodified>Fri, 01 Jan 2021 02:03:04 GMT</d:getlastmodified>
<d:getetag>&quot;8950e39a034e369237d9285e2d815a50&quot;</d:getetag>
<d:getcontenttype>image/jpeg</d:getcontenttype>
<d:resourcetype/>
<d:getcontentlength>797325</d:getcontentlength>
<nc:has-preview>true</nc:has-preview>
<x1:override-date-time xmlns:x1="com.nkming.nc_photos">2021-01-02T03:04:05.000Z</x1:override-date-time>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""");
final results = WebdavFileParser()(xml);
expect(results, [
File(
path: "remote.php/dav/files/admin/Photos/Nextcloud community.jpg",
contentLength: 797325,
contentType: "image/jpeg",
etag: "8950e39a034e369237d9285e2d815a50",
lastModified: DateTime.utc(2021, 1, 1, 2, 3, 4),
hasPreview: true,
isCollection: false,
overrideDateTime: DateTime.utc(2021, 1, 2, 3, 4, 5),
),
]);
});
test("multiple files", () {
final xml = XmlDocument.parse("""
<?xml version="1.0"?>